diff --git a/common/control.cpp b/common/control.cpp index d3f230e..b972937 100644 --- a/common/control.cpp +++ b/common/control.cpp @@ -1,644 +1,644 @@ /******************************************************************** 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) { + 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); } qreal ControlConfig::getScale(const KScreen::OutputPtr &output) const { return getScale(output->hashMd5(), output->name()); } qreal ControlConfig::getScale(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) { + for (const auto &variantInfo : outputsInfo) { const QVariantMap info = variantInfo.toMap(); if (!infoIsOutput(info, outputId, outputName)) { continue; } const auto val = info[QStringLiteral("scale")]; return val.canConvert() ? val.toReal() : -1; } } // Retention is global or info for output not in config control file. if (auto *outputControl = getOutputControl(outputId, outputName)) { return outputControl->getScale(); } // Info for output not found. return -1; } void ControlConfig::setScale(const KScreen::OutputPtr &output, qreal value) { setScale(output->hashMd5(), output->name(), value); } // TODO: combine methods (templated functions) void ControlConfig::setScale(const QString &outputId, const QString &outputName, qreal value) { QList::iterator it; QVariantList outputsInfo = getOutputs(); auto setOutputScale = [&outputId, &outputName, value, this]() { if (auto *control = getOutputControl(outputId, outputName)) { control->setScale(value); } }; for (it = outputsInfo.begin(); it != outputsInfo.end(); ++it) { QVariantMap outputInfo = (*it).toMap(); if (!infoIsOutput(outputInfo, outputId, outputName)) { continue; } outputInfo[QStringLiteral("scale")] = value; *it = outputInfo; setOutputs(outputsInfo); setOutputScale(); return; } // no entry yet, create one auto outputInfo = createOutputInfo(outputId, outputName); outputInfo[QStringLiteral("scale")] = value; outputsInfo << outputInfo; setOutputs(outputsInfo); setOutputScale(); } 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) { + 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. 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(); } bool ControlConfig::getAutoRotateOnlyInTabletMode(const KScreen::OutputPtr &output) const { return getAutoRotateOnlyInTabletMode(output->hashMd5(), output->name()); } bool ControlConfig::getAutoRotateOnlyInTabletMode(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) { + for (const auto &variantInfo : outputsInfo) { const QVariantMap info = variantInfo.toMap(); if (!infoIsOutput(info, outputId, outputName)) { continue; } const auto val = info[QStringLiteral("autorotate-tablet-only")]; 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->getAutoRotateOnlyInTabletMode(); } // Info for output not found. return true; } void ControlConfig::setAutoRotateOnlyInTabletMode(const KScreen::OutputPtr &output, bool value) { setAutoRotateOnlyInTabletMode(output->hashMd5(), output->name(), value); } // TODO: combine methods (templated functions) void ControlConfig::setAutoRotateOnlyInTabletMode(const QString &outputId, const QString &outputName, bool value) { QList::iterator it; QVariantList outputsInfo = getOutputs(); auto setOutputAutoRotateOnlyInTabletMode = [&outputId, &outputName, value, this]() { if (auto *control = getOutputControl(outputId, outputName)) { control->setAutoRotateOnlyInTabletMode(value); } }; for (it = outputsInfo.begin(); it != outputsInfo.end(); ++it) { QVariantMap outputInfo = (*it).toMap(); if (!infoIsOutput(outputInfo, outputId, outputName)) { continue; } outputInfo[QStringLiteral("autorotate-tablet-only")] = value; *it = outputInfo; setOutputs(outputsInfo); setOutputAutoRotateOnlyInTabletMode(); return; } // no entry yet, create one auto outputInfo = createOutputInfo(outputId, outputName); outputInfo[QStringLiteral("autorotate-tablet-only")] = value; outputsInfo << outputInfo; setOutputs(outputsInfo); setOutputAutoRotateOnlyInTabletMode(); } 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) { + 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()) { + for (const 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 ? 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()); } qreal ControlOutput::getScale() const { const auto val = constInfo()[QStringLiteral("scale")]; return val.canConvert() ? val.toReal() : -1; } void ControlOutput::setScale(qreal value) { auto &infoMap = info(); if (infoMap.isEmpty()) { infoMap = createOutputInfo(m_output->hashMd5(), m_output->name()); } infoMap[QStringLiteral("scale")] = value; } 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; } bool ControlOutput::getAutoRotateOnlyInTabletMode() const { const auto val = constInfo()[QStringLiteral("autorotate-tablet-only")]; return !val.canConvert() || val.toBool(); } void ControlOutput::setAutoRotateOnlyInTabletMode(bool value) { auto &infoMap = info(); if (infoMap.isEmpty()) { infoMap = createOutputInfo(m_output->hashMd5(), m_output->name()); } infoMap[QStringLiteral("autorotate-tablet-only")] = value; } diff --git a/common/orientation_sensor.h b/common/orientation_sensor.h index 2724855..6aace60 100644 --- a/common/orientation_sensor.h +++ b/common/orientation_sensor.h @@ -1,47 +1,47 @@ /******************************************************************** 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 -class OrientationSensor : public QObject +class OrientationSensor final : public QObject { Q_OBJECT public: explicit OrientationSensor(QObject *parent = nullptr); ~OrientationSensor() override final; QOrientationReading::Orientation value() const; bool available() const; bool enabled() const; void setEnabled(bool enable); Q_SIGNALS: void valueChanged(QOrientationReading::Orientation orientation); void availableChanged(bool available); void enabledChanged(bool enabled); private: void refresh(); void updateState(); QOrientationSensor *m_sensor; QOrientationReading::Orientation m_value = QOrientationReading::Undefined; bool m_enabled = false; }; diff --git a/kcm/kcm.cpp b/kcm/kcm.cpp index 0cce7a2..966f70b 100644 --- a/kcm/kcm.cpp +++ b/kcm/kcm.cpp @@ -1,448 +1,448 @@ /******************************************************************** 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 "../common/orientation_sensor.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); m_orientationSensor = new OrientationSensor(this); connect(m_orientationSensor, &OrientationSensor::availableChanged, this, &KCMKScreen::orientationSensorAvailableChanged); } void KCMKScreen::configReady(ConfigOperation *op) { qCDebug(KSCREEN_KCM) << "Reading in config now."; if (op->hasError()) { m_config.reset(); Q_EMIT backendError(); return; } KScreen::ConfigPtr config = qobject_cast(op)->config(); const bool autoRotationSupported = config->supportedFeatures() & (KScreen::Config::Feature::AutoRotation | KScreen::Config::Feature::TabletMode); m_orientationSensor->setEnabled(autoRotationSupported); m_config->setConfig(config); setBackendReady(true); Q_EMIT perOutputScalingChanged(); Q_EMIT primaryOutputSupportedChanged(); Q_EMIT outputReplicationSupportedChanged(); Q_EMIT tabletModeAvailableChanged(); Q_EMIT autoRotationSupportedChanged(); } 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->updateInitialData(); } ); } 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); } bool KCMKScreen::autoRotationSupported() const { if (!m_config || !m_config->config()) { return false; } return m_config->config()->supportedFeatures() & (KScreen::Config::Feature::AutoRotation | KScreen::Config::Feature::TabletMode); } bool KCMKScreen::orientationSensorAvailable() const { return m_orientationSensor->available(); } bool KCMKScreen::tabletModeAvailable() const { if (!m_config || !m_config->config()) { return false; } return m_config->config()->tabletModeAvailable(); } void KCMKScreen::setScreenNormalized(bool normalized) { if (m_screenNormalized == normalized) { return; } m_screenNormalized = normalized; Q_EMIT screenNormalizedChanged(); } void KCMKScreen::defaults() { qCDebug(KSCREEN_KCM) << "Applying defaults."; load(); } void KCMKScreen::load() { qCDebug(KSCREEN_KCM) << "About to read in config."; 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()) { + for (const auto &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; } m_config->setRetention(retention); } #include "kcm.moc" diff --git a/kcm/output_model.cpp b/kcm/output_model.cpp index d295257..e2d1141 100644 --- a/kcm/output_model.cpp +++ b/kcm/output_model.cpp @@ -1,965 +1,965 @@ /******************************************************************** 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 InternalRole: return output->type() == KScreen::Output::Type::Panel; 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 AutoRotateRole: return m_config->autoRotate(output); case AutoRotateOnlyInTabletModeRole: return m_config->autoRotateOnlyInTabletMode(output); 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()); 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; } 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 AutoRotateRole: if (value.canConvert()) { return setAutoRotate(index.row(), value.value()); } break; case AutoRotateOnlyInTabletModeRole: if (value.canConvert()) { return setAutoRotateOnlyInTabletMode(index.row(), value.value()); } 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); m_config->setScale(output.ptr, 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[InternalRole] = "internal"; roles[PrimaryRole] = "primary"; roles[SizeRole] = "size"; roles[PositionRole] = "position"; roles[NormalizedPositionRole] = "normalizedPosition"; roles[AutoRotateRole] = "autoRotate"; roles[AutoRotateOnlyInTabletModeRole] = "autoRotateOnlyInTabletMode"; 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}); + 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::setAutoRotate(int outputIndex, bool value) { Output &output = m_outputs[outputIndex]; if (m_config->autoRotate(output.ptr) == value) { return false; } m_config->setAutoRotate(output.ptr, value); QModelIndex index = createIndex(outputIndex, 0); Q_EMIT dataChanged(index, index, {AutoRotateRole}); return true; } bool OutputModel::setAutoRotateOnlyInTabletMode(int outputIndex, bool value) { Output &output = m_outputs[outputIndex]; if (m_config->autoRotateOnlyInTabletMode(output.ptr) == value) { return false; } m_config->setAutoRotateOnlyInTabletMode(output.ptr, value); QModelIndex index = createIndex(outputIndex, 0); Q_EMIT dataChanged(index, index, {AutoRotateOnlyInTabletModeRole}); 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()) { 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 (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 = replicationSourceId(output); if (sourceIndex < 0) { if (oldSourceId == 0) { // no change return false; } m_config->setReplicationSource(output.ptr, nullptr); output.ptr->setLogicalSize(QSizeF()); resetPosition(output); } else { const auto source = m_outputs[sourceIndex].ptr; if (oldSourceId == source->id()) { // no change return false; } m_config->setReplicationSource(output.ptr, source); output.posReset = output.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) 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 (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 5bb9e4a..eb63eb5 100644 --- a/kcm/output_model.h +++ b/kcm/output_model.h @@ -1,140 +1,149 @@ /******************************************************************** 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, InternalRole, 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, AutoRotateRole, AutoRotateOnlyInTabletModeRole, 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(Output &&) noexcept = default; Output(KScreen::OutputPtr _ptr, const QPoint &_pos) : ptr(_ptr) , pos(_pos) {} + Output &operator=(const Output &output) { + ptr = output.ptr; + pos = output.pos; + posReset = QPoint(-1, -1); + return *this; + } + Output &operator=(Output &&) noexcept = default; + 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); bool setAutoRotate(int outputIndex, bool value); bool setAutoRotateOnlyInTabletMode(int outputIndex, bool value); 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) 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 f60d529..2d4eac9 100644 --- a/kded/config.cpp +++ b/kded/config.cpp @@ -1,276 +1,276 @@ /******************************************************************** 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/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::autoRotationRequested() const { for (KScreen::OutputPtr &output : m_data->outputs()) { if (m_control->getAutoRotate(output)) { return true; } } return false; } void Config::setDeviceOrientation(QOrientationReading::Orientation orientation) { for (KScreen::OutputPtr &output : m_data->outputs()) { if (!m_control->getAutoRotate(output)) { continue; } auto finalOrientation = orientation; if (m_control->getAutoRotateOnlyInTabletMode(output) && !m_data->tabletModeEngaged()) { finalOrientation = QOrientationReading::Orientation::TopUp; } if (Output::updateOrientation(output, finalOrientation)) { // TODO: call Layouter to find fitting positions for other outputs again return; } } } 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) { + auto setOutputConfigInfo = [&info](const KScreen::OutputPtr &out) { if (!out) { return; } 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) { + for (const auto &o : outputs) { if (o->isConnected()) { qCDebug(KSCREEN_KDED) << o; } } } diff --git a/kded/generator.cpp b/kded/generator.cpp index f74d58d..69b2d3f 100644 --- a/kded/generator.cpp +++ b/kded/generator.cpp @@ -1,674 +1,674 @@ /************************************************************************************* * Copyright (C) 2012 by Alejandro Fiestas Olivares * * * * 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, write to the Free Software * * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA * *************************************************************************************/ #include "generator.h" #include "device.h" #include "kscreen_daemon_debug.h" #include #include #if defined(QT_NO_DEBUG) #define ASSERT_OUTPUTS(outputs) #else #define ASSERT_OUTPUTS(outputs) \ while(true) { \ Q_ASSERT(!outputs.isEmpty()); \ Q_FOREACH (const KScreen::OutputPtr &output, outputs) { \ Q_ASSERT(output); \ Q_ASSERT(output->isConnected()); \ } break; \ } #endif Generator* Generator::instance = nullptr; bool operator<(const QSize &s1, const QSize &s2) { return s1.width() * s1.height() < s2.width() * s2.height(); } Generator* Generator::self() { if (!Generator::instance) { Generator::instance = new Generator(); } return Generator::instance; } Generator::Generator() : QObject() , m_forceLaptop(false) , m_forceLidClosed(false) , m_forceNotLaptop(false) , m_forceDocked(false) { connect(Device::self(), &Device::ready, this, &Generator::ready); } void Generator::destroy() { delete Generator::instance; Generator::instance = nullptr; } Generator::~Generator() { } void Generator::setCurrentConfig(const KScreen::ConfigPtr ¤tConfig) { m_currentConfig = currentConfig; } KScreen::ConfigPtr Generator::idealConfig(const KScreen::ConfigPtr ¤tConfig) { Q_ASSERT(currentConfig); // KDebug::Block idealBlock("Ideal Config"); KScreen::ConfigPtr config = currentConfig->clone(); disableAllDisconnectedOutputs(config->outputs()); KScreen::OutputList connectedOutputs = config->connectedOutputs(); qCDebug(KSCREEN_KDED) << "Connected outputs: " << connectedOutputs.count(); if (connectedOutputs.isEmpty()) { return config; } //the scale will generally be independent no matter where the output is //scale will affect geometry, so do this first if (config->supportedFeatures().testFlag(KScreen::Config::Feature::PerOutputScaling)) { for(auto output: qAsConst(connectedOutputs)) { output->setScale(bestScaleForOutput(output)); } } if (connectedOutputs.count() == 1) { singleOutput(connectedOutputs); return config; } if (isLaptop()) { laptop(connectedOutputs); return fallbackIfNeeded(config); } qCDebug(KSCREEN_KDED) << "Extend to Right"; extendToRight(connectedOutputs); return fallbackIfNeeded(config); } KScreen::ConfigPtr Generator::fallbackIfNeeded(const KScreen::ConfigPtr &config) { qCDebug(KSCREEN_KDED) << "fallbackIfNeeded()"; KScreen::ConfigPtr newConfig; //If the ideal config can't be applied, try clonning if (!KScreen::Config::canBeApplied(config)) { if (isLaptop()) { newConfig = displaySwitch(Generator::Clone); // Try to clone at our best } else { newConfig = config; KScreen::OutputList connectedOutputs = config->connectedOutputs(); if (connectedOutputs.isEmpty()) { return config; } connectedOutputs.value(connectedOutputs.keys().first())->setPrimary(true); cloneScreens(connectedOutputs); } } else { newConfig = config; } //If after trying to clone at our best, we fail... return current if (!KScreen::Config::canBeApplied(newConfig)) { qCDebug(KSCREEN_KDED) << "Config cannot be applied"; newConfig = config; } return config; } KScreen::ConfigPtr Generator::displaySwitch(DisplaySwitchAction action) { // KDebug::Block switchBlock("Display Switch"); KScreen::ConfigPtr config = m_currentConfig; Q_ASSERT(config); KScreen::OutputList connectedOutputs = config->connectedOutputs(); //the scale will generally be independent no matter where the output is //scale will affect geometry, so do this first if (config->supportedFeatures().testFlag(KScreen::Config::Feature::PerOutputScaling)) { for(auto output: qAsConst(connectedOutputs)) { output->setScale(bestScaleForOutput(output)); } } // There's not much else we can do with only one output if (connectedOutputs.count() < 2) { singleOutput(connectedOutputs); return config; } // We cannot try all possible combinations with two and more outputs if (connectedOutputs.count() > 2) { extendToRight(connectedOutputs); return config; } KScreen::OutputPtr embedded, external; embedded = embeddedOutput(connectedOutputs); // If we don't have an embedded output (desktop with two external screens // for instance), then pretend one of them is embedded if (!embedded) { embedded = connectedOutputs.value(connectedOutputs.keys().first()); } // Just to be sure if (embedded->modes().isEmpty()) { return config; } if (action == Generator::Clone) { qCDebug(KSCREEN_KDED) << "Cloning"; embedded->setPrimary(true); cloneScreens(connectedOutputs); return config; } connectedOutputs.remove(embedded->id()); external = connectedOutputs.value(connectedOutputs.keys().first()); // Just to be sure if (external->modes().isEmpty()) { return config; } switch (action) { case Generator::ExtendToLeft: { qCDebug(KSCREEN_KDED) << "Extend to left"; external->setPos(QPoint(0,0)); external->setEnabled(true); const KScreen::ModePtr extMode = bestModeForOutput(external); Q_ASSERT(extMode); external->setCurrentModeId(extMode->id()); Q_ASSERT(external->currentMode()); // we must have a mode now const QSize size = external->geometry().size(); embedded->setPos(QPoint(size.width(), 0)); embedded->setEnabled(true); embedded->setPrimary(true); const KScreen::ModePtr embeddedMode = bestModeForOutput(embedded); Q_ASSERT(embeddedMode); embedded->setCurrentModeId(embeddedMode->id()); return config; } case Generator::TurnOffEmbedded: { qCDebug(KSCREEN_KDED) << "Turn off embedded (laptop)"; embedded->setEnabled(false); embedded->setPrimary(false); external->setEnabled(true); external->setPrimary(true); const KScreen::ModePtr extMode = bestModeForOutput(external); Q_ASSERT(extMode); external->setCurrentModeId(extMode->id()); return config; } case Generator::TurnOffExternal: { qCDebug(KSCREEN_KDED) << "Turn off external screen"; embedded->setPos(QPoint(0,0)); embedded->setEnabled(true); embedded->setPrimary(true); const KScreen::ModePtr embeddedMode = bestModeForOutput(embedded); Q_ASSERT(embeddedMode); embedded->setCurrentModeId(embeddedMode->id()); external->setEnabled(false); external->setPrimary(false); return config; } case Generator::ExtendToRight: { qCDebug(KSCREEN_KDED) << "Extend to the right"; embedded->setPos(QPoint(0,0)); embedded->setEnabled(true); embedded->setPrimary(true); const KScreen::ModePtr embeddedMode = bestModeForOutput(embedded); Q_ASSERT(embeddedMode); embedded->setCurrentModeId(embeddedMode->id()); Q_ASSERT(embedded->currentMode()); // we must have a mode now const QSize size = embedded->geometry().size(); external->setPos(QPoint(size.width(), 0)); external->setEnabled(true); external->setPrimary(false); const KScreen::ModePtr extMode = bestModeForOutput(external); Q_ASSERT(extMode); external->setCurrentModeId(extMode->id()); return config; } case Generator::None: // just return config case Generator::Clone: // handled above break; } // switch return config; } uint qHash(const QSize &size) { return size.width() * size.height(); } void Generator::cloneScreens(KScreen::OutputList &connectedOutputs) { ASSERT_OUTPUTS(connectedOutputs); if (connectedOutputs.isEmpty()) { return; } QSet commonSizes; const QSize maxScreenSize = m_currentConfig->screen()->maxSize(); Q_FOREACH(const KScreen::OutputPtr &output, connectedOutputs) { QSet modeSizes; Q_FOREACH(const KScreen::ModePtr &mode, output->modes()) { const QSize size = mode->size(); if (size.width() > maxScreenSize.width() || size.height() > maxScreenSize.height()) { continue; } modeSizes.insert(mode->size()); } //If we have nothing to compare against if (commonSizes.isEmpty()) { commonSizes = modeSizes; continue; } commonSizes.intersect(modeSizes); } qCDebug(KSCREEN_KDED) << "Common sizes: " << commonSizes; //fallback to biggestMode if no commonSizes have been found if (commonSizes.isEmpty()) { Q_FOREACH(KScreen::OutputPtr output, connectedOutputs) { if (output->modes().isEmpty()) { continue; } output->setEnabled(true); output->setPos(QPoint(0, 0)); const KScreen::ModePtr mode = biggestMode(output->modes()); Q_ASSERT(mode); output->setCurrentModeId(mode->id()); } return; } //At this point, we know we have common sizes, let's get the biggest on - QList commonSizeList = commonSizes.toList(); + QList commonSizeList = commonSizes.values(); std::sort(commonSizeList.begin(), commonSizeList.end()); const QSize biggestSize = commonSizeList.last(); //Finally, look for the mode with biggestSize and biggest refreshRate and set it qCDebug(KSCREEN_KDED) << "Biggest Size: " << biggestSize; KScreen::ModePtr bestMode; Q_FOREACH(KScreen::OutputPtr output, connectedOutputs) { if (output->modes().isEmpty()) { continue; } bestMode = bestModeForSize(output->modes(), biggestSize); Q_ASSERT(bestMode); // we resolved this mode previously, so it better works output->setEnabled(true); output->setPos(QPoint(0, 0)); output->setCurrentModeId(bestMode->id()); } } void Generator::singleOutput(KScreen::OutputList &connectedOutputs) { ASSERT_OUTPUTS(connectedOutputs); if (connectedOutputs.isEmpty()) { return; } KScreen::OutputPtr output = connectedOutputs.take(connectedOutputs.keys().first()); if (output->modes().isEmpty()) { return; } const KScreen::ModePtr bestMode = bestModeForOutput(output); Q_ASSERT(bestMode); output->setCurrentModeId(bestMode->id()); output->setEnabled(true); output->setPrimary(true); output->setPos(QPoint(0,0)); } void Generator::laptop(KScreen::OutputList &connectedOutputs) { ASSERT_OUTPUTS(connectedOutputs) if (connectedOutputs.isEmpty()) { return; } // KDebug::Block laptopBlock("Laptop config"); KScreen::OutputPtr embedded = embeddedOutput(connectedOutputs); /* Apparently older laptops use "VGA-*" as embedded output ID, so embeddedOutput() * will fail, because it looks only for modern "LVDS", "EDP", etc. If we * fail to detect which output is embedded, just use the one with the lowest * ID. It's a wild guess, but I think it's highly probable that it will work. * See bug #318907 for further reference. -- dvratil */ if (!embedded) { QList keys = connectedOutputs.keys(); std::sort(keys.begin(), keys.end()); embedded = connectedOutputs.value(keys.first()); } connectedOutputs.remove(embedded->id()); if (connectedOutputs.isEmpty() || embedded->modes().isEmpty()) { qCWarning(KSCREEN_KDED) << "No external outputs found, going for singleOutput()"; connectedOutputs.insert(embedded->id(), embedded); return singleOutput(connectedOutputs); } if (isLidClosed() && connectedOutputs.count() == 1) { qCDebug(KSCREEN_KDED) << "With lid closed"; embedded->setEnabled(false); embedded->setPrimary(false); KScreen::OutputPtr external = connectedOutputs.value(connectedOutputs.keys().first()); if (external->modes().isEmpty()) { return; } external->setEnabled(true); external->setPrimary(true); const KScreen::ModePtr bestMode = bestModeForOutput(external); Q_ASSERT(bestMode); external->setCurrentModeId(bestMode->id()); external->setPos(QPoint(0, 0)); return; } if (isLidClosed() && connectedOutputs.count() > 1) { qCDebug(KSCREEN_KDED) << "Lid is closed, and more than one output"; embedded->setEnabled(false); embedded->setPrimary(false); extendToRight(connectedOutputs); return; } qCDebug(KSCREEN_KDED) << "Lid is open"; //If lid is open, laptop screen should be primary embedded->setPos(QPoint(0,0)); embedded->setPrimary(true); embedded->setEnabled(true); const KScreen::ModePtr embeddedMode = bestModeForOutput(embedded); Q_ASSERT(embeddedMode); embedded->setCurrentModeId(embeddedMode->id()); int globalWidth = embedded->geometry().width(); KScreen::OutputPtr biggest = biggestOutput(connectedOutputs); Q_ASSERT(biggest); connectedOutputs.remove(biggest->id()); biggest->setPos(QPoint(globalWidth, 0)); biggest->setEnabled(true); biggest->setPrimary(false); const KScreen::ModePtr mode = bestModeForOutput(biggest); biggest->setCurrentModeId(mode->id()); globalWidth += biggest->geometry().width(); Q_FOREACH(KScreen::OutputPtr output, connectedOutputs) { output->setEnabled(true); output->setPrimary(false); output->setPos(QPoint(globalWidth, 0)); const KScreen::ModePtr mode = bestModeForOutput(output); Q_ASSERT(mode); output->setCurrentModeId(mode->id()); globalWidth += output->geometry().width(); } if (isDocked()) { qCDebug(KSCREEN_KDED) << "Docked"; embedded->setPrimary(false); biggest->setPrimary(true); } } void Generator::extendToRight(KScreen::OutputList &connectedOutputs) { ASSERT_OUTPUTS(connectedOutputs); if (connectedOutputs.isEmpty()) { return; } qCDebug(KSCREEN_KDED) << "Extending to the right"; KScreen::OutputPtr biggest = biggestOutput(connectedOutputs); Q_ASSERT(biggest); connectedOutputs.remove(biggest->id()); biggest->setEnabled(true); biggest->setPrimary(true); biggest->setPos(QPoint(0,0)); const KScreen::ModePtr mode = bestModeForOutput(biggest); Q_ASSERT(mode); biggest->setCurrentModeId(mode->id()); int globalWidth = biggest->geometry().width(); Q_FOREACH(KScreen::OutputPtr output, connectedOutputs) { output->setEnabled(true); output->setPrimary(false); output->setPos(QPoint(globalWidth, 0)); const KScreen::ModePtr mode = bestModeForOutput(output); Q_ASSERT(mode); output->setCurrentModeId(mode->id()); globalWidth += output->geometry().width(); } } KScreen::ModePtr Generator::biggestMode(const KScreen::ModeList &modes) { Q_ASSERT(!modes.isEmpty()); int modeArea, biggestArea = 0; KScreen::ModePtr biggestMode; Q_FOREACH(const KScreen::ModePtr &mode, modes) { modeArea = mode->size().width() * mode->size().height(); if (modeArea < biggestArea) { continue; } if (modeArea == biggestArea && mode->refreshRate() < biggestMode->refreshRate()) { continue; } if (modeArea == biggestArea && mode->refreshRate() > biggestMode->refreshRate()) { biggestMode = mode; continue; } biggestArea = modeArea; biggestMode = mode; } return biggestMode; } KScreen::ModePtr Generator::bestModeForSize(const KScreen::ModeList &modes, const QSize &size) { KScreen::ModePtr bestMode; Q_FOREACH(const KScreen::ModePtr &mode, modes) { if (mode->size() != size) { continue; } if (!bestMode) { bestMode = mode; continue; } if (mode->refreshRate() > bestMode->refreshRate()) { bestMode = mode; } } return bestMode; } qreal Generator::bestScaleForOutput(const KScreen::OutputPtr &output) { //if we have no physical size, we can't determine the DPI properly. Fallback to scale 1 if (output->sizeMm().height() <= 0) { return 1.0; } const auto mode = bestModeForOutput(output); const qreal dpi = mode->size().height() / (output->sizeMm().height() / 25.4); //if reported DPI is closer to two times normal DPI, followed by a sanity check of having the sort of vertical resolution //you'd find in a high res screen if (dpi > 96 * 1.5 && mode->size().height() >= 1440) { return 2.0; } return 1.0; } KScreen::ModePtr Generator::bestModeForOutput(const KScreen::OutputPtr &output) { if (KScreen::ModePtr outputMode = output->preferredMode()) { return outputMode; } return biggestMode(output->modes()); } KScreen::OutputPtr Generator::biggestOutput(const KScreen::OutputList &outputs) { ASSERT_OUTPUTS(outputs) int area, total = 0; KScreen::OutputPtr biggest; Q_FOREACH(const KScreen::OutputPtr &output, outputs) { const KScreen::ModePtr mode = bestModeForOutput(output); if (!mode) { continue; } area = mode->size().width() * mode->size().height(); if (area <= total) { continue; } total = area; biggest = output; } return biggest; } void Generator::disableAllDisconnectedOutputs(const KScreen::OutputList &outputs) { // KDebug::Block disableBlock("Disabling disconnected screens"); Q_FOREACH(KScreen::OutputPtr output, outputs) { if (!output->isConnected()) { qCDebug(KSCREEN_KDED) << output->name() << " Disabled"; output->setEnabled(false); output->setPrimary(false); } } } KScreen::OutputPtr Generator::embeddedOutput(const KScreen::OutputList &outputs) { Q_FOREACH(const KScreen::OutputPtr &output, outputs) { if (output->type() != KScreen::Output::Panel) { continue; } return output; } return KScreen::OutputPtr(); } bool Generator::isLaptop() const { if (m_forceLaptop) { return true; } if (m_forceNotLaptop) { return false; } return Device::self()->isLaptop(); } bool Generator::isLidClosed() const { if (m_forceLidClosed) { return true; } if (m_forceNotLaptop) { return false; } return Device::self()->isLidClosed(); } bool Generator::isDocked() const { if (m_forceDocked) { return true; } return Device::self()->isDocked(); } void Generator::setForceLaptop(bool force) { m_forceLaptop = force; } void Generator::setForceLidClosed(bool force) { m_forceLidClosed = force; } void Generator::setForceDocked(bool force) { m_forceDocked = force; } void Generator::setForceNotLaptop(bool force) { m_forceNotLaptop = force; } diff --git a/kded/output.cpp b/kded/output.cpp index 8763f02..230d818 100644 --- a/kded/output.cpp +++ b/kded/output.cpp @@ -1,465 +1,465 @@ /******************************************************************** 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 "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; } KScreen::Output::Rotation orientationToRotation(QOrientationReading::Orientation orientation, KScreen::Output::Rotation fallback) { using Orientation = QOrientationReading::Orientation; switch (orientation) { case Orientation::TopUp: return KScreen::Output::Rotation::None; case Orientation::TopDown: return KScreen::Output::Rotation::Inverted; case Orientation::LeftUp: return KScreen::Output::Rotation::Right; case Orientation::RightUp: return KScreen::Output::Rotation::Left; case Orientation::Undefined: case Orientation::FaceUp: case Orientation::FaceDown: return fallback; default: Q_UNREACHABLE(); } } bool Output::updateOrientation(KScreen::OutputPtr &output, QOrientationReading::Orientation orientation) { if (output->type() != KScreen::Output::Type::Panel) { return false; } const auto currentRotation = output->rotation(); const auto rotation = orientationToRotation(orientation, currentRotation); if (rotation == currentRotation) { return true; } output->setRotation(rotation); return true; } // TODO: move this into the Layouter class. 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) { + 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 || + 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)); 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; } diff --git a/tests/kded/configtest.cpp b/tests/kded/configtest.cpp index 149810f..34e979f 100644 --- a/tests/kded/configtest.cpp +++ b/tests/kded/configtest.cpp @@ -1,516 +1,516 @@ /******************************************************************** Copyright 2015 Daniel Vrátil Copyright 2018 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 "../../kded/config.h" #include "../../common/globals.h" #include #include #include #include #include #include #include #include class TestConfig : public QObject { Q_OBJECT private Q_SLOTS: void init(); void initTestCase(); void testSimpleConfig(); void testTwoScreenConfig(); void testRotatedScreenConfig(); void testDisabledScreenConfig(); void testConfig404(); void testCorruptConfig(); void testCorruptEmptyConfig(); void testCorruptUselessConfig(); void testNullConfig(); void testIdenticalOutputs(); void testMoveConfig(); void testFixedConfig(); private: std::unique_ptr createConfig(bool output1Connected, bool output2Conected); }; std::unique_ptr TestConfig::createConfig(bool output1Connected, bool output2Connected) { KScreen::ScreenPtr screen = KScreen::ScreenPtr::create(); screen->setCurrentSize(QSize(1920, 1080)); screen->setMaxSize(QSize(32768, 32768)); screen->setMinSize(QSize(8, 8)); QList sizes({ QSize(320, 240), QSize(640, 480), QSize(1024, 768), QSize(1280, 1024), QSize(1920, 1280) }); KScreen::ModeList modes; for (int i = 0; i < sizes.count(); ++i) { const QSize &size = sizes[i]; KScreen::ModePtr mode = KScreen::ModePtr::create(); mode->setId(QStringLiteral("MODE-%1").arg(i)); mode->setName(QStringLiteral("%1x%2").arg(size.width()).arg(size.height())); mode->setSize(size); mode->setRefreshRate(60.0); modes.insert(mode->id(), mode); } KScreen::OutputPtr output1 = KScreen::OutputPtr::create(); output1->setId(1); output1->setName(QStringLiteral("OUTPUT-1")); output1->setPos(QPoint(0, 0)); output1->setConnected(output1Connected); output1->setEnabled(output1Connected); if (output1Connected) { output1->setModes(modes); } KScreen::OutputPtr output2 = KScreen::OutputPtr::create(); output2->setId(2); output2->setName(QStringLiteral("OUTPUT-2")); output2->setPos(QPoint(0, 0)); output2->setConnected(output2Connected); if (output2Connected) { output2->setModes(modes); } KScreen::ConfigPtr config = KScreen::ConfigPtr::create(); config->setScreen(screen); config->addOutput(output1); config->addOutput(output2); auto configWrapper = std::unique_ptr(new Config(config)); return configWrapper; } void TestConfig::init() { Globals::setDirPath(QStringLiteral(TEST_DATA "serializerdata/")); } void TestConfig::initTestCase() { qputenv("KSCREEN_LOGGING", "false"); } void TestConfig::testSimpleConfig() { auto configWrapper = createConfig(true, false); - configWrapper = std::move(configWrapper->readFile(QStringLiteral("simpleConfig.json"))); + configWrapper = configWrapper->readFile(QStringLiteral("simpleConfig.json")); auto config = configWrapper->data(); QVERIFY(config); QCOMPARE(config->connectedOutputs().count(), 1); auto output = config->connectedOutputs().first(); QCOMPARE(output->name(), QLatin1String("OUTPUT-1")); QCOMPARE(output->currentModeId(), QLatin1String("MODE-4")); QCOMPARE(output->currentMode()->size(), QSize(1920, 1280)); QCOMPARE(output->isEnabled(), true); QCOMPARE(output->rotation(), KScreen::Output::None); QCOMPARE(output->pos(), QPoint(0, 0)); QCOMPARE(output->isPrimary(), true); auto screen = config->screen(); QCOMPARE(screen->currentSize(), QSize(1920, 1280)); } void TestConfig::testTwoScreenConfig() { auto configWrapper = createConfig(true, true); - configWrapper = std::move(configWrapper->readFile(QStringLiteral("twoScreenConfig.json"))); + configWrapper = configWrapper->readFile(QStringLiteral("twoScreenConfig.json")); auto config = configWrapper->data(); QVERIFY(config); QCOMPARE(config->connectedOutputs().count(), 2); auto output = config->connectedOutputs().first(); QCOMPARE(output->name(), QLatin1String("OUTPUT-1")); QCOMPARE(output->currentModeId(), QLatin1String("MODE-4")); QCOMPARE(output->currentMode()->size(), QSize(1920, 1280)); QCOMPARE(output->isEnabled(), true); QCOMPARE(output->rotation(), KScreen::Output::None); QCOMPARE(output->pos(), QPoint(0, 0)); QCOMPARE(output->isPrimary(), true); output = config->connectedOutputs().last(); QCOMPARE(output->name(), QLatin1String("OUTPUT-2")); QCOMPARE(output->currentModeId(), QLatin1String("MODE-3")); QCOMPARE(output->currentMode()->size(), QSize(1280, 1024)); QCOMPARE(output->isEnabled(), true); QCOMPARE(output->rotation(), KScreen::Output::None); QCOMPARE(output->pos(), QPoint(1920, 0)); QCOMPARE(output->isPrimary(), false); auto screen = config->screen(); QCOMPARE(screen->currentSize(), QSize(3200, 1280)); } void TestConfig::testRotatedScreenConfig() { auto configWrapper = createConfig(true, true); - configWrapper = std::move(configWrapper->readFile(QStringLiteral("rotatedScreenConfig.json"))); + configWrapper = configWrapper->readFile(QStringLiteral("rotatedScreenConfig.json")); auto config = configWrapper->data(); QVERIFY(config); QCOMPARE(config->connectedOutputs().count(), 2); auto output = config->connectedOutputs().first(); QCOMPARE(output->name(), QLatin1String("OUTPUT-1")); QCOMPARE(output->currentModeId(), QLatin1String("MODE-4")); QCOMPARE(output->currentMode()->size(), QSize(1920, 1280)); QCOMPARE(output->isEnabled(), true); QCOMPARE(output->rotation(), KScreen::Output::None); QCOMPARE(output->pos(), QPoint(0, 0)); QCOMPARE(output->isPrimary(), true); output = config->connectedOutputs().last(); QCOMPARE(output->name(), QLatin1String("OUTPUT-2")); QCOMPARE(output->currentModeId(), QLatin1String("MODE-3")); QCOMPARE(output->currentMode()->size(), QSize(1280, 1024)); QCOMPARE(output->isEnabled(), true); QCOMPARE(output->rotation(), KScreen::Output::Left); QCOMPARE(output->pos(), QPoint(1920, 0)); QCOMPARE(output->isPrimary(), false); auto screen = config->screen(); QCOMPARE(screen->currentSize(), QSize(2944, 1280)); } void TestConfig::testDisabledScreenConfig() { auto configWrapper = createConfig(true, true); - configWrapper = std::move(configWrapper->readFile(QStringLiteral("disabledScreenConfig.json"))); + configWrapper = configWrapper->readFile(QStringLiteral("disabledScreenConfig.json")); auto config = configWrapper->data(); QVERIFY(config); QCOMPARE(config->connectedOutputs().count(), 2); auto output = config->connectedOutputs().first(); QCOMPARE(output->name(), QLatin1String("OUTPUT-1")); QCOMPARE(output->currentModeId(), QLatin1String("MODE-4")); QCOMPARE(output->currentMode()->size(), QSize(1920, 1280)); QCOMPARE(output->isEnabled(), true); QCOMPARE(output->rotation(), KScreen::Output::None); QCOMPARE(output->pos(), QPoint(0, 0)); QCOMPARE(output->isPrimary(), true); output = config->connectedOutputs().last(); QCOMPARE(output->name(), QLatin1String("OUTPUT-2")); QCOMPARE(output->isEnabled(), false); auto screen = config->screen(); QCOMPARE(screen->currentSize(), QSize(1920, 1280)); } void TestConfig::testConfig404() { auto configWrapper = createConfig(true, true); - configWrapper = std::move(configWrapper->readFile(QStringLiteral("filenotfoundConfig.json"))); + configWrapper = configWrapper->readFile(QStringLiteral("filenotfoundConfig.json")); QVERIFY(!configWrapper); } void TestConfig::testCorruptConfig() { auto configWrapper = createConfig(true, true); - configWrapper = std::move(configWrapper->readFile(QStringLiteral("corruptConfig.json"))); + configWrapper = configWrapper->readFile(QStringLiteral("corruptConfig.json")); auto config = configWrapper->data(); QVERIFY(config); QCOMPARE(config->outputs().count(), 2); QVERIFY(config->isValid()); } void TestConfig::testCorruptEmptyConfig() { auto configWrapper = createConfig(true, true); - configWrapper = std::move(configWrapper->readFile(QStringLiteral("corruptEmptyConfig.json"))); + configWrapper = configWrapper->readFile(QStringLiteral("corruptEmptyConfig.json")); auto config = configWrapper->data(); QVERIFY(config); QCOMPARE(config->outputs().count(), 2); QVERIFY(config->isValid()); } void TestConfig::testCorruptUselessConfig() { auto configWrapper = createConfig(true, true); - configWrapper = std::move(configWrapper->readFile(QStringLiteral("corruptUselessConfig.json"))); + configWrapper = configWrapper->readFile(QStringLiteral("corruptUselessConfig.json")); auto config = configWrapper->data(); QVERIFY(config); QCOMPARE(config->outputs().count(), 2); QVERIFY(config->isValid()); } void TestConfig::testNullConfig() { Config nullConfig(nullptr); QVERIFY(!nullConfig.data()); // Null configs have empty configIds QVERIFY(nullConfig.id().isEmpty()); // Load config from a file not found results in a nullptr auto config = createConfig(true, true); QVERIFY(!config->readFile(QString())); // Wrong config file name should fail to save QVERIFY(!config->writeFile(QString())); } void TestConfig::testIdenticalOutputs() { // Test configuration of a video wall with 6 identical outputs connected // this is the autotest for https://bugs.kde.org/show_bug.cgi?id=325277 KScreen::ScreenPtr screen = KScreen::ScreenPtr::create(); screen->setCurrentSize(QSize(1920, 1080)); screen->setMaxSize(QSize(32768, 32768)); screen->setMinSize(QSize(8, 8)); QList sizes({ QSize(640, 480), QSize(1024, 768), QSize(1920, 1080), QSize(1280, 1024), QSize(1920, 1280) }); KScreen::ModeList modes; for (int i = 0; i < sizes.count(); ++i) { const QSize &size = sizes[i]; KScreen::ModePtr mode = KScreen::ModePtr::create(); mode->setId(QStringLiteral("MODE-%1").arg(i)); mode->setName(QStringLiteral("%1x%2").arg(size.width()).arg(size.height())); mode->setSize(size); mode->setRefreshRate(60.0); modes.insert(mode->id(), mode); } // This one is important, the output id in the config file is a hash of it QByteArray data = QByteArray::fromBase64("AP///////wAQrBbwTExLQQ4WAQOANCB46h7Frk80sSYOUFSlSwCBgKlA0QBxTwEBAQEBAQEBKDyAoHCwI0AwIDYABkQhAAAaAAAA/wBGNTI1TTI0NUFLTEwKAAAA/ABERUxMIFUyNDEwCiAgAAAA/QA4TB5REQAKICAgICAgAToCAynxUJAFBAMCBxYBHxITFCAVEQYjCQcHZwMMABAAOC2DAQAA4wUDAQI6gBhxOC1AWCxFAAZEIQAAHgEdgBhxHBYgWCwlAAZEIQAAngEdAHJR0B4gbihVAAZEIQAAHowK0Iog4C0QED6WAAZEIQAAGAAAAAAAAAAAAAAAAAAAPg=="); // When setting up the outputs, make sure they're not added in alphabetical order // or in the same order of the config file, as that makes the tests accidentally pass KScreen::OutputPtr output1 = KScreen::OutputPtr::create(); output1->setId(1); output1->setEdid(data); output1->setName(QStringLiteral("DisplayPort-0")); output1->setPos(QPoint(0, 0)); output1->setConnected(true); output1->setEnabled(false); output1->setModes(modes); KScreen::OutputPtr output2 = KScreen::OutputPtr::create(); output2->setId(2); output2->setEdid(data); output2->setName(QStringLiteral("DisplayPort-1")); output2->setPos(QPoint(0, 0)); output2->setConnected(true); output2->setEnabled(false); output2->setModes(modes); KScreen::OutputPtr output3 = KScreen::OutputPtr::create(); output3->setId(3); output3->setEdid(data); output3->setName(QStringLiteral("DisplayPort-2")); output3->setPos(QPoint(0, 0)); output3->setConnected(true); output3->setEnabled(false); output3->setModes(modes); KScreen::OutputPtr output6 = KScreen::OutputPtr::create(); output6->setId(6); output6->setEdid(data); output6->setName(QStringLiteral("DVI-0")); output6->setPos(QPoint(0, 0)); output6->setConnected(true); output6->setEnabled(false); output6->setModes(modes); KScreen::OutputPtr output4 = KScreen::OutputPtr::create(); output4->setId(4); output4->setEdid(data); output4->setName(QStringLiteral("DisplayPort-3")); output4->setPos(QPoint(0, 0)); output4->setConnected(true); output4->setEnabled(false); output4->setModes(modes); KScreen::OutputPtr output5 = KScreen::OutputPtr::create(); output5->setId(5); output5->setEdid(data); output5->setName(QStringLiteral("DVI-1")); output5->setPos(QPoint(0, 0)); output5->setConnected(true); output5->setEnabled(false); output5->setModes(modes); KScreen::ConfigPtr config = KScreen::ConfigPtr::create(); config->setScreen(screen); config->addOutput(output6); config->addOutput(output2); config->addOutput(output5); config->addOutput(output4); config->addOutput(output3); config->addOutput(output1); Config configWrapper(config); QHash positions; positions[QStringLiteral("DisplayPort-0")] = QPoint(0, 1080); positions[QStringLiteral("DisplayPort-1")] = QPoint(2100, 30); positions[QStringLiteral("DisplayPort-2")] = QPoint(2100, 1080); positions[QStringLiteral("DisplayPort-3")] = QPoint(4020, 0); positions[QStringLiteral("DVI-0")] = QPoint(4020, 1080); positions[QStringLiteral("DVI-1")] = QPoint(0, 0); - auto configWrapper2 = std::move(configWrapper.readFile(QStringLiteral("outputgrid_2x3.json"))); + auto configWrapper2 = configWrapper.readFile(QStringLiteral("outputgrid_2x3.json")); KScreen::ConfigPtr config2 = configWrapper2->data(); QVERIFY(config2); QVERIFY(config != config2); QCOMPARE(config2->connectedOutputs().count(), 6); Q_FOREACH (auto output, config2->connectedOutputs()) { QVERIFY(positions.keys().contains(output->name())); QVERIFY(output->name() != output->hash()); QCOMPARE(positions[output->name()], output->pos()); QCOMPARE(output->currentMode()->size(), QSize(1920, 1080)); QCOMPARE(output->currentMode()->refreshRate(), 60.0); QVERIFY(output->isEnabled()); } QCOMPARE(config2->screen()->currentSize(), QSize(5940, 2160)); } void TestConfig::testMoveConfig() { // Test if restoring a config using Serializer::moveConfig(src, dest) works // https://bugs.kde.org/show_bug.cgi?id=353029 // Load a dualhead config auto configWrapper = createConfig(true, true); - configWrapper = std::move(configWrapper->readFile(QStringLiteral("twoScreenConfig.json"))); + configWrapper = configWrapper->readFile(QStringLiteral("twoScreenConfig.json")); auto config = configWrapper->data(); QVERIFY(config); // Make sure we don't write into TEST_DATA QStandardPaths::setTestModeEnabled(true); Globals::setDirPath(QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) % QStringLiteral("/kscreen/")); // TODO: this needs setup of the control directory // Basic assumptions for the remainder of our tests, this is the situation where the lid is opened QCOMPARE(config->connectedOutputs().count(), 2); auto output = config->connectedOutputs().first(); QCOMPARE(output->name(), QLatin1String("OUTPUT-1")); QCOMPARE(output->isEnabled(), true); QCOMPARE(output->isPrimary(), true); auto output2 = config->connectedOutputs().last(); QCOMPARE(output2->name(), QLatin1String("OUTPUT-2")); QCOMPARE(output2->isEnabled(), true); QCOMPARE(output2->isPrimary(), false); // we fake the lid being closed, first save our current config to _lidOpened configWrapper->writeOpenLidFile(); // ... then switch off the panel, set primary to the other output output->setEnabled(false); output->setPrimary(false); output2->setPrimary(true); // save config as the current one, this is the config we don't want restored, and which we'll overwrite configWrapper->writeFile(); QCOMPARE(output->isEnabled(), false); QCOMPARE(output->isPrimary(), false); QCOMPARE(output2->isPrimary(), true); // Check if both files exist const QString closedPath = Config::configsDirPath() % configWrapper->id(); const QString openedPath = closedPath % QStringLiteral("_lidOpened"); QFile openCfg(openedPath); QFile closedCfg(closedPath); QVERIFY(openCfg.exists()); QVERIFY(closedCfg.exists()); // Switcheroolooloo... - configWrapper = std::move(configWrapper->readOpenLidFile()); + configWrapper = configWrapper->readOpenLidFile(); QVERIFY(configWrapper); // Check actual files, src should be gone, dest must exist QVERIFY(!openCfg.exists()); QVERIFY(closedCfg.exists()); // Make sure the laptop panel is enabled and primary again config = configWrapper->data(); output = config->connectedOutputs().first(); QCOMPARE(output->name(), QLatin1String("OUTPUT-1")); QCOMPARE(output->isEnabled(), true); QCOMPARE(output->isPrimary(), true); output2 = config->connectedOutputs().last(); QCOMPARE(output2->name(), QLatin1String("OUTPUT-2")); QCOMPARE(output2->isEnabled(), true); QCOMPARE(output2->isPrimary(), false); // Make sure we don't screw up when there's no _lidOpened config - configWrapper = std::move(configWrapper->readOpenLidFile()); + configWrapper = configWrapper->readOpenLidFile(); config = configWrapper->data(); output = config->connectedOutputs().first(); QCOMPARE(output->name(), QLatin1String("OUTPUT-1")); QCOMPARE(output->isEnabled(), true); QCOMPARE(output->isPrimary(), true); output2 = config->connectedOutputs().last(); QCOMPARE(output2->name(), QLatin1String("OUTPUT-2")); QCOMPARE(output2->isEnabled(), true); QCOMPARE(output2->isPrimary(), false); } void TestConfig::testFixedConfig() { // Load a dualhead config auto configWrapper = createConfig(true, true); - configWrapper = std::move(configWrapper->readFile(QStringLiteral("twoScreenConfig.json"))); + configWrapper = configWrapper->readFile(QStringLiteral("twoScreenConfig.json")); auto config = configWrapper->data(); QVERIFY(config); // Make sure we don't write into TEST_DATA QStandardPaths::setTestModeEnabled(true); Globals::setDirPath(QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) % QStringLiteral("/kscreen/")); // TODO: this needs setup of the control directory const QString fixedCfgPath = Config::configsDirPath() % Config::s_fixedConfigFileName; // save config as the current one, this is the config we don't want restored, and which we'll overwrite configWrapper->writeFile(fixedCfgPath); // Check if both files exist QFile fixedCfg(fixedCfgPath); QVERIFY(fixedCfg.exists()); } QTEST_MAIN(TestConfig) #include "configtest.moc"