diff --git a/common/control.cpp b/common/control.cpp index 24e879c..56f7065 100644 --- a/common/control.cpp +++ b/common/control.cpp @@ -1,481 +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(); + const QString sourceHash = source ? source->hashMd5() : QStringLiteral(""); + const QString sourceName = source ? source->name() : QStringLiteral(""); 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/kcm/config_handler.cpp b/kcm/config_handler.cpp index 3b40d40..1f7f6a5 100644 --- a/kcm/config_handler.cpp +++ b/kcm/config_handler.cpp @@ -1,271 +1,282 @@ /******************************************************************** 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 "config_handler.h" #include "kcm_screen_debug.h" #include "output_model.h" #include #include #include #include using namespace KScreen; ConfigHandler::ConfigHandler(QObject *parent) : QObject(parent) { } void ConfigHandler::setConfig(KScreen::ConfigPtr config) { m_config = config; m_initialConfig = m_config->clone(); KScreen::ConfigMonitor::instance()->addConfig(m_config); m_control.reset(new ControlConfig(config)); m_outputs = new OutputModel(this); connect(m_outputs, &OutputModel::positionChanged, this, &ConfigHandler::checkScreenNormalization); connect(m_outputs, &OutputModel::sizeChanged, this, &ConfigHandler::checkScreenNormalization); for (const KScreen::OutputPtr &output : config->outputs()) { initOutput(output); } m_lastNormalizedScreenSize = screenSize(); m_initialRetention = getRetention(); Q_EMIT retentionChanged(); connect(m_outputs, &OutputModel::changed, this, [this]() { checkNeedsSave(); Q_EMIT changed(); }); connect(m_config.data(), &KScreen::Config::outputAdded, this, [this]() { Q_EMIT outputConnect(true); }); connect(m_config.data(), &KScreen::Config::outputRemoved, this, [this]() { Q_EMIT outputConnect(false); }); connect(m_config.data(), &KScreen::Config::primaryOutputChanged, this, &ConfigHandler::primaryOutputChanged); Q_EMIT outputModelChanged(); } void ConfigHandler::initOutput(const KScreen::OutputPtr &output) { if (output->isConnected()) { m_outputs->add(output); } connect(output.data(), &KScreen::Output::isConnectedChanged, this, [this, output]() { Q_EMIT outputConnect(output->isConnected()); }); } void ConfigHandler::updateInitialConfig() { m_initialRetention = getRetention(); connect(new GetConfigOperation(), &GetConfigOperation::finished, this, [this](ConfigOperation *op) { if (op->hasError()) { return; } m_initialConfig = qobject_cast(op)->config(); checkNeedsSave(); }); } void ConfigHandler::checkNeedsSave() { if (m_config->supportedFeatures() & KScreen::Config::Feature::PrimaryDisplay) { if (m_config->primaryOutput() && m_initialConfig->primaryOutput()) { if (m_config->primaryOutput()->hashMd5() != m_initialConfig->primaryOutput()->hashMd5() ) { Q_EMIT needsSaveChecked(true); return; } } else if ((bool)m_config->primaryOutput() != (bool)m_initialConfig->primaryOutput()) { Q_EMIT needsSaveChecked(true); return; } } if (m_initialRetention != getRetention()) { Q_EMIT needsSaveChecked(true); return; } for (const auto &output : m_config->connectedOutputs()) { const QString hash = output->hashMd5(); for (const auto &initialOutput : m_initialConfig->outputs()) { if (hash != initialOutput->hashMd5()) { continue; } bool needsSave = false; if (output->isEnabled() != initialOutput->isEnabled()) { needsSave = true; } if (output->isEnabled()) { needsSave |= output->currentModeId() != initialOutput->currentModeId() || output->pos() != initialOutput->pos() || output->scale() != initialOutput->scale() || output->rotation() != initialOutput->rotation() || output->replicationSource() != initialOutput->replicationSource(); } if (needsSave) { Q_EMIT needsSaveChecked(true); return; } break; } } Q_EMIT needsSaveChecked(false); } QSize ConfigHandler::screenSize() const { int width = 0, height = 0; QSize size; for (const auto &output : m_config->connectedOutputs()) { if (!output->isPositionable()) { continue; } const int outputRight = output->geometry().right(); const int outputBottom = output->geometry().bottom(); if (outputRight > width) { width = outputRight; } if (outputBottom > height) { height = outputBottom; } } if (width > 0 && height > 0) { size = QSize(width, height); } else { size = QSize(); } return size; } QSize ConfigHandler::normalizeScreen() { if (!m_config) { return QSize(); } bool changed = m_outputs->normalizePositions(); const auto currentScreenSize = screenSize(); changed |= m_lastNormalizedScreenSize != currentScreenSize; m_lastNormalizedScreenSize = currentScreenSize; Q_EMIT screenNormalizationUpdate(true); return currentScreenSize; } void ConfigHandler::checkScreenNormalization() { const bool normalized = !m_config || (m_lastNormalizedScreenSize == screenSize() && m_outputs->positionsNormalized()); Q_EMIT screenNormalizationUpdate(normalized); } void ConfigHandler::primaryOutputSelected(int index) { Q_UNUSED(index) // TODO } void ConfigHandler::primaryOutputChanged(const KScreen::OutputPtr &output) { Q_UNUSED(output) } Control::OutputRetention ConfigHandler::getRetention() const { using Retention = Control::OutputRetention; auto ret = Retention::Undefined; if (!m_control) { return ret; } const auto outputs = m_config->connectedOutputs(); if (outputs.isEmpty()) { return ret; } ret = m_control->getOutputRetention(outputs.first()); for (const auto &output : outputs) { const auto outputRet = m_control->getOutputRetention(output); if (ret != outputRet ) { // Control file with different retention values per output. return Retention::Undefined; } } if (ret == Retention::Undefined) { // If all outputs have undefined retention, // this should be displayed as global retention. return Retention::Global; } return ret; } int ConfigHandler::retention() const { return static_cast(getRetention()); } void ConfigHandler::setRetention(int retention) { using Retention = Control::OutputRetention; if (!m_control) { return; } if (retention != static_cast(Retention::Global) && retention != static_cast(Retention::Individual)) { // We only allow setting to global or individual retention. return; } if (retention == ConfigHandler::retention()) { return; } auto ret = static_cast(retention); for (const auto &output : m_config->connectedOutputs()) { m_control->setOutputRetention(output, ret); } checkNeedsSave(); Q_EMIT retentionChanged(); Q_EMIT changed(); } +KScreen::OutputPtr ConfigHandler::replicationSource(const KScreen::OutputPtr &output) const +{ + return m_control->getReplicationSource(output); +} + +void ConfigHandler::setReplicationSource(KScreen::OutputPtr &output, + const KScreen::OutputPtr &source) +{ + m_control->setReplicationSource(output, source); +} + void ConfigHandler::writeControl() { if (!m_control) { return; } m_control->writeFile(); } diff --git a/kcm/config_handler.h b/kcm/config_handler.h index 4588a94..4a3d4f9 100644 --- a/kcm/config_handler.h +++ b/kcm/config_handler.h @@ -1,78 +1,81 @@ /******************************************************************** 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 . *********************************************************************/ #pragma once #include "../common/control.h" #include #include class OutputModel; class ConfigHandler : public QObject { Q_OBJECT public: explicit ConfigHandler (QObject *parent = nullptr); ~ConfigHandler() override = default; void setConfig(KScreen::ConfigPtr config); void updateInitialConfig(); OutputModel* outputModel() const { return m_outputs; } QSize normalizeScreen(); KScreen::ConfigPtr config() const { return m_config; } int retention() const; void setRetention(int retention); + KScreen::OutputPtr replicationSource(const KScreen::OutputPtr &output) const; + void setReplicationSource(KScreen::OutputPtr &output, const KScreen::OutputPtr &source); + void writeControl(); void checkNeedsSave(); Q_SIGNALS: void outputModelChanged(); void changed(); void screenNormalizationUpdate(bool normalized); void needsSaveChecked(bool need); void retentionChanged(); void outputConnect(bool connected); private: void checkScreenNormalization(); QSize screenSize() const; Control::OutputRetention getRetention() const; void primaryOutputSelected(int index); void primaryOutputChanged(const KScreen::OutputPtr &output); void initOutput(const KScreen::OutputPtr &output); KScreen::ConfigPtr m_config = nullptr; KScreen::ConfigPtr m_initialConfig; OutputModel *m_outputs = nullptr; std::unique_ptr m_control; Control::OutputRetention m_initialRetention = Control::OutputRetention:: Undefined; QSize m_lastNormalizedScreenSize; }; diff --git a/kcm/output_model.cpp b/kcm/output_model.cpp index ab09a1d..eafd9c5 100644 --- a/kcm/output_model.cpp +++ b/kcm/output_model.cpp @@ -1,901 +1,917 @@ /******************************************************************** 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 "output_model.h" #include "../common/utils.h" #include "config_handler.h" #include #include OutputModel::OutputModel(ConfigHandler *configHandler) : QAbstractListModel(configHandler) , m_config(configHandler) { connect(this, &OutputModel::dataChanged, this, &OutputModel::changed); } int OutputModel::rowCount(const QModelIndex &parent) const { Q_UNUSED(parent) return m_outputs.count(); } QVariant OutputModel::data(const QModelIndex &index, int role) const { if (index.row() < 0 || index.row() >= m_outputs.count()) { return QVariant(); } const KScreen::OutputPtr &output = m_outputs[index.row()].ptr; switch (role) { case Qt::DisplayRole: return Utils::outputName(output); case EnabledRole: return output->isEnabled(); case PrimaryRole: return output->isPrimary(); case SizeRole: return output->geometry().size(); case PositionRole: return m_outputs[index.row()].pos; case NormalizedPositionRole: return output->geometry().topLeft(); case RotationRole: return output->rotation(); case ScaleRole: return output->scale(); case ResolutionIndexRole: return resolutionIndex(output); case ResolutionsRole: return resolutionsStrings(output); case RefreshRateIndexRole: return refreshRateIndex(output); case ReplicationSourceModelRole: return replicationSourceModel(output); case ReplicationSourceIndexRole: - return replicationSourceIndex(index.row(), output->replicationSource()); + return replicationSourceIndex(index.row()); case ReplicasModelRole: return replicasModel(output); case RefreshRatesRole: QVariantList ret; for (const auto rate : refreshRates(output)) { ret << i18n("%1 Hz", int(rate + 0.5)); } return ret; } return QVariant(); } bool OutputModel::setData(const QModelIndex &index, const QVariant &value, int role) { if (index.row() < 0 || index.row() >= m_outputs.count()) { return false; } const Output &output = m_outputs[index.row()]; switch (role) { case PositionRole: if (value.canConvert()) { QPoint val = value.toPoint(); if (output.pos == val) { return false; } snap(output, val); m_outputs[index.row()].pos = val; updatePositions(); Q_EMIT positionChanged(); Q_EMIT dataChanged(index, index, {role}); return true; } break; case EnabledRole: if (value.canConvert()) { return setEnabled(index.row(), value.toBool()); } break; case PrimaryRole: if (value.canConvert()) { bool primary = value.toBool(); if (output.ptr->isPrimary() == primary) { return false; } m_config->config()->setPrimaryOutput(output.ptr); Q_EMIT dataChanged(index, index, {role}); return true; } break; case ResolutionIndexRole: if (value.canConvert()) { return setResolution(index.row(), value.toInt()); } break; case RefreshRateIndexRole: if (value.canConvert()) { return setRefreshRate(index.row(), value.toInt()); } break; case RotationRole: if (value.canConvert()) { return setRotation(index.row(), value.value()); } break; case ReplicationSourceIndexRole: if (value.canConvert()) { return setReplicationSourceIndex(index.row(), value.toInt() - 1); } break; case ScaleRole: bool ok; const qreal scale = value.toReal(&ok); if (ok && !qFuzzyCompare(output.ptr->scale(), scale)) { output.ptr->setScale(scale); Q_EMIT sizeChanged(); Q_EMIT dataChanged(index, index, {role, SizeRole}); return true; } break; } return false; } QHash OutputModel::roleNames() const { QHash roles = QAbstractItemModel::roleNames(); roles[EnabledRole] = "enabled"; roles[PrimaryRole] = "primary"; roles[SizeRole] = "size"; roles[PositionRole] = "position"; roles[NormalizedPositionRole] = "normalizedPosition"; roles[RotationRole] = "rotation"; roles[ScaleRole] = "scale"; roles[ResolutionIndexRole] = "resolutionIndex"; roles[ResolutionsRole] = "resolutions"; roles[RefreshRateIndexRole] = "refreshRateIndex"; roles[RefreshRatesRole] = "refreshRates"; roles[ReplicationSourceModelRole] = "replicationSourceModel"; roles[ReplicationSourceIndexRole] = "replicationSourceIndex"; roles[ReplicasModelRole] = "replicasModel"; return roles; } void OutputModel::add(const KScreen::OutputPtr &output) { const int insertPos = m_outputs.count(); Q_EMIT beginInsertRows(QModelIndex(), insertPos, insertPos); int i = 0; while (i < m_outputs.size()) { const QPoint pos = m_outputs[i].ptr->pos(); if (output->pos().x() < pos.x()) { break; } if (output->pos().x() == pos.x() && output->pos().y() < pos.y()) { break; } i++; } // Set the initial non-normalized position to be the normalized // position plus the current delta. QPoint pos = output->pos(); if (!m_outputs.isEmpty()) { const QPoint delta = m_outputs[0].pos - m_outputs[0].ptr->pos(); pos = output->pos() + delta; } m_outputs.insert(i, Output(output, pos)); connect(output.data(), &KScreen::Output::isPrimaryChanged, this, [this, output](){ roleChanged(output->id(), {PrimaryRole}); }); Q_EMIT endInsertRows(); // Update replications. for (int j = 0; j < m_outputs.size(); j++) { if (i == j) { continue; } QModelIndex index = createIndex(j, 0); // Calling this directly ignores possible optimization when the // refresh rate hasn't changed in fact. But that's ok. Q_EMIT dataChanged(index, index, {ReplicationSourceModelRole, ReplicationSourceIndexRole}); } } void OutputModel::remove(int outputId) { auto it = std::find_if(m_outputs.begin(), m_outputs.end(), [outputId](const Output &output) { return output.ptr->id() == outputId; }); if (it != m_outputs.end()) { const int index = it - m_outputs.begin(); Q_EMIT beginRemoveRows(QModelIndex(), index, index); m_outputs.erase(it); Q_EMIT endRemoveRows(); } } void OutputModel::resetPosition(const Output &output) { if (output.posReset.x() < 0) { // KCM was closed in between. for (const Output &out : m_outputs) { if (out.ptr->id() == output.ptr->id()) { continue; } if (out.ptr->geometry().right() > output.ptr->pos().x()) { output.ptr->setPos(out.ptr->geometry().topRight()); } } } else { output.ptr->setPos(/*output.ptr->pos() - */output.posReset); } } bool OutputModel::setEnabled(int outputIndex, bool enable) { Output &output = m_outputs[outputIndex]; if (output.ptr->isEnabled() == enable) { return false; } output.ptr->setEnabled(enable); if (enable) { resetPosition(output); setResolution(outputIndex, resolutionIndex(output.ptr)); reposition(); } else { output.posReset = output.ptr->pos(); } QModelIndex index = createIndex(outputIndex, 0); Q_EMIT dataChanged(index, index, {EnabledRole}); return true; } inline bool refreshRateCompare(float rate1, float rate2) { return qAbs(rate1 - rate2) < 0.5; } bool OutputModel::setResolution(int outputIndex, int resIndex) { const Output &output = m_outputs[outputIndex]; const auto resolutionList = resolutions(output.ptr); if (resIndex < 0 || resIndex >= resolutionList.size()) { return false; } const QSize size = resolutionList[resIndex]; const float oldRate = output.ptr->currentMode() ? output.ptr->currentMode()->refreshRate() : -1; const auto modes = output.ptr->modes(); auto modeIt = std::find_if(modes.begin(), modes.end(), [size, oldRate](const KScreen::ModePtr &mode) { // TODO: we don't want to compare against old refresh rate if // refresh rate selection is auto. return mode->size() == size && refreshRateCompare(mode->refreshRate(), oldRate); }); if (modeIt == modes.end()) { // New resolution does not support previous refresh rate. // Get the highest one instead. float bestRefreshRate = 0; auto it = modes.begin(); while (it != modes.end()) { if ((*it)->size() == size && (*it)->refreshRate() > bestRefreshRate) { modeIt = it; } it++; } } Q_ASSERT(modeIt != modes.end()); const auto id = (*modeIt)->id(); if (output.ptr->currentModeId() == id) { return false; } output.ptr->setCurrentModeId(id); QModelIndex index = createIndex(outputIndex, 0); // Calling this directly ignores possible optimization when the // refresh rate hasn't changed in fact. But that's ok. Q_EMIT dataChanged(index, index, {ResolutionIndexRole, SizeRole, RefreshRateIndexRole}); Q_EMIT sizeChanged(); return true; } bool OutputModel::setRefreshRate(int outputIndex, int refIndex) { const Output &output = m_outputs[outputIndex]; const auto rates = refreshRates(output.ptr); if (refIndex < 0 || refIndex >= rates.size()) { return false; } const float refreshRate = rates[refIndex]; const auto modes = output.ptr->modes(); const auto oldMode = output.ptr->currentMode(); auto modeIt = std::find_if(modes.begin(), modes.end(), [oldMode, refreshRate](const KScreen::ModePtr &mode) { // TODO: we don't want to compare against old refresh rate if // refresh rate selection is auto. return mode->size() == oldMode->size() && refreshRateCompare(mode->refreshRate(), refreshRate); }); Q_ASSERT(modeIt != modes.end()); if (refreshRateCompare(oldMode->refreshRate(), (*modeIt)->refreshRate())) { // no change return false; } output.ptr->setCurrentModeId((*modeIt)->id()); QModelIndex index = createIndex(outputIndex, 0); Q_EMIT dataChanged(index, index, {RefreshRateIndexRole}); return true; } bool OutputModel::setRotation(int outputIndex, KScreen::Output::Rotation rotation) { const Output &output = m_outputs[outputIndex]; if (rotation != KScreen::Output::None && rotation != KScreen::Output::Left && rotation != KScreen::Output::Inverted && rotation != KScreen::Output::Right) { return false; } if (output.ptr->rotation() == rotation) { return false; } output.ptr->setRotation(rotation); QModelIndex index = createIndex(outputIndex, 0); Q_EMIT dataChanged(index, index, {RotationRole, SizeRole}); Q_EMIT sizeChanged(); return true; } int OutputModel::resolutionIndex(const KScreen::OutputPtr &output) const { const QSize currentResolution = output->enforcedModeSize(); if (!currentResolution.isValid()) { return 0; } const auto sizes = resolutions(output); const auto it = std::find_if(sizes.begin(), sizes.end(), [currentResolution](const QSize &size) { return size == currentResolution; }); if (it == sizes.end()) { return -1; } return it - sizes.begin(); } int OutputModel::refreshRateIndex(const KScreen::OutputPtr &output) const { if (!output->currentMode()) { return 0; } const auto rates = refreshRates(output); const float currentRate = output->currentMode()->refreshRate(); const auto it = std::find_if(rates.begin(), rates.end(), [currentRate](float rate) { return refreshRateCompare(rate, currentRate); }); if (it == rates.end()) { return 0; } return it - rates.begin(); } QVariantList OutputModel::resolutionsStrings(const KScreen::OutputPtr &output) const { QVariantList ret; for (const QSize &size : resolutions(output)) { const QString text = QString::number(size.width()) + QStringLiteral("x") + QString::number(size.height()); ret << text; } return ret; } QVector OutputModel::resolutions(const KScreen::OutputPtr &output) const { QVector hits; for (const auto &mode : output->modes()) { const QSize size = mode->size(); if (!hits.contains(size)) { hits << size; } } std::sort(hits.begin(), hits.end(), [](const QSize &a, const QSize &b) { if (a.width() > b.width()) { return true; } if (a.width() == b.width() && a.height() > b.height()) { return true; } return false; }); return hits; } QVector OutputModel::refreshRates(const KScreen::OutputPtr &output) const { QVector hits; QSize baseSize; if (output->currentMode()) { baseSize = output->currentMode()->size(); } else if (output->preferredMode()) { baseSize = output->preferredMode()->size(); } if (!baseSize.isValid()) { return hits; } for (const auto &mode : output->modes()) { if (mode->size() != baseSize) { continue; } const float rate = mode->refreshRate(); if (std::find_if(hits.begin(), hits.end(), [rate](float r) { return refreshRateCompare(r, rate); }) != hits.end()) { continue; } hits << rate; } return hits; } +int OutputModel::replicationSourceId(const Output &output) const +{ + const KScreen::OutputPtr source = m_config->replicationSource(output.ptr); + if (!source) { + return 0; + } + return source->id(); +} + QStringList OutputModel::replicationSourceModel(const KScreen::OutputPtr &output) const { QStringList ret = { i18n("None") }; for (const auto &out : m_outputs) { if (out.ptr->id() != output->id()) { - if (out.ptr->replicationSource() == output->id()) { + const int outSourceId = replicationSourceId(out); + if (outSourceId == output->id()) { // 'output' is already source for replication, can't be replica itself return { i18n("Replicated by other output") }; } - if (out.ptr->replicationSource()) { + if (outSourceId) { // This 'out' is a replica. Can't be a replication source. continue; } ret.append(Utils::outputName(out.ptr)); } } return ret; } bool OutputModel::setReplicationSourceIndex(int outputIndex, int sourceIndex) { if (outputIndex <= sourceIndex) { sourceIndex++; } if (sourceIndex >= m_outputs.count()) { return false; } Output &output = m_outputs[outputIndex]; - const int oldSourceId = output.ptr->replicationSource(); + const int oldSourceId = replicationSourceId(output); if (sourceIndex < 0) { if (oldSourceId == 0) { // no change return false; } - output.ptr->setReplicationSource(0); + m_config->setReplicationSource(output.ptr, nullptr); + output.ptr->setLogicalSize(QSizeF()); resetPosition(output); } else { - const int sourceId = m_outputs[sourceIndex].ptr->id(); - if (oldSourceId == sourceId) { + const auto source = m_outputs[sourceIndex].ptr; + if (oldSourceId == source->id()) { // no change return false; } - output.ptr->setReplicationSource(sourceId); + m_config->setReplicationSource(output.ptr, source); output.posReset = output.ptr->pos(); - output.ptr->setPos(m_outputs[sourceIndex].ptr->pos()); + output.ptr->setPos(source->pos()); + output.ptr->setLogicalSize(source->logicalSize()); } reposition(); QModelIndex index = createIndex(outputIndex, 0); Q_EMIT dataChanged(index, index, {ReplicationSourceIndexRole}); if (oldSourceId != 0) { auto it = std::find_if(m_outputs.begin(), m_outputs.end(), [oldSourceId](const Output &out) { return out.ptr->id() == oldSourceId; }); if (it != m_outputs.end()) { QModelIndex index = createIndex(it - m_outputs.begin(), 0); Q_EMIT dataChanged(index, index, {ReplicationSourceModelRole, ReplicasModelRole}); } } if (sourceIndex >= 0) { QModelIndex index = createIndex(sourceIndex, 0); Q_EMIT dataChanged(index, index, {ReplicationSourceModelRole, ReplicasModelRole}); } return true; } -int OutputModel::replicationSourceIndex(int outputIndex, int sourceId) const +int OutputModel::replicationSourceIndex(int outputIndex) const { + const int sourceId = replicationSourceId(m_outputs[outputIndex]); + if (!sourceId) { + return 0; + } for (int i = 0; i < m_outputs.size(); i++) { const Output &output = m_outputs[i]; if (output.ptr->id() == sourceId) { return i + (outputIndex > i ? 1 : 0); } } return 0; } QVariantList OutputModel::replicasModel(const KScreen::OutputPtr &output) const { QVariantList ret; for (int i = 0; i < m_outputs.size(); i++) { const Output &out = m_outputs[i]; if (out.ptr->id() != output->id()) { - if (out.ptr->replicationSource() == output->id()) { + if (replicationSourceId(out) == output->id()) { ret << i; } } } return ret; } void OutputModel::roleChanged(int outputId, OutputRoles role) { for (int i = 0; i < m_outputs.size(); i++) { Output &output = m_outputs[i]; if (output.ptr->id() == outputId) { QModelIndex index = createIndex(i, 0); Q_EMIT dataChanged(index, index, {role}); return; } } } bool OutputModel::positionable(const Output &output) const { return output.ptr->isPositionable(); } void OutputModel::reposition() { int x = 0; int y = 0; // Find first valid output. for (const auto &out : m_outputs) { if (positionable(out)) { x = out.ptr->pos().x(); y = out.ptr->pos().y(); break; } } for (int i = 0; i < m_outputs.size(); i++) { if (!positionable(m_outputs[i])) { continue; } const QPoint &cmp = m_outputs[i].ptr->pos(); if (cmp.x() < x) { x = cmp.x(); } if (cmp.y() < y) { y = cmp.y(); } } if (x == 0 && y == 0) { return; } for (int i = 0; i < m_outputs.size(); i++) { auto &out = m_outputs[i]; out.ptr->setPos(out.ptr->pos() - QPoint(x, y)); QModelIndex index = createIndex(i, 0); Q_EMIT dataChanged(index, index, {NormalizedPositionRole}); } m_config->normalizeScreen(); } QPoint OutputModel::originDelta() const { int x = 0; int y = 0; // Find first valid output. for (const auto &out : m_outputs) { if (positionable(out)) { x = out.pos.x(); y = out.pos.y(); break; } } for (int i = 1; i < m_outputs.size(); i++) { if (!positionable(m_outputs[i])) { continue; } const QPoint &cmp = m_outputs[i].pos; if (cmp.x() < x) { x = cmp.x(); } if (cmp.y() < y) { y = cmp.y(); } } return QPoint(x, y); } void OutputModel::updatePositions() { const QPoint delta = originDelta(); for (int i = 0; i < m_outputs.size(); i++) { const auto &out = m_outputs[i]; if (!positionable(out)) { continue; } const QPoint set = out.pos - delta; if (out.ptr->pos() != set) { out.ptr->setPos(set); QModelIndex index = createIndex(i, 0); Q_EMIT dataChanged(index, index, {NormalizedPositionRole}); } } updateOrder(); } void OutputModel::updateOrder() { auto order = m_outputs; std::sort(order.begin(), order.end(), [](const Output &a, const Output &b) { const int xDiff = b.ptr->pos().x() - a.ptr->pos().x(); const int yDiff = b.ptr->pos().y() - a.ptr->pos().y(); if (xDiff > 0) { return true; } if (xDiff == 0 && yDiff > 0) { return true; } return false; }); for (int i = 0; i < order.size(); i++) { for (int j = 0; j < m_outputs.size(); j++) { if (order[i].ptr->id() != m_outputs[j].ptr->id()) { continue; } if (i != j) { beginMoveRows(QModelIndex(), j, j, QModelIndex(), i); m_outputs.remove(j); m_outputs.insert(i, order[i]); endMoveRows(); } break; } } // TODO: Could this be optimized by only outputs updating where replica indices changed? for (int i = 0; i < m_outputs.size(); i++) { QModelIndex index = createIndex(i, 0); Q_EMIT dataChanged(index, index, { ReplicasModelRole }); } } bool OutputModel::normalizePositions() { bool changed = false; for (int i = 0; i < m_outputs.size(); i++) { auto &output = m_outputs[i]; if (output.pos == output.ptr->pos()) { continue; } if (!positionable(output)) { continue; } changed = true; auto index = createIndex(i, 0); output.pos = output.ptr->pos(); Q_EMIT dataChanged(index, index, {PositionRole}); } return changed; } bool OutputModel::positionsNormalized() const { // There might be slight deviations because of snapping. return originDelta().manhattanLength() < 5; } const int s_snapArea = 80; bool isVerticalClose(const QRect &rect1, const QRect &rect2) { if (rect2.top() - rect1.bottom() > s_snapArea ) { return false; } if (rect1.top() - rect2.bottom() > s_snapArea ) { return false; } return true; } bool snapToRight(const QRect &target, const QSize &size, QPoint &dest) { if (qAbs(target.right() - dest.x()) < s_snapArea) { // In snap zone for left to right snap. dest.setX(target.right() + 1); return true; } if (qAbs(target.right() - (dest.x() + size.width())) < s_snapArea) { // In snap zone for right to right snap. dest.setX(target.right() - size.width()); return true; } return false; } bool snapToLeft(const QRect &target, const QSize &size, QPoint &dest) { if (qAbs(target.left() - dest.x()) < s_snapArea) { // In snap zone for left to left snap. dest.setX(target.left()); return true; } if (qAbs(target.left() - (dest.x() + size.width())) < s_snapArea) { // In snap zone for right to left snap. dest.setX(target.left() - size.width()); return true; } return false; } bool snapToMiddle(const QRect &target, const QSize &size, QPoint &dest) { const int outputMid = dest.y() + size.height() / 2; const int targetMid = target.top() + target.height() / 2; if (qAbs(targetMid - outputMid) < s_snapArea) { // In snap zone for middle to middle snap. dest.setY(targetMid - size.height() / 2); return true; } return false; } bool snapToTop(const QRect &target, const QSize &size, QPoint &dest) { if (qAbs(target.top() - dest.y()) < s_snapArea) { // In snap zone for bottom to top snap. dest.setY(target.top()); return true; } if (qAbs(target.top() - (dest.y() + size.height())) < s_snapArea) { // In snap zone for top to top snap. dest.setY(target.top() - size.height()); return true; } return false; } bool snapToBottom(const QRect &target, const QSize &size, QPoint &dest) { if (qAbs(target.bottom() - dest.y()) < s_snapArea) { // In snap zone for top to bottom snap. dest.setY(target.bottom() + 1); return true; } if (qAbs(target.bottom() - (dest.y() + size.height())) < s_snapArea) { // In snap zone for bottom to bottom snap. dest.setY(target.bottom() - size.height() + 1); return true; } return false; } bool snapVertical(const QRect &target, const QSize &size, QPoint &dest) { if (snapToMiddle(target, size, dest)) { return true; } if (snapToBottom(target, size, dest)) { return true; } if (snapToTop(target, size, dest)) { return true; } return false; } void OutputModel::snap(const Output &output, QPoint &dest) { const QSize size = output.ptr->geometry().size(); for (const Output &out : m_outputs) { if (out.ptr->id() == output.ptr->id()) { // Can not snap to itself. continue; } if (!positionable(out)) { continue; } const QRect target(out.pos, out.ptr->geometry().size()); if (!isVerticalClose(target, QRect(dest, size))) { continue; } // try snap left to right first if (snapToRight(target, size, dest)) { snapVertical(target, size, dest); continue; } if (snapToLeft(target, size, dest)) { snapVertical(target, size, dest); continue; } if (snapVertical(target, size, dest)) { continue; } } } diff --git a/kcm/output_model.h b/kcm/output_model.h index da16f48..2c97be6 100644 --- a/kcm/output_model.h +++ b/kcm/output_model.h @@ -1,134 +1,135 @@ /******************************************************************** 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 . *********************************************************************/ #pragma once #include #include #include #include class ConfigHandler; class OutputModel : public QAbstractListModel { Q_OBJECT public: enum OutputRoles { EnabledRole = Qt::UserRole + 1, PrimaryRole, SizeRole, /** Position in the graphical view relative to some arbitrary but fixed origin. */ PositionRole, /** Position for backend relative to most northwest display corner. */ NormalizedPositionRole, RotationRole, ScaleRole, ResolutionIndexRole, ResolutionsRole, RefreshRateIndexRole, RefreshRatesRole, ReplicationSourceModelRole, ReplicationSourceIndexRole, ReplicasModelRole }; explicit OutputModel (ConfigHandler *configHandler); ~OutputModel() override = default; int rowCount(const QModelIndex &parent = QModelIndex()) const override; QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override; void add(const KScreen::OutputPtr &output); void remove(int outputId); /** * Resets the origin for calculation of positions to the most northwest display corner * while keeping the normalized positions untouched. * * @return true if some (unnormalized) output position changed on this call, otherwise false. */ bool normalizePositions(); bool positionsNormalized() const; Q_SIGNALS: void positionChanged(); void sizeChanged(); void changed(); protected: QHash roleNames() const override; private: struct Output { Output() {} Output(const Output &output) : ptr(output.ptr) , pos(output.pos) {} Output(KScreen::OutputPtr _ptr, const QPoint &_pos) : ptr(_ptr) , pos(_pos) {} KScreen::OutputPtr ptr; QPoint pos; QPoint posReset = QPoint(-1, -1); }; void roleChanged(int outputId, OutputRoles role); void resetPosition(const Output &output); void reposition(); void updatePositions(); void updateOrder(); QPoint originDelta() const; /** * @brief Snaps moved output to others * @param output the moved output * @param dest the desired destination to be adjusted by snapping */ void snap(const Output &output, QPoint &dest); bool setEnabled(int outputIndex, bool enable); bool setResolution(int outputIndex, int resIndex); bool setRefreshRate(int outputIndex, int refIndex); bool setRotation(int outputIndex, KScreen::Output::Rotation rotation); int resolutionIndex(const KScreen::OutputPtr &output) const; int refreshRateIndex(const KScreen::OutputPtr &output) const; QVariantList resolutionsStrings(const KScreen::OutputPtr &output) const; QVector resolutions(const KScreen::OutputPtr &output) const; QVector refreshRates(const KScreen::OutputPtr &output) const; bool positionable(const Output &output) const; QStringList replicationSourceModel(const KScreen::OutputPtr &output) const; bool setReplicationSourceIndex(int outputIndex, int sourceIndex); - int replicationSourceIndex(int outputIndex, int sourceId) const; + int replicationSourceIndex(int outputIndex) const; + int replicationSourceId(const Output &output) const; QVariantList replicasModel(const KScreen::OutputPtr &output) const; QVector m_outputs; ConfigHandler *m_config; }; diff --git a/kded/config.cpp b/kded/config.cpp index 7087f1c..e691c08 100644 --- a/kded/config.cpp +++ b/kded/config.cpp @@ -1,255 +1,250 @@ /******************************************************************** Copyright 2012 Alejandro Fiestas Olivares 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 "config.h" #include "output.h" #include "../common/globals.h" #include "../common/control.h" #include "kscreen_daemon_debug.h" #include "device.h" #include #include #include #include #include #include #include QString Config::s_fixedConfigFileName = QStringLiteral("fixed-config"); QString Config::s_configsDirName = QStringLiteral("" /*"configs/"*/); // TODO: KDE6 - move these files into the subfolder QString Config::configsDirPath() { return Globals::dirPath() % s_configsDirName; } Config::Config(KScreen::ConfigPtr config, QObject *parent) : QObject(parent) , m_data(config) , m_control(new ControlConfig(config, this)) { } QString Config::filePath() { if (!QDir().mkpath(configsDirPath())) { return QString(); } return configsDirPath() % id(); } QString Config::id() const { if (!m_data) { return QString(); } return m_data->connectedOutputsHash(); } void Config::activateControlWatching() { connect(m_control, &ControlConfig::changed, this, &Config::controlChanged); m_control->activateWatcher(); } bool Config::fileExists() const { return (QFile::exists(configsDirPath() % id()) || QFile::exists(configsDirPath() % s_fixedConfigFileName)); } std::unique_ptr Config::readFile() { if (Device::self()->isLaptop() && !Device::self()->isLidClosed()) { // We may look for a config that has been set when the lid was closed, Bug: 353029 const QString lidOpenedFilePath(filePath() % QStringLiteral("_lidOpened")); const QFile srcFile(lidOpenedFilePath); if (srcFile.exists()) { QFile::remove(filePath()); if (QFile::copy(lidOpenedFilePath, filePath())) { QFile::remove(lidOpenedFilePath); qCDebug(KSCREEN_KDED) << "Restored lid opened config to" << id(); } } } return readFile(id()); } std::unique_ptr Config::readOpenLidFile() { const QString openLidFile = id() % QStringLiteral("_lidOpened"); auto config = readFile(openLidFile); QFile::remove(configsDirPath() % openLidFile); return config; } std::unique_ptr Config::readFile(const QString &fileName) { if (!m_data) { return nullptr; } auto config = std::unique_ptr(new Config(m_data->clone())); config->setValidityFlags(m_validityFlags); QFile file; if (QFile::exists(configsDirPath() % s_fixedConfigFileName)) { file.setFileName(configsDirPath() % s_fixedConfigFileName); qCDebug(KSCREEN_KDED) << "found a fixed config, will use " << file.fileName(); } else { file.setFileName(configsDirPath() % fileName); } if (!file.open(QIODevice::ReadOnly)) { qCDebug(KSCREEN_KDED) << "failed to open file" << file.fileName(); return nullptr; } QJsonDocument parser; QVariantList outputs = parser.fromJson(file.readAll()).toVariant().toList(); Output::readInOutputs(config->data(), outputs); QSize screenSize; for (const auto &output : config->data()->outputs()) { if (!output->isPositionable()) { continue; } const QRect geom = output->geometry(); if (geom.x() + geom.width() > screenSize.width()) { screenSize.setWidth(geom.x() + geom.width()); } if (geom.y() + geom.height() > screenSize.height()) { screenSize.setHeight(geom.y() + geom.height()); } } config->data()->screen()->setCurrentSize(screenSize); if (!canBeApplied(config->data())) { return nullptr; } return config; } bool Config::canBeApplied() const { return canBeApplied(m_data); } bool Config::canBeApplied(KScreen::ConfigPtr config) const { #ifdef KDED_UNIT_TEST Q_UNUSED(config); return true; #else return KScreen::Config::canBeApplied(config, m_validityFlags); #endif } bool Config::writeFile() { return writeFile(filePath()); } bool Config::writeOpenLidFile() { return writeFile(filePath() % QStringLiteral("_lidOpened")); } bool Config::writeFile(const QString &filePath) { if (id().isEmpty()) { return false; } const KScreen::OutputList outputs = m_data->outputs(); const auto oldConfig = readFile(); KScreen::OutputList oldOutputs; if (oldConfig) { oldOutputs = oldConfig->data()->outputs(); } QVariantList outputList; for (const KScreen::OutputPtr &output : outputs) { QVariantMap info; const auto oldOutputIt = std::find_if(oldOutputs.constBegin(), oldOutputs.constEnd(), [output](const KScreen::OutputPtr &out) { return out->hashMd5() == output->hashMd5(); } ); const KScreen::OutputPtr oldOutput = oldOutputIt != oldOutputs.constEnd() ? *oldOutputIt : nullptr; if (!output->isConnected()) { continue; } Output::writeGlobalPart(output, info, oldOutput); info[QStringLiteral("primary")] = output->isPrimary(); info[QStringLiteral("enabled")] = output->isEnabled(); auto setOutputConfigInfo = [this, &info](const KScreen::OutputPtr &out) { if (!out) { return; } - QString replicationSourceHash; - if (int sourceId = out->replicationSource()) { - replicationSourceHash = m_data->output(sourceId)->hashMd5(); - } - info[QStringLiteral("replicate")] = replicationSourceHash; QVariantMap pos; pos[QStringLiteral("x")] = out->pos().x(); pos[QStringLiteral("y")] = out->pos().y(); info[QStringLiteral("pos")] = pos; }; setOutputConfigInfo(output->isEnabled() ? output : oldOutput); if (output->isEnabled() && m_control->getOutputRetention(output->hash(), output->name()) != Control::OutputRetention::Individual) { // try to update global output data Output::writeGlobal(output); } outputList.append(info); } QFile file(filePath); if (!file.open(QIODevice::WriteOnly)) { qCWarning(KSCREEN_KDED) << "Failed to open config file for writing! " << file.errorString(); return false; } file.write(QJsonDocument::fromVariant(outputList).toJson()); qCDebug(KSCREEN_KDED) << "Config saved on: " << file.fileName(); return true; } void Config::log() { if (!m_data) { return; } const auto outputs = m_data->outputs(); for (const auto o : outputs) { if (o->isConnected()) { qCDebug(KSCREEN_KDED) << o; } } } diff --git a/kded/output.cpp b/kded/output.cpp index fa37fb7..ce1104e 100644 --- a/kded/output.cpp +++ b/kded/output.cpp @@ -1,425 +1,427 @@ /******************************************************************** 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 "output.h" #include "config.h" #include "../common/globals.h" #include "kscreen_daemon_debug.h" #include "generator.h" #include #include #include #include #include #include #include #include QString Output::s_dirName = QStringLiteral("outputs/"); QString Output::dirPath() { return Globals::dirPath() % s_dirName; } QString Output::globalFileName(const QString &hash) { const auto dir = dirPath(); if (!QDir().mkpath(dir)) { return QString(); } return dir % hash; } void Output::readInGlobalPartFromInfo(KScreen::OutputPtr output, const QVariantMap &info) { output->setRotation(static_cast(info.value(QStringLiteral("rotation"), 1).toInt())); bool scaleOk; const qreal scale = info.value(QStringLiteral("scale"), 1.).toDouble(&scaleOk); if (scaleOk) { output->setScale(scale); } const QVariantMap modeInfo = info[QStringLiteral("mode")].toMap(); const QVariantMap modeSize = modeInfo[QStringLiteral("size")].toMap(); const QSize size = QSize(modeSize[QStringLiteral("width")].toInt(), modeSize[QStringLiteral("height")].toInt()); qCDebug(KSCREEN_KDED) << "Finding a mode for" << size << "@" << modeInfo[QStringLiteral("refresh")].toFloat(); KScreen::ModeList modes = output->modes(); KScreen::ModePtr matchingMode; for(const KScreen::ModePtr &mode : modes) { if (mode->size() != size) { continue; } if (!qFuzzyCompare(mode->refreshRate(), modeInfo[QStringLiteral("refresh")].toFloat())) { continue; } qCDebug(KSCREEN_KDED) << "\tFound: " << mode->id() << " " << mode->size() << "@" << mode->refreshRate(); matchingMode = mode; break; } if (!matchingMode) { qCWarning(KSCREEN_KDED) << "\tFailed to find a matching mode - this means that our config is corrupted" "or a different device with the same serial number has been connected (very unlikely)." "Falling back to preferred modes."; matchingMode = output->preferredMode(); } if (!matchingMode) { qCWarning(KSCREEN_KDED) << "\tFailed to get a preferred mode, falling back to biggest mode."; matchingMode = Generator::biggestMode(modes); } if (!matchingMode) { qCWarning(KSCREEN_KDED) << "\tFailed to get biggest mode. Which means there are no modes. Turning off the screen."; output->setEnabled(false); return; } output->setCurrentModeId(matchingMode->id()); } QVariantMap Output::getGlobalData(KScreen::OutputPtr output) { QFile file(globalFileName(output->hashMd5())); if (!file.open(QIODevice::ReadOnly)) { qCDebug(KSCREEN_KDED) << "Failed to open file" << file.fileName(); return QVariantMap(); } QJsonDocument parser; return parser.fromJson(file.readAll()).toVariant().toMap(); } bool Output::readInGlobal(KScreen::OutputPtr output) { const QVariantMap info = getGlobalData(output); if (info.empty()) { // if info is empty, the global file does not exists, or is in an unreadable state return false; } readInGlobalPartFromInfo(output, info); return true; } void Output::adjustPositions(KScreen::ConfigPtr config, const QVariantList &outputsInfo) { typedef QPair Out; KScreen::OutputList outputs = config->outputs(); QVector sortedOutputs; // for (const KScreen::OutputPtr output : outputs) { sortedOutputs.append(Out(output->id(), output->pos())); } // go from left to right, top to bottom std::sort(sortedOutputs.begin(), sortedOutputs.end(), [](const Out &o1, const Out &o2) { const int x1 = o1.second.x(); const int x2 = o2.second.x(); return x1 < x2 || (x1 == x2 && o1.second.y() < o2.second.y()); }); for (int cnt = 1; cnt < sortedOutputs.length(); cnt++) { auto getOutputInfoProperties = [outputsInfo](KScreen::OutputPtr output, QRect &geo) -> bool { if (!output) { return false; } const auto hash = output->hash(); auto it = std::find_if(outputsInfo.begin(), outputsInfo.end(), [hash](QVariant v) { const QVariantMap info = v.toMap(); return info[QStringLiteral("id")].toString() == hash; } ); if (it == outputsInfo.end()) { return false; } auto isPortrait = [](const QVariant &info) { bool ok; const int rot = info.toInt(&ok); if (!ok) { return false; } return rot & KScreen::Output::Rotation::Left || rot & KScreen::Output::Rotation::Right; }; const QVariantMap outputInfo = it->toMap(); const QVariantMap posInfo = outputInfo[QStringLiteral("pos")].toMap(); const QVariant scaleInfo = outputInfo[QStringLiteral("scale")]; const QVariantMap modeInfo = outputInfo[QStringLiteral("mode")].toMap(); const QVariantMap modeSize = modeInfo[QStringLiteral("size")].toMap(); const bool portrait = isPortrait(outputInfo[QStringLiteral("rotation")]); if (posInfo.isEmpty() || modeSize.isEmpty() || !scaleInfo.canConvert()) { return false; } const qreal scale = scaleInfo.toDouble(); if (scale <= 0) { return false; } const QPoint pos = QPoint(posInfo[QStringLiteral("x")].toInt(), posInfo[QStringLiteral("y")].toInt()); QSize size = QSize(modeSize[QStringLiteral("width")].toInt() / scale, modeSize[QStringLiteral("height")].toInt() / scale); if (portrait) { size.transpose(); } geo = QRect(pos, size); return true; }; // it's guaranteed that we find the following values in the QMap KScreen::OutputPtr prevPtr = outputs.find(sortedOutputs[cnt - 1].first).value(); KScreen::OutputPtr curPtr = outputs.find(sortedOutputs[cnt].first).value(); QRect prevInfoGeo, curInfoGeo; if (!getOutputInfoProperties(prevPtr, prevInfoGeo) || !getOutputInfoProperties(curPtr, curInfoGeo)) { // no info found, nothing can be adjusted for the next output continue; } const QRect prevGeo = prevPtr->geometry(); const QRect curGeo = curPtr->geometry(); // the old difference between previous and current output read from the config file const int xInfoDiff = curInfoGeo.x() - (prevInfoGeo.x() + prevInfoGeo.width()); // the proposed new difference const int prevRight = prevGeo.x() + prevGeo.width(); const int xCorrected = prevRight + prevGeo.width() * xInfoDiff / (double)prevInfoGeo.width(); const int xDiff = curGeo.x() - prevRight; // In the following calculate the y-correction. This is more involved since we // differentiate between overlapping and non-overlapping pairs and align either // top to top/bottom or bottom to top/bottom const bool yOverlap = prevInfoGeo.y() + prevInfoGeo.height() > curInfoGeo.y() && prevInfoGeo.y() < curInfoGeo.y() + curInfoGeo.height(); // these values determine which horizontal edge of previous output we align with const int topToTopDiffAbs = qAbs(prevInfoGeo.y() - curInfoGeo.y()); const int topToBottomDiffAbs = qAbs(prevInfoGeo.y() - curInfoGeo.y() - curInfoGeo.height()); const int bottomToBottomDiffAbs = qAbs(prevInfoGeo.y() + prevInfoGeo.height() - curInfoGeo.y() - curInfoGeo.height()); const int bottomToTopDiffAbs = qAbs(prevInfoGeo.y() + prevInfoGeo.height() - curInfoGeo.y()); const bool yTopAligned = topToTopDiffAbs < bottomToBottomDiffAbs && topToTopDiffAbs <= bottomToTopDiffAbs || topToBottomDiffAbs < bottomToBottomDiffAbs; int yInfoDiff = curInfoGeo.y() - prevInfoGeo.y(); int yDiff = curGeo.y() - prevGeo.y(); int yCorrected; if (yTopAligned) { // align to previous top if (!yOverlap) { // align previous top with current bottom yInfoDiff += curInfoGeo.height(); yDiff += curGeo.height(); } // When we align with previous top we are interested in the changes to the // current geometry and not in the ones of the previous one. const double yInfoRel = yInfoDiff / (double)curInfoGeo.height(); yCorrected = prevGeo.y() + yInfoRel * curGeo.height(); } else { // align previous bottom... yInfoDiff -= prevInfoGeo.height(); yDiff -= prevGeo.height(); yCorrected = prevGeo.y() + prevGeo.height(); if (yOverlap) { // ... with current bottom yInfoDiff += curInfoGeo.height(); yDiff += curGeo.height(); yCorrected -= curGeo.height(); } // ... else with current top // When we align with previous bottom we are interested in changes to the // previous geometry. const double yInfoRel = yInfoDiff / (double)prevInfoGeo.height(); yCorrected += yInfoRel * prevGeo.height(); } const int x = xDiff == xInfoDiff ? curGeo.x() : xCorrected; const int y = yDiff == yInfoDiff ? curGeo.y() : yCorrected; curPtr->setPos(QPoint(x, y)); } } void Output::readIn(KScreen::OutputPtr output, const QVariantMap &info, Control::OutputRetention retention) { const QVariantMap posInfo = info[QStringLiteral("pos")].toMap(); QPoint point(posInfo[QStringLiteral("x")].toInt(), posInfo[QStringLiteral("y")].toInt()); output->setPos(point); output->setPrimary(info[QStringLiteral("primary")].toBool()); output->setEnabled(info[QStringLiteral("enabled")].toBool()); if (retention != Control::OutputRetention::Individual && readInGlobal(output)) { // output data read from global output file return; } // output data read directly from info readInGlobalPartFromInfo(output, info); } void Output::readInOutputs(KScreen::ConfigPtr config, const QVariantList &outputsInfo) { KScreen::OutputList outputs = config->outputs(); ControlConfig control(config); // 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 duplicateIds; { QStringList allIds; allIds.reserve(outputs.count()); for (const KScreen::OutputPtr &output : outputs) { const auto outputId = output->hash(); if (allIds.contains(outputId) && !duplicateIds.contains(outputId)) { duplicateIds << outputId; } allIds << outputId; } allIds.clear(); } for (KScreen::OutputPtr output : outputs) { if (!output->isConnected()) { output->setEnabled(false); continue; } const auto outputId = output->hash(); bool infoFound = false; for (const auto &variantInfo : outputsInfo) { const QVariantMap info = variantInfo.toMap(); if (outputId != info[QStringLiteral("id")].toString()) { continue; } if (!output->name().isEmpty() && duplicateIds.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 outputName = metadata[QStringLiteral("name")].toString(); if (output->name() != outputName) { // was a duplicate id, but info not for this output continue; } } infoFound = true; readIn(output, info, control.getOutputRetention(output)); - - const QString replicationSourceHash = info[QStringLiteral("replicate")].toString(); - if (replicationSourceHash.isEmpty()) { - output->setReplicationSource(0); - } else { - for (const KScreen::OutputPtr out : outputs) { - if (out != output && out->hashMd5() == replicationSourceHash) { - output->setReplicationSource(out->id()); - break; - } - } - } break; } if (!infoFound) { // no info in info for this output, try reading in global output info at least or set some default values qCWarning(KSCREEN_KDED) << "\tFailed to find a matching output in the current info data - this means that our info is corrupted" "or a different device with the same serial number has been connected (very unlikely)."; if (!readInGlobal(output)) { // set some default values instead readInGlobalPartFromInfo(output, QVariantMap()); } } } + + for (KScreen::OutputPtr output : outputs) { + auto replicationSource = control.getReplicationSource(output); + if (replicationSource) { + output->setPos(replicationSource->pos()); + output->setLogicalSize(replicationSource->logicalSize()); + } else { + output->setLogicalSize(QSizeF()); + } + } + + // TODO: this does not work at the moment with logical size replication. Deactivate for now. // correct positional config regressions on global output data changes +#if 0 adjustPositions(config, outputsInfo); +#endif } static QVariantMap metadata(const KScreen::OutputPtr &output) { QVariantMap metadata; metadata[QStringLiteral("name")] = output->name(); if (!output->edid() || !output->edid()->isValid()) { return metadata; } metadata[QStringLiteral("fullname")] = output->edid()->deviceId(); return metadata; } bool Output::writeGlobalPart(const KScreen::OutputPtr &output, QVariantMap &info, const KScreen::OutputPtr &fallback) { info[QStringLiteral("id")] = output->hash(); info[QStringLiteral("metadata")] = metadata(output); info[QStringLiteral("rotation")] = output->rotation(); // Round scale to four digits info[QStringLiteral("scale")] = int(output->scale() * 10000 + 0.5) / 10000.; QVariantMap modeInfo; float refreshRate = -1.; QSize modeSize; if (output->currentMode() && output->isEnabled()) { refreshRate = output->currentMode()->refreshRate(); modeSize = output->currentMode()->size(); } else if (fallback && fallback->currentMode()) { refreshRate = fallback->currentMode()->refreshRate(); modeSize = fallback->currentMode()->size(); } if (refreshRate < 0 || !modeSize.isValid()) { return false; } modeInfo[QStringLiteral("refresh")] = refreshRate; QVariantMap modeSizeMap; modeSizeMap[QStringLiteral("width")] = modeSize.width(); modeSizeMap[QStringLiteral("height")] = modeSize.height(); modeInfo[QStringLiteral("size")] = modeSizeMap; info[QStringLiteral("mode")] = modeInfo; return true; } void Output::writeGlobal(const KScreen::OutputPtr &output) { // get old values and subsequently override QVariantMap info = getGlobalData(output); if (!writeGlobalPart(output, info, nullptr)) { return; } QFile file(globalFileName(output->hashMd5())); if (!file.open(QIODevice::WriteOnly)) { qCWarning(KSCREEN_KDED) << "Failed to open global output file for writing! " << file.errorString(); return; } file.write(QJsonDocument::fromVariant(info).toJson()); return; }