diff --git a/kcm/config_handler.cpp b/kcm/config_handler.cpp --- a/kcm/config_handler.cpp +++ b/kcm/config_handler.cpp @@ -120,7 +120,8 @@ initialOutput->currentModeId() || output->pos() != initialOutput->pos() || output->scale() != initialOutput->scale() - || output->rotation() != initialOutput->rotation(); + || output->rotation() != initialOutput->rotation() + || output->replicationSource() != initialOutput->replicationSource(); } if (needsSave) { Q_EMIT needsSaveChecked(true); @@ -138,7 +139,7 @@ QSize size; for (const auto &output : m_config->connectedOutputs()) { - if (!output->isEnabled()) { + if (!output->isPositionable()) { continue; } const int outputRight = output->geometry().right(); diff --git a/kcm/kcm.h b/kcm/kcm.h --- a/kcm/kcm.h +++ b/kcm/kcm.h @@ -41,6 +41,8 @@ NOTIFY perOutputScalingChanged) Q_PROPERTY(bool primaryOutputSupported READ primaryOutputSupported NOTIFY primaryOutputSupportedChanged) + Q_PROPERTY(bool outputReplicationSupported READ outputReplicationSupported + NOTIFY outputReplicationSupportedChanged) Q_PROPERTY(qreal globalScale READ globalScale WRITE setGlobalScale NOTIFY globalScaleChanged) Q_PROPERTY(int outputRetention READ outputRetention WRITE setOutputRetention @@ -66,6 +68,7 @@ bool perOutputScaling() const; bool primaryOutputSupported() const; + bool outputReplicationSupported() const; qreal globalScale() const; void setGlobalScale(qreal scale); @@ -83,6 +86,7 @@ void screenNormalizedChanged(); void perOutputScalingChanged(); void primaryOutputSupportedChanged(); + void outputReplicationSupportedChanged(); void globalScaleChanged(); void outputRetentionChanged(); void dangerousSave(); diff --git a/kcm/kcm.cpp b/kcm/kcm.cpp --- a/kcm/kcm.cpp +++ b/kcm/kcm.cpp @@ -79,6 +79,7 @@ m_config->setConfig(qobject_cast(op)->config()); Q_EMIT perOutputScalingChanged(); Q_EMIT primaryOutputSupportedChanged(); + Q_EMIT outputReplicationSupportedChanged(); } void KCMKScreen::forceSave() @@ -117,7 +118,8 @@ << "@" << (mode ? mode->refreshRate() : 0.0) << "Hz" << "\n" << " Position:" << output->pos().x() << "x" << output->pos().y() << "\n" << " Scale:" << (perOutputScaling() ? QString::number(output->scale()) : - QStringLiteral("global")); + QStringLiteral("global")) << "\n" + << " Replicates:" << (output->replicationSource() == 0 ? "no" : "yes"); } if (!atLeastOneEnabledOutput && !force) { @@ -222,6 +224,15 @@ PrimaryDisplay); } +bool KCMKScreen::outputReplicationSupported() const +{ + if (!m_config || !m_config->config()) { + return false; + } + return m_config->config()->supportedFeatures().testFlag(Config::Feature:: + OutputReplication); +} + void KCMKScreen::setScreenNormalized(bool normalized) { if (m_screenNormalized == normalized) { diff --git a/kcm/output_model.h b/kcm/output_model.h --- a/kcm/output_model.h +++ b/kcm/output_model.h @@ -41,7 +41,10 @@ ResolutionIndexRole, ResolutionsRole, RefreshRateIndexRole, - RefreshRatesRole + RefreshRatesRole, + ReplicationSourceModelRole, + ReplicationSourceIndexRole, + ReplicasModelRole }; explicit OutputModel (ConfigHandler *configHandler); @@ -87,6 +90,7 @@ {} KScreen::OutputPtr ptr; QPoint pos; + QPoint replicaReset; }; void roleChanged(int outputId, OutputRoles role); @@ -113,6 +117,12 @@ bool positionable(const Output &output) const; + QStringList replicationSourceModel(const KScreen::OutputPtr &output) const; + bool setReplicationSourceIndex(int outputIndex, int sourceIndex); + int replicationSourceIndex(int outputIndex, int sourceId) const; + + QVariantList replicasModel(const KScreen::OutputPtr &output) const; + QVector m_outputs; ConfigHandler *m_config; diff --git a/kcm/output_model.cpp b/kcm/output_model.cpp --- a/kcm/output_model.cpp +++ b/kcm/output_model.cpp @@ -65,6 +65,12 @@ return resolutionsStrings(output); case RefreshRateIndexRole: return refreshRateIndex(output); + case ReplicationSourceModelRole: + return replicationSourceModel(output); + case ReplicationSourceIndexRole: + return replicationSourceIndex(index.row(), output->replicationSource()); + case ReplicasModelRole: + return replicasModel(output); case RefreshRatesRole: QVariantList ret; for (const auto rate : refreshRates(output)) { @@ -138,6 +144,11 @@ value.value()); } break; + case ReplicationSourceIndexRole: + if (value.canConvert()) { + return setReplicationSourceIndex(index.row(), value.toInt() - 1); + } + break; case ScaleRole: bool ok; const qreal scale = value.toReal(&ok); @@ -165,6 +176,9 @@ roles[ResolutionsRole] = "resolutions"; roles[RefreshRateIndexRole] = "refreshRateIndex"; roles[RefreshRatesRole] = "refreshRates"; + roles[ReplicationSourceModelRole] = "replicationSourceModel"; + roles[ReplicationSourceIndexRole] = "replicationSourceIndex"; + roles[ReplicasModelRole] = "replicasModel"; return roles; } @@ -199,6 +213,18 @@ roleChanged(output->id(), {PrimaryRole}); }); Q_EMIT endInsertRows(); + + // Update replications. + for (int j = 0; j < m_outputs.size(); j++) { + if (i == j) { + continue; + } + QModelIndex index = createIndex(j, 0); + // Calling this directly ignores possible optimization when the + // refresh rate hasn't changed in fact. But that's ok. + Q_EMIT dataChanged(index, index, {ReplicationSourceModelRole, + ReplicationSourceIndexRole}); + } } void OutputModel::remove(int outputId) @@ -420,6 +446,116 @@ return hits; } +QStringList OutputModel::replicationSourceModel(const KScreen::OutputPtr &output) const +{ + QStringList ret = { i18n("None") }; + + for (const auto &out : m_outputs) { + if (out.ptr->id() != output->id()) { + if (out.ptr->replicationSource() == output->id()) { + // 'output' is already source for replication, can't be replica itself + return { i18n("Replicated by other output") }; + } + if (out.ptr->replicationSource()) { + // This 'out' is a replica. Can't be a replication source. + continue; + } + ret.append(out.ptr->name()); + } + } + return ret; +} + +bool OutputModel::setReplicationSourceIndex(int outputIndex, int sourceIndex) +{ + if (outputIndex <= sourceIndex) { + sourceIndex++; + } + if (sourceIndex >= m_outputs.count()) { + return false; + } + + Output &output = m_outputs[outputIndex]; + const int oldSourceId = output.ptr->replicationSource(); + + if (sourceIndex < 0) { + if (oldSourceId == 0) { + // no change + return false; + } + output.ptr->setReplicationSource(0); + + if (output.replicaReset.isNull()) { + // KCM was closed in between. + for (const Output &out : m_outputs) { + if (out.ptr->id() == output.ptr->id()) { + continue; + } + if (out.ptr->geometry().right() > output.ptr->pos().x()) { + output.ptr->setPos(out.ptr->geometry().topRight()); + } + } + } else { + output.ptr->setPos(output.ptr->pos() - output.replicaReset); + } + } else { + const int sourceId = m_outputs[sourceIndex].ptr->id(); + if (oldSourceId == sourceId) { + // no change + return false; + } + output.ptr->setReplicationSource(sourceId); + output.replicaReset = m_outputs[sourceIndex].ptr->pos() - output.ptr->pos(); + output.ptr->setPos(m_outputs[sourceIndex].ptr->pos()); + } + + reposition(); + + QModelIndex index = createIndex(outputIndex, 0); + Q_EMIT dataChanged(index, index, {ReplicationSourceIndexRole}); + + if (oldSourceId != 0) { + auto it = std::find_if(m_outputs.begin(), m_outputs.end(), + [oldSourceId](const Output &out) { + return out.ptr->id() == oldSourceId; + }); + if (it != m_outputs.end()) { + QModelIndex index = createIndex(it - m_outputs.begin(), 0); + Q_EMIT dataChanged(index, index, {ReplicationSourceModelRole, ReplicasModelRole}); + } + } + if (sourceIndex >= 0) { + QModelIndex index = createIndex(sourceIndex, 0); + Q_EMIT dataChanged(index, index, {ReplicationSourceModelRole, ReplicasModelRole}); + } + return true; +} + +int OutputModel::replicationSourceIndex(int outputIndex, int sourceId) const +{ + for (int i = 0; i < m_outputs.size(); i++) { + const Output &output = m_outputs[i]; + if (output.ptr->id() == sourceId) { + return i + (outputIndex > i ? 1 : 0); + } + } + return 0; +} + +QVariantList OutputModel::replicasModel(const KScreen::OutputPtr &output) const +{ + QVariantList ret; + for (int i = 0; i < m_outputs.size(); i++) { + const Output &out = m_outputs[i]; + if (out.ptr->id() != output->id()) { + if (out.ptr->replicationSource() == output->id()) { + ret << i; + } + } + } + return ret; +} + void OutputModel::roleChanged(int outputId, OutputRoles role) { for (int i = 0; i < m_outputs.size(); i++) { @@ -434,7 +570,7 @@ bool OutputModel::positionable(const Output &output) const { - return output.ptr->isEnabled(); + return output.ptr->isPositionable(); } void OutputModel::reposition() @@ -555,6 +691,12 @@ break; } } + + // TODO: Could this be optimized by only outputs updating where replica indices changed? + for (int i = 0; i < m_outputs.size(); i++) { + QModelIndex index = createIndex(i, 0); + Q_EMIT dataChanged(index, index, { ReplicasModelRole }); + } } bool OutputModel::normalizePositions() diff --git a/kcm/package/contents/ui/Output.qml b/kcm/package/contents/ui/Output.qml --- a/kcm/package/contents/ui/Output.qml +++ b/kcm/package/contents/ui/Output.qml @@ -38,7 +38,7 @@ (pos.y - screen.yOffset) * screen.relativeFactor) ; } - visible: model.enabled + visible: model.enabled && model.replicationSourceIndex === 0 onVisibleChanged: screen.resetTotalSize() x: model.position.x / screen.relativeFactor + screen.xOffset @@ -130,6 +130,36 @@ } } + Controls.ToolButton { + id: replicas + + property int selectedReplica: -1 + + height: output.height / 4 + width: output.width / 5 + anchors.top: output.top + anchors.right: output.right + anchors.margins: 5 + + visible: model.replicasModel.length > 0 + icon.name: "osd-duplicate" + + Controls.ToolTip { + text: i18n("Replicas") + } + + onClicked: { + var index = selectedReplica + 1; + if (index >= model.replicasModel.length) { + index = 0; + } + if (root.selectedOutput !== model.replicasModel[index]) { + root.selectedOutput = model.replicasModel[index]; + } + } + + } + Item { id: orientationPanelContainer diff --git a/kcm/package/contents/ui/OutputPanel.qml b/kcm/package/contents/ui/OutputPanel.qml --- a/kcm/package/contents/ui/OutputPanel.qml +++ b/kcm/package/contents/ui/OutputPanel.qml @@ -102,5 +102,17 @@ element.refreshRateIndex : 0 onActivated: element.refreshRateIndex = currentIndex } + + Controls.ComboBox { + Kirigami.FormData.label: i18n("Replica of:") + model: element.replicationSourceModel + visible: kcm.outputReplicationSupported + + onModelChanged: enabled = (count > 1); + onCountChanged: enabled = (count > 1); + + currentIndex: element.replicationSourceIndex + onActivated: element.replicationSourceIndex = currentIndex + } } }