File indexing completed on 2024-05-19 04:29:22
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 if (mainWindow->isVisible()) { 0149 mainWindow->hide(); 0150 } 0151 } 0152 } 0153 0154 migrateViewsFromClosingWindows(windowsToClose); 0155 0156 Q_FOREACH(QPointer<KisMainWindow> mainWindow, windowsToClose) { 0157 mainWindow->close(); 0158 } 0159 } 0160 0161 void migrateViewsFromClosingWindows(QVector<QPointer<KisMainWindow>> &closingWindows) const 0162 { 0163 auto *kisPart = KisPart::instance(); 0164 KisMainWindow *migrationTarget = nullptr; 0165 0166 Q_FOREACH(KisMainWindow *mainWindow, kisPart->mainWindows()) { 0167 if (!closingWindows.contains(mainWindow)) { 0168 migrationTarget = mainWindow; 0169 break; 0170 } 0171 } 0172 0173 if (!migrationTarget) { 0174 qWarning() << "Problem: window layout with no windows would leave user with zero main windows."; 0175 migrationTarget = closingWindows.takeLast(); 0176 migrationTarget->show(); 0177 } 0178 0179 QVector<KisDocument*> visibleDocuments; 0180 Q_FOREACH(KisView *view, kisPart->views()) { 0181 KisMainWindow *window = view->mainWindow(); 0182 if (!closingWindows.contains(window)) { 0183 visibleDocuments.append(view->document()); 0184 } 0185 } 0186 0187 Q_FOREACH(KisDocument *document, kisPart->documents()) { 0188 if (!visibleDocuments.contains(document)) { 0189 visibleDocuments.append(document); 0190 migrationTarget->newView(document); 0191 } 0192 } 0193 } 0194 0195 0196 QList<QScreen*> getScreensInConsistentOrder() { 0197 QList<QScreen*> screens = QGuiApplication::screens(); 0198 0199 std::sort(screens.begin(), screens.end(), [](const QScreen *a, const QScreen *b) { 0200 QRect aRect = a->geometry(); 0201 QRect bRect = b->geometry(); 0202 0203 if (aRect.y() == bRect.y()) return aRect.x() < bRect.x(); 0204 return (aRect.y() < bRect.y()); 0205 }); 0206 0207 return screens; 0208 } 0209 }; 0210 0211 KisWindowLayoutResource::KisWindowLayoutResource(const QString &filename) 0212 : KoResource(filename) 0213 , d(new Private) 0214 {} 0215 0216 KisWindowLayoutResource::~KisWindowLayoutResource() 0217 {} 0218 0219 KisWindowLayoutResource::KisWindowLayoutResource(const KisWindowLayoutResource &rhs) 0220 : KoResource(rhs) 0221 , d(new Private(*rhs.d)) 0222 { 0223 } 0224 0225 KoResourceSP KisWindowLayoutResource::clone() const 0226 { 0227 return KoResourceSP(new KisWindowLayoutResource(*this)); 0228 } 0229 0230 KisWindowLayoutResourceSP KisWindowLayoutResource::fromCurrentWindows( 0231 const QString &filename, const QList<QPointer<KisMainWindow>> &mainWindows, bool showImageInAllWindows, 0232 bool primaryWorkspaceFollowsFocus, KisMainWindow *primaryWindow 0233 ) 0234 { 0235 KisWindowLayoutResourceSP resource(new KisWindowLayoutResource(filename)); 0236 resource->setWindows(mainWindows); 0237 resource->d->showImageInAllWindows = showImageInAllWindows; 0238 resource->d->primaryWorkspaceFollowsFocus = primaryWorkspaceFollowsFocus; 0239 resource->d->primaryWindow = primaryWindow->id(); 0240 return resource; 0241 } 0242 0243 void KisWindowLayoutResource::applyLayout() 0244 { 0245 auto *kisPart = KisPart::instance(); 0246 auto *layoutManager= KisWindowLayoutManager::instance(); 0247 0248 layoutManager->setLastUsedLayout(this); 0249 0250 QList<QPointer<KisMainWindow>> currentWindows = kisPart->mainWindows(); 0251 0252 if (d->windows.isEmpty()) { 0253 // No windows defined (e.g. fresh new session). Leave things as they are, but make sure there's at least one visible main window 0254 if (kisPart->mainwindowCount() == 0) { 0255 kisPart->createMainWindow(); 0256 } else { 0257 kisPart->mainWindows().first()->show(); 0258 } 0259 } else { 0260 d->openNecessaryWindows(currentWindows); 0261 d->closeUnneededWindows(currentWindows); 0262 } 0263 0264 // Wait for the windows to finish opening / closing before applying saved geometry. 0265 // If we don't, the geometry may get reset after we apply it. 0266 QApplication::processEvents(QEventLoop::ExcludeUserInputEvents); 0267 0268 Q_FOREACH(const auto &window, d->windows) { 0269 QPointer<KisMainWindow> mainWindow = kisPart->windowById(window.windowId); 0270 KIS_SAFE_ASSERT_RECOVER_BREAK(mainWindow); 0271 0272 mainWindow->restoreGeometry(window.geometry.data); 0273 mainWindow->restoreWorkspaceState(window.windowState); 0274 0275 mainWindow->setCanvasDetached(window.canvasDetached); 0276 if (window.canvasDetached) { 0277 QWidget *canvasWindow = mainWindow->canvasWindow(); 0278 canvasWindow->restoreGeometry(window.canvasWindowGeometry.data); 0279 } 0280 } 0281 0282 QApplication::processEvents(QEventLoop::ExcludeUserInputEvents); 0283 0284 QList<QScreen*> screens = d->getScreensInConsistentOrder(); 0285 Q_FOREACH(const auto &window, d->windows) { 0286 Private::WindowGeometry geometry = window.geometry; 0287 QPointer<KisMainWindow> mainWindow = kisPart->windowById(window.windowId); 0288 KIS_SAFE_ASSERT_RECOVER_BREAK(mainWindow); 0289 0290 if (geometry.screen >= 0 && geometry.screen < screens.size()) { 0291 geometry.forceOntoCorrectScreen(mainWindow, screens); 0292 } 0293 if (window.canvasDetached) { 0294 Private::WindowGeometry canvasWindowGeometry = window.canvasWindowGeometry; 0295 if (canvasWindowGeometry.screen >= 0 && canvasWindowGeometry.screen < screens.size()) { 0296 canvasWindowGeometry.forceOntoCorrectScreen(mainWindow->canvasWindow(), screens); 0297 } 0298 } 0299 } 0300 0301 layoutManager->setShowImageInAllWindowsEnabled(d->showImageInAllWindows); 0302 layoutManager->setPrimaryWorkspaceFollowsFocus(d->primaryWorkspaceFollowsFocus, d->primaryWindow); 0303 } 0304 0305 bool KisWindowLayoutResource::saveToDevice(QIODevice *dev) const 0306 { 0307 QDomDocument doc; 0308 QDomElement root = doc.createElement("WindowLayout"); 0309 root.setAttribute("name", name()); 0310 root.setAttribute("version", WINDOW_LAYOUT_VERSION); 0311 0312 saveXml(doc, root); 0313 0314 doc.appendChild(root); 0315 0316 QTextStream textStream(dev); 0317 textStream.setCodec("UTF-8"); 0318 doc.save(textStream, 4); 0319 return true; 0320 } 0321 0322 bool KisWindowLayoutResource::loadFromDevice(QIODevice *dev, KisResourcesInterfaceSP resourcesInterface) 0323 { 0324 Q_UNUSED(resourcesInterface); 0325 0326 QDomDocument doc; 0327 if (!doc.setContent(dev)) { 0328 return false; 0329 } 0330 0331 QDomElement element = doc.documentElement(); 0332 setName(element.attribute("name")); 0333 0334 d->windows.clear(); 0335 0336 loadXml(element); 0337 0338 setValid(true); 0339 return true; 0340 } 0341 0342 void KisWindowLayoutResource::saveXml(QDomDocument &doc, QDomElement &root) const 0343 { 0344 root.setAttribute("showImageInAllWindows", (int)d->showImageInAllWindows); 0345 root.setAttribute("primaryWorkspaceFollowsFocus", (int)d->primaryWorkspaceFollowsFocus); 0346 root.setAttribute("primaryWindow", d->primaryWindow.toString()); 0347 0348 Q_FOREACH(const auto &window, d->windows) { 0349 QDomElement elem = doc.createElement("window"); 0350 elem.setAttribute("id", window.windowId.toString()); 0351 0352 window.geometry.save(doc, elem); 0353 0354 if (window.canvasDetached) { 0355 QDomElement canvasWindowElement = doc.createElement("canvasWindow"); 0356 window.canvasWindowGeometry.save(doc, canvasWindowElement); 0357 elem.appendChild(canvasWindowElement); 0358 } 0359 0360 QDomElement state = doc.createElement("windowState"); 0361 state.appendChild(doc.createCDATASection(window.windowState.toBase64())); 0362 elem.appendChild(state); 0363 root.appendChild(elem); 0364 } 0365 } 0366 0367 void KisWindowLayoutResource::loadXml(const QDomElement &element) const 0368 { 0369 d->showImageInAllWindows = KisDomUtils::toInt(element.attribute("showImageInAllWindows", "0")); 0370 d->primaryWorkspaceFollowsFocus = KisDomUtils::toInt(element.attribute("primaryWorkspaceFollowsFocus", "0")); 0371 d->primaryWindow = element.attribute("primaryWindow"); 0372 0373 #ifdef Q_OS_ANDROID 0374 if (element.firstChildElement("window") != element.lastChildElement("window")) { 0375 QMessageBox::warning(qApp->activeWindow(), i18nc("@title:window", "Krita"), 0376 "Workspaces with multiple windows isn't supported on Android"); 0377 return; 0378 } 0379 #endif 0380 0381 for (auto windowElement = element.firstChildElement("window"); 0382 !windowElement.isNull(); 0383 windowElement = windowElement.nextSiblingElement("window")) { 0384 0385 Private::Window window; 0386 0387 window.windowId = QUuid(windowElement.attribute("id", QUuid().toString())); 0388 if (window.windowId.isNull()) { 0389 window.windowId = QUuid::createUuid(); 0390 } 0391 0392 window.geometry = Private::WindowGeometry::load(windowElement); 0393 0394 QDomElement canvasWindowElement = windowElement.firstChildElement("canvasWindow"); 0395 if (!canvasWindowElement.isNull()) { 0396 window.canvasDetached = true; 0397 window.canvasWindowGeometry = Private::WindowGeometry::load(canvasWindowElement); 0398 } 0399 0400 QDomElement state = windowElement.firstChildElement("windowState"); 0401 window.windowState = QByteArray::fromBase64(state.text().toLatin1()); 0402 0403 d->windows.append(window); 0404 } 0405 } 0406 0407 QString KisWindowLayoutResource::defaultFileExtension() const 0408 { 0409 return QString(".kwl"); 0410 } 0411 0412 void KisWindowLayoutResource::setWindows(const QList<QPointer<KisMainWindow>> &mainWindows) 0413 { 0414 d->windows.clear(); 0415 0416 QList<QScreen*> screens = d->getScreensInConsistentOrder(); 0417 0418 Q_FOREACH(auto window, mainWindows) { 0419 if (!window->isVisible()) continue; 0420 0421 Private::Window state; 0422 state.windowId = window->id(); 0423 state.windowState = window->saveState(); 0424 state.geometry = Private::WindowGeometry::fromWindow(window, screens); 0425 0426 state.canvasDetached = window->canvasDetached(); 0427 if (state.canvasDetached) { 0428 state.canvasWindowGeometry = Private::WindowGeometry::fromWindow(window->canvasWindow(), screens); 0429 } 0430 0431 d->windows.append(state); 0432 } 0433 }