File indexing completed on 2024-05-19 05:32:28

0001 /*
0002     KWin - the KDE window manager
0003     This file is part of the KDE project.
0004 
0005     SPDX-FileCopyrightText: 2022 Marco Martin <mart@kde.org>
0006 
0007     SPDX-License-Identifier: GPL-2.0-or-later
0008 */
0009 
0010 #include "tilemanager.h"
0011 #include "core/output.h"
0012 #include "quicktile.h"
0013 #include "virtualdesktops.h"
0014 #include "workspace.h"
0015 
0016 #include <KConfigGroup>
0017 #include <KLocalizedString>
0018 #include <KSharedConfig>
0019 
0020 #include <QJsonArray>
0021 #include <QJsonDocument>
0022 #include <QJsonObject>
0023 #include <QTimer>
0024 
0025 namespace KWin
0026 {
0027 
0028 QDebug operator<<(QDebug debug, const TileManager *tileManager)
0029 {
0030     if (tileManager) {
0031         QList<Tile *> tiles({tileManager->rootTile()});
0032         QList<Tile *> tilePath;
0033         QString indent(QStringLiteral("|-"));
0034         debug << tileManager->metaObject()->className() << '(' << static_cast<const void *>(tileManager) << ')' << '\n';
0035         while (!tiles.isEmpty()) {
0036             auto *tile = tiles.last();
0037             tiles.pop_back();
0038             debug << indent << qobject_cast<CustomTile *>(tile) << '\n';
0039             if (tile->childCount() > 0) {
0040                 tiles.append(tile->childTiles());
0041                 tilePath.append(tile->childTiles().first());
0042                 indent.prepend(QStringLiteral("| "));
0043             }
0044             if (!tilePath.isEmpty() && tile == tilePath.last()) {
0045                 tilePath.pop_back();
0046                 indent.remove(0, 2);
0047             }
0048         }
0049 
0050     } else {
0051         debug << "Tile(0x0)";
0052     }
0053     return debug;
0054 }
0055 
0056 TileManager::TileManager(Output *parent)
0057     : QObject(parent)
0058     , m_output(parent)
0059     , m_tileModel(new TileModel(this))
0060 {
0061     m_saveTimer = std::make_unique<QTimer>(this);
0062     m_saveTimer->setSingleShot(true);
0063     m_saveTimer->setInterval(2000);
0064     connect(m_saveTimer.get(), &QTimer::timeout, this, &TileManager::saveSettings);
0065 
0066     m_rootTile = std::make_unique<RootTile>(this);
0067     m_rootTile->setRelativeGeometry(QRectF(0, 0, 1, 1));
0068     connect(m_rootTile.get(), &CustomTile::paddingChanged, m_saveTimer.get(), static_cast<void (QTimer::*)()>(&QTimer::start));
0069     connect(m_rootTile.get(), &CustomTile::layoutModified, m_saveTimer.get(), static_cast<void (QTimer::*)()>(&QTimer::start));
0070 
0071     m_quickRootTile = std::make_unique<QuickRootTile>(this);
0072 
0073     readSettings();
0074 }
0075 
0076 TileManager::~TileManager()
0077 {
0078 }
0079 
0080 Output *TileManager::output() const
0081 {
0082     return m_output;
0083 }
0084 
0085 Tile *TileManager::bestTileForPosition(const QPointF &pos)
0086 {
0087     const auto tiles = m_rootTile->descendants();
0088     qreal minimumDistance = std::numeric_limits<qreal>::max();
0089     Tile *ret = nullptr;
0090 
0091     for (auto *t : tiles) {
0092         if (!t->isLayout()) {
0093             const auto r = t->absoluteGeometry();
0094             // It's possible for tiles to overlap, so take the one which center is nearer to mouse pos
0095             qreal distance = (r.center() - pos).manhattanLength();
0096             if (!exclusiveContains(r, pos)) {
0097                 // This gives a strong preference for tiles that contain the point
0098                 // still base on distance though as floating tiles can overlap
0099                 distance += m_output->geometryF().width();
0100             }
0101             if (distance < minimumDistance) {
0102                 minimumDistance = distance;
0103                 ret = t;
0104             }
0105         }
0106     }
0107     return ret;
0108 }
0109 
0110 Tile *TileManager::bestTileForPosition(qreal x, qreal y)
0111 {
0112     return bestTileForPosition({x, y});
0113 }
0114 
0115 CustomTile *TileManager::rootTile() const
0116 {
0117     return m_rootTile.get();
0118 }
0119 
0120 Tile *TileManager::quickTile(QuickTileMode mode) const
0121 {
0122     return m_quickRootTile->tileForMode(mode);
0123 }
0124 
0125 TileModel *TileManager::model() const
0126 {
0127     return m_tileModel.get();
0128 }
0129 
0130 Tile::LayoutDirection strToLayoutDirection(const QString &dir)
0131 {
0132     if (dir == QStringLiteral("horizontal")) {
0133         return Tile::LayoutDirection::Horizontal;
0134     } else if (dir == QStringLiteral("vertical")) {
0135         return Tile::LayoutDirection::Vertical;
0136     } else {
0137         return Tile::LayoutDirection::Floating;
0138     }
0139 }
0140 
0141 CustomTile *TileManager::parseTilingJSon(const QJsonValue &val, const QRectF &availableArea, CustomTile *parentTile)
0142 {
0143     if (availableArea.isEmpty()) {
0144         return nullptr;
0145     }
0146 
0147     if (val.isObject()) {
0148         const auto &obj = val.toObject();
0149         CustomTile *createdTile = nullptr;
0150 
0151         if (parentTile->layoutDirection() == Tile::LayoutDirection::Horizontal) {
0152             QRectF rect = availableArea;
0153             const auto width = obj.value(QStringLiteral("width"));
0154             if (width.isDouble()) {
0155                 rect.setWidth(std::min(width.toDouble(), availableArea.width()));
0156             }
0157             if (!rect.isEmpty()) {
0158                 createdTile = parentTile->createChildAt(rect, parentTile->layoutDirection(), parentTile->childCount());
0159             }
0160 
0161         } else if (parentTile->layoutDirection() == Tile::LayoutDirection::Vertical) {
0162             QRectF rect = availableArea;
0163             const auto height = obj.value(QStringLiteral("height"));
0164             if (height.isDouble()) {
0165                 rect.setHeight(std::min(height.toDouble(), availableArea.height()));
0166             }
0167             if (!rect.isEmpty()) {
0168                 createdTile = parentTile->createChildAt(rect, parentTile->layoutDirection(), parentTile->childCount());
0169             }
0170 
0171         } else if (parentTile->layoutDirection() == Tile::LayoutDirection::Floating) {
0172             QRectF rect(0, 0, 1, 1);
0173             rect = QRectF(obj.value(QStringLiteral("x")).toDouble(),
0174                           obj.value(QStringLiteral("y")).toDouble(),
0175                           obj.value(QStringLiteral("width")).toDouble(),
0176                           obj.value(QStringLiteral("height")).toDouble());
0177 
0178             if (!rect.isEmpty()) {
0179                 createdTile = parentTile->createChildAt(rect, parentTile->layoutDirection(), parentTile->childCount());
0180             }
0181         }
0182 
0183         if (createdTile && obj.contains(QStringLiteral("tiles"))) {
0184             // It's a layout
0185             const auto arr = obj.value(QStringLiteral("tiles"));
0186             const auto direction = obj.value(QStringLiteral("layoutDirection"));
0187             // Ignore arrays with only a single item in it
0188             if (arr.isArray() && arr.toArray().count() > 0) {
0189                 const Tile::LayoutDirection dir = strToLayoutDirection(direction.toString());
0190                 createdTile->setLayoutDirection(dir);
0191                 parseTilingJSon(arr, createdTile->relativeGeometry(), createdTile);
0192             }
0193         }
0194         return createdTile;
0195     } else if (val.isArray()) {
0196         const auto arr = val.toArray();
0197         auto avail = availableArea;
0198         for (auto it = arr.cbegin(); it != arr.cend(); it++) {
0199             if ((*it).isObject()) {
0200                 auto *tile = parseTilingJSon(*it, avail, parentTile);
0201                 if (tile && parentTile->layoutDirection() == Tile::LayoutDirection::Horizontal) {
0202                     avail.setLeft(tile->relativeGeometry().right());
0203                 } else if (tile && parentTile->layoutDirection() == Tile::LayoutDirection::Vertical) {
0204                     avail.setTop(tile->relativeGeometry().bottom());
0205                 }
0206             }
0207         }
0208         // make sure the children fill exactly the parent, eventually enlarging the last
0209         if (parentTile->layoutDirection() != Tile::LayoutDirection::Floating
0210             && parentTile->childCount() > 0) {
0211             auto *last = parentTile->childTile(parentTile->childCount() - 1);
0212             auto geom = last->relativeGeometry();
0213             geom.setRight(parentTile->relativeGeometry().right());
0214             last->setRelativeGeometry(geom);
0215         }
0216         return nullptr;
0217     }
0218     return nullptr;
0219 }
0220 
0221 void TileManager::readSettings()
0222 {
0223     KConfigGroup cg = kwinApp()->config()->group(QStringLiteral("Tiling"));
0224     qreal padding = cg.readEntry("padding", 4);
0225     cg = KConfigGroup(&cg, m_output->uuid().toString(QUuid::WithoutBraces));
0226 
0227     auto createDefaultSetup = [this]() {
0228         Q_ASSERT(m_rootTile->childCount() == 0);
0229         // If empty create an horizontal 3 columns layout
0230         m_rootTile->setLayoutDirection(Tile::LayoutDirection::Horizontal);
0231         m_rootTile->split(Tile::LayoutDirection::Horizontal);
0232         static_cast<CustomTile *>(m_rootTile->childTile(0))->split(Tile::LayoutDirection::Horizontal);
0233         Q_ASSERT(m_rootTile->childCount() == 3);
0234         // Resize middle column, the other two will be auto resized accordingly
0235         m_rootTile->childTile(1)->setRelativeGeometry({0.25, 0.0, 0.5, 1.0});
0236     };
0237 
0238     QJsonParseError error;
0239     const auto tiles = cg.readEntry("tiles", QByteArray());
0240     if (tiles.isEmpty()) {
0241         qCDebug(KWIN_CORE) << "Empty tiles configuration for monitor" << m_output->uuid().toString(QUuid::WithoutBraces) << ":"
0242                            << "Creating default setup";
0243         createDefaultSetup();
0244         return;
0245     }
0246     QJsonDocument doc = QJsonDocument::fromJson(tiles, &error);
0247 
0248     if (error.error != QJsonParseError::NoError) {
0249         qCWarning(KWIN_CORE) << "Parse error in tiles configuration for monitor" << m_output->uuid().toString(QUuid::WithoutBraces) << ":" << error.errorString() << "Creating default setup";
0250         createDefaultSetup();
0251         return;
0252     }
0253 
0254     if (doc.object().contains(QStringLiteral("tiles"))) {
0255         const auto arr = doc.object().value(QStringLiteral("tiles"));
0256         if (arr.isArray() && arr.toArray().count() > 0) {
0257             m_rootTile->setLayoutDirection(strToLayoutDirection(doc.object().value(QStringLiteral("layoutDirection")).toString()));
0258             parseTilingJSon(arr, QRectF(0, 0, 1, 1), m_rootTile.get());
0259         }
0260     }
0261 
0262     m_rootTile->setPadding(padding);
0263 }
0264 
0265 QJsonObject TileManager::tileToJSon(CustomTile *tile)
0266 {
0267     QJsonObject obj;
0268 
0269     auto *parentTile = static_cast<CustomTile *>(tile->parentTile());
0270 
0271     // Exclude the root and the two children
0272     if (parentTile) {
0273         switch (parentTile->layoutDirection()) {
0274         case Tile::LayoutDirection::Horizontal:
0275             obj[QStringLiteral("width")] = tile->relativeGeometry().width();
0276             break;
0277         case Tile::LayoutDirection::Vertical:
0278             obj[QStringLiteral("height")] = tile->relativeGeometry().height();
0279             break;
0280         case Tile::LayoutDirection::Floating:
0281         default:
0282             obj[QStringLiteral("x")] = tile->relativeGeometry().x();
0283             obj[QStringLiteral("y")] = tile->relativeGeometry().y();
0284             obj[QStringLiteral("width")] = tile->relativeGeometry().width();
0285             obj[QStringLiteral("height")] = tile->relativeGeometry().height();
0286         }
0287     }
0288 
0289     if (tile->isLayout()) {
0290         switch (tile->layoutDirection()) {
0291         case Tile::LayoutDirection::Horizontal:
0292             obj[QStringLiteral("layoutDirection")] = QStringLiteral("horizontal");
0293             break;
0294         case Tile::LayoutDirection::Vertical:
0295             obj[QStringLiteral("layoutDirection")] = QStringLiteral("vertical");
0296             break;
0297         case Tile::LayoutDirection::Floating:
0298         default:
0299             obj[QStringLiteral("layoutDirection")] = QStringLiteral("floating");
0300         }
0301 
0302         QJsonArray tiles;
0303         const int nChildren = tile->childCount();
0304         for (int i = 0; i < nChildren; ++i) {
0305             tiles.append(tileToJSon(static_cast<CustomTile *>(tile->childTile(i))));
0306         }
0307         obj[QStringLiteral("tiles")] = tiles;
0308     }
0309 
0310     return obj;
0311 }
0312 
0313 void TileManager::saveSettings()
0314 {
0315     auto obj = tileToJSon(m_rootTile.get());
0316     QJsonDocument doc(obj);
0317     KConfigGroup cg = kwinApp()->config()->group(QStringLiteral("Tiling"));
0318     cg.writeEntry("padding", m_rootTile->padding());
0319     cg = KConfigGroup(&cg, m_output->uuid().toString(QUuid::WithoutBraces));
0320     cg.writeEntry("tiles", doc.toJson(QJsonDocument::Compact));
0321     cg.sync(); // FIXME: less frequent?
0322 }
0323 
0324 } // namespace KWin
0325 
0326 #include "moc_tilemanager.cpp"