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>> &currentWindows) {
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>> &currentWindows) {
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 }