File indexing completed on 2024-04-28 05:27:39

0001 /*
0002     SPDX-FileCopyrightText: 2019 Roman Gilg <subdiff@gmail.com>
0003 
0004     SPDX-License-Identifier: GPL-2.0-or-later
0005 */
0006 #include "output_model.h"
0007 
0008 #include <cstdint>
0009 
0010 #include <kscreen/edid.h>
0011 #include <kscreen/mode.h>
0012 
0013 #include "../common/utils.h"
0014 #include "config_handler.h"
0015 
0016 #include <KLocalizedString>
0017 
0018 #include <QRect>
0019 #include <numeric>
0020 
0021 OutputModel::OutputModel(ConfigHandler *configHandler)
0022     : QAbstractListModel(configHandler)
0023     , m_config(configHandler)
0024 {
0025     connect(m_config->config().data(), &KScreen::Config::prioritiesChanged, this, [this]() {
0026         if (rowCount() > 0) {
0027             Q_EMIT dataChanged(createIndex(0, 0), createIndex(rowCount() - 1, 0), {PriorityRole});
0028         }
0029     });
0030 }
0031 
0032 int OutputModel::rowCount(const QModelIndex &parent) const
0033 {
0034     Q_UNUSED(parent)
0035     return m_outputs.count();
0036 }
0037 
0038 QVariant OutputModel::data(const QModelIndex &index, int role) const
0039 {
0040     if (index.row() < 0 || index.row() >= m_outputs.count()) {
0041         return QVariant();
0042     }
0043 
0044     const KScreen::OutputPtr &output = m_outputs[index.row()].ptr;
0045     switch (role) {
0046     case Qt::DisplayRole: {
0047         const bool shouldShowSerialNumber = std::any_of(m_outputs.cbegin(), m_outputs.cend(), [output](const OutputModel::Output &other) {
0048             return other.ptr->id() != output->id() // avoid same output
0049                 && other.ptr->edid() && output->edid() //
0050                 && other.ptr->edid()->vendor() == output->edid()->vendor() //
0051                 && other.ptr->edid()->name() == output->edid()->name(); // model
0052         });
0053         const bool shouldShowConnector =
0054             shouldShowSerialNumber && std::any_of(m_outputs.cbegin(), m_outputs.cend(), [output](const OutputModel::Output &other) {
0055                 return other.ptr->id() != output->id() // avoid same output
0056                     && other.ptr->edid()->serial() == output->edid()->serial();
0057             });
0058         return Utils::outputName(output, shouldShowSerialNumber, shouldShowConnector);
0059     }
0060     case EnabledRole:
0061         return output->isEnabled();
0062     case InternalRole:
0063         return output->type() == KScreen::Output::Type::Panel;
0064     case PriorityRole:
0065         return output->priority();
0066     case SizeRole:
0067         return output->geometry().size();
0068     case PositionRole:
0069         return m_outputs[index.row()].pos;
0070     case NormalizedPositionRole:
0071         return output->geometry().topLeft();
0072     case AutoRotateRole:
0073         return static_cast<uint32_t>(output->autoRotatePolicy());
0074     case RotationRole:
0075         return output->rotation();
0076     case ScaleRole:
0077         return output->scale();
0078     case ResolutionIndexRole:
0079         return resolutionIndex(output);
0080     case ResolutionsRole:
0081         return resolutionsStrings(output);
0082     case ResolutionRole:
0083         return resolution(output);
0084     case RefreshRateIndexRole:
0085         return refreshRateIndex(output);
0086     case ReplicationSourceModelRole:
0087         return replicationSourceModel(output);
0088     case ReplicationSourceIndexRole:
0089         return replicationSourceIndex(index.row());
0090     case ReplicasModelRole:
0091         return replicasModel(output);
0092     case RefreshRatesRole: {
0093         QVariantList ret;
0094         const auto rates = refreshRates(output);
0095         for (const auto rate : rates) {
0096             ret << i18n("%1 Hz", int(rate + 0.5));
0097         }
0098         return ret;
0099     }
0100     case CapabilitiesRole:
0101         return static_cast<uint32_t>(output->capabilities());
0102     case OverscanRole:
0103         return output->overscan();
0104     case VrrPolicyRole:
0105         return static_cast<uint32_t>(output->vrrPolicy());
0106     case RgbRangeRole:
0107         return static_cast<uint32_t>(output->rgbRange());
0108     case InteractiveMoveRole:
0109         return m_outputs[index.row()].moving;
0110     case IccProfileRole:
0111         return output->iccProfilePath();
0112     case HdrRole:
0113         return output->isHdrEnabled() && output->isWcgEnabled();
0114     case SdrBrightnessRole:
0115         return output->sdrBrightness();
0116     case MaxBrightnessRole:
0117         return output->maxPeakBrightnessOverride().value_or(output->maxPeakBrightness());
0118     case SdrGamutWideness:
0119         return output->sdrGamutWideness();
0120     }
0121     return QVariant();
0122 }
0123 
0124 bool OutputModel::setData(const QModelIndex &index, const QVariant &value, int role)
0125 {
0126     if (index.row() < 0 || index.row() >= m_outputs.count()) {
0127         return false;
0128     }
0129 
0130     Output &output = m_outputs[index.row()];
0131     switch (role) {
0132     case PositionRole:
0133         if (value.canConvert<QPoint>()) {
0134             QPoint val = value.toPoint();
0135             if (output.pos == val) {
0136                 return false;
0137             }
0138             snap(output, val);
0139             m_outputs[index.row()].pos = val;
0140             updatePositions();
0141             Q_EMIT positionChanged();
0142             Q_EMIT dataChanged(index, index, {role});
0143             return true;
0144         }
0145         break;
0146     case EnabledRole:
0147         if (value.canConvert<bool>()) {
0148             return setEnabled(index.row(), value.toBool());
0149         }
0150         break;
0151     case PriorityRole:
0152         if (value.canConvert<uint32_t>()) {
0153             const uint32_t priority = value.toUInt();
0154             if (output.ptr->priority() == priority) {
0155                 return false;
0156             }
0157             m_config->config()->setOutputPriority(output.ptr, priority);
0158             return true;
0159         }
0160         break;
0161     case ResolutionIndexRole:
0162         if (value.canConvert<int>()) {
0163             return setResolution(index.row(), value.toInt());
0164         }
0165         break;
0166     case RefreshRateIndexRole:
0167         if (value.canConvert<int>()) {
0168             return setRefreshRate(index.row(), value.toInt());
0169         }
0170         break;
0171     case ResolutionRole:
0172         // unimplemented
0173         return false;
0174         break;
0175     case AutoRotateRole:
0176         if (value.canConvert<uint32_t>()) {
0177             Output &output = m_outputs[index.row()];
0178             const auto policy = static_cast<KScreen::Output::AutoRotatePolicy>(value.toUInt());
0179             if (output.ptr->autoRotatePolicy() == policy) {
0180                 return false;
0181             }
0182             output.ptr->setAutoRotatePolicy(policy);
0183             Q_EMIT dataChanged(index, index, {AutoRotateRole});
0184         }
0185         break;
0186     case RotationRole:
0187         if (value.canConvert<KScreen::Output::Rotation>()) {
0188             return setRotation(index.row(), value.value<KScreen::Output::Rotation>());
0189         }
0190         break;
0191     case ReplicationSourceIndexRole:
0192         if (value.canConvert<int>()) {
0193             return setReplicationSourceIndex(index.row(), value.toInt() - 1);
0194         }
0195         break;
0196     case ScaleRole: {
0197         bool ok;
0198         const qreal scale = value.toReal(&ok);
0199         if (ok && !qFuzzyCompare(output.ptr->scale(), scale)) {
0200             const auto oldSize = output.ptr->explicitLogicalSizeInt();
0201 
0202             output.ptr->setScale(scale);
0203 
0204             const auto newSize = m_config->config()->logicalSizeForOutputInt(*output.ptr);
0205             output.ptr->setExplicitLogicalSize(newSize);
0206 
0207             maintainSnapping(output, oldSize, newSize);
0208 
0209             Q_EMIT sizeChanged();
0210             Q_EMIT dataChanged(index, index, {role, SizeRole});
0211             return true;
0212         }
0213         break;
0214     }
0215     case OverscanRole:
0216         if (value.canConvert<uint32_t>()) {
0217             Output &output = m_outputs[index.row()];
0218             const uint32_t overscan = value.toUInt();
0219             if (output.ptr->overscan() == overscan) {
0220                 return false;
0221             }
0222             output.ptr->setOverscan(overscan);
0223             m_config->setOverscan(output.ptr, overscan);
0224             Q_EMIT dataChanged(index, index, {role});
0225             return true;
0226         }
0227         break;
0228     case VrrPolicyRole:
0229         if (value.canConvert<uint32_t>()) {
0230             Output &output = m_outputs[index.row()];
0231             const auto policy = static_cast<KScreen::Output::VrrPolicy>(value.toUInt());
0232             if (output.ptr->vrrPolicy() == policy) {
0233                 return false;
0234             }
0235             output.ptr->setVrrPolicy(policy);
0236             m_config->setVrrPolicy(output.ptr, policy);
0237             Q_EMIT dataChanged(index, index, {role});
0238             return true;
0239         }
0240         break;
0241     case RgbRangeRole:
0242         if (value.canConvert<uint32_t>()) {
0243             Output &output = m_outputs[index.row()];
0244             const auto range = static_cast<KScreen::Output::RgbRange>(value.toUInt());
0245             if (output.ptr->rgbRange() == range) {
0246                 return false;
0247             }
0248             output.ptr->setRgbRange(range);
0249             m_config->setRgbRange(output.ptr, range);
0250             Q_EMIT dataChanged(index, index, {role});
0251             return true;
0252         }
0253         break;
0254     case InteractiveMoveRole:
0255         if (value.canConvert<bool>()) {
0256             m_outputs[index.row()].moving = value.toBool();
0257             Q_EMIT dataChanged(index, index, {role});
0258             return true;
0259         }
0260         break;
0261     case IccProfileRole:
0262         m_outputs[index.row()].ptr->setIccProfilePath(value.toString());
0263         Q_EMIT dataChanged(index, index, {role});
0264         return true;
0265     case HdrRole:
0266         output.ptr->setHdrEnabled(value.toBool());
0267         output.ptr->setWcgEnabled(value.toBool());
0268         Q_EMIT dataChanged(index, index, {role});
0269         return true;
0270     case SdrBrightnessRole:
0271         output.ptr->setSdrBrightness(value.toUInt());
0272         Q_EMIT dataChanged(index, index, {role});
0273         return true;
0274     case SdrGamutWideness:
0275         output.ptr->setSdrGamutWideness(value.toDouble());
0276         Q_EMIT dataChanged(index, index, {role});
0277         return true;
0278     }
0279     return false;
0280 }
0281 
0282 QHash<int, QByteArray> OutputModel::roleNames() const
0283 {
0284     QHash<int, QByteArray> roles = QAbstractItemModel::roleNames();
0285     roles[EnabledRole] = "enabled";
0286     roles[InternalRole] = "internal";
0287     roles[PriorityRole] = "priority";
0288     roles[SizeRole] = "size";
0289     roles[PositionRole] = "position";
0290     roles[NormalizedPositionRole] = "normalizedPosition";
0291     roles[AutoRotateRole] = "autoRotate";
0292     roles[RotationRole] = "rotation";
0293     roles[ScaleRole] = "scale";
0294     roles[ResolutionIndexRole] = "resolutionIndex";
0295     roles[ResolutionsRole] = "resolutions";
0296     roles[ResolutionRole] = "resolution";
0297     roles[RefreshRateIndexRole] = "refreshRateIndex";
0298     roles[RefreshRatesRole] = "refreshRates";
0299     roles[ReplicationSourceModelRole] = "replicationSourceModel";
0300     roles[ReplicationSourceIndexRole] = "replicationSourceIndex";
0301     roles[ReplicasModelRole] = "replicasModel";
0302     roles[CapabilitiesRole] = "capabilities";
0303     roles[OverscanRole] = "overscan";
0304     roles[VrrPolicyRole] = "vrrPolicy";
0305     roles[RgbRangeRole] = "rgbRange";
0306     roles[InteractiveMoveRole] = "interactiveMove";
0307     roles[IccProfileRole] = "iccProfilePath";
0308     roles[HdrRole] = "hdr";
0309     roles[SdrBrightnessRole] = "sdrBrightness";
0310     roles[MaxBrightnessRole] = "peakBrightness";
0311     roles[SdrGamutWideness] = "sdrGamutWideness";
0312     return roles;
0313 }
0314 
0315 void OutputModel::add(const KScreen::OutputPtr &output)
0316 {
0317     const int insertPos = m_outputs.count();
0318     beginInsertRows(QModelIndex(), insertPos, insertPos);
0319 
0320     int i = 0;
0321     while (i < m_outputs.size()) {
0322         const QPoint pos = m_outputs[i].ptr->pos();
0323         if (output->pos().x() < pos.x()) {
0324             break;
0325         }
0326         if (output->pos().x() == pos.x() && output->pos().y() < pos.y()) {
0327             break;
0328         }
0329         i++;
0330     }
0331     // Set the initial non-normalized position to be the normalized
0332     // position plus the current delta.
0333     QPoint pos = output->pos();
0334     if (!m_outputs.isEmpty()) {
0335         const QPoint delta = m_outputs[0].pos - m_outputs[0].ptr->pos();
0336         pos = output->pos() + delta;
0337     }
0338     m_outputs.insert(i, Output(output, pos));
0339 
0340     endInsertRows();
0341 
0342     connect(output.data(), &KScreen::Output::modesChanged, this, [this, output]() {
0343         rolesChanged(output->id(), {ResolutionsRole, ResolutionIndexRole, ResolutionRole, SizeRole});
0344         Q_EMIT sizeChanged();
0345     });
0346 
0347     // Update replications.
0348     for (int j = 0; j < m_outputs.size(); j++) {
0349         if (i == j) {
0350             continue;
0351         }
0352         QModelIndex index = createIndex(j, 0);
0353         // Calling this directly ignores possible optimization when the
0354         // refresh rate hasn't changed in fact. But that's ok.
0355         Q_EMIT dataChanged(index, index, {ReplicationSourceModelRole, ReplicationSourceIndexRole});
0356     }
0357 }
0358 
0359 void OutputModel::remove(int outputId)
0360 {
0361     auto it = std::find_if(m_outputs.begin(), m_outputs.end(), [outputId](const Output &output) {
0362         return output.ptr->id() == outputId;
0363     });
0364     if (it != m_outputs.end()) {
0365         const int index = it - m_outputs.begin();
0366         beginRemoveRows(QModelIndex(), index, index);
0367         m_outputs.erase(it);
0368         endRemoveRows();
0369     }
0370 }
0371 
0372 void OutputModel::resetPosition(Output &output)
0373 {
0374     if (!output.posReset.has_value()) {
0375         // KCM was closed in between.
0376         for (const Output &out : std::as_const(m_outputs)) {
0377             if (out.ptr->id() == output.ptr->id()) {
0378                 continue;
0379             }
0380             const auto geometry = out.ptr->geometry();
0381             if (geometry.x() + geometry.width() > output.ptr->pos().x()) {
0382                 output.ptr->setPos(QPoint(geometry.x() + geometry.width(), geometry.top()));
0383             }
0384         }
0385     } else {
0386         QPoint reset = output.posReset.value();
0387         output.posReset.reset();
0388         QPoint shift = QPoint(0, 0);
0389 
0390         if (reset.x() < 0) {
0391             shift.setX(-reset.x());
0392             reset.setX(0);
0393         }
0394         if (reset.y() < 0) {
0395             shift.setY(-reset.y());
0396             reset.setY(0);
0397         }
0398 
0399         for (Output &out : m_outputs) {
0400             if (out.ptr->id() == output.ptr->id()) {
0401                 continue;
0402             }
0403             if (positionable(out)) {
0404                 out.ptr->setPos(out.ptr->pos() + shift);
0405             }
0406         }
0407         output.ptr->setPos(reset);
0408     }
0409 
0410     // TODO: this function is called when positioning programatically,
0411     //   it may make sense to run the final positions through the snapping logic
0412     //   to make sure the results are consistent with manual snapping
0413 }
0414 
0415 QPoint OutputModel::mostTopLeftLocationOfPositionableOutputOptionallyIgnoringOneOfThem(std::optional<KScreen::OutputPtr> ignored) const
0416 {
0417     auto foldTopLeft = [this, ignored](std::optional<QPoint> a, const Output &out) {
0418         if (!positionable(out) || (ignored.has_value() && out.ptr->id() == ignored.value()->id())) {
0419             return a;
0420         }
0421         if (a.has_value()) {
0422             return std::optional(QPoint(std::min(a.value().x(), out.pos.x()), std::min(a.value().y(), out.pos.y())));
0423         } else {
0424             return std::optional(out.pos);
0425         }
0426     };
0427     return std::accumulate(m_outputs.constBegin(), m_outputs.constEnd(), std::optional<QPoint>(), foldTopLeft).value_or(QPoint(0, 0));
0428 }
0429 
0430 bool OutputModel::setEnabled(int outputIndex, bool enable)
0431 {
0432     Output &output = m_outputs[outputIndex];
0433 
0434     if (output.ptr->isEnabled() == enable) {
0435         return false;
0436     }
0437 
0438     output.ptr->setEnabled(enable);
0439 
0440     if (enable) {
0441         resetPosition(output);
0442 
0443         setResolution(outputIndex, resolutionIndex(output.ptr));
0444     } else {
0445         // assuming it was already properly normalized, so current topleft (without disabling) is (0,0)
0446         const QPoint topLeft = mostTopLeftLocationOfPositionableOutputOptionallyIgnoringOneOfThem(std::optional(output.ptr));
0447 
0448         QPoint reset = output.ptr->pos();
0449         if (topLeft.x() > 0) {
0450             reset.setX(-topLeft.x());
0451         }
0452         if (topLeft.y() > 0) {
0453             reset.setY(-topLeft.y());
0454         }
0455 
0456         output.posReset = std::optional(reset);
0457     }
0458 
0459     reposition();
0460 
0461     QModelIndex index = createIndex(outputIndex, 0);
0462     Q_EMIT dataChanged(index, index, {EnabledRole});
0463     return true;
0464 }
0465 
0466 inline bool refreshRateCompare(float rate1, float rate2)
0467 {
0468     return qAbs(rate1 - rate2) < 0.5;
0469 }
0470 
0471 bool OutputModel::setResolution(int outputIndex, int resIndex)
0472 {
0473     const Output &output = m_outputs[outputIndex];
0474     const auto resolutionList = resolutions(output.ptr);
0475     if (resIndex < 0 || resIndex >= resolutionList.size()) {
0476         return false;
0477     }
0478     const QSize size = resolutionList[resIndex];
0479 
0480     const float oldRate = output.ptr->currentMode() ? output.ptr->currentMode()->refreshRate() : -1;
0481     const auto modes = output.ptr->modes();
0482 
0483     auto modeIt = std::find_if(modes.begin(), modes.end(), [size, oldRate](const KScreen::ModePtr &mode) {
0484         // TODO: we don't want to compare against old refresh rate if
0485         //       refresh rate selection is auto.
0486         return mode->size() == size && refreshRateCompare(mode->refreshRate(), oldRate);
0487     });
0488 
0489     if (modeIt == modes.end()) {
0490         // New resolution does not support previous refresh rate.
0491         // Get the highest one instead.
0492         float bestRefreshRate = 0;
0493         auto it = modes.begin();
0494         while (it != modes.end()) {
0495             if ((*it)->size() == size && (*it)->refreshRate() > bestRefreshRate) {
0496                 bestRefreshRate = (*it)->refreshRate();
0497                 modeIt = it;
0498             }
0499             it++;
0500         }
0501     }
0502     Q_ASSERT(modeIt != modes.end());
0503 
0504     const auto id = (*modeIt)->id();
0505     if (output.ptr->currentModeId() == id) {
0506         return false;
0507     }
0508     const auto oldSize = output.ptr->explicitLogicalSizeInt();
0509     output.ptr->setCurrentModeId(id);
0510     output.ptr->setSize(output.ptr->currentMode()->size());
0511 
0512     const auto newSize = m_config->config()->logicalSizeForOutputInt(*output.ptr);
0513     output.ptr->setExplicitLogicalSize(newSize);
0514 
0515     maintainSnapping(output, oldSize, newSize);
0516 
0517     QModelIndex index = createIndex(outputIndex, 0);
0518     // Calling this directly ignores possible optimization when the
0519     // refresh rate hasn't changed in fact. But that's ok.
0520     Q_EMIT dataChanged(index, index, {ResolutionIndexRole, ResolutionRole, SizeRole, RefreshRatesRole, RefreshRateIndexRole});
0521     Q_EMIT sizeChanged();
0522     return true;
0523 }
0524 
0525 bool OutputModel::setRefreshRate(int outputIndex, int refIndex)
0526 {
0527     const Output &output = m_outputs[outputIndex];
0528     const auto rates = refreshRates(output.ptr);
0529     if (refIndex < 0 || refIndex >= rates.size() || !output.ptr->isEnabled()) {
0530         return false;
0531     }
0532     const float refreshRate = rates[refIndex];
0533 
0534     const auto modes = output.ptr->modes();
0535     const auto oldMode = output.ptr->currentMode();
0536 
0537     auto modeIt = std::find_if(modes.begin(), modes.end(), [oldMode, refreshRate](const KScreen::ModePtr &mode) {
0538         // TODO: we don't want to compare against old refresh rate if
0539         //       refresh rate selection is auto.
0540         return mode->size() == oldMode->size() && refreshRateCompare(mode->refreshRate(), refreshRate);
0541     });
0542     Q_ASSERT(modeIt != modes.end());
0543 
0544     if (refreshRateCompare(oldMode->refreshRate(), (*modeIt)->refreshRate())) {
0545         // no change
0546         return false;
0547     }
0548     output.ptr->setCurrentModeId((*modeIt)->id());
0549     QModelIndex index = createIndex(outputIndex, 0);
0550     Q_EMIT dataChanged(index, index, {RefreshRateIndexRole});
0551     return true;
0552 }
0553 
0554 bool OutputModel::setRotation(int outputIndex, KScreen::Output::Rotation rotation)
0555 {
0556     const Output &output = m_outputs[outputIndex];
0557 
0558     if (rotation != KScreen::Output::None && rotation != KScreen::Output::Left && rotation != KScreen::Output::Inverted && rotation != KScreen::Output::Right) {
0559         return false;
0560     }
0561     if (output.ptr->rotation() == rotation) {
0562         return false;
0563     }
0564     const auto oldSize = output.ptr->explicitLogicalSizeInt();
0565     output.ptr->setRotation(rotation);
0566 
0567     const auto newSize = m_config->config()->logicalSizeForOutputInt(*output.ptr);
0568     output.ptr->setExplicitLogicalSize(newSize);
0569 
0570     maintainSnapping(output, oldSize, newSize);
0571 
0572     QModelIndex index = createIndex(outputIndex, 0);
0573     Q_EMIT dataChanged(index, index, {RotationRole, SizeRole});
0574     Q_EMIT sizeChanged();
0575     return true;
0576 }
0577 
0578 int OutputModel::resolutionIndex(const KScreen::OutputPtr &output) const
0579 {
0580     const QSize currentResolution = output->enforcedModeSize();
0581 
0582     if (!currentResolution.isValid()) {
0583         return 0;
0584     }
0585 
0586     const auto sizes = resolutions(output);
0587 
0588     const auto it = std::find_if(sizes.begin(), sizes.end(), [currentResolution](const QSize &size) {
0589         return size == currentResolution;
0590     });
0591     if (it == sizes.end()) {
0592         return -1;
0593     }
0594     return it - sizes.begin();
0595 }
0596 
0597 QSize OutputModel::resolution(const KScreen::OutputPtr &output) const
0598 {
0599     const QSize currentResolution = output->enforcedModeSize();
0600 
0601     if (!currentResolution.isValid()) {
0602         return QSize();
0603     }
0604 
0605     return currentResolution;
0606 }
0607 
0608 int OutputModel::refreshRateIndex(const KScreen::OutputPtr &output) const
0609 {
0610     if (!output->currentMode()) {
0611         return 0;
0612     }
0613     const auto rates = refreshRates(output);
0614     const float currentRate = output->currentMode()->refreshRate();
0615 
0616     const auto it = std::find_if(rates.begin(), rates.end(), [currentRate](float rate) {
0617         return refreshRateCompare(rate, currentRate);
0618     });
0619     if (it == rates.end()) {
0620         return 0;
0621     }
0622     return it - rates.begin();
0623 }
0624 
0625 static int greatestCommonDivisor(int a, int b)
0626 {
0627     if (b == 0) {
0628         return a;
0629     }
0630     return greatestCommonDivisor(b, a % b);
0631 }
0632 
0633 QVariantList OutputModel::resolutionsStrings(const KScreen::OutputPtr &output) const
0634 {
0635     QVariantList ret;
0636     const auto resolutionList = resolutions(output);
0637     for (const QSize &size : resolutionList) {
0638         if (size.isEmpty()) {
0639             const QString text = i18nc("Width x height",
0640                                        "%1x%2",
0641                                        // Explicitly not have it add thousand-separators.
0642                                        QString::number(size.width()),
0643                                        QString::number(size.height()));
0644             ret << text;
0645         } else {
0646             int divisor = greatestCommonDivisor(size.width(), size.height());
0647 
0648             if (size.height() / divisor == 5 || size.height() / divisor == 8) { // Prefer "16:10" over "8:5"
0649                 divisor /= 2;
0650             } else if (size.height() / divisor == 27 || size.height() / divisor == 64) { // Prefer "21:9" over "64:27"
0651                 divisor *= 3;
0652             } else if (size.height() / divisor == 18 || size.height() / divisor == 43) { // Prefer "21:9" over "43:18"
0653                 divisor *= 2;
0654             } else if (size.height() / divisor == 384 || size.height() / divisor == 683) { // Prefer "16:9" over "683:384"
0655                 divisor *= 41;
0656             }
0657 
0658             const QString text = i18nc("Width x height (aspect ratio)",
0659                                        "%1x%2 (%3:%4)",
0660                                        // Explicitly not have it add thousand-separators.
0661                                        QString::number(size.width()),
0662                                        QString::number(size.height()),
0663                                        size.width() / divisor,
0664                                        size.height() / divisor);
0665 
0666             ret << text;
0667         }
0668     }
0669     return ret;
0670 }
0671 
0672 QList<QSize> OutputModel::resolutions(const KScreen::OutputPtr &output) const
0673 {
0674     QList<QSize> hits;
0675 
0676     const auto modes = output->modes();
0677     for (const auto &mode : modes) {
0678         const QSize size = mode->size();
0679         if (!hits.contains(size)) {
0680             hits << size;
0681         }
0682     }
0683     std::sort(hits.begin(), hits.end(), [](const QSize &a, const QSize &b) {
0684         if (a.width() > b.width()) {
0685             return true;
0686         }
0687         if (a.width() == b.width() && a.height() > b.height()) {
0688             return true;
0689         }
0690         return false;
0691     });
0692     return hits;
0693 }
0694 
0695 QList<float> OutputModel::refreshRates(const KScreen::OutputPtr &output) const
0696 {
0697     QList<float> hits;
0698 
0699     QSize baseSize;
0700     if (output->currentMode()) {
0701         baseSize = output->currentMode()->size();
0702     } else if (output->preferredMode()) {
0703         baseSize = output->preferredMode()->size();
0704     }
0705     if (!baseSize.isValid()) {
0706         return hits;
0707     }
0708 
0709     const auto modes = output->modes();
0710     for (const auto &mode : modes) {
0711         if (mode->size() != baseSize) {
0712             continue;
0713         }
0714         const float rate = mode->refreshRate();
0715         if (std::find_if(hits.begin(),
0716                          hits.end(),
0717                          [rate](float r) {
0718                              return refreshRateCompare(r, rate);
0719                          })
0720             != hits.end()) {
0721             continue;
0722         }
0723         hits << rate;
0724     }
0725     std::stable_sort(hits.begin(), hits.end(), std::greater<>());
0726     return hits;
0727 }
0728 
0729 int OutputModel::replicationSourceId(const Output &output) const
0730 {
0731     const KScreen::OutputPtr source = m_config->replicationSource(output.ptr);
0732     if (!source) {
0733         return 0;
0734     }
0735     return source->id();
0736 }
0737 
0738 QStringList OutputModel::replicationSourceModel(const KScreen::OutputPtr &output) const
0739 {
0740     QStringList ret = {i18n("None")};
0741 
0742     for (const auto &out : m_outputs) {
0743         if (out.ptr->id() != output->id()) {
0744             const int outSourceId = replicationSourceId(out);
0745             if (outSourceId == output->id()) {
0746                 // 'output' is already source for replication, can't be replica itself
0747                 return {i18n("Replicated by other output")};
0748             }
0749             if (outSourceId) {
0750                 // This 'out' is a replica. Can't be a replication source.
0751                 continue;
0752             }
0753             ret.append(Utils::outputName(out.ptr));
0754         }
0755     }
0756     return ret;
0757 }
0758 
0759 static KScreen::ModePtr getBestMode(const KScreen::OutputPtr &output, const KScreen::OutputPtr &source)
0760 {
0761     auto calculateAspectRatio = [](const auto &output, const auto &mode) {
0762         const qreal ratio = mode->size().width() / qreal(mode->size().height());
0763         const qreal ratioTransposed = mode->size().height() / qreal(mode->size().width());
0764         switch (output->rotation()) {
0765         case KScreen::Output::Left:
0766         case KScreen::Output::Right:
0767             return ratioTransposed;
0768         default:
0769             return ratio;
0770         }
0771     };
0772     const qreal sourceRatio = calculateAspectRatio(source, source->currentMode());
0773     // 1.1: Find modes with the same aspect ratio as the source output; if none, don't change the mode
0774     std::vector<KScreen::ModePtr> availableModes(output->modes().cbegin(), output->modes().cend());
0775     std::erase_if(availableModes, [&calculateAspectRatio, &output, sourceRatio](const auto &mode) {
0776         return !qFuzzyCompare(calculateAspectRatio(output, mode), sourceRatio);
0777     });
0778     if (availableModes.empty()) {
0779         return output->currentMode();
0780     }
0781 
0782     // 1.2: Use the smallest mode at least as large as the source output; if none, use the largest mode
0783     std::sort(availableModes.begin(), availableModes.end(), [&output](const auto &a, const auto &b) {
0784         return a->size().width() < b->size().width();
0785     });
0786     auto getRotatedSize = [](const auto &output, const auto &mode) {
0787         if (output->rotation() == KScreen::Output::Left || output->rotation() == KScreen::Output::Right) {
0788             return mode->size().transposed();
0789         }
0790         return mode->size();
0791     };
0792     const auto sourceSize = getRotatedSize(source, source->currentMode());
0793     const auto it = std::find_if(availableModes.begin(), availableModes.end(), [sourceSize](const auto &mode) {
0794         return mode->size().width() >= sourceSize.width();
0795     });
0796     if (it != availableModes.end()) {
0797         return *it;
0798     } else {
0799         return availableModes.back();
0800     }
0801 }
0802 
0803 bool OutputModel::setReplicationSourceIndex(int outputIndex, int sourceIndex)
0804 {
0805     if (outputIndex <= sourceIndex) {
0806         sourceIndex++;
0807     }
0808     if (sourceIndex >= m_outputs.count()) {
0809         return false;
0810     }
0811 
0812     Output &output = m_outputs[outputIndex];
0813     const int oldSourceId = replicationSourceId(output);
0814 
0815     if (sourceIndex < 0) {
0816         if (oldSourceId == 0) {
0817             // no change
0818             return false;
0819         }
0820         m_config->setReplicationSource(output.ptr, nullptr);
0821         output.ptr->setExplicitLogicalSize(QSizeF());
0822         resetPosition(output);
0823     } else {
0824         const auto source = m_outputs[sourceIndex].ptr;
0825         if (oldSourceId == source->id()) {
0826             // no change
0827             return false;
0828         }
0829 
0830         // In replicating outputs, we don't change the source
0831         // Step 1: set the destination mode, if possible
0832         const auto bestMode = getBestMode(output.ptr, source);
0833         if (!bestMode) {
0834             return false;
0835         }
0836         // Step 2: reposition ans scale destination output to be centered inside the source output
0837         auto sourceSize = source->currentMode()->size();
0838         auto destinationSize = bestMode->size();
0839         if (source->rotation() == KScreen::Output::Left || source->rotation() == KScreen::Output::Right) {
0840             sourceSize = sourceSize.transposed();
0841         }
0842         if (output.ptr->rotation() == KScreen::Output::Left || output.ptr->rotation() == KScreen::Output::Right) {
0843             destinationSize = destinationSize.transposed();
0844         }
0845         qreal scale = source->scale() * std::max(destinationSize.width() / qreal(sourceSize.width()), destinationSize.height() / qreal(sourceSize.height()));
0846         // round up to integer multiples of 1/120 to avoid issues with triggering hidden panels from the edge
0847         scale = std::ceil(scale * 120.0) / 120.0;
0848         const QPoint relPos((sourceSize.width() / source->scale() - destinationSize.width() / scale) / 2,
0849                             (sourceSize.height() / source->scale() - destinationSize.height() / scale) / 2);
0850 
0851         output.ptr->setCurrentModeId(bestMode->id());
0852         output.ptr->setScale(scale);
0853         m_config->setReplicationSource(output.ptr, source);
0854         output.posReset = std::optional(output.ptr->pos());
0855         output.ptr->setPos(source->pos() + relPos);
0856     }
0857 
0858     reposition();
0859 
0860     QModelIndex index = createIndex(outputIndex, 0);
0861     Q_EMIT dataChanged(
0862         index,
0863         index,
0864         {ReplicationSourceIndexRole, SizeRole, NormalizedPositionRole, ScaleRole, RefreshRatesRole, RefreshRateIndexRole, ResolutionRole, ResolutionIndexRole});
0865 
0866     if (oldSourceId != 0) {
0867         auto it = std::find_if(m_outputs.begin(), m_outputs.end(), [oldSourceId](const Output &out) {
0868             return out.ptr->id() == oldSourceId;
0869         });
0870         if (it != m_outputs.end()) {
0871             QModelIndex index = createIndex(it - m_outputs.begin(), 0);
0872             Q_EMIT dataChanged(index, index, {ReplicationSourceModelRole, ReplicasModelRole});
0873         }
0874     }
0875     if (sourceIndex >= 0) {
0876         QModelIndex index = createIndex(sourceIndex, 0);
0877         Q_EMIT dataChanged(index, index, {ReplicationSourceModelRole, ReplicasModelRole});
0878     }
0879     return true;
0880 }
0881 
0882 int OutputModel::replicationSourceIndex(int outputIndex) const
0883 {
0884     const int sourceId = replicationSourceId(m_outputs[outputIndex]);
0885     if (!sourceId) {
0886         return 0;
0887     }
0888     for (int i = 0; i < m_outputs.size(); i++) {
0889         const Output &output = m_outputs[i];
0890         if (output.ptr->id() == sourceId) {
0891             return i + (outputIndex > i ? 1 : 0);
0892         }
0893     }
0894     return 0;
0895 }
0896 
0897 QVariantList OutputModel::replicasModel(const KScreen::OutputPtr &output) const
0898 {
0899     QVariantList ret;
0900     for (int i = 0; i < m_outputs.size(); i++) {
0901         const Output &out = m_outputs[i];
0902         if (out.ptr->id() != output->id()) {
0903             if (replicationSourceId(out) == output->id()) {
0904                 ret << i;
0905             }
0906         }
0907     }
0908     return ret;
0909 }
0910 
0911 void OutputModel::rolesChanged(int outputId, const QList<int> &roles)
0912 {
0913     const auto index = indexForOutputId(outputId);
0914     if (index.isValid()) {
0915         Q_EMIT dataChanged(index, index, roles);
0916     }
0917 }
0918 
0919 QModelIndex OutputModel::indexForOutputId(int outputId) const
0920 {
0921     for (int i = 0; i < m_outputs.size(); i++) {
0922         const Output &output = m_outputs[i];
0923         if (output.ptr->id() == outputId) {
0924             return createIndex(i, 0);
0925         }
0926     }
0927     return QModelIndex();
0928 }
0929 
0930 bool OutputModel::positionable(const Output &output) const
0931 {
0932     return output.ptr->isPositionable();
0933 }
0934 
0935 bool OutputModel::isMoving() const
0936 {
0937     return std::any_of(m_outputs.cbegin(), m_outputs.cend(), std::mem_fn(&Output::moving));
0938 }
0939 
0940 void OutputModel::reposition()
0941 {
0942     int x = 0;
0943     int y = 0;
0944 
0945     // Find first valid output.
0946     for (const auto &out : std::as_const(m_outputs)) {
0947         if (positionable(out)) {
0948             x = out.ptr->pos().x();
0949             y = out.ptr->pos().y();
0950             break;
0951         }
0952     }
0953 
0954     for (int i = 0; i < m_outputs.size(); i++) {
0955         if (!positionable(m_outputs[i])) {
0956             continue;
0957         }
0958         const QPoint &cmp = m_outputs[i].ptr->pos();
0959 
0960         if (cmp.x() < x) {
0961             x = cmp.x();
0962         }
0963         if (cmp.y() < y) {
0964             y = cmp.y();
0965         }
0966     }
0967 
0968     if (x == 0 && y == 0) {
0969         return;
0970     }
0971 
0972     for (int i = 0; i < m_outputs.size(); i++) {
0973         auto &out = m_outputs[i];
0974         out.ptr->setPos(out.ptr->pos() - QPoint(x, y));
0975         QModelIndex index = createIndex(i, 0);
0976         Q_EMIT dataChanged(index, index, {NormalizedPositionRole});
0977     }
0978     m_config->normalizeScreen();
0979 }
0980 
0981 void OutputModel::updatePositions()
0982 {
0983     const QPoint delta = mostTopLeftLocationOfPositionableOutputOptionallyIgnoringOneOfThem();
0984     for (int i = 0; i < m_outputs.size(); i++) {
0985         const auto &out = m_outputs[i];
0986         if (!positionable(out)) {
0987             continue;
0988         }
0989         const QPoint set = out.pos - delta;
0990         if (out.ptr->pos() != set) {
0991             out.ptr->setPos(set);
0992             QModelIndex index = createIndex(i, 0);
0993             Q_EMIT dataChanged(index, index, {NormalizedPositionRole});
0994         }
0995     }
0996 }
0997 
0998 bool OutputModel::normalizePositions()
0999 {
1000     bool changed = false;
1001     for (int i = 0; i < m_outputs.size(); i++) {
1002         auto &output = m_outputs[i];
1003         if (output.pos == output.ptr->pos()) {
1004             continue;
1005         }
1006         if (!positionable(output)) {
1007             continue;
1008         }
1009         changed = true;
1010         auto index = createIndex(i, 0);
1011         output.pos = output.ptr->pos();
1012         Q_EMIT dataChanged(index, index, {PositionRole});
1013     }
1014     return changed;
1015 }
1016 
1017 bool OutputModel::positionsNormalized() const
1018 {
1019     // There might be slight deviations because of snapping.
1020     return mostTopLeftLocationOfPositionableOutputOptionallyIgnoringOneOfThem().manhattanLength() < 5;
1021 }
1022 
1023 const int s_snapArea = 80;
1024 
1025 bool isVerticalClose(const QRect &rect1, const QRect &rect2)
1026 {
1027     if (rect2.top() - (rect1.y() + rect1.height()) > s_snapArea) {
1028         return false;
1029     }
1030     if (rect1.top() - (rect2.y() + rect2.height()) > s_snapArea) {
1031         return false;
1032     }
1033     return true;
1034 }
1035 
1036 bool snapToRight(const QRect &target, const QSize &size, QPoint &dest)
1037 {
1038     if (qAbs(target.x() + target.width() - dest.x()) < s_snapArea) {
1039         // In snap zone for left to right snap.
1040         dest.setX(target.x() + target.width());
1041         return true;
1042     }
1043     if (qAbs(target.x() + target.width() - (dest.x() + size.width())) < s_snapArea) {
1044         // In snap zone for right to right snap.
1045         dest.setX(target.x() + target.width() - size.width());
1046         return true;
1047     }
1048     return false;
1049 }
1050 
1051 bool snapToLeft(const QRect &target, const QSize &size, QPoint &dest)
1052 {
1053     if (qAbs(target.left() - dest.x()) < s_snapArea) {
1054         // In snap zone for left to left snap.
1055         dest.setX(target.left());
1056         return true;
1057     }
1058     if (qAbs(target.left() - (dest.x() + size.width())) < s_snapArea) {
1059         // In snap zone for right to left snap.
1060         dest.setX(target.left() - size.width());
1061         return true;
1062     }
1063     return false;
1064 }
1065 
1066 bool snapHorizontal(const QRect &target, const QSize &size, QPoint &dest)
1067 {
1068     if (snapToRight(target, size, dest)) {
1069         return true;
1070     }
1071     if (snapToLeft(target, size, dest)) {
1072         return true;
1073     }
1074     return false;
1075 }
1076 
1077 bool snapToMiddle(const QRect &target, const QSize &size, QPoint &dest)
1078 {
1079     const int outputMid = dest.y() + size.height() / 2;
1080     const int targetMid = target.top() + target.height() / 2;
1081     if (qAbs(targetMid - outputMid) < s_snapArea) {
1082         // In snap zone for middle to middle snap.
1083         dest.setY(targetMid - size.height() / 2);
1084         return true;
1085     }
1086     return false;
1087 }
1088 
1089 bool snapToTop(const QRect &target, const QSize &size, QPoint &dest)
1090 {
1091     if (qAbs(target.top() - dest.y()) < s_snapArea) {
1092         // In snap zone for bottom to top snap.
1093         dest.setY(target.top());
1094         return true;
1095     }
1096     if (qAbs(target.top() - (dest.y() + size.height())) < s_snapArea) {
1097         // In snap zone for top to top snap.
1098         dest.setY(target.top() - size.height());
1099         return true;
1100     }
1101     return false;
1102 }
1103 
1104 bool snapToBottom(const QRect &target, const QSize &size, QPoint &dest)
1105 {
1106     if (qAbs(target.y() + target.height() - dest.y()) < s_snapArea) {
1107         // In snap zone for top to bottom snap.
1108         dest.setY(target.y() + target.height());
1109         return true;
1110     }
1111     if (qAbs(target.y() + target.height() - (dest.y() + size.height())) < s_snapArea) {
1112         // In snap zone for bottom to bottom snap.
1113         dest.setY(target.y() + target.height() - size.height());
1114         return true;
1115     }
1116     return false;
1117 }
1118 
1119 bool snapVertical(const QRect &target, const QSize &size, QPoint &dest)
1120 {
1121     if (snapToMiddle(target, size, dest)) {
1122         return true;
1123     }
1124     if (snapToBottom(target, size, dest)) {
1125         return true;
1126     }
1127     if (snapToTop(target, size, dest)) {
1128         return true;
1129     }
1130     return false;
1131 }
1132 
1133 void OutputModel::snap(const Output &output, QPoint &dest)
1134 {
1135     const QSize size = output.ptr->geometry().size();
1136     const QRect outputRect(dest, size);
1137 
1138     QList<std::reference_wrapper<const Output>> positionableOutputs;
1139     positionableOutputs.reserve(m_outputs.size());
1140     std::copy_if(m_outputs.cbegin(), m_outputs.cend(), std::back_inserter(positionableOutputs), [](const Output &output) {
1141         return output.ptr->isPositionable();
1142     });
1143 
1144     // Special case for two outputs, we want to make sure they always touch;
1145     if (positionableOutputs.size() == 2) {
1146         const Output &other = positionableOutputs.at(0).get().ptr->id() == output.ptr->id() ? positionableOutputs.at(1) : positionableOutputs.at(0);
1147         const QRect target(other.pos, other.ptr->geometry().size());
1148         const bool xOverlap = dest.x() <= target.x() + target.width() && target.x() <= dest.x() + size.width();
1149         const bool yOverlap = dest.y() <= target.y() + target.height() && target.y() <= dest.y() + size.height();
1150         // Special special case, snap to center if centers are close
1151         if (std::abs((outputRect.center() - target.center()).manhattanLength()) < s_snapArea * 2) {
1152             dest = target.center() - (outputRect.center() - outputRect.topLeft());
1153             return;
1154         }
1155         if (xOverlap) {
1156             const int topDist = std::abs(dest.y() - target.y() - target.height());
1157             const int bottomDist = std::abs(outputRect.y() + outputRect.height() - target.y());
1158             if (topDist < bottomDist) {
1159                 dest.setY(target.y() + target.height());
1160             } else {
1161                 dest.setY(target.y() - size.height());
1162             }
1163             // Secondary snap to align the other edges if close - right to right, left to left
1164             snapHorizontal(target, size, dest);
1165             return;
1166         }
1167         if (yOverlap) {
1168             const int leftDist = std::abs(dest.x() - target.x() - target.width());
1169             const int rightDiff = std::abs(outputRect.x() + outputRect.width() - target.x());
1170             if (leftDist < rightDiff) {
1171                 dest.setX(target.x() + target.width());
1172             } else {
1173                 dest.setX(target.x() - size.width());
1174             }
1175             // Secondary snap to align the other edges if close - top to top, bottom to bottom, center to center
1176             snapVertical(target, size, dest);
1177             return;
1178         }
1179         // No overlap at all can happen at a corner, do not let the output move away
1180         dest = output.pos;
1181         return;
1182     }
1183 
1184     for (const Output &out : std::as_const(positionableOutputs)) {
1185         if (out.ptr->id() == output.ptr->id()) {
1186             // Can not snap to itself.
1187             continue;
1188         }
1189 
1190         const QRect target(out.pos, out.ptr->geometry().size());
1191 
1192         if (!isVerticalClose(target, QRect(dest, size))) {
1193             continue;
1194         }
1195 
1196         // try snap left to right first
1197         if (snapToRight(target, size, dest)) {
1198             snapVertical(target, size, dest);
1199             continue;
1200         }
1201         if (snapToLeft(target, size, dest)) {
1202             snapVertical(target, size, dest);
1203             continue;
1204         }
1205         if (snapVertical(target, size, dest)) {
1206             continue;
1207         }
1208     }
1209 }
1210 
1211 void OutputModel::maintainSnapping(const OutputModel::Output &changedOutput, const QSize &oldSize, const QSize &newSize)
1212 {
1213     const auto changedCenter = QRect(changedOutput.ptr->pos(), oldSize).center();
1214 
1215     const auto dSize = newSize - oldSize;
1216     const auto delta = QPoint(dSize.width(), dSize.height());
1217 
1218     auto updated = false;
1219     for (auto &output : m_outputs) {
1220         if (output.ptr->id() == changedOutput.ptr->id()) {
1221             continue;
1222         }
1223 
1224         const auto pos = output.ptr->pos();
1225         const auto isXTranslate = pos.x() >= changedCenter.x();
1226         const auto isYTranslate = pos.y() >= changedCenter.y();
1227         const auto translation = QPoint(isXTranslate ? delta.x() : 0, isYTranslate ? delta.y() : 0);
1228         if (translation.isNull()) {
1229             continue;
1230         }
1231 
1232         output.pos = pos + translation;
1233         updated = true;
1234     }
1235 
1236     if (updated) {
1237         updatePositions();
1238     }
1239 }
1240 
1241 #include "moc_output_model.cpp"