File indexing completed on 2024-11-10 04:57:22
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"