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: