File indexing completed on 2024-05-12 16:01:58
0001 /* 0002 * SPDX-FileCopyrightText: 2018 Jouni Pentikäinen <joupent@gmail.com> 0003 * 0004 * SPDX-License-Identifier: GPL-2.0-or-later 0005 */ 0006 #include "KisWindowLayoutResource.h" 0007 #include "KisWindowLayoutManager.h" 0008 0009 #include <QVector> 0010 #include <QList> 0011 #include <QFile> 0012 #include <QDomDocument> 0013 #include <QApplication> 0014 #include <QEventLoop> 0015 #include <QMessageBox> 0016 #include <QWindow> 0017 #include <QScreen> 0018 0019 #include <KisPart.h> 0020 #include <KisDocument.h> 0021 #include <kis_dom_utils.h> 0022 #include <KisMainWindow.h> 0023 0024 static const int WINDOW_LAYOUT_VERSION = 1; 0025 0026 struct KisWindowLayoutResource::Private 0027 { 0028 struct WindowGeometry{ 0029 int screen = -1; 0030 Qt::WindowStates stateFlags = Qt::WindowNoState; 0031 QByteArray data; 0032 0033 static WindowGeometry fromWindow(const QWidget *window, QList<QScreen*> screens) 0034 { 0035 WindowGeometry geometry; 0036 QWindow *windowHandle = window->windowHandle(); 0037 0038 geometry.data = window->saveGeometry(); 0039 geometry.stateFlags = windowHandle->windowState(); 0040 0041 int index = screens.indexOf(windowHandle->screen()); 0042 if (index >= 0) { 0043 geometry.screen = index; 0044 } 0045 0046 return geometry; 0047 } 0048 0049 void forceOntoCorrectScreen(QWidget *window, QList<QScreen*> screens) 0050 { 0051 QWindow *windowHandle = window->windowHandle(); 0052 0053 if (screens.indexOf(windowHandle->screen()) != screen) { 0054 QScreen *qScreen = screens[screen]; 0055 windowHandle->setScreen(qScreen); 0056 windowHandle->setPosition(qScreen->availableGeometry().topLeft()); 0057 } 0058 0059 if (stateFlags) { 0060 window->setWindowState(stateFlags); 0061 } 0062 } 0063 0064 void save(QDomDocument &doc, QDomElement &elem) const 0065 { 0066 if (screen >= 0) { 0067 elem.setAttribute("screen", screen); 0068 } 0069 0070 if (stateFlags & Qt::WindowMaximized) { 0071 elem.setAttribute("maximized", "1"); 0072 } 0073 0074 QDomElement geometry = doc.createElement("geometry"); 0075 geometry.appendChild(doc.createCDATASection(data.toBase64())); 0076 elem.appendChild(geometry); 0077 } 0078 0079 static WindowGeometry load(const QDomElement &element) 0080 { 0081 WindowGeometry geometry; 0082 geometry.screen = element.attribute("screen", "-1").toInt(); 0083 0084 if (element.attribute("maximized", "0") != "0") { 0085 geometry.stateFlags |= Qt::WindowMaximized; 0086 } 0087 0088 QDomElement dataElement = element.firstChildElement("geometry"); 0089 geometry.data = QByteArray::fromBase64(dataElement.text().toLatin1()); 0090 0091 return geometry; 0092 } 0093 }; 0094 0095 struct Window { 0096 QUuid windowId; 0097 QByteArray windowState; 0098 WindowGeometry geometry; 0099 0100 bool canvasDetached = false; 0101 WindowGeometry canvasWindowGeometry; 0102 }; 0103 0104 QVector<Window> windows; 0105 bool showImageInAllWindows {false}; 0106 bool primaryWorkspaceFollowsFocus {false}; 0107 QUuid primaryWindow; 0108 0109 Private() = default; 0110 Private(const Private &rhs) = default; 0111 0112 0113 explicit Private(QVector<Window> windows) 0114 : windows(std::move(windows)) 0115 {} 0116 0117 void openNecessaryWindows(QList<QPointer<KisMainWindow>> ¤tWindows) { 0118 auto *kisPart = KisPart::instance(); 0119 0120 Q_FOREACH(const Window &window, windows) { 0121 QPointer<KisMainWindow> mainWindow = kisPart->windowById(window.windowId); 0122 0123 if (mainWindow.isNull()) { 0124 mainWindow = kisPart->createMainWindow(window.windowId); 0125 currentWindows.append(mainWindow); 0126 mainWindow->show(); 0127 } 0128 } 0129 } 0130 0131 void closeUnneededWindows(QList<QPointer<KisMainWindow>> ¤tWindows) { 0132 QVector<QPointer<KisMainWindow>> windowsToClose; 0133 0134 Q_FOREACH(KisMainWindow *mainWindow, currentWindows) { 0135 bool keep = false; 0136 Q_FOREACH(const Window &window, windows) { 0137 if (window.windowId == mainWindow->id()) { 0138 keep = true; 0139 break; 0140 } 0141 } 0142 0143 if (!keep) { 0144 windowsToClose.append(mainWindow); 0145 0146 // Set the window hidden to prevent "show image in all windows" feature from opening new views on it 0147 // while we migrate views onto the remaining windows 0148 mainWindow->hide(); 0149 } 0150 } 0151 0152 migrateViewsFromClosingWindows(windowsToClose); 0153 0154 Q_FOREACH(QPointer<KisMainWindow> mainWindow, windowsToClose) { 0155 mainWindow->close(); 0156 } 0157 } 0158 0159 void migrateViewsFromClosingWindows(QVector<QPointer<KisMainWindow>> &closingWindows) const 0160 { 0161 auto *kisPart = KisPart::instance(); 0162 KisMainWindow *migrationTarget = nullptr; 0163 0164 Q_FOREACH(KisMainWindow *mainWindow, kisPart->mainWindows()) { 0165 if (!closingWindows.contains(mainWindow)) { 0166 migrationTarget = mainWindow; 0167 break; 0168 } 0169 } 0170 0171 if (!migrationTarget) { 0172 qWarning() << "Problem: window layout with no windows would leave user with zero main windows."; 0173 migrationTarget = closingWindows.takeLast(); 0174 migrationTarget->show(); 0175 } 0176 0177 QVector<KisDocument*> visibleDocuments; 0178 Q_FOREACH(KisView *view, kisPart->views()) { 0179 KisMainWindow *window = view->mainWindow(); 0180 if (!closingWindows.contains(window)) { 0181 visibleDocuments.append(view->document()); 0182 } 0183 } 0184 0185 Q_FOREACH(KisDocument *document, kisPart->documents()) { 0186 if (!visibleDocuments.contains(document)) { 0187 visibleDocuments.append(document); 0188 migrationTarget->newView(document); 0189 } 0190 } 0191 } 0192 0193 0194 QList<QScreen*> getScreensInConsistentOrder() { 0195 QList<QScreen*> screens = QGuiApplication::screens(); 0196 0197 std::sort(screens.begin(), screens.end(), [](const QScreen *a, const QScreen *b) { 0198 QRect aRect = a->geometry(); 0199 QRect bRect = b->geometry(); 0200 0201 if (aRect.y() == bRect.y()) return aRect.x() < bRect.x(); 0202 return (aRect.y() < bRect.y()); 0203 }); 0204 0205 return screens; 0206 } 0207 }; 0208 0209 KisWindowLayoutResource::KisWindowLayoutResource(const QString &filename) 0210 : KoResource(filename) 0211 , d(new Private) 0212 {} 0213 0214 KisWindowLayoutResource::~KisWindowLayoutResource() 0215 {} 0216 0217 KisWindowLayoutResource::KisWindowLayoutResource(const KisWindowLayoutResource &rhs) 0218 : KoResource(rhs) 0219 , d(new Private(*rhs.d)) 0220 { 0221 } 0222 0223 KoResourceSP KisWindowLayoutResource::clone() const 0224 { 0225 return KoResourceSP(new KisWindowLayoutResource(*this)); 0226 } 0227 0228 KisWindowLayoutResourceSP KisWindowLayoutResource::fromCurrentWindows( 0229 const QString &filename, const QList<QPointer<KisMainWindow>> &mainWindows, bool showImageInAllWindows, 0230 bool primaryWorkspaceFollowsFocus, KisMainWindow *primaryWindow 0231 ) 0232 { 0233 KisWindowLayoutResourceSP resource(new KisWindowLayoutResource(filename)); 0234 resource->setWindows(mainWindows); 0235 resource->d->showImageInAllWindows = showImageInAllWindows; 0236 resource->d->primaryWorkspaceFollowsFocus = primaryWorkspaceFollowsFocus; 0237 resource->d->primaryWindow = primaryWindow->id(); 0238 return resource; 0239 } 0240 0241 void KisWindowLayoutResource::applyLayout() 0242 { 0243 auto *kisPart = KisPart::instance(); 0244 auto *layoutManager= KisWindowLayoutManager::instance(); 0245 0246 layoutManager->setLastUsedLayout(this); 0247 0248 QList<QPointer<KisMainWindow>> currentWindows = kisPart->mainWindows(); 0249 0250 if (d->windows.isEmpty()) { 0251 // No windows defined (e.g. fresh new session). Leave things as they are, but make sure there's at least one visible main window 0252 if (kisPart->mainwindowCount() == 0) { 0253 kisPart->createMainWindow(); 0254 } else { 0255 kisPart->mainWindows().first()->show(); 0256 } 0257 } else { 0258 d->openNecessaryWindows(currentWindows); 0259 d->closeUnneededWindows(currentWindows); 0260 } 0261 0262 // Wait for the windows to finish opening / closing before applying saved geometry. 0263 // If we don't, the geometry may get reset after we apply it. 0264 QApplication::processEvents(QEventLoop::ExcludeUserInputEvents); 0265 0266 Q_FOREACH(const auto &window, d->windows) { 0267 QPointer<KisMainWindow> mainWindow = kisPart->windowById(window.windowId); 0268 KIS_SAFE_ASSERT_RECOVER_BREAK(mainWindow); 0269 0270 mainWindow->restoreGeometry(window.geometry.data); 0271 mainWindow->restoreWorkspaceState(window.windowState); 0272 0273 mainWindow->setCanvasDetached(window.canvasDetached); 0274 if (window.canvasDetached) { 0275 QWidget *canvasWindow = mainWindow->canvasWindow(); 0276 canvasWindow->restoreGeometry(window.canvasWindowGeometry.data); 0277 } 0278 } 0279 0280 QApplication::processEvents(QEventLoop::ExcludeUserInputEvents); 0281 0282 QList<QScreen*> screens = d->getScreensInConsistentOrder(); 0283 Q_FOREACH(const auto &window, d->windows) { 0284 Private::WindowGeometry geometry = window.geometry; 0285 QPointer<KisMainWindow> mainWindow = kisPart->windowById(window.windowId); 0286 KIS_SAFE_ASSERT_RECOVER_BREAK(mainWindow); 0287 0288 if (geometry.screen >= 0 && geometry.screen < screens.size()) { 0289 geometry.forceOntoCorrectScreen(mainWindow, screens); 0290 } 0291 if (window.canvasDetached) { 0292 Private::WindowGeometry canvasWindowGeometry = window.canvasWindowGeometry; 0293 if (canvasWindowGeometry.screen >= 0 && canvasWindowGeometry.screen < screens.size()) { 0294 canvasWindowGeometry.forceOntoCorrectScreen(mainWindow->canvasWindow(), screens); 0295 } 0296 } 0297 } 0298 0299 layoutManager->setShowImageInAllWindowsEnabled(d->showImageInAllWindows); 0300 layoutManager->setPrimaryWorkspaceFollowsFocus(d->primaryWorkspaceFollowsFocus, d->primaryWindow); 0301 } 0302 0303 bool KisWindowLayoutResource::saveToDevice(QIODevice *dev) const 0304 { 0305 QDomDocument doc; 0306 QDomElement root = doc.createElement("WindowLayout"); 0307 root.setAttribute("name", name()); 0308 root.setAttribute("version", WINDOW_LAYOUT_VERSION); 0309 0310 saveXml(doc, root); 0311 0312 doc.appendChild(root); 0313 0314 QTextStream textStream(dev); 0315 textStream.setCodec("UTF-8"); 0316 doc.save(textStream, 4); 0317 return true; 0318 } 0319 0320 bool KisWindowLayoutResource::loadFromDevice(QIODevice *dev, KisResourcesInterfaceSP resourcesInterface) 0321 { 0322 Q_UNUSED(resourcesInterface); 0323 0324 QDomDocument doc; 0325 if (!doc.setContent(dev)) { 0326 return false; 0327 } 0328 0329 QDomElement element = doc.documentElement(); 0330 setName(element.attribute("name")); 0331 0332 d->windows.clear(); 0333 0334 loadXml(element); 0335 0336 setValid(true); 0337 return true; 0338 } 0339 0340 void KisWindowLayoutResource::saveXml(QDomDocument &doc, QDomElement &root) const 0341 { 0342 root.setAttribute("showImageInAllWindows", (int)d->showImageInAllWindows); 0343 root.setAttribute("primaryWorkspaceFollowsFocus", (int)d->primaryWorkspaceFollowsFocus); 0344 root.setAttribute("primaryWindow", d->primaryWindow.toString()); 0345 0346 Q_FOREACH(const auto &window, d->windows) { 0347 QDomElement elem = doc.createElement("window"); 0348 elem.setAttribute("id", window.windowId.toString()); 0349 0350 window.geometry.save(doc, elem); 0351 0352 if (window.canvasDetached) { 0353 QDomElement canvasWindowElement = doc.createElement("canvasWindow"); 0354 window.canvasWindowGeometry.save(doc, canvasWindowElement); 0355 elem.appendChild(canvasWindowElement); 0356 } 0357 0358 QDomElement state = doc.createElement("windowState"); 0359 state.appendChild(doc.createCDATASection(window.windowState.toBase64())); 0360 elem.appendChild(state); 0361 root.appendChild(elem); 0362 } 0363 } 0364 0365 void KisWindowLayoutResource::loadXml(const QDomElement &element) const 0366 { 0367 d->showImageInAllWindows = KisDomUtils::toInt(element.attribute("showImageInAllWindows", "0")); 0368 d->primaryWorkspaceFollowsFocus = KisDomUtils::toInt(element.attribute("primaryWorkspaceFollowsFocus", "0")); 0369 d->primaryWindow = element.attribute("primaryWindow"); 0370 0371 #ifdef Q_OS_ANDROID 0372 if (element.firstChildElement("window") != element.lastChildElement("window")) { 0373 QMessageBox::warning(qApp->activeWindow(), i18nc("@title:window", "Krita"), 0374 "Workspaces with multiple windows isn't supported on Android"); 0375 return; 0376 } 0377 #endif 0378 0379 for (auto windowElement = element.firstChildElement("window"); 0380 !windowElement.isNull(); 0381 windowElement = windowElement.nextSiblingElement("window")) { 0382 0383 Private::Window window; 0384 0385 window.windowId = QUuid(windowElement.attribute("id", QUuid().toString())); 0386 if (window.windowId.isNull()) { 0387 window.windowId = QUuid::createUuid(); 0388 } 0389 0390 window.geometry = Private::WindowGeometry::load(windowElement); 0391 0392 QDomElement canvasWindowElement = windowElement.firstChildElement("canvasWindow"); 0393 if (!canvasWindowElement.isNull()) { 0394 window.canvasDetached = true; 0395 window.canvasWindowGeometry = Private::WindowGeometry::load(canvasWindowElement); 0396 } 0397 0398 QDomElement state = windowElement.firstChildElement("windowState"); 0399 window.windowState = QByteArray::fromBase64(state.text().toLatin1()); 0400 0401 d->windows.append(window); 0402 } 0403 } 0404 0405 QString KisWindowLayoutResource::defaultFileExtension() const 0406 { 0407 return QString(".kwl"); 0408 } 0409 0410 void KisWindowLayoutResource::setWindows(const QList<QPointer<KisMainWindow>> &mainWindows) 0411 { 0412 d->windows.clear(); 0413 0414 QList<QScreen*> screens = d->getScreensInConsistentOrder(); 0415 0416 Q_FOREACH(auto window, mainWindows) { 0417 if (!window->isVisible()) continue; 0418 0419 Private::Window state; 0420 state.windowId = window->id(); 0421 state.windowState = window->saveState(); 0422 state.geometry = Private::WindowGeometry::fromWindow(window, screens); 0423 0424 state.canvasDetached = window->canvasDetached(); 0425 if (state.canvasDetached) { 0426 state.canvasWindowGeometry = Private::WindowGeometry::fromWindow(window->canvasWindow(), screens); 0427 } 0428 0429 d->windows.append(state); 0430 } 0431 }