/* KWin - the KDE window manager This file is part of the KDE project. SPDX-FileCopyrightText: 2022 Marco Martin 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 #include #include #include #include #include #include namespace KWin { QDebug operator<<(QDebug debug, const TileManager *tileManager) { if (tileManager) { QList tiles({tileManager->rootTile()}); QList tilePath; QString indent(QStringLiteral("|-")); debug << tileManager->metaObject()->className() << '(' << static_cast(tileManager) << ')' << '\n'; while (!tiles.isEmpty()) { auto *tile = tiles.last(); tiles.pop_back(); debug << indent << qobject_cast(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(this); m_saveTimer->setSingleShot(true); m_saveTimer->setInterval(2000); connect(m_saveTimer.get(), &QTimer::timeout, this, &TileManager::saveSettings); m_rootTile = std::make_unique(this); m_rootTile->setRelativeGeometry(QRectF(0, 0, 1, 1)); connect(m_rootTile.get(), &CustomTile::paddingChanged, m_saveTimer.get(), static_cast(&QTimer::start)); connect(m_rootTile.get(), &CustomTile::layoutModified, m_saveTimer.get(), static_cast(&QTimer::start)); m_quickRootTile = std::make_unique(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::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(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(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(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"