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>> &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                 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 }