File indexing completed on 2024-04-28 16:45:07

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