diff --git a/CMakeLists.txt b/CMakeLists.txt index 0d64f0c..310cb7a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,50 +1,49 @@ cmake_minimum_required(VERSION 3.0) project(KScreen) set(PROJECT_VERSION "5.17.80") - set(KSCREEN_VERSION ${PROJECT_VERSION}) add_definitions("-DKSCREEN_VERSION=\"${KSCREEN_VERSION}\"") set(QT_MIN_VERSION "5.12.0") set(KF5_MIN_VERSION "5.62.0") find_package(ECM ${KF5_MIN_VERSION} REQUIRED NO_MODULE) set(CMAKE_MODULE_PATH ${ECM_MODULE_PATH}) include(KDEInstallDirs) include(KDECMakeSettings) include(KDEFrameworkCompilerSettings NO_POLICY_SCOPE) include(ECMInstallIcons) include(ECMMarkAsTest) include(ECMQtDeclareLoggingCategory) include(FeatureSummary) find_package(Qt5 ${QT_MIN_VERSION} REQUIRED COMPONENTS Test) find_package(KF5 ${KF5_MIN_VERSION} REQUIRED COMPONENTS Config DBusAddons Declarative GlobalAccel I18n IconThemes KCMUtils Plasma XmlGui ) # WARNING PlasmaQuick provides unversioned CMake config find_package(KF5 REQUIRED COMPONENTS PlasmaQuick) set(MIN_LIBKSCREEN_VERSION "5.2.91") find_package(KF5Screen ${MIN_LIBKSCREEN_VERSION} REQUIRED) add_subdirectory(kcm) add_subdirectory(kded) add_subdirectory(plasmoid) add_subdirectory(tests) add_subdirectory(console) install(FILES kscreen.categories DESTINATION ${KDE_INSTALL_LOGGINGCATEGORIESDIR}) feature_summary(WHAT ALL FATAL_ON_MISSING_REQUIRED_PACKAGES) diff --git a/kcm/output_model.cpp b/kcm/output_model.cpp index adc5810..ab09a1d 100644 --- a/kcm/output_model.cpp +++ b/kcm/output_model.cpp @@ -1,901 +1,901 @@ /******************************************************************** Copyright © 2019 Roman Gilg This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . *********************************************************************/ #include "output_model.h" #include "../common/utils.h" #include "config_handler.h" #include #include OutputModel::OutputModel(ConfigHandler *configHandler) : QAbstractListModel(configHandler) , m_config(configHandler) { connect(this, &OutputModel::dataChanged, this, &OutputModel::changed); } int OutputModel::rowCount(const QModelIndex &parent) const { Q_UNUSED(parent) return m_outputs.count(); } QVariant OutputModel::data(const QModelIndex &index, int role) const { if (index.row() < 0 || index.row() >= m_outputs.count()) { return QVariant(); } const KScreen::OutputPtr &output = m_outputs[index.row()].ptr; switch (role) { case Qt::DisplayRole: return Utils::outputName(output); case EnabledRole: return output->isEnabled(); case PrimaryRole: return output->isPrimary(); case SizeRole: return output->geometry().size(); case PositionRole: return m_outputs[index.row()].pos; case NormalizedPositionRole: return output->geometry().topLeft(); case RotationRole: return output->rotation(); case ScaleRole: return output->scale(); case ResolutionIndexRole: return resolutionIndex(output); case ResolutionsRole: 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)) { ret << i18n("%1 Hz", int(rate + 0.5)); } return ret; } return QVariant(); } bool OutputModel::setData(const QModelIndex &index, const QVariant &value, int role) { if (index.row() < 0 || index.row() >= m_outputs.count()) { return false; } const Output &output = m_outputs[index.row()]; switch (role) { case PositionRole: if (value.canConvert()) { QPoint val = value.toPoint(); if (output.pos == val) { return false; } snap(output, val); m_outputs[index.row()].pos = val; updatePositions(); Q_EMIT positionChanged(); Q_EMIT dataChanged(index, index, {role}); return true; } break; case EnabledRole: if (value.canConvert()) { return setEnabled(index.row(), value.toBool()); } break; case PrimaryRole: if (value.canConvert()) { bool primary = value.toBool(); if (output.ptr->isPrimary() == primary) { return false; } m_config->config()->setPrimaryOutput(output.ptr); Q_EMIT dataChanged(index, index, {role}); return true; } break; case ResolutionIndexRole: if (value.canConvert()) { return setResolution(index.row(), value.toInt()); } break; case RefreshRateIndexRole: if (value.canConvert()) { return setRefreshRate(index.row(), value.toInt()); } break; case RotationRole: if (value.canConvert()) { return setRotation(index.row(), 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); if (ok && !qFuzzyCompare(output.ptr->scale(), scale)) { output.ptr->setScale(scale); Q_EMIT sizeChanged(); Q_EMIT dataChanged(index, index, {role, SizeRole}); return true; } break; } return false; } QHash OutputModel::roleNames() const { QHash roles = QAbstractItemModel::roleNames(); roles[EnabledRole] = "enabled"; roles[PrimaryRole] = "primary"; roles[SizeRole] = "size"; roles[PositionRole] = "position"; roles[NormalizedPositionRole] = "normalizedPosition"; roles[RotationRole] = "rotation"; roles[ScaleRole] = "scale"; roles[ResolutionIndexRole] = "resolutionIndex"; roles[ResolutionsRole] = "resolutions"; roles[RefreshRateIndexRole] = "refreshRateIndex"; roles[RefreshRatesRole] = "refreshRates"; roles[ReplicationSourceModelRole] = "replicationSourceModel"; roles[ReplicationSourceIndexRole] = "replicationSourceIndex"; roles[ReplicasModelRole] = "replicasModel"; return roles; } void OutputModel::add(const KScreen::OutputPtr &output) { const int insertPos = m_outputs.count(); Q_EMIT beginInsertRows(QModelIndex(), insertPos, insertPos); int i = 0; while (i < m_outputs.size()) { const QPoint pos = m_outputs[i].ptr->pos(); if (output->pos().x() < pos.x()) { break; } if (output->pos().x() == pos.x() && output->pos().y() < pos.y()) { break; } i++; } // Set the initial non-normalized position to be the normalized // position plus the current delta. QPoint pos = output->pos(); if (!m_outputs.isEmpty()) { const QPoint delta = m_outputs[0].pos - m_outputs[0].ptr->pos(); pos = output->pos() + delta; } m_outputs.insert(i, Output(output, pos)); connect(output.data(), &KScreen::Output::isPrimaryChanged, this, [this, output](){ 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) { auto it = std::find_if(m_outputs.begin(), m_outputs.end(), [outputId](const Output &output) { return output.ptr->id() == outputId; }); if (it != m_outputs.end()) { const int index = it - m_outputs.begin(); Q_EMIT beginRemoveRows(QModelIndex(), index, index); m_outputs.erase(it); Q_EMIT endRemoveRows(); } } void OutputModel::resetPosition(const Output &output) { if (output.posReset.x() < 0) { // 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.posReset); } } bool OutputModel::setEnabled(int outputIndex, bool enable) { Output &output = m_outputs[outputIndex]; if (output.ptr->isEnabled() == enable) { return false; } output.ptr->setEnabled(enable); if (enable) { resetPosition(output); setResolution(outputIndex, resolutionIndex(output.ptr)); reposition(); } else { output.posReset = output.ptr->pos(); } QModelIndex index = createIndex(outputIndex, 0); Q_EMIT dataChanged(index, index, {EnabledRole}); return true; } inline bool refreshRateCompare(float rate1, float rate2) { return qAbs(rate1 - rate2) < 0.5; } bool OutputModel::setResolution(int outputIndex, int resIndex) { const Output &output = m_outputs[outputIndex]; const auto resolutionList = resolutions(output.ptr); if (resIndex < 0 || resIndex >= resolutionList.size()) { return false; } const QSize size = resolutionList[resIndex]; const float oldRate = output.ptr->currentMode() ? output.ptr->currentMode()->refreshRate() : -1; const auto modes = output.ptr->modes(); auto modeIt = std::find_if(modes.begin(), modes.end(), [size, oldRate](const KScreen::ModePtr &mode) { // TODO: we don't want to compare against old refresh rate if // refresh rate selection is auto. return mode->size() == size && refreshRateCompare(mode->refreshRate(), oldRate); }); if (modeIt == modes.end()) { // New resolution does not support previous refresh rate. // Get the highest one instead. float bestRefreshRate = 0; auto it = modes.begin(); while (it != modes.end()) { if ((*it)->size() == size && (*it)->refreshRate() > bestRefreshRate) { modeIt = it; } it++; } } Q_ASSERT(modeIt != modes.end()); const auto id = (*modeIt)->id(); if (output.ptr->currentModeId() == id) { return false; } output.ptr->setCurrentModeId(id); QModelIndex index = createIndex(outputIndex, 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, {ResolutionIndexRole, SizeRole, RefreshRateIndexRole}); Q_EMIT sizeChanged(); return true; } bool OutputModel::setRefreshRate(int outputIndex, int refIndex) { const Output &output = m_outputs[outputIndex]; const auto rates = refreshRates(output.ptr); if (refIndex < 0 || refIndex >= rates.size()) { return false; } const float refreshRate = rates[refIndex]; const auto modes = output.ptr->modes(); const auto oldMode = output.ptr->currentMode(); auto modeIt = std::find_if(modes.begin(), modes.end(), [oldMode, refreshRate](const KScreen::ModePtr &mode) { // TODO: we don't want to compare against old refresh rate if // refresh rate selection is auto. return mode->size() == oldMode->size() && refreshRateCompare(mode->refreshRate(), refreshRate); }); Q_ASSERT(modeIt != modes.end()); if (refreshRateCompare(oldMode->refreshRate(), (*modeIt)->refreshRate())) { // no change return false; } output.ptr->setCurrentModeId((*modeIt)->id()); QModelIndex index = createIndex(outputIndex, 0); Q_EMIT dataChanged(index, index, {RefreshRateIndexRole}); return true; } bool OutputModel::setRotation(int outputIndex, KScreen::Output::Rotation rotation) { const Output &output = m_outputs[outputIndex]; if (rotation != KScreen::Output::None && rotation != KScreen::Output::Left && rotation != KScreen::Output::Inverted && rotation != KScreen::Output::Right) { return false; } if (output.ptr->rotation() == rotation) { return false; } output.ptr->setRotation(rotation); QModelIndex index = createIndex(outputIndex, 0); Q_EMIT dataChanged(index, index, {RotationRole, SizeRole}); Q_EMIT sizeChanged(); return true; } int OutputModel::resolutionIndex(const KScreen::OutputPtr &output) const { const QSize currentResolution = output->enforcedModeSize(); if (!currentResolution.isValid()) { return 0; } const auto sizes = resolutions(output); const auto it = std::find_if(sizes.begin(), sizes.end(), [currentResolution](const QSize &size) { return size == currentResolution; }); if (it == sizes.end()) { return -1; } return it - sizes.begin(); } int OutputModel::refreshRateIndex(const KScreen::OutputPtr &output) const { if (!output->currentMode()) { return 0; } const auto rates = refreshRates(output); const float currentRate = output->currentMode()->refreshRate(); const auto it = std::find_if(rates.begin(), rates.end(), [currentRate](float rate) { return refreshRateCompare(rate, currentRate); }); if (it == rates.end()) { return 0; } return it - rates.begin(); } QVariantList OutputModel::resolutionsStrings(const KScreen::OutputPtr &output) const { QVariantList ret; for (const QSize &size : resolutions(output)) { const QString text = QString::number(size.width()) + QStringLiteral("x") + QString::number(size.height()); ret << text; } return ret; } QVector OutputModel::resolutions(const KScreen::OutputPtr &output) const { QVector hits; for (const auto &mode : output->modes()) { const QSize size = mode->size(); if (!hits.contains(size)) { hits << size; } } std::sort(hits.begin(), hits.end(), [](const QSize &a, const QSize &b) { if (a.width() > b.width()) { return true; } if (a.width() == b.width() && a.height() > b.height()) { return true; } return false; }); return hits; } QVector OutputModel::refreshRates(const KScreen::OutputPtr &output) const { QVector hits; QSize baseSize; if (output->currentMode()) { baseSize = output->currentMode()->size(); } else if (output->preferredMode()) { baseSize = output->preferredMode()->size(); } if (!baseSize.isValid()) { return hits; } for (const auto &mode : output->modes()) { if (mode->size() != baseSize) { continue; } const float rate = mode->refreshRate(); if (std::find_if(hits.begin(), hits.end(), [rate](float r) { return refreshRateCompare(r, rate); }) != hits.end()) { continue; } hits << rate; } 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()); + ret.append(Utils::outputName(out.ptr)); } } 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); resetPosition(output); } else { const int sourceId = m_outputs[sourceIndex].ptr->id(); if (oldSourceId == sourceId) { // no change return false; } output.ptr->setReplicationSource(sourceId); output.posReset = 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++) { Output &output = m_outputs[i]; if (output.ptr->id() == outputId) { QModelIndex index = createIndex(i, 0); Q_EMIT dataChanged(index, index, {role}); return; } } } bool OutputModel::positionable(const Output &output) const { return output.ptr->isPositionable(); } void OutputModel::reposition() { int x = 0; int y = 0; // Find first valid output. for (const auto &out : m_outputs) { if (positionable(out)) { x = out.ptr->pos().x(); y = out.ptr->pos().y(); break; } } for (int i = 0; i < m_outputs.size(); i++) { if (!positionable(m_outputs[i])) { continue; } const QPoint &cmp = m_outputs[i].ptr->pos(); if (cmp.x() < x) { x = cmp.x(); } if (cmp.y() < y) { y = cmp.y(); } } if (x == 0 && y == 0) { return; } for (int i = 0; i < m_outputs.size(); i++) { auto &out = m_outputs[i]; out.ptr->setPos(out.ptr->pos() - QPoint(x, y)); QModelIndex index = createIndex(i, 0); Q_EMIT dataChanged(index, index, {NormalizedPositionRole}); } m_config->normalizeScreen(); } QPoint OutputModel::originDelta() const { int x = 0; int y = 0; // Find first valid output. for (const auto &out : m_outputs) { if (positionable(out)) { x = out.pos.x(); y = out.pos.y(); break; } } for (int i = 1; i < m_outputs.size(); i++) { if (!positionable(m_outputs[i])) { continue; } const QPoint &cmp = m_outputs[i].pos; if (cmp.x() < x) { x = cmp.x(); } if (cmp.y() < y) { y = cmp.y(); } } return QPoint(x, y); } void OutputModel::updatePositions() { const QPoint delta = originDelta(); for (int i = 0; i < m_outputs.size(); i++) { const auto &out = m_outputs[i]; if (!positionable(out)) { continue; } const QPoint set = out.pos - delta; if (out.ptr->pos() != set) { out.ptr->setPos(set); QModelIndex index = createIndex(i, 0); Q_EMIT dataChanged(index, index, {NormalizedPositionRole}); } } updateOrder(); } void OutputModel::updateOrder() { auto order = m_outputs; std::sort(order.begin(), order.end(), [](const Output &a, const Output &b) { const int xDiff = b.ptr->pos().x() - a.ptr->pos().x(); const int yDiff = b.ptr->pos().y() - a.ptr->pos().y(); if (xDiff > 0) { return true; } if (xDiff == 0 && yDiff > 0) { return true; } return false; }); for (int i = 0; i < order.size(); i++) { for (int j = 0; j < m_outputs.size(); j++) { if (order[i].ptr->id() != m_outputs[j].ptr->id()) { continue; } if (i != j) { beginMoveRows(QModelIndex(), j, j, QModelIndex(), i); m_outputs.remove(j); m_outputs.insert(i, order[i]); endMoveRows(); } 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() { bool changed = false; for (int i = 0; i < m_outputs.size(); i++) { auto &output = m_outputs[i]; if (output.pos == output.ptr->pos()) { continue; } if (!positionable(output)) { continue; } changed = true; auto index = createIndex(i, 0); output.pos = output.ptr->pos(); Q_EMIT dataChanged(index, index, {PositionRole}); } return changed; } bool OutputModel::positionsNormalized() const { // There might be slight deviations because of snapping. return originDelta().manhattanLength() < 5; } const int s_snapArea = 80; bool isVerticalClose(const QRect &rect1, const QRect &rect2) { if (rect2.top() - rect1.bottom() > s_snapArea ) { return false; } if (rect1.top() - rect2.bottom() > s_snapArea ) { return false; } return true; } bool snapToRight(const QRect &target, const QSize &size, QPoint &dest) { if (qAbs(target.right() - dest.x()) < s_snapArea) { // In snap zone for left to right snap. dest.setX(target.right() + 1); return true; } if (qAbs(target.right() - (dest.x() + size.width())) < s_snapArea) { // In snap zone for right to right snap. dest.setX(target.right() - size.width()); return true; } return false; } bool snapToLeft(const QRect &target, const QSize &size, QPoint &dest) { if (qAbs(target.left() - dest.x()) < s_snapArea) { // In snap zone for left to left snap. dest.setX(target.left()); return true; } if (qAbs(target.left() - (dest.x() + size.width())) < s_snapArea) { // In snap zone for right to left snap. dest.setX(target.left() - size.width()); return true; } return false; } bool snapToMiddle(const QRect &target, const QSize &size, QPoint &dest) { const int outputMid = dest.y() + size.height() / 2; const int targetMid = target.top() + target.height() / 2; if (qAbs(targetMid - outputMid) < s_snapArea) { // In snap zone for middle to middle snap. dest.setY(targetMid - size.height() / 2); return true; } return false; } bool snapToTop(const QRect &target, const QSize &size, QPoint &dest) { if (qAbs(target.top() - dest.y()) < s_snapArea) { // In snap zone for bottom to top snap. dest.setY(target.top()); return true; } if (qAbs(target.top() - (dest.y() + size.height())) < s_snapArea) { // In snap zone for top to top snap. dest.setY(target.top() - size.height()); return true; } return false; } bool snapToBottom(const QRect &target, const QSize &size, QPoint &dest) { if (qAbs(target.bottom() - dest.y()) < s_snapArea) { // In snap zone for top to bottom snap. dest.setY(target.bottom() + 1); return true; } if (qAbs(target.bottom() - (dest.y() + size.height())) < s_snapArea) { // In snap zone for bottom to bottom snap. dest.setY(target.bottom() - size.height() + 1); return true; } return false; } bool snapVertical(const QRect &target, const QSize &size, QPoint &dest) { if (snapToMiddle(target, size, dest)) { return true; } if (snapToBottom(target, size, dest)) { return true; } if (snapToTop(target, size, dest)) { return true; } return false; } void OutputModel::snap(const Output &output, QPoint &dest) { const QSize size = output.ptr->geometry().size(); for (const Output &out : m_outputs) { if (out.ptr->id() == output.ptr->id()) { // Can not snap to itself. continue; } if (!positionable(out)) { continue; } const QRect target(out.pos, out.ptr->geometry().size()); if (!isVerticalClose(target, QRect(dest, size))) { continue; } // try snap left to right first if (snapToRight(target, size, dest)) { snapVertical(target, size, dest); continue; } if (snapToLeft(target, size, dest)) { snapVertical(target, size, dest); continue; } if (snapVertical(target, size, dest)) { continue; } } }