workspace: move kscreen integration into separate files
Putting all the code into workspace.cpp makes the file harder to manage, and kscreen integration is really its own topic.
This commit is contained in:
parent
ea66fad153
commit
cc535a1ecd
4 changed files with 272 additions and 214 deletions
|
@ -97,6 +97,7 @@ target_sources(kwin PRIVATE
|
|||
keyboard_layout_switching.cpp
|
||||
keyboard_repeat.cpp
|
||||
killwindow.cpp
|
||||
kscreenintegration.cpp
|
||||
layers.cpp
|
||||
layershellv1integration.cpp
|
||||
layershellv1window.cpp
|
||||
|
|
236
src/kscreenintegration.cpp
Normal file
236
src/kscreenintegration.cpp
Normal file
|
@ -0,0 +1,236 @@
|
|||
/*
|
||||
KWin - the KDE window manager
|
||||
This file is part of the KDE project.
|
||||
|
||||
SPDX-FileCopyrightText: 2021 Aleix Pol Gonzalez <aleixpol@kde.org>
|
||||
SPDX-FileCopyrightText: 2023 Xaver Hugl <xaver.hugl@gmail.com>
|
||||
|
||||
SPDX-License-Identifier: GPL-2.0-or-later
|
||||
*/
|
||||
#include "kscreenintegration.h"
|
||||
#include "utils/common.h"
|
||||
|
||||
#include <QCryptographicHash>
|
||||
#include <QFile>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
namespace KWin
|
||||
{
|
||||
namespace KScreenIntegration
|
||||
{
|
||||
/// See KScreen::Output::hashMd5
|
||||
static QString outputHash(Output *output)
|
||||
{
|
||||
if (!output->edid().isEmpty()) {
|
||||
QCryptographicHash hash(QCryptographicHash::Md5);
|
||||
hash.addData(output->edid());
|
||||
return QString::fromLatin1(hash.result().toHex());
|
||||
} else {
|
||||
return output->name();
|
||||
}
|
||||
}
|
||||
|
||||
/// See KScreen::Config::connectedOutputsHash in libkscreen
|
||||
QString connectedOutputsHash(const QVector<Output *> &outputs)
|
||||
{
|
||||
QStringList hashedOutputs;
|
||||
hashedOutputs.reserve(outputs.count());
|
||||
for (auto output : std::as_const(outputs)) {
|
||||
if (!output->isPlaceholder() && !output->isNonDesktop()) {
|
||||
hashedOutputs << outputHash(output);
|
||||
}
|
||||
}
|
||||
std::sort(hashedOutputs.begin(), hashedOutputs.end());
|
||||
const auto hash = QCryptographicHash::hash(hashedOutputs.join(QString()).toLatin1(), QCryptographicHash::Md5);
|
||||
return QString::fromLatin1(hash.toHex());
|
||||
}
|
||||
|
||||
static QMap<Output *, QJsonObject> outputsConfig(const QVector<Output *> &outputs, const QString &hash)
|
||||
{
|
||||
const QString kscreenJsonPath = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("kscreen/") % hash);
|
||||
if (kscreenJsonPath.isEmpty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
QFile f(kscreenJsonPath);
|
||||
if (!f.open(QIODevice::ReadOnly)) {
|
||||
qCWarning(KWIN_CORE) << "Could not open file" << kscreenJsonPath;
|
||||
return {};
|
||||
}
|
||||
|
||||
QJsonParseError error;
|
||||
const auto doc = QJsonDocument::fromJson(f.readAll(), &error);
|
||||
if (error.error != QJsonParseError::NoError) {
|
||||
qCWarning(KWIN_CORE) << "Failed to parse" << kscreenJsonPath << error.errorString();
|
||||
return {};
|
||||
}
|
||||
|
||||
QHash<Output *, bool> duplicate;
|
||||
QHash<Output *, QString> outputHashes;
|
||||
for (Output *output : outputs) {
|
||||
const QString hash = outputHash(output);
|
||||
const auto it = std::find_if(outputHashes.cbegin(), outputHashes.cend(), [hash](const auto &value) {
|
||||
return value == hash;
|
||||
});
|
||||
if (it == outputHashes.cend()) {
|
||||
duplicate[output] = false;
|
||||
} else {
|
||||
duplicate[output] = true;
|
||||
duplicate[it.key()] = true;
|
||||
}
|
||||
outputHashes[output] = hash;
|
||||
}
|
||||
|
||||
QMap<Output *, QJsonObject> ret;
|
||||
const auto outputsJson = doc.array();
|
||||
for (const auto &outputJson : outputsJson) {
|
||||
const auto outputObject = outputJson.toObject();
|
||||
const auto id = outputObject["id"];
|
||||
const auto output = std::find_if(outputs.begin(), outputs.end(), [&duplicate, &id, &outputObject](Output *output) {
|
||||
if (outputHash(output) != id.toString()) {
|
||||
return false;
|
||||
}
|
||||
if (duplicate[output]) {
|
||||
// can't distinguish between outputs by hash alone, need to look at connector names
|
||||
const auto metadata = outputObject[QStringLiteral("metadata")];
|
||||
const auto outputName = metadata[QStringLiteral("name")].toString();
|
||||
return outputName == output->name();
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
if (output != outputs.end()) {
|
||||
ret[*output] = outputObject;
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
/// See KScreen::Output::Rotation
|
||||
enum Rotation {
|
||||
None = 1,
|
||||
Left = 2,
|
||||
Inverted = 4,
|
||||
Right = 8,
|
||||
};
|
||||
|
||||
Output::Transform toDrmTransform(int rotation)
|
||||
{
|
||||
switch (Rotation(rotation)) {
|
||||
case None:
|
||||
return Output::Transform::Normal;
|
||||
case Left:
|
||||
return Output::Transform::Rotated90;
|
||||
case Inverted:
|
||||
return Output::Transform::Rotated180;
|
||||
case Right:
|
||||
return Output::Transform::Rotated270;
|
||||
default:
|
||||
Q_UNREACHABLE();
|
||||
}
|
||||
}
|
||||
|
||||
std::shared_ptr<OutputMode> parseMode(Output *output, const QJsonObject &modeInfo)
|
||||
{
|
||||
const QJsonObject size = modeInfo["size"].toObject();
|
||||
const QSize modeSize = QSize(size["width"].toInt(), size["height"].toInt());
|
||||
const uint32_t refreshRate = std::round(modeInfo["refresh"].toDouble() * 1000);
|
||||
|
||||
const auto modes = output->modes();
|
||||
auto it = std::find_if(modes.begin(), modes.end(), [&modeSize, &refreshRate](const auto &mode) {
|
||||
return mode->size() == modeSize && mode->refreshRate() == refreshRate;
|
||||
});
|
||||
return (it != modes.end()) ? *it : nullptr;
|
||||
}
|
||||
|
||||
std::optional<std::pair<OutputConfiguration, QVector<Output *>>> readOutputConfig(const QVector<Output *> &outputs, const QString &hash)
|
||||
{
|
||||
const auto outputsInfo = outputsConfig(outputs, hash);
|
||||
std::vector<std::pair<uint32_t, Output *>> outputOrder;
|
||||
OutputConfiguration cfg;
|
||||
// default position goes from left to right
|
||||
QPoint pos(0, 0);
|
||||
for (const auto &output : std::as_const(outputs)) {
|
||||
if (output->isPlaceholder() || output->isNonDesktop()) {
|
||||
continue;
|
||||
}
|
||||
auto props = cfg.changeSet(output);
|
||||
const QJsonObject outputInfo = outputsInfo[output];
|
||||
qCDebug(KWIN_CORE) << "Reading output configuration for " << output;
|
||||
if (!outputInfo.isEmpty()) {
|
||||
props->enabled = outputInfo["enabled"].toBool(true);
|
||||
if (outputInfo["primary"].toBool()) {
|
||||
outputOrder.push_back(std::make_pair(1, output));
|
||||
if (!props->enabled) {
|
||||
qCWarning(KWIN_CORE) << "KScreen config would disable the primary output!";
|
||||
return std::nullopt;
|
||||
}
|
||||
} else if (int prio = outputInfo["priority"].toInt(); prio > 0) {
|
||||
outputOrder.push_back(std::make_pair(prio, output));
|
||||
if (!props->enabled) {
|
||||
qCWarning(KWIN_CORE) << "KScreen config would disable an output with priority!";
|
||||
return std::nullopt;
|
||||
}
|
||||
} else {
|
||||
outputOrder.push_back(std::make_pair(0, output));
|
||||
}
|
||||
const QJsonObject pos = outputInfo["pos"].toObject();
|
||||
props->pos = QPoint(pos["x"].toInt(), pos["y"].toInt());
|
||||
if (const QJsonValue scale = outputInfo["scale"]; !scale.isUndefined()) {
|
||||
props->scale = scale.toDouble(1.);
|
||||
}
|
||||
props->transform = KScreenIntegration::toDrmTransform(outputInfo["rotation"].toInt());
|
||||
|
||||
props->overscan = static_cast<uint32_t>(outputInfo["overscan"].toInt(props->overscan));
|
||||
props->vrrPolicy = static_cast<RenderLoop::VrrPolicy>(outputInfo["vrrpolicy"].toInt(static_cast<uint32_t>(props->vrrPolicy)));
|
||||
props->rgbRange = static_cast<Output::RgbRange>(outputInfo["rgbrange"].toInt(static_cast<uint32_t>(props->rgbRange)));
|
||||
|
||||
if (const QJsonObject modeInfo = outputInfo["mode"].toObject(); !modeInfo.isEmpty()) {
|
||||
if (auto mode = KScreenIntegration::parseMode(output, modeInfo)) {
|
||||
props->mode = mode;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
props->enabled = true;
|
||||
props->pos = pos;
|
||||
props->transform = output->panelOrientation();
|
||||
outputOrder.push_back(std::make_pair(0, output));
|
||||
}
|
||||
pos.setX(pos.x() + output->geometry().width());
|
||||
}
|
||||
|
||||
bool allDisabled = std::all_of(outputs.begin(), outputs.end(), [&cfg](const auto &output) {
|
||||
return !cfg.changeSet(output)->enabled;
|
||||
});
|
||||
if (allDisabled) {
|
||||
qCWarning(KWIN_CORE) << "KScreen config would disable all outputs!";
|
||||
return std::nullopt;
|
||||
}
|
||||
std::erase_if(outputOrder, [&cfg](const auto &pair) {
|
||||
return !cfg.constChangeSet(pair.second)->enabled;
|
||||
});
|
||||
std::sort(outputOrder.begin(), outputOrder.end(), [](const auto &left, const auto &right) {
|
||||
if (left.first == right.first) {
|
||||
// sort alphabetically as a fallback
|
||||
return left.second->name() < right.second->name();
|
||||
} else if (left.first == 0) {
|
||||
return false;
|
||||
} else {
|
||||
return left.first < right.first;
|
||||
}
|
||||
});
|
||||
|
||||
QVector<Output *> order;
|
||||
order.reserve(outputOrder.size());
|
||||
std::transform(outputOrder.begin(), outputOrder.end(), std::back_inserter(order), [](const auto &pair) {
|
||||
return pair.second;
|
||||
});
|
||||
return std::make_pair(cfg, order);
|
||||
}
|
||||
}
|
||||
}
|
24
src/kscreenintegration.h
Normal file
24
src/kscreenintegration.h
Normal file
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
KWin - the KDE window manager
|
||||
This file is part of the KDE project.
|
||||
|
||||
SPDX-FileCopyrightText: 2023 Xaver Hugl <xaver.hugl@gmail.com>
|
||||
|
||||
SPDX-License-Identifier: GPL-2.0-or-later
|
||||
*/
|
||||
#pragma once
|
||||
#include "core/output.h"
|
||||
#include "core/outputconfiguration.h"
|
||||
|
||||
#include <QVector>
|
||||
#include <optional>
|
||||
|
||||
namespace KWin
|
||||
{
|
||||
namespace KScreenIntegration
|
||||
{
|
||||
|
||||
QString connectedOutputsHash(const QVector<Output *> &outputs);
|
||||
std::optional<std::pair<OutputConfiguration, QVector<Output *>>> readOutputConfig(const QVector<Output *> &outputs, const QString &hash);
|
||||
}
|
||||
}
|
|
@ -47,6 +47,7 @@
|
|||
#include "tabbox.h"
|
||||
#endif
|
||||
#include "decorations/decorationbridge.h"
|
||||
#include "kscreenintegration.h"
|
||||
#include "main.h"
|
||||
#include "placeholderinputeventfilter.h"
|
||||
#include "placeholderoutput.h"
|
||||
|
@ -508,134 +509,6 @@ Workspace::~Workspace()
|
|||
_self = nullptr;
|
||||
}
|
||||
|
||||
namespace KWinKScreenIntegration
|
||||
{
|
||||
/// See KScreen::Output::hashMd5
|
||||
QString outputHash(Output *output)
|
||||
{
|
||||
if (!output->edid().isEmpty()) {
|
||||
QCryptographicHash hash(QCryptographicHash::Md5);
|
||||
hash.addData(output->edid());
|
||||
return QString::fromLatin1(hash.result().toHex());
|
||||
} else {
|
||||
return output->name();
|
||||
}
|
||||
}
|
||||
|
||||
/// See KScreen::Config::connectedOutputsHash in libkscreen
|
||||
QString connectedOutputsHash(const QVector<Output *> &outputs)
|
||||
{
|
||||
QStringList hashedOutputs;
|
||||
hashedOutputs.reserve(outputs.count());
|
||||
for (auto output : std::as_const(outputs)) {
|
||||
if (!output->isPlaceholder() && !output->isNonDesktop()) {
|
||||
hashedOutputs << outputHash(output);
|
||||
}
|
||||
}
|
||||
std::sort(hashedOutputs.begin(), hashedOutputs.end());
|
||||
const auto hash = QCryptographicHash::hash(hashedOutputs.join(QString()).toLatin1(), QCryptographicHash::Md5);
|
||||
return QString::fromLatin1(hash.toHex());
|
||||
}
|
||||
|
||||
QMap<Output *, QJsonObject> outputsConfig(const QVector<Output *> &outputs, const QString &hash)
|
||||
{
|
||||
const QString kscreenJsonPath = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("kscreen/") % hash);
|
||||
if (kscreenJsonPath.isEmpty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
QFile f(kscreenJsonPath);
|
||||
if (!f.open(QIODevice::ReadOnly)) {
|
||||
qCWarning(KWIN_CORE) << "Could not open file" << kscreenJsonPath;
|
||||
return {};
|
||||
}
|
||||
|
||||
QJsonParseError error;
|
||||
const auto doc = QJsonDocument::fromJson(f.readAll(), &error);
|
||||
if (error.error != QJsonParseError::NoError) {
|
||||
qCWarning(KWIN_CORE) << "Failed to parse" << kscreenJsonPath << error.errorString();
|
||||
return {};
|
||||
}
|
||||
|
||||
QHash<Output *, bool> duplicate;
|
||||
QHash<Output *, QString> outputHashes;
|
||||
for (Output *output : outputs) {
|
||||
const QString hash = outputHash(output);
|
||||
const auto it = std::find_if(outputHashes.cbegin(), outputHashes.cend(), [hash](const auto &value) {
|
||||
return value == hash;
|
||||
});
|
||||
if (it == outputHashes.cend()) {
|
||||
duplicate[output] = false;
|
||||
} else {
|
||||
duplicate[output] = true;
|
||||
duplicate[it.key()] = true;
|
||||
}
|
||||
outputHashes[output] = hash;
|
||||
}
|
||||
|
||||
QMap<Output *, QJsonObject> ret;
|
||||
const auto outputsJson = doc.array();
|
||||
for (const auto &outputJson : outputsJson) {
|
||||
const auto outputObject = outputJson.toObject();
|
||||
const auto id = outputObject["id"];
|
||||
const auto output = std::find_if(outputs.begin(), outputs.end(), [&duplicate, &id, &outputObject](Output *output) {
|
||||
if (outputHash(output) != id.toString()) {
|
||||
return false;
|
||||
}
|
||||
if (duplicate[output]) {
|
||||
// can't distinguish between outputs by hash alone, need to look at connector names
|
||||
const auto metadata = outputObject[QStringLiteral("metadata")];
|
||||
const auto outputName = metadata[QStringLiteral("name")].toString();
|
||||
return outputName == output->name();
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
if (output != outputs.end()) {
|
||||
ret[*output] = outputObject;
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
/// See KScreen::Output::Rotation
|
||||
enum Rotation {
|
||||
None = 1,
|
||||
Left = 2,
|
||||
Inverted = 4,
|
||||
Right = 8,
|
||||
};
|
||||
|
||||
Output::Transform toDrmTransform(int rotation)
|
||||
{
|
||||
switch (Rotation(rotation)) {
|
||||
case None:
|
||||
return Output::Transform::Normal;
|
||||
case Left:
|
||||
return Output::Transform::Rotated90;
|
||||
case Inverted:
|
||||
return Output::Transform::Rotated180;
|
||||
case Right:
|
||||
return Output::Transform::Rotated270;
|
||||
default:
|
||||
Q_UNREACHABLE();
|
||||
}
|
||||
}
|
||||
|
||||
std::shared_ptr<OutputMode> parseMode(Output *output, const QJsonObject &modeInfo)
|
||||
{
|
||||
const QJsonObject size = modeInfo["size"].toObject();
|
||||
const QSize modeSize = QSize(size["width"].toInt(), size["height"].toInt());
|
||||
const uint32_t refreshRate = std::round(modeInfo["refresh"].toDouble() * 1000);
|
||||
|
||||
const auto modes = output->modes();
|
||||
auto it = std::find_if(modes.begin(), modes.end(), [&modeSize, &refreshRate](const auto &mode) {
|
||||
return mode->size() == modeSize && mode->refreshRate() == refreshRate;
|
||||
});
|
||||
return (it != modes.end()) ? *it : nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
bool Workspace::applyOutputConfiguration(const OutputConfiguration &config, const QVector<Output *> &outputOrder)
|
||||
{
|
||||
if (!kwinApp()->outputBackend()->applyOutputChanges(config)) {
|
||||
|
@ -655,11 +528,9 @@ void Workspace::updateOutputConfiguration()
|
|||
const auto outputs = kwinApp()->outputBackend()->outputs();
|
||||
if (outputs.empty()) {
|
||||
// nothing to do
|
||||
setOutputOrder({});
|
||||
return;
|
||||
}
|
||||
const QString hash = KWinKScreenIntegration::connectedOutputsHash(outputs);
|
||||
const auto outputsInfo = KWinKScreenIntegration::outputsConfig(outputs, hash);
|
||||
m_outputsHash = hash;
|
||||
|
||||
// Update the output order to a fallback list, to avoid dangling pointers
|
||||
const auto setFallbackOutputOrder = [this, &outputs]() {
|
||||
|
@ -674,92 +545,18 @@ void Workspace::updateOutputConfiguration()
|
|||
setOutputOrder(newOrder);
|
||||
};
|
||||
|
||||
std::vector<std::pair<uint32_t, Output *>> outputOrder;
|
||||
OutputConfiguration cfg;
|
||||
// default position goes from left to right
|
||||
QPoint pos(0, 0);
|
||||
for (const auto &output : std::as_const(outputs)) {
|
||||
if (output->isPlaceholder() || output->isNonDesktop()) {
|
||||
continue;
|
||||
m_outputsHash = KScreenIntegration::connectedOutputsHash(outputs);
|
||||
if (const auto config = KScreenIntegration::readOutputConfig(outputs, m_outputsHash)) {
|
||||
const auto &[cfg, order] = config.value();
|
||||
if (!kwinApp()->outputBackend()->applyOutputChanges(cfg)) {
|
||||
qCWarning(KWIN_CORE) << "Applying KScreen config failed!";
|
||||
setFallbackOutputOrder();
|
||||
return;
|
||||
}
|
||||
auto props = cfg.changeSet(output);
|
||||
const QJsonObject outputInfo = outputsInfo[output];
|
||||
qCDebug(KWIN_CORE) << "Reading output configuration for " << output;
|
||||
if (!outputInfo.isEmpty()) {
|
||||
props->enabled = outputInfo["enabled"].toBool(true);
|
||||
if (outputInfo["primary"].toBool()) {
|
||||
outputOrder.push_back(std::make_pair(1, output));
|
||||
if (!props->enabled) {
|
||||
qCWarning(KWIN_CORE) << "KScreen config would disable the primary output!";
|
||||
setFallbackOutputOrder();
|
||||
return;
|
||||
}
|
||||
} else if (int prio = outputInfo["priority"].toInt(); prio > 0) {
|
||||
outputOrder.push_back(std::make_pair(prio, output));
|
||||
if (!props->enabled) {
|
||||
qCWarning(KWIN_CORE) << "KScreen config would disable an output with priority!";
|
||||
setFallbackOutputOrder();
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
outputOrder.push_back(std::make_pair(0, output));
|
||||
}
|
||||
const QJsonObject pos = outputInfo["pos"].toObject();
|
||||
props->pos = QPoint(pos["x"].toInt(), pos["y"].toInt());
|
||||
if (const QJsonValue scale = outputInfo["scale"]; !scale.isUndefined()) {
|
||||
props->scale = scale.toDouble(1.);
|
||||
}
|
||||
props->transform = KWinKScreenIntegration::toDrmTransform(outputInfo["rotation"].toInt());
|
||||
|
||||
props->overscan = static_cast<uint32_t>(outputInfo["overscan"].toInt(props->overscan));
|
||||
props->vrrPolicy = static_cast<RenderLoop::VrrPolicy>(outputInfo["vrrpolicy"].toInt(static_cast<uint32_t>(props->vrrPolicy)));
|
||||
props->rgbRange = static_cast<Output::RgbRange>(outputInfo["rgbrange"].toInt(static_cast<uint32_t>(props->rgbRange)));
|
||||
|
||||
if (const QJsonObject modeInfo = outputInfo["mode"].toObject(); !modeInfo.isEmpty()) {
|
||||
if (auto mode = KWinKScreenIntegration::parseMode(output, modeInfo)) {
|
||||
props->mode = mode;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
props->enabled = true;
|
||||
props->pos = pos;
|
||||
props->transform = output->panelOrientation();
|
||||
outputOrder.push_back(std::make_pair(0, output));
|
||||
}
|
||||
pos.setX(pos.x() + output->geometry().width());
|
||||
}
|
||||
bool allDisabled = std::all_of(outputs.begin(), outputs.end(), [&cfg](const auto &output) {
|
||||
return !cfg.changeSet(output)->enabled;
|
||||
});
|
||||
if (allDisabled) {
|
||||
qCWarning(KWIN_CORE) << "KScreen config would disable all outputs!";
|
||||
setOutputOrder(order);
|
||||
} else {
|
||||
setFallbackOutputOrder();
|
||||
return;
|
||||
}
|
||||
std::erase_if(outputOrder, [&cfg](const auto &pair) {
|
||||
return !cfg.constChangeSet(pair.second)->enabled;
|
||||
});
|
||||
std::sort(outputOrder.begin(), outputOrder.end(), [](const auto &left, const auto &right) {
|
||||
if (left.first == right.first) {
|
||||
// sort alphabetically as a fallback
|
||||
return left.second->name() < right.second->name();
|
||||
} else if (left.first == 0) {
|
||||
return false;
|
||||
} else {
|
||||
return left.first < right.first;
|
||||
}
|
||||
});
|
||||
if (!kwinApp()->outputBackend()->applyOutputChanges(cfg)) {
|
||||
qCWarning(KWIN_CORE) << "Applying KScreen config failed!";
|
||||
setFallbackOutputOrder();
|
||||
return;
|
||||
}
|
||||
QVector<Output *> order;
|
||||
order.reserve(outputOrder.size());
|
||||
std::transform(outputOrder.begin(), outputOrder.end(), std::back_inserter(order), [](const auto &pair) {
|
||||
return pair.second;
|
||||
});
|
||||
setOutputOrder(order);
|
||||
}
|
||||
|
||||
void Workspace::setupWindowConnections(Window *window)
|
||||
|
|
Loading…
Reference in a new issue