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 }