333 lines
12 KiB
C++
333 lines
12 KiB
C++
/*
|
|
KWin - the KDE window manager
|
|
This file is part of the KDE project.
|
|
|
|
SPDX-FileCopyrightText: 2022 Marco Martin <mart@kde.org>
|
|
|
|
SPDX-License-Identifier: GPL-2.0-or-later
|
|
*/
|
|
|
|
#include "tilemanager.h"
|
|
#include "core/output.h"
|
|
#include "quicktile.h"
|
|
#include "virtualdesktops.h"
|
|
#include "workspace.h"
|
|
|
|
#include <KConfigGroup>
|
|
#include <KLocalizedString>
|
|
#include <KSharedConfig>
|
|
|
|
#include <QJsonArray>
|
|
#include <QJsonDocument>
|
|
#include <QJsonObject>
|
|
#include <QTimer>
|
|
|
|
namespace KWin
|
|
{
|
|
|
|
QDebug operator<<(QDebug debug, const TileManager *tileManager)
|
|
{
|
|
if (tileManager) {
|
|
QList<Tile *> tiles({tileManager->rootTile()});
|
|
QList<Tile *> tilePath;
|
|
QString indent(QStringLiteral("|-"));
|
|
debug << tileManager->metaObject()->className() << '(' << static_cast<const void *>(tileManager) << ')' << '\n';
|
|
while (!tiles.isEmpty()) {
|
|
auto *tile = tiles.last();
|
|
tiles.pop_back();
|
|
debug << indent << qobject_cast<CustomTile *>(tile) << '\n';
|
|
if (tile->childCount() > 0) {
|
|
tiles.append(tile->childTiles());
|
|
tilePath.append(tile->childTiles().first());
|
|
indent.prepend(QStringLiteral("| "));
|
|
}
|
|
if (!tilePath.isEmpty() && tile == tilePath.last()) {
|
|
tilePath.pop_back();
|
|
indent.remove(0, 2);
|
|
}
|
|
}
|
|
|
|
} else {
|
|
debug << "Tile(0x0)";
|
|
}
|
|
return debug;
|
|
}
|
|
|
|
TileManager::TileManager(Output *parent)
|
|
: QObject(parent)
|
|
, m_output(parent)
|
|
, m_tileModel(new TileModel(this))
|
|
{
|
|
m_saveTimer = std::make_unique<QTimer>(this);
|
|
m_saveTimer->setSingleShot(true);
|
|
m_saveTimer->setInterval(2000);
|
|
connect(m_saveTimer.get(), &QTimer::timeout, this, &TileManager::saveSettings);
|
|
|
|
m_rootTile = std::make_unique<RootTile>(this);
|
|
m_rootTile->setRelativeGeometry(QRectF(0, 0, 1, 1));
|
|
connect(m_rootTile.get(), &CustomTile::paddingChanged, m_saveTimer.get(), static_cast<void (QTimer::*)()>(&QTimer::start));
|
|
connect(m_rootTile.get(), &CustomTile::layoutModified, m_saveTimer.get(), static_cast<void (QTimer::*)()>(&QTimer::start));
|
|
|
|
m_quickRootTile = std::make_unique<QuickRootTile>(this);
|
|
|
|
readSettings();
|
|
}
|
|
|
|
TileManager::~TileManager()
|
|
{
|
|
m_tearingDown = true;
|
|
}
|
|
|
|
bool TileManager::tearingDown() const
|
|
{
|
|
return m_tearingDown;
|
|
}
|
|
|
|
Output *TileManager::output() const
|
|
{
|
|
return m_output;
|
|
}
|
|
|
|
Tile *TileManager::bestTileForPosition(const QPointF &pos)
|
|
{
|
|
const auto tiles = m_rootTile->descendants();
|
|
qreal minimumDistance = std::numeric_limits<qreal>::max();
|
|
Tile *ret = nullptr;
|
|
|
|
for (auto *t : tiles) {
|
|
if (!t->isLayout()) {
|
|
const auto r = t->absoluteGeometry();
|
|
// It's possible for tiles to overlap, so take the one which center is nearer to mouse pos
|
|
qreal distance = (r.center() - pos).manhattanLength();
|
|
if (!exclusiveContains(r, pos)) {
|
|
// This gives a strong preference for tiles that contain the point
|
|
// still base on distance though as floating tiles can overlap
|
|
distance += m_output->geometryF().width();
|
|
}
|
|
if (distance < minimumDistance) {
|
|
minimumDistance = distance;
|
|
ret = t;
|
|
}
|
|
}
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
Tile *TileManager::bestTileForPosition(qreal x, qreal y)
|
|
{
|
|
return bestTileForPosition({x, y});
|
|
}
|
|
|
|
CustomTile *TileManager::rootTile() const
|
|
{
|
|
return m_rootTile.get();
|
|
}
|
|
|
|
Tile *TileManager::quickTile(QuickTileMode mode) const
|
|
{
|
|
return m_quickRootTile->tileForMode(mode);
|
|
}
|
|
|
|
TileModel *TileManager::model() const
|
|
{
|
|
return m_tileModel.get();
|
|
}
|
|
|
|
Tile::LayoutDirection strToLayoutDirection(const QString &dir)
|
|
{
|
|
if (dir == QLatin1StringView("horizontal")) {
|
|
return Tile::LayoutDirection::Horizontal;
|
|
} else if (dir == QLatin1StringView("vertical")) {
|
|
return Tile::LayoutDirection::Vertical;
|
|
} else {
|
|
return Tile::LayoutDirection::Floating;
|
|
}
|
|
}
|
|
|
|
CustomTile *TileManager::parseTilingJSon(const QJsonValue &val, const QRectF &availableArea, CustomTile *parentTile)
|
|
{
|
|
if (availableArea.isEmpty()) {
|
|
return nullptr;
|
|
}
|
|
|
|
if (val.isObject()) {
|
|
const auto &obj = val.toObject();
|
|
CustomTile *createdTile = nullptr;
|
|
|
|
if (parentTile->layoutDirection() == Tile::LayoutDirection::Horizontal) {
|
|
QRectF rect = availableArea;
|
|
const auto width = obj.value(QStringLiteral("width"));
|
|
if (width.isDouble()) {
|
|
rect.setWidth(std::min(width.toDouble(), availableArea.width()));
|
|
}
|
|
if (!rect.isEmpty()) {
|
|
createdTile = parentTile->createChildAt(rect, parentTile->layoutDirection(), parentTile->childCount());
|
|
}
|
|
|
|
} else if (parentTile->layoutDirection() == Tile::LayoutDirection::Vertical) {
|
|
QRectF rect = availableArea;
|
|
const auto height = obj.value(QStringLiteral("height"));
|
|
if (height.isDouble()) {
|
|
rect.setHeight(std::min(height.toDouble(), availableArea.height()));
|
|
}
|
|
if (!rect.isEmpty()) {
|
|
createdTile = parentTile->createChildAt(rect, parentTile->layoutDirection(), parentTile->childCount());
|
|
}
|
|
|
|
} else if (parentTile->layoutDirection() == Tile::LayoutDirection::Floating) {
|
|
QRectF rect(0, 0, 1, 1);
|
|
rect = QRectF(obj.value(QStringLiteral("x")).toDouble(),
|
|
obj.value(QStringLiteral("y")).toDouble(),
|
|
obj.value(QStringLiteral("width")).toDouble(),
|
|
obj.value(QStringLiteral("height")).toDouble());
|
|
|
|
if (!rect.isEmpty()) {
|
|
createdTile = parentTile->createChildAt(rect, parentTile->layoutDirection(), parentTile->childCount());
|
|
}
|
|
}
|
|
|
|
if (createdTile && obj.contains(QStringLiteral("tiles"))) {
|
|
// It's a layout
|
|
const auto arr = obj.value(QStringLiteral("tiles"));
|
|
const auto direction = obj.value(QStringLiteral("layoutDirection"));
|
|
// Ignore arrays with only a single item in it
|
|
if (arr.isArray() && arr.toArray().count() > 0) {
|
|
const Tile::LayoutDirection dir = strToLayoutDirection(direction.toString());
|
|
createdTile->setLayoutDirection(dir);
|
|
parseTilingJSon(arr, createdTile->relativeGeometry(), createdTile);
|
|
}
|
|
}
|
|
return createdTile;
|
|
} else if (val.isArray()) {
|
|
const auto arr = val.toArray();
|
|
auto avail = availableArea;
|
|
for (auto it = arr.cbegin(); it != arr.cend(); it++) {
|
|
if ((*it).isObject()) {
|
|
auto *tile = parseTilingJSon(*it, avail, parentTile);
|
|
if (tile && parentTile->layoutDirection() == Tile::LayoutDirection::Horizontal) {
|
|
avail.setLeft(tile->relativeGeometry().right());
|
|
} else if (tile && parentTile->layoutDirection() == Tile::LayoutDirection::Vertical) {
|
|
avail.setTop(tile->relativeGeometry().bottom());
|
|
}
|
|
}
|
|
}
|
|
// make sure the children fill exactly the parent, eventually enlarging the last
|
|
if (parentTile->layoutDirection() != Tile::LayoutDirection::Floating
|
|
&& parentTile->childCount() > 0) {
|
|
auto *last = parentTile->childTile(parentTile->childCount() - 1);
|
|
auto geom = last->relativeGeometry();
|
|
geom.setRight(parentTile->relativeGeometry().right());
|
|
last->setRelativeGeometry(geom);
|
|
}
|
|
return nullptr;
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
void TileManager::readSettings()
|
|
{
|
|
KConfigGroup cg = kwinApp()->config()->group(QStringLiteral("Tiling"));
|
|
qreal padding = cg.readEntry("padding", 4);
|
|
cg = KConfigGroup(&cg, m_output->uuid().toString(QUuid::WithoutBraces));
|
|
|
|
auto createDefaultSetup = [this]() {
|
|
Q_ASSERT(m_rootTile->childCount() == 0);
|
|
// If empty create an horizontal 3 columns layout
|
|
m_rootTile->setLayoutDirection(Tile::LayoutDirection::Horizontal);
|
|
m_rootTile->split(Tile::LayoutDirection::Horizontal);
|
|
static_cast<CustomTile *>(m_rootTile->childTile(0))->split(Tile::LayoutDirection::Horizontal);
|
|
Q_ASSERT(m_rootTile->childCount() == 3);
|
|
// Resize middle column, the other two will be auto resized accordingly
|
|
m_rootTile->childTile(1)->setRelativeGeometry({0.25, 0.0, 0.5, 1.0});
|
|
};
|
|
|
|
QJsonParseError error;
|
|
const auto tiles = cg.readEntry("tiles", QByteArray());
|
|
if (tiles.isEmpty()) {
|
|
qCDebug(KWIN_CORE) << "Empty tiles configuration for monitor" << m_output->uuid().toString(QUuid::WithoutBraces) << ":"
|
|
<< "Creating default setup";
|
|
createDefaultSetup();
|
|
return;
|
|
}
|
|
QJsonDocument doc = QJsonDocument::fromJson(tiles, &error);
|
|
|
|
if (error.error != QJsonParseError::NoError) {
|
|
qCWarning(KWIN_CORE) << "Parse error in tiles configuration for monitor" << m_output->uuid().toString(QUuid::WithoutBraces) << ":" << error.errorString() << "Creating default setup";
|
|
createDefaultSetup();
|
|
return;
|
|
}
|
|
|
|
if (doc.object().contains(QStringLiteral("tiles"))) {
|
|
const auto arr = doc.object().value(QStringLiteral("tiles"));
|
|
if (arr.isArray() && arr.toArray().count() > 0) {
|
|
m_rootTile->setLayoutDirection(strToLayoutDirection(doc.object().value(QStringLiteral("layoutDirection")).toString()));
|
|
parseTilingJSon(arr, QRectF(0, 0, 1, 1), m_rootTile.get());
|
|
}
|
|
}
|
|
|
|
m_rootTile->setPadding(padding);
|
|
}
|
|
|
|
QJsonObject TileManager::tileToJSon(CustomTile *tile)
|
|
{
|
|
QJsonObject obj;
|
|
|
|
auto *parentTile = static_cast<CustomTile *>(tile->parentTile());
|
|
|
|
// Exclude the root and the two children
|
|
if (parentTile) {
|
|
switch (parentTile->layoutDirection()) {
|
|
case Tile::LayoutDirection::Horizontal:
|
|
obj[QStringLiteral("width")] = tile->relativeGeometry().width();
|
|
break;
|
|
case Tile::LayoutDirection::Vertical:
|
|
obj[QStringLiteral("height")] = tile->relativeGeometry().height();
|
|
break;
|
|
case Tile::LayoutDirection::Floating:
|
|
default:
|
|
obj[QStringLiteral("x")] = tile->relativeGeometry().x();
|
|
obj[QStringLiteral("y")] = tile->relativeGeometry().y();
|
|
obj[QStringLiteral("width")] = tile->relativeGeometry().width();
|
|
obj[QStringLiteral("height")] = tile->relativeGeometry().height();
|
|
}
|
|
}
|
|
|
|
if (tile->isLayout()) {
|
|
switch (tile->layoutDirection()) {
|
|
case Tile::LayoutDirection::Horizontal:
|
|
obj[QStringLiteral("layoutDirection")] = QStringLiteral("horizontal");
|
|
break;
|
|
case Tile::LayoutDirection::Vertical:
|
|
obj[QStringLiteral("layoutDirection")] = QStringLiteral("vertical");
|
|
break;
|
|
case Tile::LayoutDirection::Floating:
|
|
default:
|
|
obj[QStringLiteral("layoutDirection")] = QStringLiteral("floating");
|
|
}
|
|
|
|
QJsonArray tiles;
|
|
const int nChildren = tile->childCount();
|
|
for (int i = 0; i < nChildren; ++i) {
|
|
tiles.append(tileToJSon(static_cast<CustomTile *>(tile->childTile(i))));
|
|
}
|
|
obj[QStringLiteral("tiles")] = tiles;
|
|
}
|
|
|
|
return obj;
|
|
}
|
|
|
|
void TileManager::saveSettings()
|
|
{
|
|
auto obj = tileToJSon(m_rootTile.get());
|
|
QJsonDocument doc(obj);
|
|
KConfigGroup cg = kwinApp()->config()->group(QStringLiteral("Tiling"));
|
|
cg.writeEntry("padding", m_rootTile->padding());
|
|
cg = KConfigGroup(&cg, m_output->uuid().toString(QUuid::WithoutBraces));
|
|
cg.writeEntry("tiles", doc.toJson(QJsonDocument::Compact));
|
|
cg.sync(); // FIXME: less frequent?
|
|
}
|
|
|
|
} // namespace KWin
|
|
|
|
#include "moc_tilemanager.cpp"
|