From ae84480fbfdc684b8ee4b0207d3ce679f6fb4cd7 Mon Sep 17 00:00:00 2001 From: Xaver Hugl Date: Wed, 13 Sep 2023 17:15:04 +0200 Subject: [PATCH] outputconfigurationstore: add new config system Instead of an external service (like KScreen) storing and restoring output configurations, with this commit KWin takes over that responsibility. This allows it to, among other things, generate appropriate configs for new sets of outputs immediately, and take KWin-internal information about outputs into account when generating them. CCBUG: 474021 CCBUG: 469653 CCBUG: 466342 CCBUG: 470863 CCBUG: 466556 BUG: 466208 BUG: 455082 BUG: 457430 --- CMakeLists.txt | 1 + autotests/integration/kwin_wayland_test.cpp | 1 + src/CMakeLists.txt | 1 + src/backends/drm/drm_output.cpp | 5 + src/core/output.cpp | 9 + src/core/output.h | 11 + src/core/outputconfiguration.h | 1 + src/main.cpp | 7 + src/main.h | 1 + src/main_wayland.cpp | 4 - src/outputconfigurationstore.cpp | 729 +++++++++++++++++++- src/outputconfigurationstore.h | 72 +- src/utils/CMakeLists.txt | 1 + src/utils/edid.cpp | 8 + src/utils/edid.h | 8 + src/utils/orientationsensor.cpp | 54 ++ src/utils/orientationsensor.h | 38 + src/wayland/outputdevice_v2.cpp | 35 +- src/wayland/outputdevice_v2.h | 1 + src/wayland/outputmanagement_v2.cpp | 13 +- src/workspace.cpp | 55 +- src/workspace.h | 2 + 22 files changed, 1021 insertions(+), 36 deletions(-) create mode 100644 src/utils/orientationsensor.cpp create mode 100644 src/utils/orientationsensor.h diff --git a/CMakeLists.txt b/CMakeLists.txt index e952c123f8..786763a018 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -56,6 +56,7 @@ find_package(Qt6 ${QT_MIN_VERSION} CONFIG REQUIRED COMPONENTS Quick UiTools Widgets + Sensors ) find_package(Qt6Test ${QT_MIN_VERSION} CONFIG QUIET) diff --git a/autotests/integration/kwin_wayland_test.cpp b/autotests/integration/kwin_wayland_test.cpp index bbfbe51a96..49e4077b29 100644 --- a/autotests/integration/kwin_wayland_test.cpp +++ b/autotests/integration/kwin_wayland_test.cpp @@ -144,6 +144,7 @@ void WaylandTestApplication::performStartup() // try creating the Wayland Backend createInput(); createVirtualInputDevices(); + createTabletModeManager(); WaylandCompositor::create(); createWorkspace(); diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 691dd3f132..1ffcfd414d 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -204,6 +204,7 @@ target_link_libraries(kwin Qt::DBus Qt::Quick Qt::Widgets + Qt::Sensors Wayland::Server KF6::I18n diff --git a/src/backends/drm/drm_output.cpp b/src/backends/drm/drm_output.cpp index 85a3103666..d3bb310d7c 100644 --- a/src/backends/drm/drm_output.cpp +++ b/src/backends/drm/drm_output.cpp @@ -63,6 +63,10 @@ DrmOutput::DrmOutput(const std::shared_ptr &conn) && m_connector->edid() && m_connector->edid()->hdrMetadata() && m_connector->edid()->hdrMetadata()->supportsBT2020) { capabilities |= Capability::WideColorGamut; } + if (conn->isInternal()) { + // TODO only set this if an orientation sensor is available? + capabilities |= Capability::AutoRotation; + } const Edid *edid = conn->edid(); @@ -335,6 +339,7 @@ void DrmOutput::applyQueuedChanges(const std::shared_ptr &props next.highDynamicRange = props->highDynamicRange.value_or(m_state.highDynamicRange); next.sdrBrightness = props->sdrBrightness.value_or(m_state.sdrBrightness); next.wideColorGamut = props->wideColorGamut.value_or(m_state.wideColorGamut); + next.autoRotatePolicy = props->autoRotationPolicy.value_or(m_state.autoRotatePolicy); if (m_state.highDynamicRange != next.highDynamicRange || m_state.sdrBrightness != next.sdrBrightness || m_state.wideColorGamut != next.wideColorGamut) { m_renderLoop->scheduleRepaint(); } diff --git a/src/core/output.cpp b/src/core/output.cpp index 015de6cb22..5b693e03a4 100644 --- a/src/core/output.cpp +++ b/src/core/output.cpp @@ -328,6 +328,7 @@ void Output::applyChanges(const OutputConfiguration &config) next.position = props->pos.value_or(m_state.position); next.scale = props->scale.value_or(m_state.scale); next.rgbRange = props->rgbRange.value_or(m_state.rgbRange); + next.autoRotatePolicy = props->autoRotationPolicy.value_or(m_state.autoRotatePolicy); setState(next); setVrrPolicy(props->vrrPolicy.value_or(vrrPolicy())); @@ -401,6 +402,9 @@ void Output::setState(const State &state) if (oldState.wideColorGamut != state.wideColorGamut) { Q_EMIT wideColorGamutChanged(); } + if (oldState.autoRotatePolicy != state.autoRotatePolicy) { + Q_EMIT autoRotationPolicyChanged(); + } if (oldState.enabled != state.enabled) { Q_EMIT enabledChanged(); } @@ -544,6 +548,11 @@ uint32_t Output::sdrBrightness() const return m_state.sdrBrightness; } +Output::AutoRotationPolicy Output::autoRotationPolicy() const +{ + return m_state.autoRotatePolicy; +} + bool Output::updateCursorLayer() { return false; diff --git a/src/core/output.h b/src/core/output.h index 85c14b10b3..8ddba48256 100644 --- a/src/core/output.h +++ b/src/core/output.h @@ -133,6 +133,7 @@ public: RgbRange = 1 << 3, HighDynamicRange = 1 << 4, WideColorGamut = 1 << 5, + AutoRotation = 1 << 6, }; Q_DECLARE_FLAGS(Capabilities, Capability) @@ -153,6 +154,13 @@ public: }; Q_ENUM(RgbRange) + enum class AutoRotationPolicy { + Never = 0, + InTabletMode, + Always + }; + Q_ENUM(AutoRotationPolicy); + explicit Output(QObject *parent = nullptr); ~Output() override; @@ -299,6 +307,7 @@ public: bool wideColorGamut() const; bool highDynamicRange() const; uint32_t sdrBrightness() const; + AutoRotationPolicy autoRotationPolicy() const; virtual bool setGammaRamp(const std::shared_ptr &transformation); virtual bool setChannelFactors(const QVector3D &rgb); @@ -361,6 +370,7 @@ Q_SIGNALS: void wideColorGamutChanged(); void sdrBrightnessChanged(); void highDynamicRangeChanged(); + void autoRotationPolicyChanged(); protected: struct Information @@ -395,6 +405,7 @@ protected: bool wideColorGamut = false; bool highDynamicRange = false; uint32_t sdrBrightness = 200; + AutoRotationPolicy autoRotatePolicy = AutoRotationPolicy::InTabletMode; }; void setInformation(const Information &information); diff --git a/src/core/outputconfiguration.h b/src/core/outputconfiguration.h index 1996a6cc66..329f5c96fb 100644 --- a/src/core/outputconfiguration.h +++ b/src/core/outputconfiguration.h @@ -32,6 +32,7 @@ public: std::optional highDynamicRange; std::optional sdrBrightness; std::optional wideColorGamut; + std::optional autoRotationPolicy; }; class KWIN_EXPORT OutputConfiguration diff --git a/src/main.cpp b/src/main.cpp index b6a8fe43cb..84646c99ac 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -290,6 +290,11 @@ void Application::createTabletModeManager() m_tabletModeManager = std::make_unique(); } +TabletModeManager *Application::tabletModeManager() const +{ + return m_tabletModeManager.get(); +} + void Application::installNativeX11EventFilter() { installNativeEventFilter(m_eventFilter.get()); @@ -378,6 +383,8 @@ void Application::setXwaylandScale(qreal scale) { if (scale != m_xwaylandScale) { m_xwaylandScale = scale; + // rerun the fonts kcm init that does the appropriate xrdb call with the new settings + QProcess::startDetached("kcminit", {"kcm_fonts", "kcm_style"}); Q_EMIT xwaylandScaleChanged(); } } diff --git a/src/main.h b/src/main.h index 50ab407cce..19b9998d37 100644 --- a/src/main.h +++ b/src/main.h @@ -285,6 +285,7 @@ public: #if KWIN_BUILD_SCREENLOCKER ScreenLockerWatcher *screenLockerWatcher() const; #endif + TabletModeManager *tabletModeManager() const; /** * Starts an interactive window selection process. diff --git a/src/main_wayland.cpp b/src/main_wayland.cpp index 2013b520c8..c844a44c63 100644 --- a/src/main_wayland.cpp +++ b/src/main_wayland.cpp @@ -177,10 +177,6 @@ void ApplicationWayland::refreshSettings(const KConfigGroup &group, const QByteA KDesktopFile file(group.readPathEntry("InputMethod", QString())); kwinApp()->inputMethod()->setInputMethodCommand(file.desktopGroup().readEntry("Exec", QString())); } - - if (m_startXWayland && group.name() == "Xwayland" && names.contains("Scale")) { - setXwaylandScale(group.readEntry("Scale", 1.0)); - } } void ApplicationWayland::startSession() diff --git a/src/outputconfigurationstore.cpp b/src/outputconfigurationstore.cpp index 353c2aa801..23b482de97 100644 --- a/src/outputconfigurationstore.cpp +++ b/src/outputconfigurationstore.cpp @@ -16,45 +16,362 @@ #include "kscreenintegration.h" #include "workspace.h" +#include +#include +#include +#include +#include + namespace KWin { -std::pair> OutputConfigurationStore::queryConfig(const QVector &outputs, bool isLidClosed) +OutputConfigurationStore::OutputConfigurationStore() +{ + load(); +} + +OutputConfigurationStore::~OutputConfigurationStore() +{ + save(); +} + +std::optional, OutputConfigurationStore::ConfigType>> OutputConfigurationStore::queryConfig(const QVector &outputs, bool isLidClosed, QOrientationReading *orientation, bool isTabletMode) +{ + QVector relevantOutputs; + std::copy_if(outputs.begin(), outputs.end(), std::back_inserter(relevantOutputs), [](Output *output) { + return !output->isNonDesktop() && !output->isPlaceholder(); + }); + if (relevantOutputs.isEmpty()) { + return std::nullopt; + } + if (const auto opt = findSetup(relevantOutputs, isLidClosed)) { + const auto &[setup, outputStates] = *opt; + auto [config, order] = setupToConfig(setup, outputStates); + applyOrientationReading(config, relevantOutputs, orientation, isTabletMode); + storeConfig(relevantOutputs, isLidClosed, config, order); + return std::make_tuple(config, order, ConfigType::Preexisting); + } + if (auto kscreenConfig = KScreenIntegration::readOutputConfig(relevantOutputs, KScreenIntegration::connectedOutputsHash(relevantOutputs, isLidClosed))) { + auto &[config, order] = *kscreenConfig; + applyOrientationReading(config, relevantOutputs, orientation, isTabletMode); + storeConfig(relevantOutputs, isLidClosed, config, order); + return std::make_tuple(config, order, ConfigType::Preexisting); + } + auto [config, order] = generateConfig(relevantOutputs, isLidClosed); + applyOrientationReading(config, relevantOutputs, orientation, isTabletMode); + storeConfig(relevantOutputs, isLidClosed, config, order); + return std::make_tuple(config, order, ConfigType::Generated); +} + +void OutputConfigurationStore::applyOrientationReading(OutputConfiguration &config, const QVector &outputs, QOrientationReading *orientation, bool isTabletMode) +{ + if (!isAutoRotateActive(outputs, isTabletMode) || !orientation || orientation->orientation() == QOrientationReading::Orientation::Undefined) { + return; + } + const auto output = std::find_if(outputs.begin(), outputs.end(), [&config](Output *output) { + return output->isInternal() && config.changeSet(output)->enabled.value_or(output->isEnabled()); + }); + if (output == outputs.end()) { + return; + } + // TODO move other outputs to matching positions + const auto changeset = config.changeSet(*output); + switch (orientation->orientation()) { + case QOrientationReading::Orientation::TopUp: + changeset->transform = OutputTransform::Kind::Normal; + return; + case QOrientationReading::Orientation::TopDown: + changeset->transform = OutputTransform::Kind::Rotated180; + return; + case QOrientationReading::Orientation::LeftUp: + changeset->transform = OutputTransform::Kind::Rotated90; + return; + case QOrientationReading::Orientation::RightUp: + changeset->transform = OutputTransform::Kind::Rotated270; + return; + case QOrientationReading::Orientation::FaceUp: + case QOrientationReading::Orientation::FaceDown: + case QOrientationReading::Orientation::Undefined: + return; + } +} + +std::optional>> OutputConfigurationStore::findSetup(const QVector &outputs, bool lidClosed) +{ + std::unordered_map outputStates; + for (Output *output : outputs) { + if (auto opt = findOutput(output, outputs)) { + outputStates[output] = *opt; + } else { + return std::nullopt; + } + } + const auto setup = std::find_if(m_setups.begin(), m_setups.end(), [lidClosed, &outputStates](const auto &setup) { + if (setup.lidClosed != lidClosed || size_t(setup.outputs.size()) != outputStates.size()) { + return false; + } + return std::all_of(outputStates.begin(), outputStates.end(), [&setup](const auto &outputIt) { + return std::any_of(setup.outputs.begin(), setup.outputs.end(), [&outputIt](const auto &outputInfo) { + return outputInfo.outputIndex == outputIt.second; + }); + }); + }); + if (setup == m_setups.end()) { + return std::nullopt; + } else { + return std::make_pair(&(*setup), outputStates); + } +} + +std::optional OutputConfigurationStore::findOutput(Output *output, const QVector &allOutputs) const { - // TODO to make use of isLidClosed, move config writing into KWin - // Currently, the interactions of KWin's config generation on lid close with KScreen's config writing - // causes settings changes that shouldn't be happening - isLidClosed = false; - const auto kscreenConfig = KScreenIntegration::readOutputConfig(outputs, KScreenIntegration::connectedOutputsHash(outputs, isLidClosed)); - if (kscreenConfig) { - return kscreenConfig.value(); + const bool hasDuplicate = std::any_of(allOutputs.begin(), allOutputs.end(), [output](Output *otherOutput) { + return otherOutput != output && otherOutput->edid().identifier() == output->edid().identifier(); + }); + const auto it = std::find_if(m_outputs.begin(), m_outputs.end(), [hasDuplicate, output](const auto &outputState) { + return outputState.edidIdentifier == output->edid().identifier() + && (!hasDuplicate || outputState.connectorName == output->name()); + }); + if (it != m_outputs.end()) { + return std::distance(m_outputs.begin(), it); } else { - // no config file atm -> generate a new one - return generateConfig(outputs, isLidClosed); + return std::nullopt; } } -std::pair> OutputConfigurationStore::generateConfig(const QVector &outputs, bool isLidClosed) const +void OutputConfigurationStore::storeConfig(const QVector &allOutputs, bool isLidClosed, const OutputConfiguration &config, const QVector &outputOrder) +{ + QVector relevantOutputs; + std::copy_if(allOutputs.begin(), allOutputs.end(), std::back_inserter(relevantOutputs), [](Output *output) { + return !output->isNonDesktop() && !output->isPlaceholder(); + }); + if (relevantOutputs.isEmpty()) { + return; + } + const auto opt = findSetup(relevantOutputs, isLidClosed); + Setup *setup = nullptr; + if (opt) { + setup = opt->first; + } else { + m_setups.push_back(Setup{}); + setup = &m_setups.back(); + setup->lidClosed = isLidClosed; + } + for (Output *output : relevantOutputs) { + auto outputIndex = findOutput(output, outputOrder); + if (!outputIndex) { + m_outputs.push_back(OutputState{}); + outputIndex = m_outputs.size() - 1; + } + auto outputIt = std::find_if(setup->outputs.begin(), setup->outputs.end(), [outputIndex](const auto &output) { + return output.outputIndex == outputIndex; + }); + if (outputIt == setup->outputs.end()) { + setup->outputs.push_back(SetupState{}); + outputIt = setup->outputs.end() - 1; + } + if (const auto changeSet = config.constChangeSet(output)) { + std::shared_ptr mode = changeSet->mode.value_or(output->currentMode()).lock(); + if (!mode) { + mode = output->currentMode(); + } + m_outputs[*outputIndex] = OutputState{ + .edidIdentifier = output->edid().identifier(), + .connectorName = output->name(), + .mode = ModeData{ + .size = mode->size(), + .refreshRate = mode->refreshRate(), + }, + .scale = changeSet->scale.value_or(output->scale()), + .transform = changeSet->transform.value_or(output->transform()), + .overscan = changeSet->overscan.value_or(output->overscan()), + .rgbRange = changeSet->rgbRange.value_or(output->rgbRange()), + .vrrPolicy = changeSet->vrrPolicy.value_or(output->vrrPolicy()), + .highDynamicRange = changeSet->highDynamicRange.value_or(output->highDynamicRange()), + .sdrBrightness = changeSet->sdrBrightness.value_or(output->sdrBrightness()), + .wideColorGamut = changeSet->wideColorGamut.value_or(output->wideColorGamut()), + .autoRotation = changeSet->autoRotationPolicy.value_or(output->autoRotationPolicy()), + }; + *outputIt = SetupState{ + .outputIndex = *outputIndex, + .position = changeSet->pos.value_or(output->geometry().topLeft()), + .enabled = changeSet->enabled.value_or(output->isEnabled()), + .priority = int(outputOrder.indexOf(output)), + }; + } else { + const auto mode = output->currentMode(); + m_outputs[*outputIndex] = OutputState{ + .edidIdentifier = output->edid().identifier(), + .connectorName = output->name(), + .mode = ModeData{ + .size = mode->size(), + .refreshRate = mode->refreshRate(), + }, + .scale = output->scale(), + .transform = output->transform(), + .overscan = output->overscan(), + .rgbRange = output->rgbRange(), + .vrrPolicy = output->vrrPolicy(), + .highDynamicRange = output->highDynamicRange(), + .sdrBrightness = output->sdrBrightness(), + .wideColorGamut = output->wideColorGamut(), + .autoRotation = output->autoRotationPolicy(), + }; + *outputIt = SetupState{ + .outputIndex = *outputIndex, + .position = output->geometry().topLeft(), + .enabled = output->isEnabled(), + .priority = int(outputOrder.indexOf(output)), + }; + } + } + save(); +} + +std::pair> OutputConfigurationStore::setupToConfig(Setup *setup, const std::unordered_map &outputMap) const { + OutputConfiguration ret; + QVector> priorities; + for (const auto &[output, outputIndex] : outputMap) { + const OutputState &state = m_outputs[outputIndex]; + const auto &setupState = *std::find_if(setup->outputs.begin(), setup->outputs.end(), [outputIndex = outputIndex](const auto &state) { + return state.outputIndex == outputIndex; + }); + const auto modes = output->modes(); + const auto mode = std::find_if(modes.begin(), modes.end(), [&state](const auto &mode) { + return state.mode + && mode->size() == state.mode->size + && mode->refreshRate() == state.mode->refreshRate; + }); + *ret.changeSet(output) = OutputChangeSet{ + .mode = mode == modes.end() ? std::nullopt : std::optional(*mode), + .enabled = setupState.enabled, + .pos = setupState.position, + .scale = state.scale, + .transform = state.transform, + .overscan = state.overscan, + .rgbRange = state.rgbRange, + .vrrPolicy = state.vrrPolicy, + .highDynamicRange = state.highDynamicRange, + .sdrBrightness = state.sdrBrightness, + .wideColorGamut = state.wideColorGamut, + .autoRotationPolicy = state.autoRotation, + }; + if (setupState.enabled) { + priorities.push_back(std::make_pair(output, setupState.priority)); + } + } + std::sort(priorities.begin(), priorities.end(), [](const auto &left, const auto &right) { + return left.second < right.second; + }); + QVector order; + std::transform(priorities.begin(), priorities.end(), std::back_inserter(order), [](const auto &pair) { + return pair.first; + }); + return std::make_pair(ret, order); +} + +std::optional>> OutputConfigurationStore::generateLidClosedConfig(const QVector &outputs) +{ + const auto internalIt = std::find_if(outputs.begin(), outputs.end(), [](Output *output) { + return output->isInternal(); + }); + if (internalIt == outputs.end()) { + return std::nullopt; + } + const auto setup = findSetup(outputs, false); + if (!setup) { + return std::nullopt; + } + Output *const internalOutput = *internalIt; + auto [config, order] = setupToConfig(setup->first, setup->second); + auto internalChangeset = config.changeSet(internalOutput); + internalChangeset->enabled = false; + order.removeOne(internalOutput); + + const bool anyEnabled = std::any_of(outputs.begin(), outputs.end(), [&config = config](Output *output) { + return config.changeSet(output)->enabled.value_or(output->isEnabled()); + }); + if (!anyEnabled) { + return std::nullopt; + } + + const auto getSize = [](OutputChangeSet *changeset, Output *output) { + auto mode = changeset->mode ? changeset->mode->lock() : nullptr; + if (!mode) { + mode = output->currentMode(); + } + const auto scale = changeset->scale.value_or(output->scale()); + return QSize(std::ceil(mode->size().width() / scale), std::ceil(mode->size().height() / scale)); + }; + const QPoint internalPos = internalChangeset->pos.value_or(internalOutput->geometry().topLeft()); + const QSize internalSize = getSize(internalChangeset.get(), internalOutput); + for (Output *otherOutput : outputs) { + auto changeset = config.changeSet(otherOutput); + QPoint otherPos = changeset->pos.value_or(otherOutput->geometry().topLeft()); + if (otherPos.x() >= internalPos.x() + internalSize.width()) { + otherPos.rx() -= std::floor(internalSize.width()); + } + if (otherPos.y() >= internalPos.y() + internalSize.height()) { + otherPos.ry() -= std::floor(internalSize.height()); + } + // make sure this doesn't make outputs overlap, which is neither supported nor expected by users + const QSize otherSize = getSize(changeset.get(), otherOutput); + const bool overlap = std::any_of(outputs.begin(), outputs.end(), [&, &config = config](Output *output) { + if (otherOutput == output) { + return false; + } + const auto changeset = config.changeSet(output); + const QPoint pos = changeset->pos.value_or(output->geometry().topLeft()); + return QRect(pos, otherSize).intersects(QRect(otherPos, getSize(changeset.get(), output))); + }); + if (!overlap) { + changeset->pos = otherPos; + } + } + return std::make_pair(config, order); +} + +std::pair> OutputConfigurationStore::generateConfig(const QVector &outputs, bool isLidClosed) +{ + if (isLidClosed) { + if (const auto closedConfig = generateLidClosedConfig(outputs)) { + return *closedConfig; + } + } OutputConfiguration ret; QVector outputOrder; QPoint pos(0, 0); for (const auto output : outputs) { - const auto mode = chooseMode(output); - const double scale = chooseScale(output, mode.get()); + const auto outputIndex = findOutput(output, outputs); const bool enable = !isLidClosed || !output->isInternal(); - *ret.changeSet(output) = { + const OutputState existingData = outputIndex ? m_outputs[*outputIndex] : OutputState{}; + + const auto modes = output->modes(); + const auto modeIt = std::find_if(modes.begin(), modes.end(), [&existingData](const auto &mode) { + return existingData.mode + && mode->size() == existingData.mode->size + && mode->refreshRate() == existingData.mode->refreshRate; + }); + const auto mode = modeIt == modes.end() ? output->currentMode() : *modeIt; + + const auto changeset = ret.changeSet(output); + *changeset = { .mode = mode, .enabled = enable, .pos = pos, - .scale = scale, - .transform = output->panelOrientation(), - .overscan = 0, - .rgbRange = Output::RgbRange::Automatic, - .vrrPolicy = RenderLoop::VrrPolicy::Automatic, + .scale = existingData.scale.value_or(chooseScale(output, mode.get())), + .transform = existingData.transform.value_or(output->panelOrientation()), + .overscan = existingData.overscan.value_or(0), + .rgbRange = existingData.rgbRange.value_or(Output::RgbRange::Automatic), + .vrrPolicy = existingData.vrrPolicy.value_or(RenderLoop::VrrPolicy::Automatic), + .highDynamicRange = existingData.highDynamicRange.value_or(false), + .sdrBrightness = existingData.sdrBrightness.value_or(200), + .wideColorGamut = existingData.wideColorGamut.value_or(false), + .autoRotationPolicy = existingData.autoRotation.value_or(Output::AutoRotationPolicy::InTabletMode), }; - pos.setX(pos.x() + mode->size().width() / scale); if (enable) { + pos.setX(std::ceil(pos.x() + changeset->mode.value_or(output->currentMode()).lock()->size().width() / changeset->scale.value_or(output->scale()))); outputOrder.push_back(output); } } @@ -150,4 +467,376 @@ double OutputConfigurationStore::targetDpi(Output *output) const } } +void OutputConfigurationStore::load() +{ + const QString jsonPath = QStandardPaths::locate(QStandardPaths::ConfigLocation, QStringLiteral("kwinoutputconfig.json")); + if (jsonPath.isEmpty()) { + return; + } + + QFile f(jsonPath); + if (!f.open(QIODevice::ReadOnly)) { + qCWarning(KWIN_CORE) << "Could not open file" << jsonPath; + return; + } + QJsonParseError error; + const auto doc = QJsonDocument::fromJson(f.readAll(), &error); + if (error.error != QJsonParseError::NoError) { + qCWarning(KWIN_CORE) << "Failed to parse" << jsonPath << error.errorString(); + return; + } + const auto array = doc.array(); + std::vector objects; + std::transform(array.begin(), array.end(), std::back_inserter(objects), [](const auto &json) { + return json.toObject(); + }); + const auto outputsIt = std::find_if(objects.begin(), objects.end(), [](const auto &obj) { + return obj["name"].toString() == "outputs" && obj["data"].isArray(); + }); + const auto setupsIt = std::find_if(objects.begin(), objects.end(), [](const auto &obj) { + return obj["name"].toString() == "setups" && obj["data"].isArray(); + }); + if (outputsIt == objects.end() || setupsIt == objects.end()) { + return; + } + const auto outputs = (*outputsIt)["data"].toArray(); + + std::vector> outputDatas; + for (const auto &output : outputs) { + const auto data = output.toObject(); + OutputState state; + bool hasIdentifier = false; + if (const auto it = data.find("edidIdentifier"); it != data.end()) { + if (const auto str = it->toString(); !str.isEmpty()) { + state.edidIdentifier = str; + hasIdentifier = true; + } + } + if (const auto it = data.find("connectorName"); it != data.end()) { + if (const auto str = it->toString(); !str.isEmpty()) { + state.connectorName = str; + hasIdentifier = true; + } + } + if (!hasIdentifier) { + // without an identifier the settings are useless + // we still have to push something into the list so that the indices stay correct + outputDatas.push_back(std::nullopt); + continue; + } + if (const auto it = data.find("mode"); it != data.end()) { + const auto obj = it->toObject(); + const int width = obj["width"].toInt(0); + const int height = obj["height"].toInt(0); + const int refreshRate = obj["refreshRate"].toInt(0); + if (width > 0 && height > 0 && refreshRate > 0) { + state.mode = ModeData{ + .size = QSize(width, height), + .refreshRate = uint32_t(refreshRate), + }; + } + } + if (const auto it = data.find("scale"); it != data.end()) { + const double scale = it->toDouble(0); + if (scale > 0 && scale <= 3) { + state.scale = scale; + } + } + if (const auto it = data.find("transform"); it != data.end()) { + const auto str = it->toString(); + if (str == "Normal") { + state.transform = OutputTransform::Kind::Normal; + } else if (str == "Rotated90") { + state.transform = OutputTransform::Kind::Rotated90; + } else if (str == "Rotated180") { + state.transform = OutputTransform::Kind::Rotated180; + } else if (str == "Rotated270") { + state.transform = OutputTransform::Kind::Rotated270; + } + } + if (const auto it = data.find("overscan"); it != data.end()) { + const int overscan = it->toInt(-1); + if (overscan >= 0 && overscan <= 100) { + state.overscan = overscan; + } + } + if (const auto it = data.find("rgbRange"); it != data.end()) { + const auto str = it->toString(); + if (str == "Automatic") { + state.rgbRange = Output::RgbRange::Automatic; + } else if (str == "Limited") { + state.rgbRange = Output::RgbRange::Limited; + } else if (str == "Full") { + state.rgbRange = Output::RgbRange::Full; + } + } + if (const auto it = data.find("vrrPolicy"); it != data.end()) { + const auto str = it->toString(); + if (str == "Never") { + state.vrrPolicy = RenderLoop::VrrPolicy::Never; + } else if (str == "Automatic") { + state.vrrPolicy = RenderLoop::VrrPolicy::Automatic; + } else if (str == "Always") { + state.vrrPolicy = RenderLoop::VrrPolicy::Always; + } + } + if (const auto it = data.find("highDynamicRange"); it != data.end() && it->isBool()) { + state.highDynamicRange = it->toBool(); + } + if (const auto it = data.find("sdrBrightness"); it != data.end() && it->isDouble()) { + state.sdrBrightness = it->toInt(200); + } + if (const auto it = data.find("wideColorGamut"); it != data.end() && it->isBool()) { + state.wideColorGamut = it->toBool(); + } + if (const auto it = data.find("autoRotation"); it != data.end()) { + const auto str = it->toString(); + if (str == "Never") { + state.autoRotation = Output::AutoRotationPolicy::Never; + } else if (str == "InTabletMode") { + state.autoRotation = Output::AutoRotationPolicy::InTabletMode; + } else if (str == "Always") { + state.autoRotation = Output::AutoRotationPolicy::Always; + } + } + outputDatas.push_back(state); + } + + const auto setups = (*setupsIt)["data"].toArray(); + for (const auto &s : setups) { + const auto data = s.toObject(); + const auto outputs = data["outputs"].toArray(); + Setup setup; + bool fail = false; + for (const auto &output : outputs) { + const auto outputData = output.toObject(); + SetupState state; + if (const auto it = outputData.find("enabled"); it != outputData.end() && it->isBool()) { + state.enabled = it->toBool(); + } else { + fail = true; + break; + } + if (const auto it = outputData.find("outputIndex"); it != outputData.end()) { + const int index = it->toInt(-1); + if (index <= -1 || size_t(index) >= outputDatas.size()) { + fail = true; + break; + } + // the outputs must be unique + const bool unique = std::none_of(setup.outputs.begin(), setup.outputs.end(), [&index](const auto &output) { + return output.outputIndex == size_t(index); + }); + if (!unique) { + fail = true; + break; + } + state.outputIndex = index; + } + if (const auto it = outputData.find("position"); it != outputData.end()) { + const auto obj = it->toObject(); + const auto x = obj.find("x"); + const auto y = obj.find("y"); + if (x == obj.end() || !x->isDouble() || y == obj.end() || !y->isDouble()) { + fail = true; + break; + } + state.position = QPoint(x->toInt(0), y->toInt(0)); + } else { + fail = true; + break; + } + if (const auto it = outputData.find("priority"); it != outputData.end()) { + state.priority = it->toInt(-1); + if (state.priority < 0 && state.enabled) { + fail = true; + break; + } + } + setup.outputs.push_back(state); + } + if (fail || setup.outputs.empty()) { + continue; + } + setup.lidClosed = data["lidClosed"].toBool(false); + // there must be only one setup that refers to a given set of outputs + const bool alreadyExists = std::any_of(m_setups.begin(), m_setups.end(), [&setup](const auto &other) { + if (setup.lidClosed != other.lidClosed || setup.outputs.size() != other.outputs.size()) { + return false; + } + return std::all_of(setup.outputs.begin(), setup.outputs.end(), [&other](const auto &output) { + return std::any_of(other.outputs.begin(), other.outputs.end(), [&output](const auto &otherOutput) { + return output.outputIndex == otherOutput.outputIndex; + }); + }); + }); + if (alreadyExists) { + continue; + } + m_setups.push_back(setup); + } + + // repair the outputs list in case it's broken + for (size_t i = 0; i < outputDatas.size(); i++) { + if (!outputDatas[i]) { + for (auto setupIt = m_setups.begin(); setupIt != m_setups.end();) { + const bool broken = std::any_of(setupIt->outputs.begin(), setupIt->outputs.end(), [i](const auto &output) { + return output.outputIndex == i; + }); + if (broken) { + setupIt = m_setups.erase(setupIt); + continue; + } + for (auto &output : setupIt->outputs) { + if (output.outputIndex > i) { + output.outputIndex--; + } + } + setupIt++; + } + outputDatas.erase(outputDatas.begin() + i); + } + } + + for (const auto &o : outputDatas) { + Q_ASSERT(o); + m_outputs.push_back(*o); + } +} + +void OutputConfigurationStore::save() +{ + QJsonDocument document; + QJsonArray array; + QJsonObject outputs; + outputs["name"] = "outputs"; + QJsonArray outputsData; + for (const auto &output : m_outputs) { + QJsonObject o; + if (output.edidIdentifier) { + o["edidIdentifier"] = *output.edidIdentifier; + } + if (output.connectorName) { + o["connectorName"] = *output.connectorName; + } + if (output.mode) { + QJsonObject mode; + mode["width"] = output.mode->size.width(); + mode["height"] = output.mode->size.height(); + mode["refreshRate"] = int(output.mode->refreshRate); + o["mode"] = mode; + } + if (output.scale) { + o["scale"] = *output.scale; + } + if (output.transform == OutputTransform::Kind::Normal) { + o["transform"] = "Normal"; + } else if (output.transform == OutputTransform::Kind::Rotated90) { + o["transform"] = "Rotated90"; + } else if (output.transform == OutputTransform::Kind::Rotated180) { + o["transform"] = "Rotated180"; + } else if (output.transform == OutputTransform::Kind::Rotated270) { + o["transform"] = "Rotated270"; + } + if (output.overscan) { + o["overscan"] = int(*output.overscan); + } + if (output.rgbRange == Output::RgbRange::Automatic) { + o["rgbRange"] = "Automatic"; + } else if (output.rgbRange == Output::RgbRange::Limited) { + o["rgbRange"] = "Limited"; + } else if (output.rgbRange == Output::RgbRange::Full) { + o["rgbRange"] = "Full"; + } + if (output.vrrPolicy == RenderLoop::VrrPolicy::Never) { + o["vrrPolicy"] = "Never"; + } else if (output.vrrPolicy == RenderLoop::VrrPolicy::Automatic) { + o["vrrPolicy"] = "Automatic"; + } else if (output.vrrPolicy == RenderLoop::VrrPolicy::Always) { + o["vrrPolicy"] = "Always"; + } + if (output.highDynamicRange) { + o["highDynamicRange"] = *output.highDynamicRange; + } + if (output.sdrBrightness) { + o["sdrBrightness"] = int(*output.sdrBrightness); + } + if (output.wideColorGamut) { + o["wideColorGamut"] = *output.wideColorGamut; + } + if (output.autoRotation) { + switch (*output.autoRotation) { + case Output::AutoRotationPolicy::Never: + o["autoRotation"] = "Never"; + break; + case Output::AutoRotationPolicy::InTabletMode: + o["autoRotation"] = "InTabletMode"; + break; + case Output::AutoRotationPolicy::Always: + o["autoRotation"] = "Always"; + break; + } + } + outputsData.append(o); + } + outputs["data"] = outputsData; + array.append(outputs); + + QJsonObject setups; + setups["name"] = "setups"; + QJsonArray setupData; + for (const auto &setup : m_setups) { + QJsonObject o; + o["lidClosed"] = setup.lidClosed; + QJsonArray outputs; + for (ssize_t i = 0; i < setup.outputs.size(); i++) { + const auto &output = setup.outputs[i]; + QJsonObject o; + o["enabled"] = output.enabled; + o["outputIndex"] = int(output.outputIndex); + o["priority"] = output.priority; + QJsonObject pos; + pos["x"] = output.position.x(); + pos["y"] = output.position.y(); + o["position"] = pos; + + outputs.append(o); + } + o["outputs"] = outputs; + + setupData.append(o); + } + setups["data"] = setupData; + array.append(setups); + + const QString path = QStandardPaths::writableLocation(QStandardPaths::ConfigLocation) + "/kwinoutputconfig.json"; + QFile f(path); + if (!f.open(QIODevice::WriteOnly)) { + qCWarning(KWIN_CORE, "Couldn't open output config file %s", qPrintable(path)); + return; + } + document.setArray(array); + f.write(document.toJson()); + f.flush(); +} + +bool OutputConfigurationStore::isAutoRotateActive(const QVector &outputs, bool isTabletMode) const +{ + const auto internalIt = std::find_if(outputs.begin(), outputs.end(), [](Output *output) { + return output->isInternal() && output->isEnabled(); + }); + if (internalIt == outputs.end()) { + return false; + } + Output *internal = *internalIt; + switch (internal->autoRotationPolicy()) { + case Output::AutoRotationPolicy::Never: + return false; + case Output::AutoRotationPolicy::InTabletMode: + return isTabletMode; + case Output::AutoRotationPolicy::Always: + return true; + } + Q_UNREACHABLE(); +} } diff --git a/src/outputconfigurationstore.h b/src/outputconfigurationstore.h index 12ab8ccbd4..19638db146 100644 --- a/src/outputconfigurationstore.h +++ b/src/outputconfigurationstore.h @@ -8,25 +8,89 @@ */ #pragma once +#include "core/output.h" + +#include +#include #include #include +#include +#include +#include + +class QOrientationReading; namespace KWin { class OutputConfiguration; -class Output; -class OutputMode; class OutputConfigurationStore { public: - std::pair> queryConfig(const QVector &outputs, bool isLidClosed); + OutputConfigurationStore(); + ~OutputConfigurationStore(); + + enum class ConfigType { + Preexisting, + Generated, + }; + std::optional, ConfigType>> queryConfig(const QVector &outputs, bool isLidClosed, QOrientationReading *orientation, bool isTabletMode); + + void storeConfig(const QVector &allOutputs, bool isLidClosed, const OutputConfiguration &config, const QVector &outputOrder); + + bool isAutoRotateActive(const QVector &outputs, bool isTabletMode) const; private: - std::pair> generateConfig(const QVector &outputs, bool isLidClosed) const; + void applyOrientationReading(OutputConfiguration &config, const QVector &outputs, QOrientationReading *orientation, bool isTabletMode); + std::optional>> generateLidClosedConfig(const QVector &outputs); + std::pair> generateConfig(const QVector &outputs, bool isLidClosed); std::shared_ptr chooseMode(Output *output) const; double chooseScale(Output *output, OutputMode *mode) const; double targetDpi(Output *output) const; + void load(); + void save(); + + struct ModeData + { + QSize size; + uint32_t refreshRate; + }; + struct OutputState + { + // identification data + std::optional edidIdentifier; + std::optional connectorName; + // actual state + std::optional mode; + std::optional scale; + std::optional transform; + std::optional overscan; + std::optional rgbRange; + std::optional vrrPolicy; + std::optional highDynamicRange; + std::optional sdrBrightness; + std::optional wideColorGamut; + std::optional autoRotation; + }; + struct SetupState + { + size_t outputIndex; + QPoint position; + bool enabled; + int priority; + }; + struct Setup + { + bool lidClosed = false; + QVector outputs; + }; + + std::pair> setupToConfig(Setup *setup, const std::unordered_map &outputMap) const; + std::optional>> findSetup(const QVector &outputs, bool lidClosed); + std::optional findOutput(Output *output, const QVector &allOutputs) const; + + QVector m_outputs; + QVector m_setups; }; } diff --git a/src/utils/CMakeLists.txt b/src/utils/CMakeLists.txt index 0a32598992..519c6aeaf7 100644 --- a/src/utils/CMakeLists.txt +++ b/src/utils/CMakeLists.txt @@ -4,6 +4,7 @@ target_sources(kwin PRIVATE edid.cpp egl_context_attribute_builder.cpp filedescriptor.cpp + orientationsensor.cpp ramfile.cpp realtime.cpp softwarevsyncmonitor.cpp diff --git a/src/utils/edid.cpp b/src/utils/edid.cpp index 88ff0e4874..e0abf42853 100644 --- a/src/utils/edid.cpp +++ b/src/utils/edid.cpp @@ -120,6 +120,9 @@ Edid::Edid(const void *data, uint32_t size) hash.addData(m_raw); m_hash = QString::fromLatin1(hash.result().toHex()); + m_identifier = QByteArray(productInfo->manufacturer, 3) + " " + QByteArray::number(productInfo->product) + " " + QByteArray::number(productInfo->serial) + " " + + QByteArray::number(productInfo->manufacture_week) + " " + QByteArray::number(productInfo->manufacture_year) + " " + QByteArray::number(productInfo->model_year); + // colorimetry and HDR metadata const auto chromaticity = di_edid_get_chromaticity_coords(edid); if (chromaticity) { @@ -252,4 +255,9 @@ std::optional Edid::hdrMetadata() const return m_hdrMetadata; } +QByteArray Edid::identifier() const +{ + return m_identifier; +} + } // namespace KWin diff --git a/src/utils/edid.h b/src/utils/edid.h index e4cd0fddb9..113d1fe5a4 100644 --- a/src/utils/edid.h +++ b/src/utils/edid.h @@ -92,6 +92,12 @@ public: }; std::optional hdrMetadata() const; + /** + * @returns a string that is intended to identify the monitor uniquely. + * Note that multiple monitors can have the same EDID, so this is not always actually unique + */ + QByteArray identifier() const; + private: QSize m_physicalSize; QByteArray m_vendor; @@ -102,6 +108,8 @@ private: Colorimetry m_colorimetry; std::optional m_hdrMetadata; + QByteArray m_identifier; + QByteArray m_raw; bool m_isValid = false; }; diff --git a/src/utils/orientationsensor.cpp b/src/utils/orientationsensor.cpp new file mode 100644 index 0000000000..d3b174ff49 --- /dev/null +++ b/src/utils/orientationsensor.cpp @@ -0,0 +1,54 @@ +/* + SPDX-FileCopyrightText: 2019 Roman Gilg + SPDX-FileCopyrightText: 2023 Xaver Hugl + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "orientationsensor.h" + +#include + +namespace KWin +{ + +OrientationSensor::OrientationSensor() + : m_sensor(std::make_unique()) + , m_reading(std::make_unique()) +{ + m_reading->setOrientation(QOrientationReading::Orientation::Undefined); +} + +OrientationSensor::~OrientationSensor() = default; + +void OrientationSensor::setEnabled(bool enable) +{ + if (enable) { + connect(m_sensor.get(), &QOrientationSensor::readingChanged, this, &OrientationSensor::update, Qt::UniqueConnection); + m_sensor->start(); + } else { + disconnect(m_sensor.get(), &QOrientationSensor::readingChanged, this, &OrientationSensor::update); + m_reading->setOrientation(QOrientationReading::Undefined); + } +} + +QOrientationReading *OrientationSensor::reading() const +{ + return m_reading.get(); +} + +void OrientationSensor::update() +{ + if (auto reading = m_sensor->reading()) { + if (m_reading->orientation() != reading->orientation()) { + m_reading->setOrientation(reading->orientation()); + Q_EMIT orientationChanged(); + } + } else if (m_reading->orientation() != QOrientationReading::Orientation::Undefined) { + m_reading->setOrientation(QOrientationReading::Orientation::Undefined); + Q_EMIT orientationChanged(); + } +} + +} + +#include "moc_orientationsensor.cpp" diff --git a/src/utils/orientationsensor.h b/src/utils/orientationsensor.h new file mode 100644 index 0000000000..112646dbd5 --- /dev/null +++ b/src/utils/orientationsensor.h @@ -0,0 +1,38 @@ +/* + SPDX-FileCopyrightText: 2019 Roman Gilg + SPDX-FileCopyrightText: 2023 Xaver Hugl + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#pragma once + +#include +#include + +class QOrientationSensor; +class QOrientationReading; + +namespace KWin +{ + +class OrientationSensor : public QObject +{ + Q_OBJECT +public: + explicit OrientationSensor(); + ~OrientationSensor(); + + void setEnabled(bool enable); + QOrientationReading *reading() const; + +Q_SIGNALS: + void orientationChanged(); + +private: + void update(); + + const std::unique_ptr m_sensor; + const std::unique_ptr m_reading; +}; + +} diff --git a/src/wayland/outputdevice_v2.cpp b/src/wayland/outputdevice_v2.cpp index 0239326637..03b116671d 100644 --- a/src/wayland/outputdevice_v2.cpp +++ b/src/wayland/outputdevice_v2.cpp @@ -22,7 +22,7 @@ namespace KWin { -static const quint32 s_version = 3; +static const quint32 s_version = 4; static QtWaylandServer::kde_output_device_v2::transform kwinTransformToOutputDeviceTransform(OutputTransform transform) { @@ -52,6 +52,9 @@ static uint32_t kwinCapabilitiesToOutputDeviceCapabilities(Output::Capabilities if (caps & Output::Capability::WideColorGamut) { ret |= QtWaylandServer::kde_output_device_v2::capability_wide_color_gamut; } + if (caps & Output::Capability::AutoRotation) { + ret |= QtWaylandServer::kde_output_device_v2::capability_auto_rotate; + } return ret; } @@ -65,6 +68,11 @@ static QtWaylandServer::kde_output_device_v2::rgb_range kwinRgbRangeToOutputDevi return static_cast(range); } +static QtWaylandServer::kde_output_device_v2::auto_rotate_policy kwinAutoRotationToOutputDeviceAutoRotation(Output::AutoRotationPolicy policy) +{ + return static_cast(policy); +} + class OutputDeviceV2InterfacePrivate : public QtWaylandServer::kde_output_device_v2 { public: @@ -89,6 +97,7 @@ public: void sendHighDynamicRange(Resource *resource); void sendSdrBrightness(Resource *resource); void sendWideColorGamut(Resource *resource); + void sendAutoRotationPolicy(Resource *resource); OutputDeviceV2Interface *q; QPointer m_display; @@ -115,6 +124,7 @@ public: bool m_highDynamicRange = false; uint32_t m_sdrBrightness = 200; bool m_wideColorGamut = false; + auto_rotate_policy m_autoRotation = auto_rotate_policy::auto_rotate_policy_in_tablet_mode; protected: void kde_output_device_v2_bind_resource(Resource *resource) override; @@ -195,6 +205,7 @@ OutputDeviceV2Interface::OutputDeviceV2Interface(Display *display, Output *handl updateHighDynamicRange(); updateSdrBrightness(); updateWideColorGamut(); + updateAutoRotate(); connect(handle, &Output::geometryChanged, this, &OutputDeviceV2Interface::updateGlobalPosition); @@ -219,6 +230,7 @@ OutputDeviceV2Interface::OutputDeviceV2Interface(Display *display, Output *handl connect(handle, &Output::highDynamicRangeChanged, this, &OutputDeviceV2Interface::updateHighDynamicRange); connect(handle, &Output::sdrBrightnessChanged, this, &OutputDeviceV2Interface::updateSdrBrightness); connect(handle, &Output::wideColorGamutChanged, this, &OutputDeviceV2Interface::updateWideColorGamut); + connect(handle, &Output::autoRotationPolicyChanged, this, &OutputDeviceV2Interface::updateAutoRotate); } OutputDeviceV2Interface::~OutputDeviceV2Interface() @@ -272,6 +284,7 @@ void OutputDeviceV2InterfacePrivate::kde_output_device_v2_bind_resource(Resource sendHighDynamicRange(resource); sendSdrBrightness(resource); sendWideColorGamut(resource); + sendAutoRotationPolicy(resource); sendDone(resource); } @@ -390,6 +403,13 @@ void OutputDeviceV2InterfacePrivate::sendWideColorGamut(Resource *resource) } } +void OutputDeviceV2InterfacePrivate::sendAutoRotationPolicy(Resource *resource) +{ + if (resource->version() >= KDE_OUTPUT_DEVICE_V2_AUTO_ROTATE_POLICY_SINCE_VERSION) { + send_auto_rotate_policy(resource->handle, m_autoRotation); + } +} + void OutputDeviceV2Interface::updateGeometry() { const auto clientResources = d->resourceMap(); @@ -646,6 +666,19 @@ void OutputDeviceV2Interface::updateWideColorGamut() } } +void OutputDeviceV2Interface::updateAutoRotate() +{ + const auto policy = kwinAutoRotationToOutputDeviceAutoRotation(d->m_handle->autoRotationPolicy()); + if (d->m_autoRotation != policy) { + d->m_autoRotation = policy; + const auto clientResources = d->resourceMap(); + for (const auto &resource : clientResources) { + d->sendAutoRotationPolicy(resource); + d->sendDone(resource); + } + } +} + OutputDeviceV2Interface *OutputDeviceV2Interface::get(wl_resource *native) { if (auto devicePrivate = resource_cast(native); devicePrivate && !devicePrivate->isGlobalRemoved()) { diff --git a/src/wayland/outputdevice_v2.h b/src/wayland/outputdevice_v2.h index 37884f9c0b..95ce24d1cc 100644 --- a/src/wayland/outputdevice_v2.h +++ b/src/wayland/outputdevice_v2.h @@ -72,6 +72,7 @@ private: void updateHighDynamicRange(); void updateSdrBrightness(); void updateWideColorGamut(); + void updateAutoRotate(); std::unique_ptr d; }; diff --git a/src/wayland/outputmanagement_v2.cpp b/src/wayland/outputmanagement_v2.cpp index 4e617f3087..fd3f2046d7 100644 --- a/src/wayland/outputmanagement_v2.cpp +++ b/src/wayland/outputmanagement_v2.cpp @@ -23,7 +23,7 @@ namespace KWin { -static const quint32 s_version = 4; +static const quint32 s_version = 5; class OutputManagementV2InterfacePrivate : public QtWaylandServer::kde_output_management_v2 { @@ -62,6 +62,7 @@ protected: void kde_output_configuration_v2_set_high_dynamic_range(Resource *resource, wl_resource *outputdevice, uint32_t enable_hdr) override; void kde_output_configuration_v2_set_sdr_brightness(Resource *resource, wl_resource *outputdevice, uint32_t sdr_brightness) override; void kde_output_configuration_v2_set_wide_color_gamut(Resource *resource, wl_resource *outputdevice, uint32_t enable_wcg) override; + void kde_output_configuration_v2_set_auto_rotate_policy(Resource *resource, wl_resource *outputdevice, uint32_t auto_rotation_policy) override; }; OutputManagementV2InterfacePrivate::OutputManagementV2InterfacePrivate(Display *display) @@ -272,6 +273,16 @@ void OutputConfigurationV2Interface::kde_output_configuration_v2_set_wide_color_ } } +void OutputConfigurationV2Interface::kde_output_configuration_v2_set_auto_rotate_policy(Resource *resource, wl_resource *outputdevice, uint32_t auto_rotation_policy) +{ + if (invalid) { + return; + } + if (OutputDeviceV2Interface *output = OutputDeviceV2Interface::get(outputdevice)) { + config.changeSet(output->handle())->autoRotationPolicy = static_cast(auto_rotation_policy); + } +} + void OutputConfigurationV2Interface::kde_output_configuration_v2_destroy(Resource *resource) { wl_resource_destroy(resource->handle); diff --git a/src/workspace.cpp b/src/workspace.cpp index d2fd2d6bd6..ae7e8208db 100644 --- a/src/workspace.cpp +++ b/src/workspace.cpp @@ -52,9 +52,11 @@ #include "placeholderinputeventfilter.h" #include "placeholderoutput.h" #include "placementtracker.h" +#include "tabletmodemanager.h" #include "tiles/tilemanager.h" #include "useractions.h" #include "utils/kernel.h" +#include "utils/orientationsensor.h" #include "utils/xcbutils.h" #include "virtualdesktops.h" #include "was_user_interaction_x11_filter.h" @@ -104,6 +106,7 @@ Workspace::Workspace() , m_placementTracker(std::make_unique(this)) , m_outputConfigStore(std::make_unique()) , m_lidSwitchTracker(std::make_unique()) + , m_orientationSensor(std::make_unique()) { _self = this; @@ -231,10 +234,18 @@ void Workspace::init() connect(this, &Workspace::windowRemoved, m_placementTracker.get(), &PlacementTracker::remove); m_placementTracker->init(getPlacementTrackerHash()); - connect(m_lidSwitchTracker.get(), &LidSwitchTracker::lidStateChanged, this, [this]() { - const auto [config, order] = m_outputConfigStore->queryConfig(kwinApp()->outputBackend()->outputs(), m_lidSwitchTracker->isLidClosed()); - applyOutputConfiguration(config, order); - }); + const auto applySensorChanges = [this]() { + m_orientationSensor->setEnabled(m_outputConfigStore->isAutoRotateActive(kwinApp()->outputBackend()->outputs(), kwinApp()->tabletModeManager()->effectiveTabletMode())); + const auto opt = m_outputConfigStore->queryConfig(kwinApp()->outputBackend()->outputs(), m_lidSwitchTracker->isLidClosed(), m_orientationSensor->reading(), kwinApp()->tabletModeManager()->effectiveTabletMode()); + if (opt) { + const auto &[config, order, type] = *opt; + applyOutputConfiguration(config, order); + } + }; + connect(m_lidSwitchTracker.get(), &LidSwitchTracker::lidStateChanged, this, applySensorChanges); + connect(m_orientationSensor.get(), &OrientationSensor::orientationChanged, this, applySensorChanges); + connect(kwinApp()->tabletModeManager(), &TabletModeManager::tabletModeChanged, this, applySensorChanges); + m_orientationSensor->setEnabled(m_outputConfigStore->isAutoRotateActive(kwinApp()->outputBackend()->outputs(), kwinApp()->tabletModeManager()->effectiveTabletMode())); } QString Workspace::getPlacementTrackerHash() @@ -462,6 +473,21 @@ bool Workspace::applyOutputConfiguration(const OutputConfiguration &config, cons return false; } updateOutputs(outputOrder); + m_outputConfigStore->storeConfig(kwinApp()->outputBackend()->outputs(), m_lidSwitchTracker->isLidClosed(), config, outputOrder); + KConfig cfg(QStringLiteral("kdeglobals")); + KConfigGroup kscreenGroup = cfg.group("KScreen"); + const bool xwaylandClientsScale = kscreenGroup.readEntry("XwaylandClientsScale", true); + if (xwaylandClientsScale) { + double maxScale = 0; + for (Output *output : outputOrder) { + const auto changeset = config.constChangeSet(output); + maxScale = std::max(maxScale, changeset ? changeset->scale.value_or(output->scale()) : output->scale()); + } + kwinApp()->setXwaylandScale(maxScale); + } else { + kwinApp()->setXwaylandScale(1); + } + m_orientationSensor->setEnabled(m_outputConfigStore->isAutoRotateActive(kwinApp()->outputBackend()->outputs(), kwinApp()->tabletModeManager()->effectiveTabletMode())); return true; } @@ -492,13 +518,30 @@ void Workspace::updateOutputConfiguration() setOutputOrder(newOrder); }; - const auto &[cfg, order] = m_outputConfigStore->queryConfig(outputs, m_lidSwitchTracker->isLidClosed()); - if (!kwinApp()->outputBackend()->applyOutputChanges(cfg)) { + const auto opt = m_outputConfigStore->queryConfig(outputs, m_lidSwitchTracker->isLidClosed(), m_orientationSensor->reading(), kwinApp()->tabletModeManager()->effectiveTabletMode()); + if (!opt) { + return; + } + const auto &[cfg, order, type] = *opt; + if (!applyOutputConfiguration(cfg, order)) { qCWarning(KWIN_CORE) << "Applying output config failed!"; setFallbackOutputOrder(); return; } setOutputOrder(order); + if (type == OutputConfigurationStore::ConfigType::Generated) { + const bool hasInternal = std::any_of(outputs.begin(), outputs.end(), [](Output *o) { + return o->isInternal(); + }); + if (hasInternal && outputs.size() == 2) { + // show the OSD with output configuration presets + QDBusMessage message = QDBusMessage::createMethodCall(QStringLiteral("org.kde.kscreen.osdService"), + QStringLiteral("/org/kde/kscreen/osdService"), + QStringLiteral("org.kde.kscreen.osdService"), + QStringLiteral("showActionSelector")); + QDBusConnection::sessionBus().asyncCall(message); + } + } } void Workspace::setupWindowConnections(Window *window) diff --git a/src/workspace.h b/src/workspace.h index 84076cfed9..228dd9c8cf 100644 --- a/src/workspace.h +++ b/src/workspace.h @@ -79,6 +79,7 @@ class TileManager; class OutputConfigurationStore; class LidSwitchTracker; class DpmsInputEventFilter; +class OrientationSensor; class KWIN_EXPORT Workspace : public QObject { @@ -725,6 +726,7 @@ private: std::map> m_tileManagers; std::unique_ptr m_outputConfigStore; std::unique_ptr m_lidSwitchTracker; + std::unique_ptr m_orientationSensor; std::unique_ptr m_dpmsFilter; private: