diff --git a/common/control.cpp b/common/control.cpp index 3711bff..24e879c 100644 --- a/common/control.cpp +++ b/common/control.cpp @@ -1,411 +1,481 @@ /******************************************************************** 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 #include QString Control::s_dirName = QStringLiteral("control/"); Control::Control(QObject *parent) : QObject(parent) { } void Control::activateWatcher() { if (m_watcher) { return; } m_watcher = new QFileSystemWatcher({filePath()}, this); connect(m_watcher, &QFileSystemWatcher::fileChanged, this, [this]() { readFile(); Q_EMIT changed(); }); } QFileSystemWatcher* Control::watcher() const { return m_watcher; } 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(); 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; } for (auto output : outputs) { m_outputsControls << new ControlOutput(output, this); } // 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? } void ControlConfig::activateWatcher() { if (watcher()) { // Watcher was already activated. return; } for (auto *output : m_outputsControls) { output->activateWatcher(); connect(output, &ControlOutput::changed, this, &ControlConfig::changed); } } 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::writeFile() { bool success = true; for (auto *outputControl : m_outputsControls) { if (getOutputRetention(outputControl->id(), outputControl->name()) == OutputRetention::Individual) { continue; } success &= outputControl->writeFile(); } return success && Control::writeFile(); } 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; } QVariantMap createOutputInfo(const QString &outputId, const QString &outputName) { QVariantMap outputInfo; outputInfo[QStringLiteral("id")] = outputId; outputInfo[QStringLiteral("metadata")] = metadata(outputName); return outputInfo; } 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 auto outputInfo = createOutputInfo(outputId, outputName); outputInfo[QStringLiteral("retention")] = (int)value; outputsInfo << outputInfo; setOutputs(outputsInfo); } bool ControlConfig::getAutoRotate(const KScreen::OutputPtr &output) const { return getAutoRotate(output->hashMd5(), output->name()); } bool ControlConfig::getAutoRotate(const QString &outputId, const QString &outputName) const { const auto retention = getOutputRetention(outputId, outputName); if (retention == OutputRetention::Individual) { const QVariantList outputsInfo = getOutputs(); for (const auto variantInfo : outputsInfo) { const QVariantMap info = variantInfo.toMap(); if (!infoIsOutput(info, outputId, outputName)) { continue; } const auto val = info[QStringLiteral("autorotate")]; return !val.canConvert() || val.toBool(); } } // Retention is global or info for output not in config control file. if (auto *outputControl = getOutputControl(outputId, outputName)) { return outputControl->getAutoRotate(); } // Info for output not found. // TODO: make this return value depend on the device having a tablet state? return true; } void ControlConfig::setAutoRotate(const KScreen::OutputPtr &output, bool value) { setAutoRotate(output->hashMd5(), output->name(), value); } // TODO: combine methods (templated functions) void ControlConfig::setAutoRotate(const QString &outputId, const QString &outputName, bool value) { QList::iterator it; QVariantList outputsInfo = getOutputs(); auto setOutputAutoRotate = [&outputId, &outputName, value, this]() { if (auto *control = getOutputControl(outputId, outputName)) { control->setAutoRotate(value); } }; for (it = outputsInfo.begin(); it != outputsInfo.end(); ++it) { QVariantMap outputInfo = (*it).toMap(); if (!infoIsOutput(outputInfo, outputId, outputName)) { continue; } outputInfo[QStringLiteral("autorotate")] = value; *it = outputInfo; setOutputs(outputsInfo); setOutputAutoRotate(); return; } // no entry yet, create one auto outputInfo = createOutputInfo(outputId, outputName); outputInfo[QStringLiteral("autorotate")] = value; outputsInfo << outputInfo; setOutputs(outputsInfo); setOutputAutoRotate(); } +KScreen::OutputPtr ControlConfig::getReplicationSource(const KScreen::OutputPtr &output) const +{ + return getReplicationSource(output->hashMd5(), output->name()); +} + +KScreen::OutputPtr ControlConfig::getReplicationSource(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; + } + const QString sourceHash = info[QStringLiteral("replicate-hash")].toString(); + const QString sourceName = info[QStringLiteral("replicate-name")].toString(); + + if (sourceHash.isEmpty() && sourceName.isEmpty()) { + // Common case when the replication source has been unset. + return nullptr; + } + + for (auto output : m_config->outputs()) { + if (output->hashMd5() == sourceHash && output->name() == sourceName) { + return output; + } + } + // No match. + return nullptr; + } + // Info for output not found. + return nullptr; +} + +void ControlConfig::setReplicationSource(const KScreen::OutputPtr &output, + const KScreen::OutputPtr &source) +{ + setReplicationSource(output->hashMd5(), output->name(), source); +} + +void ControlConfig::setReplicationSource(const QString &outputId, const QString &outputName, + const KScreen::OutputPtr &source) +{ + QList::iterator it; + QVariantList outputsInfo = getOutputs(); + const QString sourceHash = source->hashMd5(); + const QString sourceName = source->name(); + + for (it = outputsInfo.begin(); it != outputsInfo.end(); ++it) { + QVariantMap outputInfo = (*it).toMap(); + if (!infoIsOutput(outputInfo, outputId, outputName)) { + continue; + } + outputInfo[QStringLiteral("replicate-hash")] = sourceHash; + outputInfo[QStringLiteral("replicate-name")] = sourceName; + *it = outputInfo; + setOutputs(outputsInfo); + // TODO: shall we set this information also as new global value (like with auto-rotate)? + return; + } + // no entry yet, create one + auto outputInfo = createOutputInfo(outputId, outputName); + outputInfo[QStringLiteral("replicate-hash")] = sourceHash; + outputInfo[QStringLiteral("replicate-name")] = sourceName; + + outputsInfo << outputInfo; + setOutputs(outputsInfo); + // TODO: shall we set this information also as new global value (like with auto-rotate)? +} + QVariantList ControlConfig::getOutputs() const { return constInfo()[QStringLiteral("outputs")].toList(); } void ControlConfig::setOutputs(QVariantList outputsInfo) { auto &infoMap = info(); infoMap[QStringLiteral("outputs")] = outputsInfo; } ControlOutput* ControlConfig::getOutputControl(const QString &outputId, const QString &outputName) const { for (auto *control : m_outputsControls) { if (control->id() == outputId && control->name() == outputName) { return control; } } return nullptr; } ControlOutput::ControlOutput(KScreen::OutputPtr output, QObject *parent) : Control(parent) , m_output(output) { readFile(); } QString ControlOutput::id() const { return m_output->hashMd5(); } QString ControlOutput::name() const { return m_output->name(); } QString ControlOutput::dirPath() const { return Control::dirPath() % QStringLiteral("outputs/"); } QString ControlOutput::filePath() const { if (!m_output) { return QString(); } return filePathFromHash(m_output->hashMd5()); } bool ControlOutput::getAutoRotate() const { const auto val = constInfo()[QStringLiteral("autorotate")]; return !val.canConvert() || val.toBool(); } void ControlOutput::setAutoRotate(bool value) { auto &infoMap = info(); if (infoMap.isEmpty()) { infoMap = createOutputInfo(m_output->hashMd5(), m_output->name()); } infoMap[QStringLiteral("autorotate")] = value; } diff --git a/common/control.h b/common/control.h index f702126..6cb2d47 100644 --- a/common/control.h +++ b/common/control.h @@ -1,123 +1,130 @@ /******************************************************************** 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 #include class QFileSystemWatcher; 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; virtual bool writeFile(); virtual void activateWatcher(); Q_SIGNALS: void changed(); protected: virtual QString dirPath() const; virtual QString filePath() const = 0; QString filePathFromHash(const QString &hash) const; void readFile(); QVariantMap& info(); const QVariantMap& constInfo() const; QFileSystemWatcher* watcher() const; static OutputRetention convertVariantToOutputRetention(QVariant variant); private: static QString s_dirName; QVariantMap m_info; QFileSystemWatcher *m_watcher = nullptr; }; class ControlOutput; 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 getAutoRotate(const KScreen::OutputPtr &output) const; bool getAutoRotate(const QString &outputId, const QString &outputName) const; void setAutoRotate(const KScreen::OutputPtr &output, bool value); void setAutoRotate(const QString &outputId, const QString &outputName, bool value); + KScreen::OutputPtr getReplicationSource(const KScreen::OutputPtr &output) const; + KScreen::OutputPtr getReplicationSource(const QString &outputId, + const QString &outputName) const; + void setReplicationSource(const KScreen::OutputPtr &output, const KScreen::OutputPtr &source); + void setReplicationSource(const QString &outputId, const QString &outputName, + const KScreen::OutputPtr &source); + QString dirPath() const override; QString filePath() const override; bool writeFile() override; void activateWatcher() override; private: QVariantList getOutputs() const; void setOutputs(QVariantList outputsInfo); bool infoIsOutput(const QVariantMap &info, const QString &outputId, const QString &outputName) const; ControlOutput* getOutputControl(const QString &outputId, const QString &outputName) const; KScreen::ConfigPtr m_config; QStringList m_duplicateOutputIds; QVector m_outputsControls; }; class ControlOutput : public Control { Q_OBJECT public: explicit ControlOutput(KScreen::OutputPtr output, QObject *parent = nullptr); QString id() const; QString name() const; // TODO: scale auto value bool getAutoRotate() const; void setAutoRotate(bool value); QString dirPath() const override; QString filePath() const override; private: KScreen::OutputPtr m_output; }; #endif