File indexing completed on 2024-05-19 16:34:57

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.reset(new 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 (!r.contains(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->fractionalGeometry().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     QJsonParseError error;
0228     QJsonDocument doc = QJsonDocument::fromJson(cg.readEntry("tiles", QByteArray()), &error);
0229 
0230     auto createDefaultSetup = [this]() {
0231         Q_ASSERT(m_rootTile->childCount() == 0);
0232         // If empty create an horizontal 3 columns layout
0233         m_rootTile->setLayoutDirection(Tile::LayoutDirection::Horizontal);
0234         m_rootTile->split(Tile::LayoutDirection::Horizontal);
0235         static_cast<CustomTile *>(m_rootTile->childTile(0))->split(Tile::LayoutDirection::Horizontal);
0236         Q_ASSERT(m_rootTile->childCount() == 3);
0237         // Resize middle column, the other two will be auto resized accordingly
0238         m_rootTile->childTile(1)->setRelativeGeometry({0.25, 0.0, 0.5, 1.0});
0239     };
0240 
0241     if (error.error != QJsonParseError::NoError) {
0242         qCWarning(KWIN_CORE) << "Parse error in tiles configuration for monitor" << m_output->uuid().toString(QUuid::WithoutBraces) << ":" << error.errorString() << "Creating default setup";
0243         createDefaultSetup();
0244         return;
0245     }
0246 
0247     if (doc.object().contains(QStringLiteral("tiles"))) {
0248         const auto arr = doc.object().value(QStringLiteral("tiles"));
0249         if (arr.isArray() && arr.toArray().count() > 0) {
0250             m_rootTile->setLayoutDirection(strToLayoutDirection(doc.object().value(QStringLiteral("layoutDirection")).toString()));
0251             parseTilingJSon(arr, QRectF(0, 0, 1, 1), m_rootTile.get());
0252         }
0253     }
0254 
0255     m_rootTile->setPadding(padding);
0256 }
0257 
0258 QJsonObject TileManager::tileToJSon(CustomTile *tile)
0259 {
0260     QJsonObject obj;
0261 
0262     auto *parentTile = static_cast<CustomTile *>(tile->parentTile());
0263 
0264     // Exclude the root and the two children
0265     if (parentTile) {
0266         switch (parentTile->layoutDirection()) {
0267         case Tile::LayoutDirection::Horizontal:
0268             obj[QStringLiteral("width")] = tile->relativeGeometry().width();
0269             break;
0270         case Tile::LayoutDirection::Vertical:
0271             obj[QStringLiteral("height")] = tile->relativeGeometry().height();
0272             break;
0273         case Tile::LayoutDirection::Floating:
0274         default:
0275             obj[QStringLiteral("x")] = tile->relativeGeometry().x();
0276             obj[QStringLiteral("y")] = tile->relativeGeometry().y();
0277             obj[QStringLiteral("width")] = tile->relativeGeometry().width();
0278             obj[QStringLiteral("height")] = tile->relativeGeometry().height();
0279         }
0280     }
0281 
0282     if (tile->isLayout()) {
0283         switch (tile->layoutDirection()) {
0284         case Tile::LayoutDirection::Horizontal:
0285             obj[QStringLiteral("layoutDirection")] = QStringLiteral("horizontal");
0286             break;
0287         case Tile::LayoutDirection::Vertical:
0288             obj[QStringLiteral("layoutDirection")] = QStringLiteral("vertical");
0289             break;
0290         case Tile::LayoutDirection::Floating:
0291         default:
0292             obj[QStringLiteral("layoutDirection")] = QStringLiteral("floating");
0293         }
0294 
0295         QJsonArray tiles;
0296         const int nChildren = tile->childCount();
0297         for (int i = 0; i < nChildren; ++i) {
0298             tiles.append(tileToJSon(static_cast<CustomTile *>(tile->childTile(i))));
0299         }
0300         obj[QStringLiteral("tiles")] = tiles;
0301     }
0302 
0303     return obj;
0304 }
0305 
0306 void TileManager::saveSettings()
0307 {
0308     auto obj = tileToJSon(m_rootTile.get());
0309     QJsonDocument doc(obj);
0310     KConfigGroup cg = kwinApp()->config()->group(QStringLiteral("Tiling"));
0311     cg.writeEntry("padding", m_rootTile->padding());
0312     cg = KConfigGroup(&cg, m_output->uuid().toString(QUuid::WithoutBraces));
0313     cg.writeEntry("tiles", doc.toJson(QJsonDocument::Compact));
0314     cg.sync(); // FIXME: less frequent?
0315 }
0316 
0317 } // namespace KWin
0318 
0319 #include "moc_tilemanager.cpp"