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"