diff --git a/kcm/package/contents/ui/Panel.qml b/kcm/package/contents/ui/Panel.qml index c1fa115..69e27de 100644 --- a/kcm/package/contents/ui/Panel.qml +++ b/kcm/package/contents/ui/Panel.qml @@ -1,146 +1,151 @@ /******************************************************************** 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 . *********************************************************************/ import QtQuick 2.9 import QtQuick.Layouts 1.1 import QtQuick.Controls 2.3 as Controls import org.kde.plasma.extras 2.0 as PlasmaExtras import org.kde.kirigami 2.4 as Kirigami import org.kde.private.kcm.kscreen 1.0 as KScreen ColumnLayout { Controls.SwipeView { id: panelView currentIndex: root.selectedOutput onCurrentIndexChanged: root.selectedOutput = Qt.binding(function() { return currentIndex; }); Layout.fillWidth: true Repeater { model: kcm.outputModel OutputPanel {} } } Controls.PageIndicator { id: indicator Layout.alignment: Qt.AlignHCenter visible: count > 1 count: panelView.count currentIndex: panelView.currentIndex interactive: true onCurrentIndexChanged: root.selectedOutput = currentIndex } Kirigami.FormLayout { id: globalSettingsLayout Layout.fillWidth: true Kirigami.Separator { Layout.fillWidth: true Kirigami.FormData.isSection: true } RowLayout { Layout.fillWidth: true Kirigami.FormData.label: i18n("Global scale:") visible: !kcm.perOutputScaling Controls.Slider { id: globalScaleSlider Layout.fillWidth: true from: 1 to: 3 stepSize: 0.25 live: true value: kcm.globalScale onMoved: kcm.globalScale = value; } Controls.SpinBox { id: spinbox + Layout.minimumWidth: Kirigami.Units.gridUnit * 6 + // Because QQC2 SpinBox doesn't natively support decimal step // sizes: https://bugreports.qt.io/browse/QTBUG-67349 - property real factor: 20.0 + property real factor: 16.0 property real realValue: value / factor from : 1.0 * factor to : 3.0 * factor - stepSize: 0.05 * factor + // On X11 We set the increment to this weird value to compensate + // for inherent difficulties with floating-point math and this + // Qt bug: https://bugreports.qt.io/browse/QTBUG-66036 + stepSize: 0.0625 * factor value: kcm.globalScale * factor validator: DoubleValidator { bottom: Math.min(spinbox.from, spinbox.to)*spinbox.factor top: Math.max(spinbox.from, spinbox.to)*spinbox.factor } textFromValue: function(value, locale) { return i18nc("Global scale factor expressed in percentage form", "%1%", parseFloat(value * 1.0 / factor * 100.0)); } valueFromText: function(text, locale) { return Number.fromLocaleString(locale, text) * factor / 100.0 } onValueModified: { kcm.globalScale = realValue; if (kcm.globalScale % 0.25) { weirdScaleFactorMsg.visible = true; } else { weirdScaleFactorMsg.visible = false; } } } } Kirigami.InlineMessage { id: weirdScaleFactorMsg Kirigami.FormData.isSection: true Layout.fillWidth: true - type: Kirigami.MessageType.Warning - text: i18n("Scale factors that are not a multiple of 25% may cause visual glitches in applications. Consider setting the scale factor to a multiple of 25% and adjusting the font size instead.") + type: Kirigami.MessageType.Info + text: i18n("The global scale factor is limited to multiples of 6.25% to minimize visual glitches in applications using the X11 windowing system.") visible: false showCloseButton: true } Controls.ButtonGroup { buttons: retentionSelector.children } ColumnLayout { id: retentionSelector Kirigami.FormData.label: i18n("Save displays' properties:") Kirigami.FormData.buddyFor: globalRetentionRadio spacing: Kirigami.Units.smallSpacing Controls.RadioButton { id: globalRetentionRadio text: i18n("For any display arrangement") checked: !individualRetentionRadio.checked onClicked: kcm.outputRetention = KScreen.Control.Global } Controls.RadioButton { id: individualRetentionRadio text: i18n("For only this specific display arrangement") checked: kcm.outputRetention === KScreen.Control.Individual onClicked: kcm.outputRetention = KScreen.Control.Individual } } } } diff --git a/kded/output.cpp b/kded/output.cpp index 9477ee2..b2d2bb8 100644 --- a/kded/output.cpp +++ b/kded/output.cpp @@ -1,408 +1,408 @@ /******************************************************************** 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.h" #include "config.h" #include "../common/globals.h" #include "kscreen_daemon_debug.h" #include "generator.h" #include #include #include #include #include #include #include #include QString Output::s_dirName = QStringLiteral("outputs/"); QString Output::dirPath() { return Globals::dirPath() % s_dirName; } QString Output::globalFileName(const QString &hash) { const auto dir = dirPath(); if (!QDir().mkpath(dir)) { return QString(); } return dir % hash; } void Output::readInGlobalPartFromInfo(KScreen::OutputPtr output, const QVariantMap &info) { output->setRotation(static_cast(info.value(QStringLiteral("rotation"), 1).toInt())); bool scaleOk; const qreal scale = info.value(QStringLiteral("scale"), 1.).toDouble(&scaleOk); if (scaleOk) { output->setScale(scale); } const QVariantMap modeInfo = info[QStringLiteral("mode")].toMap(); const QVariantMap modeSize = modeInfo[QStringLiteral("size")].toMap(); const QSize size = QSize(modeSize[QStringLiteral("width")].toInt(), modeSize[QStringLiteral("height")].toInt()); qCDebug(KSCREEN_KDED) << "Finding a mode for" << size << "@" << modeInfo[QStringLiteral("refresh")].toFloat(); KScreen::ModeList modes = output->modes(); KScreen::ModePtr matchingMode; for(const KScreen::ModePtr &mode : modes) { if (mode->size() != size) { continue; } if (!qFuzzyCompare(mode->refreshRate(), modeInfo[QStringLiteral("refresh")].toFloat())) { continue; } qCDebug(KSCREEN_KDED) << "\tFound: " << mode->id() << " " << mode->size() << "@" << mode->refreshRate(); matchingMode = mode; break; } if (!matchingMode) { qCWarning(KSCREEN_KDED) << "\tFailed to find a matching mode - this means that our config is corrupted" "or a different device with the same serial number has been connected (very unlikely)." "Falling back to preferred modes."; matchingMode = output->preferredMode(); } if (!matchingMode) { qCWarning(KSCREEN_KDED) << "\tFailed to get a preferred mode, falling back to biggest mode."; matchingMode = Generator::biggestMode(modes); } if (!matchingMode) { qCWarning(KSCREEN_KDED) << "\tFailed to get biggest mode. Which means there are no modes. Turning off the screen."; output->setEnabled(false); return; } output->setCurrentModeId(matchingMode->id()); } QVariantMap Output::getGlobalData(KScreen::OutputPtr output) { QFile file(globalFileName(output->hashMd5())); if (!file.open(QIODevice::ReadOnly)) { qCDebug(KSCREEN_KDED) << "Failed to open file" << file.fileName(); return QVariantMap(); } QJsonDocument parser; return parser.fromJson(file.readAll()).toVariant().toMap(); } bool Output::readInGlobal(KScreen::OutputPtr output) { const QVariantMap info = getGlobalData(output); if (info.empty()) { // if info is empty, the global file does not exists, or is in an unreadable state return false; } readInGlobalPartFromInfo(output, info); return true; } void Output::adjustPositions(KScreen::ConfigPtr config, const QVariantList &outputsInfo) { typedef QPair Out; KScreen::OutputList outputs = config->outputs(); QVector sortedOutputs; // for (const KScreen::OutputPtr output : outputs) { sortedOutputs.append(Out(output->id(), output->pos())); } // go from left to right, top to bottom std::sort(sortedOutputs.begin(), sortedOutputs.end(), [](const Out &o1, const Out &o2) { const int x1 = o1.second.x(); const int x2 = o2.second.x(); return x1 < x2 || (x1 == x2 && o1.second.y() < o2.second.y()); }); for (int cnt = 1; cnt < sortedOutputs.length(); cnt++) { auto getOutputInfoProperties = [outputsInfo](KScreen::OutputPtr output, QRect &geo) -> bool { if (!output) { return false; } const auto hash = output->hash(); auto it = std::find_if(outputsInfo.begin(), outputsInfo.end(), [hash](QVariant v) { const QVariantMap info = v.toMap(); return info[QStringLiteral("id")].toString() == hash; } ); if (it == outputsInfo.end()) { return false; } const QVariantMap outputInfo = it->toMap(); const QVariantMap posInfo = outputInfo[QStringLiteral("pos")].toMap(); const QVariant scaleInfo = outputInfo[QStringLiteral("scale")]; const QVariantMap modeInfo = outputInfo[QStringLiteral("mode")].toMap(); const QVariantMap modeSize = modeInfo[QStringLiteral("size")].toMap(); if (posInfo.isEmpty() || modeSize.isEmpty() || !scaleInfo.canConvert()) { return false; } const qreal scale = scaleInfo.toDouble(); if (scale <= 0) { return false; } const QPoint pos = QPoint(posInfo[QStringLiteral("x")].toInt(), posInfo[QStringLiteral("y")].toInt()); const QSize size = QSize(modeSize[QStringLiteral("width")].toInt() / scale, modeSize[QStringLiteral("height")].toInt() / scale); geo = QRect(pos, size); return true; }; // it's guaranteed that we find the following values in the QMap KScreen::OutputPtr prevPtr = outputs.find(sortedOutputs[cnt - 1].first).value(); KScreen::OutputPtr curPtr = outputs.find(sortedOutputs[cnt].first).value(); QRect prevInfoGeo, curInfoGeo; if (!getOutputInfoProperties(prevPtr, prevInfoGeo) || !getOutputInfoProperties(curPtr, curInfoGeo)) { // no info found, nothing can be adjusted for the next output continue; } const QRect prevGeo = prevPtr->geometry(); const QRect curGeo = curPtr->geometry(); // the old difference between previous and current output read from the config file const int xInfoDiff = curInfoGeo.x() - (prevInfoGeo.x() + prevInfoGeo.width()); // the proposed new difference const int prevRight = prevGeo.x() + prevGeo.width(); const int xCorrected = prevRight + prevGeo.width() * xInfoDiff / (double)prevInfoGeo.width(); const int xDiff = curGeo.x() - prevRight; // In the following calculate the y-coorection. This is more involved since we // differentiate between overlapping and non-overlapping pairs and align either // top to top/bottom or bottom to top/bottom const bool yOverlap = prevInfoGeo.y() + prevInfoGeo.height() > curInfoGeo.y() && prevInfoGeo.y() < curInfoGeo.y() + curInfoGeo.height(); // these values determine which horizontal edge of previous output we align with const int topToTopDiffAbs = qAbs(prevInfoGeo.y() - curInfoGeo.y()); const int topToBottomDiffAbs = qAbs(prevInfoGeo.y() - curInfoGeo.y() - curInfoGeo.height()); const int bottomToBottomDiffAbs = qAbs(prevInfoGeo.y() + prevInfoGeo.height() - curInfoGeo.y() - curInfoGeo.height()); const int bottomToTopDiffAbs = qAbs(prevInfoGeo.y() + prevInfoGeo.height() - curInfoGeo.y()); const bool yTopAligned = topToTopDiffAbs < bottomToBottomDiffAbs && topToTopDiffAbs <= bottomToTopDiffAbs || topToBottomDiffAbs < bottomToBottomDiffAbs; int yInfoDiff = curInfoGeo.y() - prevInfoGeo.y(); int yDiff = curGeo.y() - prevGeo.y(); int yCorrected; if (yTopAligned) { // align to previous top if (!yOverlap) { // align previous top with current bottom yInfoDiff += curInfoGeo.height(); yDiff += curGeo.height(); } // When we align with previous top we are interested in the changes to the // current geometry and not in the ones of the previous one. const double yInfoRel = yInfoDiff / (double)curInfoGeo.height(); yCorrected = prevGeo.y() + yInfoRel * curGeo.height(); } else { // align previous bottom... yInfoDiff -= prevInfoGeo.height(); yDiff -= prevGeo.height(); yCorrected = prevGeo.y() + prevGeo.height(); if (yOverlap) { // ... with current bottom yInfoDiff += curInfoGeo.height(); yDiff += curGeo.height(); yCorrected -= curGeo.height(); } // ... else with current top // When we align with previous bottom we are interested in changes to the // previous geometry. const double yInfoRel = yInfoDiff / (double)prevInfoGeo.height(); yCorrected += yInfoRel * prevGeo.height(); } const int x = xDiff == xInfoDiff ? curGeo.x() : xCorrected; const int y = yDiff == yInfoDiff ? curGeo.y() : yCorrected; curPtr->setPos(QPoint(x, y)); } } void Output::readIn(KScreen::OutputPtr output, const QVariantMap &info, Control::OutputRetention retention) { const QVariantMap posInfo = info[QStringLiteral("pos")].toMap(); QPoint point(posInfo[QStringLiteral("x")].toInt(), posInfo[QStringLiteral("y")].toInt()); output->setPos(point); output->setPrimary(info[QStringLiteral("primary")].toBool()); output->setEnabled(info[QStringLiteral("enabled")].toBool()); if (retention != Control::OutputRetention::Individual && readInGlobal(output)) { // output data read from global output file return; } // output data read directly from info readInGlobalPartFromInfo(output, info); } void Output::readInOutputs(KScreen::ConfigPtr config, const QVariantList &outputsInfo) { KScreen::OutputList outputs = config->outputs(); ControlConfig control(config); // As global outputs are indexed by a hash of their edid, which is not unique, // to be able to tell apart multiple identical outputs, these need special treatment QStringList duplicateIds; { QStringList allIds; allIds.reserve(outputs.count()); for (const KScreen::OutputPtr &output : outputs) { const auto outputId = output->hash(); if (allIds.contains(outputId) && !duplicateIds.contains(outputId)) { duplicateIds << outputId; } allIds << outputId; } allIds.clear(); } for (KScreen::OutputPtr output : outputs) { if (!output->isConnected()) { output->setEnabled(false); continue; } const auto outputId = output->hash(); bool infoFound = false; for (const auto &variantInfo : outputsInfo) { const QVariantMap info = variantInfo.toMap(); if (outputId != info[QStringLiteral("id")].toString()) { continue; } if (!output->name().isEmpty() && duplicateIds.contains(outputId)) { // We may have identical outputs connected, these will have the same id in the config // in order to find the right one, also check the output's name (usually the connector) const auto metadata = info[QStringLiteral("metadata")].toMap(); const auto outputName = metadata[QStringLiteral("name")].toString(); if (output->name() != outputName) { // was a duplicate id, but info not for this output continue; } } infoFound = true; readIn(output, info, control.getOutputRetention(output)); const QString replicationSourceHash = info[QStringLiteral("replicate")].toString(); if (replicationSourceHash.isEmpty()) { output->setReplicationSource(0); } else { for (const KScreen::OutputPtr out : outputs) { if (out != output && out->hashMd5() == replicationSourceHash) { output->setReplicationSource(out->id()); break; } } } break; } if (!infoFound) { // no info in info for this output, try reading in global output info atleast or set some default values qCWarning(KSCREEN_KDED) << "\tFailed to find a matching output in the current info data - this means that our info is corrupted" "or a different device with the same serial number has been connected (very unlikely)."; if (!readInGlobal(output)) { // set some default values instead readInGlobalPartFromInfo(output, QVariantMap()); } } } // correct positional config regressions on global output data changes adjustPositions(config, outputsInfo); } static QVariantMap metadata(const KScreen::OutputPtr &output) { QVariantMap metadata; metadata[QStringLiteral("name")] = output->name(); if (!output->edid() || !output->edid()->isValid()) { return metadata; } metadata[QStringLiteral("fullname")] = output->edid()->deviceId(); return metadata; } bool Output::writeGlobalPart(const KScreen::OutputPtr &output, QVariantMap &info, const KScreen::OutputPtr &fallback) { info[QStringLiteral("id")] = output->hash(); info[QStringLiteral("metadata")] = metadata(output); info[QStringLiteral("rotation")] = output->rotation(); - // Round scale to two digits - info[QStringLiteral("scale")] = int(output->scale() * 100 + 0.5) / 100.; + // Round scale to four digits + info[QStringLiteral("scale")] = int(output->scale() * 10000 + 0.5) / 10000.; QVariantMap modeInfo; float refreshRate = -1.; QSize modeSize; if (output->currentMode() && output->isEnabled()) { refreshRate = output->currentMode()->refreshRate(); modeSize = output->currentMode()->size(); } else if (fallback && fallback->currentMode()) { refreshRate = fallback->currentMode()->refreshRate(); modeSize = fallback->currentMode()->size(); } if (refreshRate < 0 || !modeSize.isValid()) { return false; } modeInfo[QStringLiteral("refresh")] = refreshRate; QVariantMap modeSizeMap; modeSizeMap[QStringLiteral("width")] = modeSize.width(); modeSizeMap[QStringLiteral("height")] = modeSize.height(); modeInfo[QStringLiteral("size")] = modeSizeMap; info[QStringLiteral("mode")] = modeInfo; return true; } void Output::writeGlobal(const KScreen::OutputPtr &output) { // get old values and subsequently override QVariantMap info = getGlobalData(output); if (!writeGlobalPart(output, info, nullptr)) { return; } QFile file(globalFileName(output->hashMd5())); if (!file.open(QIODevice::WriteOnly)) { qCWarning(KSCREEN_KDED) << "Failed to open global output file for writing! " << file.errorString(); return; } file.write(QJsonDocument::fromVariant(info).toJson()); return; }