diff --git a/kcm/config_handler.cpp b/kcm/config_handler.cpp index 27fb5af..3b40d40 100644 --- a/kcm/config_handler.cpp +++ b/kcm/config_handler.cpp @@ -1,264 +1,271 @@ /******************************************************************** Copyright © 2019 Roman Gilg This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . *********************************************************************/ #include "config_handler.h" #include "kcm_screen_debug.h" #include "output_model.h" #include #include #include #include using namespace KScreen; ConfigHandler::ConfigHandler(QObject *parent) : QObject(parent) { } void ConfigHandler::setConfig(KScreen::ConfigPtr config) { m_config = config; m_initialConfig = m_config->clone(); KScreen::ConfigMonitor::instance()->addConfig(m_config); m_control.reset(new ControlConfig(config)); m_outputs = new OutputModel(this); connect(m_outputs, &OutputModel::positionChanged, this, &ConfigHandler::checkScreenNormalization); connect(m_outputs, &OutputModel::sizeChanged, this, &ConfigHandler::checkScreenNormalization); - connect(m_outputs, &OutputModel::changed, - this, [this]() { - checkNeedsSave(); - Q_EMIT changed(); - }); for (const KScreen::OutputPtr &output : config->outputs()) { initOutput(output); } m_lastNormalizedScreenSize = screenSize(); m_initialRetention = getRetention(); Q_EMIT retentionChanged(); + connect(m_outputs, &OutputModel::changed, + this, [this]() { + checkNeedsSave(); + Q_EMIT changed(); + }); connect(m_config.data(), &KScreen::Config::outputAdded, this, [this]() { Q_EMIT outputConnect(true); }); connect(m_config.data(), &KScreen::Config::outputRemoved, this, [this]() { Q_EMIT outputConnect(false); }); connect(m_config.data(), &KScreen::Config::primaryOutputChanged, this, &ConfigHandler::primaryOutputChanged); Q_EMIT outputModelChanged(); } void ConfigHandler::initOutput(const KScreen::OutputPtr &output) { if (output->isConnected()) { m_outputs->add(output); } connect(output.data(), &KScreen::Output::isConnectedChanged, this, [this, output]() { Q_EMIT outputConnect(output->isConnected()); }); } void ConfigHandler::updateInitialConfig() { m_initialRetention = getRetention(); connect(new GetConfigOperation(), &GetConfigOperation::finished, this, [this](ConfigOperation *op) { if (op->hasError()) { return; } m_initialConfig = qobject_cast(op)->config(); checkNeedsSave(); }); } void ConfigHandler::checkNeedsSave() { if (m_config->supportedFeatures() & KScreen::Config::Feature::PrimaryDisplay) { - if (m_config->primaryOutput()->hashMd5() != - m_initialConfig->primaryOutput()->hashMd5() ) { + if (m_config->primaryOutput() && m_initialConfig->primaryOutput()) { + if (m_config->primaryOutput()->hashMd5() != + m_initialConfig->primaryOutput()->hashMd5() ) { + Q_EMIT needsSaveChecked(true); + return; + } + } else if ((bool)m_config->primaryOutput() != (bool)m_initialConfig->primaryOutput()) { Q_EMIT needsSaveChecked(true); return; } } + if (m_initialRetention != getRetention()) { Q_EMIT needsSaveChecked(true); return; } + for (const auto &output : m_config->connectedOutputs()) { const QString hash = output->hashMd5(); for (const auto &initialOutput : m_initialConfig->outputs()) { if (hash != initialOutput->hashMd5()) { continue; } bool needsSave = false; if (output->isEnabled() != initialOutput->isEnabled()) { needsSave = true; } if (output->isEnabled()) { needsSave |= output->currentModeId() != initialOutput->currentModeId() || output->pos() != initialOutput->pos() || output->scale() != initialOutput->scale() || output->rotation() != initialOutput->rotation() || output->replicationSource() != initialOutput->replicationSource(); } if (needsSave) { Q_EMIT needsSaveChecked(true); return; } break; } } Q_EMIT needsSaveChecked(false); } QSize ConfigHandler::screenSize() const { int width = 0, height = 0; QSize size; for (const auto &output : m_config->connectedOutputs()) { if (!output->isPositionable()) { continue; } const int outputRight = output->geometry().right(); const int outputBottom = output->geometry().bottom(); if (outputRight > width) { width = outputRight; } if (outputBottom > height) { height = outputBottom; } } if (width > 0 && height > 0) { size = QSize(width, height); } else { size = QSize(); } return size; } QSize ConfigHandler::normalizeScreen() { if (!m_config) { return QSize(); } bool changed = m_outputs->normalizePositions(); const auto currentScreenSize = screenSize(); changed |= m_lastNormalizedScreenSize != currentScreenSize; m_lastNormalizedScreenSize = currentScreenSize; Q_EMIT screenNormalizationUpdate(true); return currentScreenSize; } void ConfigHandler::checkScreenNormalization() { const bool normalized = !m_config || (m_lastNormalizedScreenSize == screenSize() && m_outputs->positionsNormalized()); Q_EMIT screenNormalizationUpdate(normalized); } void ConfigHandler::primaryOutputSelected(int index) { Q_UNUSED(index) // TODO } void ConfigHandler::primaryOutputChanged(const KScreen::OutputPtr &output) { Q_UNUSED(output) } Control::OutputRetention ConfigHandler::getRetention() const { using Retention = Control::OutputRetention; auto ret = Retention::Undefined; if (!m_control) { return ret; } const auto outputs = m_config->connectedOutputs(); if (outputs.isEmpty()) { return ret; } ret = m_control->getOutputRetention(outputs.first()); for (const auto &output : outputs) { const auto outputRet = m_control->getOutputRetention(output); if (ret != outputRet ) { // Control file with different retention values per output. return Retention::Undefined; } } if (ret == Retention::Undefined) { // If all outputs have undefined retention, // this should be displayed as global retention. return Retention::Global; } return ret; } int ConfigHandler::retention() const { return static_cast(getRetention()); } void ConfigHandler::setRetention(int retention) { using Retention = Control::OutputRetention; if (!m_control) { return; } if (retention != static_cast(Retention::Global) && retention != static_cast(Retention::Individual)) { // We only allow setting to global or individual retention. return; } if (retention == ConfigHandler::retention()) { return; } auto ret = static_cast(retention); for (const auto &output : m_config->connectedOutputs()) { m_control->setOutputRetention(output, ret); } checkNeedsSave(); Q_EMIT retentionChanged(); Q_EMIT changed(); } void ConfigHandler::writeControl() { if (!m_control) { return; } m_control->writeFile(); } diff --git a/kcm/output_model.cpp b/kcm/output_model.cpp index 785ea75..adc5810 100644 --- a/kcm/output_model.cpp +++ b/kcm/output_model.cpp @@ -1,871 +1,901 @@ /******************************************************************** Copyright © 2019 Roman Gilg This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . *********************************************************************/ #include "output_model.h" #include "../common/utils.h" #include "config_handler.h" #include #include OutputModel::OutputModel(ConfigHandler *configHandler) : QAbstractListModel(configHandler) , m_config(configHandler) { connect(this, &OutputModel::dataChanged, this, &OutputModel::changed); } int OutputModel::rowCount(const QModelIndex &parent) const { Q_UNUSED(parent) return m_outputs.count(); } QVariant OutputModel::data(const QModelIndex &index, int role) const { if (index.row() < 0 || index.row() >= m_outputs.count()) { return QVariant(); } const KScreen::OutputPtr &output = m_outputs[index.row()].ptr; switch (role) { case Qt::DisplayRole: return Utils::outputName(output); case EnabledRole: return output->isEnabled(); case PrimaryRole: return output->isPrimary(); case SizeRole: return output->geometry().size(); case PositionRole: return m_outputs[index.row()].pos; case NormalizedPositionRole: return output->geometry().topLeft(); case RotationRole: return output->rotation(); case ScaleRole: return output->scale(); case ResolutionIndexRole: return resolutionIndex(output); case ResolutionsRole: return resolutionsStrings(output); case RefreshRateIndexRole: return refreshRateIndex(output); case ReplicationSourceModelRole: return replicationSourceModel(output); case ReplicationSourceIndexRole: return replicationSourceIndex(index.row(), output->replicationSource()); case ReplicasModelRole: return replicasModel(output); case RefreshRatesRole: QVariantList ret; for (const auto rate : refreshRates(output)) { ret << i18n("%1 Hz", int(rate + 0.5)); } return ret; } return QVariant(); } bool OutputModel::setData(const QModelIndex &index, const QVariant &value, int role) { if (index.row() < 0 || index.row() >= m_outputs.count()) { return false; } const Output &output = m_outputs[index.row()]; switch (role) { case PositionRole: if (value.canConvert()) { QPoint val = value.toPoint(); if (output.pos == val) { return false; } snap(output, val); m_outputs[index.row()].pos = val; updatePositions(); Q_EMIT positionChanged(); Q_EMIT dataChanged(index, index, {role}); return true; } break; case EnabledRole: if (value.canConvert()) { - bool enable = value.toBool(); - if (output.ptr->isEnabled() == enable) { - return false; - } - output.ptr->setEnabled(enable); - reposition(); - Q_EMIT dataChanged(index, index, {role}); - return true; + return setEnabled(index.row(), value.toBool()); } break; case PrimaryRole: if (value.canConvert()) { bool primary = value.toBool(); if (output.ptr->isPrimary() == primary) { return false; } m_config->config()->setPrimaryOutput(output.ptr); Q_EMIT dataChanged(index, index, {role}); return true; } break; case ResolutionIndexRole: if (value.canConvert()) { return setResolution(index.row(), value.toInt()); } break; case RefreshRateIndexRole: if (value.canConvert()) { return setRefreshRate(index.row(), value.toInt()); } break; case RotationRole: if (value.canConvert()) { return setRotation(index.row(), value.value()); } break; case ReplicationSourceIndexRole: if (value.canConvert()) { return setReplicationSourceIndex(index.row(), value.toInt() - 1); } break; case ScaleRole: bool ok; const qreal scale = value.toReal(&ok); if (ok && !qFuzzyCompare(output.ptr->scale(), scale)) { output.ptr->setScale(scale); Q_EMIT sizeChanged(); Q_EMIT dataChanged(index, index, {role, SizeRole}); return true; } break; } return false; } QHash OutputModel::roleNames() const { QHash roles = QAbstractItemModel::roleNames(); roles[EnabledRole] = "enabled"; roles[PrimaryRole] = "primary"; roles[SizeRole] = "size"; roles[PositionRole] = "position"; roles[NormalizedPositionRole] = "normalizedPosition"; roles[RotationRole] = "rotation"; roles[ScaleRole] = "scale"; roles[ResolutionIndexRole] = "resolutionIndex"; roles[ResolutionsRole] = "resolutions"; roles[RefreshRateIndexRole] = "refreshRateIndex"; roles[RefreshRatesRole] = "refreshRates"; roles[ReplicationSourceModelRole] = "replicationSourceModel"; roles[ReplicationSourceIndexRole] = "replicationSourceIndex"; roles[ReplicasModelRole] = "replicasModel"; return roles; } void OutputModel::add(const KScreen::OutputPtr &output) { const int insertPos = m_outputs.count(); Q_EMIT beginInsertRows(QModelIndex(), insertPos, insertPos); int i = 0; while (i < m_outputs.size()) { const QPoint pos = m_outputs[i].ptr->pos(); if (output->pos().x() < pos.x()) { break; } if (output->pos().x() == pos.x() && output->pos().y() < pos.y()) { break; } i++; } // Set the initial non-normalized position to be the normalized // position plus the current delta. QPoint pos = output->pos(); if (!m_outputs.isEmpty()) { const QPoint delta = m_outputs[0].pos - m_outputs[0].ptr->pos(); pos = output->pos() + delta; } m_outputs.insert(i, Output(output, pos)); connect(output.data(), &KScreen::Output::isPrimaryChanged, this, [this, output](){ roleChanged(output->id(), {PrimaryRole}); }); Q_EMIT endInsertRows(); // Update replications. for (int j = 0; j < m_outputs.size(); j++) { if (i == j) { continue; } QModelIndex index = createIndex(j, 0); // Calling this directly ignores possible optimization when the // refresh rate hasn't changed in fact. But that's ok. Q_EMIT dataChanged(index, index, {ReplicationSourceModelRole, ReplicationSourceIndexRole}); } } void OutputModel::remove(int outputId) { auto it = std::find_if(m_outputs.begin(), m_outputs.end(), [outputId](const Output &output) { return output.ptr->id() == outputId; }); if (it != m_outputs.end()) { const int index = it - m_outputs.begin(); Q_EMIT beginRemoveRows(QModelIndex(), index, index); m_outputs.erase(it); Q_EMIT endRemoveRows(); } } +void OutputModel::resetPosition(const Output &output) +{ + if (output.posReset.x() < 0) { + // KCM was closed in between. + for (const Output &out : m_outputs) { + if (out.ptr->id() == output.ptr->id()) { + continue; + } + if (out.ptr->geometry().right() > output.ptr->pos().x()) { + output.ptr->setPos(out.ptr->geometry().topRight()); + } + } + } else { + output.ptr->setPos(/*output.ptr->pos() - */output.posReset); + } +} + +bool OutputModel::setEnabled(int outputIndex, bool enable) +{ + Output &output = m_outputs[outputIndex]; + + if (output.ptr->isEnabled() == enable) { + return false; + } + + output.ptr->setEnabled(enable); + + if (enable) { + resetPosition(output); + + setResolution(outputIndex, resolutionIndex(output.ptr)); + reposition(); + } else { + output.posReset = output.ptr->pos(); + } + + QModelIndex index = createIndex(outputIndex, 0); + Q_EMIT dataChanged(index, index, {EnabledRole}); + return true; +} + inline bool refreshRateCompare(float rate1, float rate2) { return qAbs(rate1 - rate2) < 0.5; } bool OutputModel::setResolution(int outputIndex, int resIndex) { const Output &output = m_outputs[outputIndex]; const auto resolutionList = resolutions(output.ptr); if (resIndex < 0 || resIndex >= resolutionList.size()) { return false; } const QSize size = resolutionList[resIndex]; - const float oldRefreshRate = output.ptr->currentMode()->refreshRate(); + 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, oldRefreshRate](const KScreen::ModePtr &mode) { + [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(), oldRefreshRate); + refreshRateCompare(mode->refreshRate(), oldRate); }); if (modeIt == modes.end()) { // New resolution does not support previous refresh rate. // Get the highest one instead. float bestRefreshRate = 0; auto it = modes.begin(); while (it != modes.end()) { if ((*it)->size() == size && (*it)->refreshRate() > bestRefreshRate) { modeIt = it; } it++; } } Q_ASSERT(modeIt != modes.end()); const auto id = (*modeIt)->id(); if (output.ptr->currentModeId() == id) { return false; } output.ptr->setCurrentModeId(id); QModelIndex index = createIndex(outputIndex, 0); // Calling this directly ignores possible optimization when the // refresh rate hasn't changed in fact. But that's ok. Q_EMIT dataChanged(index, index, {ResolutionIndexRole, SizeRole, RefreshRateIndexRole}); Q_EMIT sizeChanged(); return true; } bool OutputModel::setRefreshRate(int outputIndex, int refIndex) { const Output &output = m_outputs[outputIndex]; const auto rates = refreshRates(output.ptr); if (refIndex < 0 || refIndex >= rates.size()) { return false; } const float refreshRate = rates[refIndex]; const auto modes = output.ptr->modes(); const auto oldMode = output.ptr->currentMode(); auto modeIt = std::find_if(modes.begin(), modes.end(), [oldMode, refreshRate](const KScreen::ModePtr &mode) { // TODO: we don't want to compare against old refresh rate if // refresh rate selection is auto. return mode->size() == oldMode->size() && refreshRateCompare(mode->refreshRate(), refreshRate); }); Q_ASSERT(modeIt != modes.end()); if (refreshRateCompare(oldMode->refreshRate(), (*modeIt)->refreshRate())) { // no change return false; } output.ptr->setCurrentModeId((*modeIt)->id()); QModelIndex index = createIndex(outputIndex, 0); Q_EMIT dataChanged(index, index, {RefreshRateIndexRole}); return true; } bool OutputModel::setRotation(int outputIndex, KScreen::Output::Rotation rotation) { const Output &output = m_outputs[outputIndex]; if (rotation != KScreen::Output::None && rotation != KScreen::Output::Left && rotation != KScreen::Output::Inverted && rotation != KScreen::Output::Right) { return false; } if (output.ptr->rotation() == rotation) { return false; } output.ptr->setRotation(rotation); QModelIndex index = createIndex(outputIndex, 0); Q_EMIT dataChanged(index, index, {RotationRole, SizeRole}); Q_EMIT sizeChanged(); return true; } int OutputModel::resolutionIndex(const KScreen::OutputPtr &output) const { - if (!output->currentMode()) { + const QSize currentResolution = output->enforcedModeSize(); + + if (!currentResolution.isValid()) { return 0; } + const auto sizes = resolutions(output); - const QSize currentResolution = output->currentMode()->size(); 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; - if (!output->currentMode()) { + + QSize baseSize; + if (output->currentMode()) { + baseSize = output->currentMode()->size(); + } else if (output->preferredMode()) { + baseSize = output->preferredMode()->size(); + } + if (!baseSize.isValid()) { return hits; } - const auto baseSize = output->currentMode()->size(); 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; } QStringList OutputModel::replicationSourceModel(const KScreen::OutputPtr &output) const { QStringList ret = { i18n("None") }; for (const auto &out : m_outputs) { if (out.ptr->id() != output->id()) { if (out.ptr->replicationSource() == output->id()) { // 'output' is already source for replication, can't be replica itself return { i18n("Replicated by other output") }; } if (out.ptr->replicationSource()) { // This 'out' is a replica. Can't be a replication source. continue; } ret.append(out.ptr->name()); } } return ret; } bool OutputModel::setReplicationSourceIndex(int outputIndex, int sourceIndex) { if (outputIndex <= sourceIndex) { sourceIndex++; } if (sourceIndex >= m_outputs.count()) { return false; } Output &output = m_outputs[outputIndex]; const int oldSourceId = output.ptr->replicationSource(); if (sourceIndex < 0) { if (oldSourceId == 0) { // no change return false; } output.ptr->setReplicationSource(0); - - if (output.replicaReset.isNull()) { - // 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.replicaReset); - } + resetPosition(output); } else { const int sourceId = m_outputs[sourceIndex].ptr->id(); if (oldSourceId == sourceId) { // no change return false; } output.ptr->setReplicationSource(sourceId); - output.replicaReset = m_outputs[sourceIndex].ptr->pos() - output.ptr->pos(); + output.posReset = output.ptr->pos(); output.ptr->setPos(m_outputs[sourceIndex].ptr->pos()); } reposition(); QModelIndex index = createIndex(outputIndex, 0); Q_EMIT dataChanged(index, index, {ReplicationSourceIndexRole}); if (oldSourceId != 0) { auto it = std::find_if(m_outputs.begin(), m_outputs.end(), [oldSourceId](const Output &out) { return out.ptr->id() == oldSourceId; }); if (it != m_outputs.end()) { QModelIndex index = createIndex(it - m_outputs.begin(), 0); Q_EMIT dataChanged(index, index, {ReplicationSourceModelRole, ReplicasModelRole}); } } if (sourceIndex >= 0) { QModelIndex index = createIndex(sourceIndex, 0); Q_EMIT dataChanged(index, index, {ReplicationSourceModelRole, ReplicasModelRole}); } return true; } int OutputModel::replicationSourceIndex(int outputIndex, int sourceId) const { for (int i = 0; i < m_outputs.size(); i++) { const Output &output = m_outputs[i]; if (output.ptr->id() == sourceId) { return i + (outputIndex > i ? 1 : 0); } } return 0; } QVariantList OutputModel::replicasModel(const KScreen::OutputPtr &output) const { QVariantList ret; for (int i = 0; i < m_outputs.size(); i++) { const Output &out = m_outputs[i]; if (out.ptr->id() != output->id()) { if (out.ptr->replicationSource() == output->id()) { 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 3b814cf..da16f48 100644 --- a/kcm/output_model.h +++ b/kcm/output_model.h @@ -1,129 +1,134 @@ /******************************************************************** Copyright © 2019 Roman Gilg This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . *********************************************************************/ #pragma once #include #include #include #include class ConfigHandler; class OutputModel : public QAbstractListModel { Q_OBJECT public: enum OutputRoles { EnabledRole = Qt::UserRole + 1, PrimaryRole, SizeRole, /** Position in the graphical view relative to some arbitrary but fixed origin. */ PositionRole, /** Position for backend relative to most northwest display corner. */ NormalizedPositionRole, RotationRole, ScaleRole, ResolutionIndexRole, ResolutionsRole, RefreshRateIndexRole, RefreshRatesRole, ReplicationSourceModelRole, ReplicationSourceIndexRole, ReplicasModelRole }; explicit OutputModel (ConfigHandler *configHandler); ~OutputModel() override = default; int rowCount(const QModelIndex &parent = QModelIndex()) const override; QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override; void add(const KScreen::OutputPtr &output); void remove(int outputId); /** * Resets the origin for calculation of positions to the most northwest display corner * while keeping the normalized positions untouched. * * @return true if some (unnormalized) output position changed on this call, otherwise false. */ bool normalizePositions(); bool positionsNormalized() const; Q_SIGNALS: void positionChanged(); void sizeChanged(); void changed(); protected: QHash roleNames() const override; private: struct Output { Output() {} Output(const Output &output) : ptr(output.ptr) , pos(output.pos) {} Output(KScreen::OutputPtr _ptr, const QPoint &_pos) : ptr(_ptr) , pos(_pos) {} KScreen::OutputPtr ptr; QPoint pos; - QPoint replicaReset; + QPoint posReset = QPoint(-1, -1); }; void roleChanged(int outputId, OutputRoles role); + + void resetPosition(const Output &output); void reposition(); void updatePositions(); void updateOrder(); QPoint originDelta() const; /** * @brief Snaps moved output to others * @param output the moved output * @param dest the desired destination to be adjusted by snapping */ void snap(const Output &output, QPoint &dest); + bool setEnabled(int outputIndex, bool enable); + bool setResolution(int outputIndex, int resIndex); bool setRefreshRate(int outputIndex, int refIndex); bool setRotation(int outputIndex, KScreen::Output::Rotation rotation); + int resolutionIndex(const KScreen::OutputPtr &output) const; int refreshRateIndex(const KScreen::OutputPtr &output) const; QVariantList resolutionsStrings(const KScreen::OutputPtr &output) const; QVector resolutions(const KScreen::OutputPtr &output) const; QVector refreshRates(const KScreen::OutputPtr &output) const; bool positionable(const Output &output) const; QStringList replicationSourceModel(const KScreen::OutputPtr &output) const; bool setReplicationSourceIndex(int outputIndex, int sourceIndex); int replicationSourceIndex(int outputIndex, int sourceId) const; QVariantList replicasModel(const KScreen::OutputPtr &output) const; QVector m_outputs; ConfigHandler *m_config; }; diff --git a/kded/config.cpp b/kded/config.cpp index b0f9be8..d0779b1 100644 --- a/kded/config.cpp +++ b/kded/config.cpp @@ -1,229 +1,249 @@ /******************************************************************** Copyright 2012 Alejandro Fiestas Olivares Copyright 2019 Roman Gilg This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . *********************************************************************/ #include "config.h" #include "output.h" #include "../common/globals.h" #include "../common/control.h" #include "kscreen_daemon_debug.h" #include "device.h" #include #include #include #include #include #include #include QString Config::s_fixedConfigFileName = QStringLiteral("fixed-config"); QString Config::s_configsDirName = QStringLiteral("" /*"configs/"*/); // TODO: KDE6 - move these files into the subfolder QString Config::configsDirPath() { return Globals::dirPath() % s_configsDirName; } Config::Config(KScreen::ConfigPtr config) : m_data(config) { } 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(); } 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 openLidFilePath = filePath() % QStringLiteral("_lidOpened"); auto config = readFile(openLidFilePath); QFile::remove(openLidFilePath); 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->isConnected() || !output->isEnabled()) { + 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 control = ControlConfig(m_data); + const auto oldConfig = readFile(); + KScreen::OutputList oldOutputs; + if (oldConfig) { + oldOutputs = oldConfig->data()->outputs(); + } + QVariantList outputList; - Q_FOREACH(const KScreen::OutputPtr &output, outputs) { + 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; } - if (!Output::writeGlobalPart(output, info)) { - continue; - } + Output::writeGlobalPart(output, info, oldOutput); info[QStringLiteral("primary")] = output->isPrimary(); info[QStringLiteral("enabled")] = output->isEnabled(); - QString replicationSourceHash; - if (int sourceId = output->replicationSource()) { - replicationSourceHash = m_data->output(sourceId)->hashMd5(); - } - info[QStringLiteral("replicate")] = replicationSourceHash; - - QVariantMap pos; - pos[QStringLiteral("x")] = output->pos().x(); - pos[QStringLiteral("y")] = output->pos().y(); - info[QStringLiteral("pos")] = pos; - - if (control.getOutputRetention(output->hash(), output->name()) != Control::OutputRetention::Individual) { + auto setOutputConfigInfo = [this, &info](const KScreen::OutputPtr &out) { + if (!out) { + return; + } + QString replicationSourceHash; + if (int sourceId = out->replicationSource()) { + replicationSourceHash = m_data->output(sourceId)->hashMd5(); + } + info[QStringLiteral("replicate")] = replicationSourceHash; + + QVariantMap pos; + pos[QStringLiteral("x")] = out->pos().x(); + pos[QStringLiteral("y")] = out->pos().y(); + info[QStringLiteral("pos")] = pos; + }; + setOutputConfigInfo(output->isEnabled() ? output : oldOutput); + + if (output->isEnabled() && + control.getOutputRetention(output->hash(), output->name()) != + Control::OutputRetention::Individual) { // try to update global output data Output::writeGlobal(output); } outputList.append(info); } QFile file(filePath); if (!file.open(QIODevice::WriteOnly)) { qCWarning(KSCREEN_KDED) << "Failed to open config file for writing! " << file.errorString(); return false; } file.write(QJsonDocument::fromVariant(outputList).toJson()); qCDebug(KSCREEN_KDED) << "Config saved on: " << file.fileName(); return true; } void Config::log() { if (!m_data) { return; } const auto outputs = m_data->outputs(); for (const auto o : outputs) { if (o->isConnected()) { qCDebug(KSCREEN_KDED) << o; } } } diff --git a/kded/output.cpp b/kded/output.cpp index 9392e28..273a4b1 100644 --- a/kded/output.cpp +++ b/kded/output.cpp @@ -1,402 +1,408 @@ /******************************************************************** Copyright 2019 Roman Gilg This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . *********************************************************************/ #include "output.h" #include "config.h" #include "../common/globals.h" #include "kscreen_daemon_debug.h" #include "generator.h" #include #include #include #include #include #include #include #include QString Output::s_dirName = QStringLiteral("outputs/"); QString Output::dirPath() { return Globals::dirPath() % s_dirName; } QString Output::globalFileName(const QString &hash) { const auto dir = dirPath(); if (!QDir().mkpath(dir)) { return QString(); } return dir % hash; } void Output::readInGlobalPartFromInfo(KScreen::OutputPtr output, const QVariantMap &info) { output->setRotation(static_cast(info.value(QStringLiteral("rotation"), 1).toInt())); bool scaleOk; const qreal scale = info.value(QStringLiteral("scale"), 1.).toDouble(&scaleOk); if (scaleOk) { output->setScale(scale); } const QVariantMap modeInfo = info[QStringLiteral("mode")].toMap(); const QVariantMap modeSize = modeInfo[QStringLiteral("size")].toMap(); const QSize size = QSize(modeSize[QStringLiteral("width")].toInt(), modeSize[QStringLiteral("height")].toInt()); qCDebug(KSCREEN_KDED) << "Finding a mode for" << size << "@" << modeInfo[QStringLiteral("refresh")].toFloat(); KScreen::ModeList modes = output->modes(); KScreen::ModePtr matchingMode; for(const KScreen::ModePtr &mode : modes) { if (mode->size() != size) { continue; } if (!qFuzzyCompare(mode->refreshRate(), modeInfo[QStringLiteral("refresh")].toFloat())) { continue; } qCDebug(KSCREEN_KDED) << "\tFound: " << mode->id() << " " << mode->size() << "@" << mode->refreshRate(); matchingMode = mode; break; } if (!matchingMode) { qCWarning(KSCREEN_KDED) << "\tFailed to find a matching mode - this means that our config is corrupted" "or a different device with the same serial number has been connected (very unlikely)." "Falling back to preferred modes."; matchingMode = output->preferredMode(); } if (!matchingMode) { qCWarning(KSCREEN_KDED) << "\tFailed to get a preferred mode, falling back to biggest mode."; matchingMode = Generator::biggestMode(modes); } if (!matchingMode) { qCWarning(KSCREEN_KDED) << "\tFailed to get biggest mode. Which means there are no modes. Turning off the screen."; output->setEnabled(false); return; } output->setCurrentModeId(matchingMode->id()); } QVariantMap Output::getGlobalData(KScreen::OutputPtr output) { QFile file(globalFileName(output->hashMd5())); if (!file.open(QIODevice::ReadOnly)) { qCDebug(KSCREEN_KDED) << "Failed to open file" << file.fileName(); return QVariantMap(); } QJsonDocument parser; return parser.fromJson(file.readAll()).toVariant().toMap(); } bool Output::readInGlobal(KScreen::OutputPtr output) { const QVariantMap info = getGlobalData(output); if (info.empty()) { // if info is empty, the global file does not exists, or is in an unreadable state return false; } readInGlobalPartFromInfo(output, info); return true; } void Output::adjustPositions(KScreen::ConfigPtr config, const QVariantList &outputsInfo) { typedef QPair Out; KScreen::OutputList outputs = config->outputs(); QVector sortedOutputs; // for (const KScreen::OutputPtr output : outputs) { sortedOutputs.append(Out(output->id(), output->pos())); } // go from left to right, top to bottom std::sort(sortedOutputs.begin(), sortedOutputs.end(), [](const Out &o1, const Out &o2) { const int x1 = o1.second.x(); const int x2 = o2.second.x(); return x1 < x2 || (x1 == x2 && o1.second.y() < o2.second.y()); }); for (int cnt = 1; cnt < sortedOutputs.length(); cnt++) { auto getOutputInfoProperties = [outputsInfo](KScreen::OutputPtr output, QRect &geo) -> bool { if (!output) { return false; } const auto hash = output->hash(); auto it = std::find_if(outputsInfo.begin(), outputsInfo.end(), [hash](QVariant v) { const QVariantMap info = v.toMap(); return info[QStringLiteral("id")].toString() == hash; } ); if (it == outputsInfo.end()) { return false; } 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(); 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()); const QSize size = QSize(modeSize[QStringLiteral("width")].toInt() / scale, modeSize[QStringLiteral("height")].toInt() / scale); 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-coorection. This is more involved since we // differentiate between overlapping and non-overlapping pairs and align either // top to top/bottom or bottom to top/bottom const bool yOverlap = prevInfoGeo.y() + prevInfoGeo.height() > curInfoGeo.y() && prevInfoGeo.y() < curInfoGeo.y() + curInfoGeo.height(); // these values determine which horizontal edge of previous output we align with const int topToTopDiffAbs = qAbs(prevInfoGeo.y() - curInfoGeo.y()); const int topToBottomDiffAbs = qAbs(prevInfoGeo.y() - curInfoGeo.y() - curInfoGeo.height()); const int bottomToBottomDiffAbs = qAbs(prevInfoGeo.y() + prevInfoGeo.height() - curInfoGeo.y() - curInfoGeo.height()); const int bottomToTopDiffAbs = qAbs(prevInfoGeo.y() + prevInfoGeo.height() - curInfoGeo.y()); const bool yTopAligned = topToTopDiffAbs < bottomToBottomDiffAbs && topToTopDiffAbs <= bottomToTopDiffAbs || topToBottomDiffAbs < bottomToBottomDiffAbs; int yInfoDiff = curInfoGeo.y() - prevInfoGeo.y(); int yDiff = curGeo.y() - prevGeo.y(); int yCorrected; if (yTopAligned) { // align to previous top if (!yOverlap) { // align previous top with current bottom yInfoDiff += curInfoGeo.height(); yDiff += curGeo.height(); } // When we align with previous top we are interested in the changes to the // current geometry and not in the ones of the previous one. const double yInfoRel = yInfoDiff / (double)curInfoGeo.height(); yCorrected = prevGeo.y() + yInfoRel * curGeo.height(); } else { // align previous bottom... yInfoDiff -= prevInfoGeo.height(); yDiff -= prevGeo.height(); yCorrected = prevGeo.y() + prevGeo.height(); if (yOverlap) { // ... with current bottom yInfoDiff += curInfoGeo.height(); yDiff += curGeo.height(); yCorrected -= curGeo.height(); } // ... else with current top // When we align with previous bottom we are interested in changes to the // previous geometry. const double yInfoRel = yInfoDiff / (double)prevInfoGeo.height(); yCorrected += yInfoRel * prevGeo.height(); } const int x = xDiff == xInfoDiff ? curGeo.x() : xCorrected; const int y = yDiff == yInfoDiff ? curGeo.y() : yCorrected; curPtr->setPos(QPoint(x, y)); } } void Output::readIn(KScreen::OutputPtr output, const QVariantMap &info, Control::OutputRetention retention) { const QVariantMap posInfo = info[QStringLiteral("pos")].toMap(); QPoint point(posInfo[QStringLiteral("x")].toInt(), posInfo[QStringLiteral("y")].toInt()); output->setPos(point); output->setPrimary(info[QStringLiteral("primary")].toBool()); output->setEnabled(info[QStringLiteral("enabled")].toBool()); if (retention != Control::OutputRetention::Individual && readInGlobal(output)) { // output data read from global output file return; } // output data read directly from info readInGlobalPartFromInfo(output, info); } void Output::readInOutputs(KScreen::ConfigPtr config, const QVariantList &outputsInfo) { KScreen::OutputList outputs = config->outputs(); ControlConfig control(config); // As global outputs are indexed by a hash of their edid, which is not unique, // to be able to tell apart multiple identical outputs, these need special treatment QStringList duplicateIds; { QStringList allIds; allIds.reserve(outputs.count()); for (const KScreen::OutputPtr &output : outputs) { const auto outputId = output->hash(); if (allIds.contains(outputId) && !duplicateIds.contains(outputId)) { duplicateIds << outputId; } allIds << outputId; } allIds.clear(); } for (KScreen::OutputPtr output : outputs) { if (!output->isConnected()) { output->setEnabled(false); continue; } const auto outputId = output->hash(); bool infoFound = false; for (const auto &variantInfo : outputsInfo) { const QVariantMap info = variantInfo.toMap(); if (outputId != info[QStringLiteral("id")].toString()) { continue; } if (!output->name().isEmpty() && duplicateIds.contains(outputId)) { // We may have identical outputs connected, these will have the same id in the config // in order to find the right one, also check the output's name (usually the connector) const auto metadata = info[QStringLiteral("metadata")].toMap(); const auto outputName = metadata[QStringLiteral("name")].toString(); if (output->name() != outputName) { // was a duplicate id, but info not for this output continue; } } infoFound = true; readIn(output, info, control.getOutputRetention(output)); const QString replicationSourceHash = info[QStringLiteral("replicate")].toString(); if (replicationSourceHash.isEmpty()) { output->setReplicationSource(0); } else { for (const KScreen::OutputPtr out : outputs) { if (out != output && out->hashMd5() == replicationSourceHash) { output->setReplicationSource(out->id()); break; } } } break; } if (!infoFound) { // no info in info for this output, try reading in global output info atleast 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()); } } } // correct positional config regressions on global output data changes adjustPositions(config, outputsInfo); } 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) +bool Output::writeGlobalPart(const KScreen::OutputPtr &output, QVariantMap &info, + const KScreen::OutputPtr &fallback) { - if (!output->isEnabled()) { - return false; - } - const KScreen::ModePtr mode = output->currentMode(); - if (!mode) { - qWarning() << "CurrentMode is null" << output->name(); - return false; - } info[QStringLiteral("id")] = output->hash(); + info[QStringLiteral("metadata")] = metadata(output); info[QStringLiteral("rotation")] = output->rotation(); // Round scale to one digit. info[QStringLiteral("scale")] = int(output->scale() * 10 + 0.5) / 10.; - info[QStringLiteral("metadata")] = metadata(output); - QVariantMap modeInfo; - modeInfo[QStringLiteral("refresh")] = mode->refreshRate(); + 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 modeSize; - modeSize[QStringLiteral("width")] = mode->size().width(); - modeSize[QStringLiteral("height")] = mode->size().height(); - modeInfo[QStringLiteral("size")] = modeSize; + 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)) { + 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/kded/output.h b/kded/output.h index 1f124ee..2ab411b 100644 --- a/kded/output.h +++ b/kded/output.h @@ -1,53 +1,54 @@ /******************************************************************** Copyright 2019 Roman Gilg This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . *********************************************************************/ #ifndef KDED_OUTPUT_H #define KDED_OUTPUT_H #include "../common/control.h" #include #include class Output { public: static void readInOutputs(KScreen::ConfigPtr config, const QVariantList &outputsInfo); static void writeGlobal(const KScreen::OutputPtr &output); - static bool writeGlobalPart(const KScreen::OutputPtr &output, QVariantMap &info); + static bool writeGlobalPart(const KScreen::OutputPtr &output, QVariantMap &info, + const KScreen::OutputPtr &fallback); static QString dirPath(); private: static QString globalFileName(const QString &hash); static QVariantMap getGlobalData(KScreen::OutputPtr output); static void readIn(KScreen::OutputPtr output, const QVariantMap &info, Control::OutputRetention retention); static bool readInGlobal(KScreen::OutputPtr output); static void readInGlobalPartFromInfo(KScreen::OutputPtr output, const QVariantMap &info); /* * When a global output value (scale, rotation) is changed we might * need to reposition the outputs when another config is read. */ static void adjustPositions(KScreen::ConfigPtr config, const QVariantList &outputsInfo); static QString s_dirName; }; #endif