File indexing completed on 2024-05-12 15:34:14

0001 /*
0002     This file is part of the KDE libraries
0003     SPDX-FileCopyrightText: 2012 Benjamin Port <benjamin.port@ben2367.fr>
0004 
0005     SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
0006 */
0007 
0008 #include "kwindowconfig.h"
0009 #include "ksharedconfig.h"
0010 
0011 #include <QGuiApplication>
0012 #include <QScreen>
0013 #include <QWindow>
0014 
0015 // QScreen::name() returns garbage on Windows; see https://bugreports.qt.io/browse/QTBUG-74317
0016 // So we use the screens' serial numbers to identify them instead
0017 // FIXME: remove this once we can depend on Qt 6.4, where this is fixed
0018 #if defined(Q_OS_WINDOWS) && QT_VERSION < QT_VERSION_CHECK(6, 4, 0)
0019 #define SCREENNAME serialNumber
0020 #else
0021 #define SCREENNAME name
0022 #endif
0023 
0024 static const char s_initialSizePropertyName[] = "_kconfig_initial_size";
0025 static const char s_initialScreenSizePropertyName[] = "_kconfig_initial_screen_size";
0026 
0027 // Convenience function to get a space-separated list of all connected screens
0028 static QString allConnectedScreens()
0029 {
0030     QStringList names;
0031     const auto screens = QGuiApplication::screens();
0032     names.reserve(screens.length());
0033     for (auto screen : screens) {
0034         names << screen->SCREENNAME();
0035     }
0036     // A string including the connector names is used in the config file key for
0037     // storing per-screen-arrangement size and position data, which means we
0038     // need this string to be consistent for the same screen arrangement. But
0039     // connector order is non-deterministic. We need to sort the list to keep a
0040     // consistent order and avoid losing multi-screen size and position data.
0041     names.sort();
0042     return names.join(QLatin1Char(' '));
0043 }
0044 
0045 // Convenience function to return screen by its name from window screen siblings
0046 // returns current window screen if not found
0047 static QScreen *findScreenByName(const QWindow *window, const QString screenName)
0048 {
0049     if (screenName == window->screen()->SCREENNAME()) {
0050         return window->screen();
0051     }
0052     for (QScreen *s : window->screen()->virtualSiblings()) {
0053         if (s->SCREENNAME() == screenName) {
0054             return s;
0055         }
0056     }
0057     return window->screen();
0058 }
0059 
0060 // Convenience function to get an appropriate config file key under which to
0061 // save window size, position, or maximization information.
0062 static QString configFileString(const QScreen *screen, const QString &key)
0063 {
0064     Q_UNUSED(screen);
0065     QString returnString;
0066     const int numberOfScreens = QGuiApplication::screens().length();
0067 
0068     if (numberOfScreens == 1) {
0069         // For single-screen setups, we save data on a per-resolution basis.
0070         const QRect screenGeometry = QGuiApplication::primaryScreen()->geometry();
0071         returnString = QStringLiteral("%1x%2 screen: %3").arg(QString::number(screenGeometry.width()), QString::number(screenGeometry.height()), key);
0072     } else {
0073         // For multi-screen setups, we save data based on the number of screens.
0074         // Distinguishing individual screens based on their names is unreliable
0075         // due to name strings being inherently volatile.
0076         returnString = QStringLiteral("%1 screens: %2").arg(QString::number(numberOfScreens), key);
0077     }
0078     return returnString;
0079 }
0080 
0081 // Convenience function for "window is maximized" string
0082 static QString screenMaximizedString(const QScreen *screen)
0083 {
0084     return configFileString(screen, QStringLiteral("Window-Maximized"));
0085 }
0086 // Convenience function for window width string
0087 static QString windowWidthString(const QScreen *screen)
0088 {
0089     return configFileString(screen, QStringLiteral("Width"));
0090 }
0091 // Convenience function for window height string
0092 static QString windowHeightString(const QScreen *screen)
0093 {
0094     return configFileString(screen, QStringLiteral("Height"));
0095 }
0096 // Convenience function for window X position string
0097 static QString windowXPositionString(const QScreen *screen)
0098 {
0099     return configFileString(screen, QStringLiteral("XPosition"));
0100 }
0101 // Convenience function for window Y position string
0102 static QString windowYPositionString(const QScreen *screen)
0103 {
0104     return configFileString(screen, QStringLiteral("YPosition"));
0105 }
0106 static QString windowScreenPositionString()
0107 {
0108     return QStringLiteral("%1").arg(allConnectedScreens());
0109 }
0110 
0111 void KWindowConfig::saveWindowSize(const QWindow *window, KConfigGroup &config, KConfigGroup::WriteConfigFlags options)
0112 {
0113     // QWindow::screen() shouldn't return null, but it sometimes does due to bugs.
0114     if (!window || !window->screen()) {
0115         return;
0116     }
0117     const QScreen *screen = window->screen();
0118 
0119     const QSize sizeToSave = window->size();
0120     const bool isMaximized = window->windowState() & Qt::WindowMaximized;
0121 
0122     // Save size only if window is not maximized
0123     if (!isMaximized) {
0124         const QSize defaultSize(window->property(s_initialSizePropertyName).toSize());
0125         const QSize defaultScreenSize(window->property(s_initialScreenSizePropertyName).toSize());
0126         const bool sizeValid = defaultSize.isValid() && defaultScreenSize.isValid();
0127         if (!sizeValid || (sizeValid && (defaultSize != sizeToSave || defaultScreenSize != screen->geometry().size()))) {
0128             config.writeEntry(windowWidthString(screen), sizeToSave.width(), options);
0129             config.writeEntry(windowHeightString(screen), sizeToSave.height(), options);
0130             // Don't keep the maximized string in the file since the window is
0131             // no longer maximized at this point
0132             config.deleteEntry(screenMaximizedString(screen));
0133         }
0134     }
0135     if ((isMaximized == false) && !config.hasDefault(screenMaximizedString(screen))) {
0136         config.revertToDefault(screenMaximizedString(screen));
0137     } else {
0138         config.writeEntry(screenMaximizedString(screen), isMaximized, options);
0139     }
0140 }
0141 
0142 void KWindowConfig::restoreWindowSize(QWindow *window, const KConfigGroup &config)
0143 {
0144     if (!window) {
0145         return;
0146     }
0147 
0148     const QString screenName = config.readEntry(windowScreenPositionString(), window->screen()->SCREENNAME());
0149     const QScreen *screen = findScreenByName(window, screenName);
0150 
0151     // Fall back to non-per-screen-arrangement info if it's available but
0152     // per-screen-arrangement information is not
0153     // TODO: Remove in KF6 or maybe even KF5.80 or something. It really only needs
0154     // to be here to transition existing users once they upgrade from 5.73 -> 5.74
0155     const int fallbackWidth = config.readEntry(QStringLiteral("Width %1").arg(screen->geometry().width()), window->size().width());
0156     const int fallbackHeight = config.readEntry(QStringLiteral("Height %1").arg(screen->geometry().height()), window->size().height());
0157 
0158     const int width = config.readEntry(windowWidthString(screen), fallbackWidth);
0159     const int height = config.readEntry(windowHeightString(screen), fallbackHeight);
0160     const bool isMaximized = config.readEntry(configFileString(screen, QStringLiteral("Window-Maximized")), false);
0161 
0162     // Check default size
0163     const QSize defaultSize(window->property(s_initialSizePropertyName).toSize());
0164     const QSize defaultScreenSize(window->property(s_initialScreenSizePropertyName).toSize());
0165     if (!defaultSize.isValid() || !defaultScreenSize.isValid()) {
0166         window->setProperty(s_initialSizePropertyName, window->size());
0167         window->setProperty(s_initialScreenSizePropertyName, screen->geometry().size());
0168     }
0169 
0170     // If window is maximized set maximized state and in all case set the size
0171     window->resize(width, height);
0172     if (isMaximized) {
0173         window->setWindowState(Qt::WindowMaximized);
0174     }
0175 }
0176 
0177 void KWindowConfig::saveWindowPosition(const QWindow *window, KConfigGroup &config, KConfigGroup::WriteConfigFlags options)
0178 {
0179     // On Wayland, the compositor is solely responsible for window positioning,
0180     // So this needs to be a no-op
0181     if (!window || QGuiApplication::platformName() == QLatin1String{"wayland"}) {
0182         return;
0183     }
0184 
0185     // If the window is maximized, saving the position will only serve to mis-position
0186     // it once de-maximized, so let's not do that
0187     if (window->windowState() & Qt::WindowMaximized) {
0188         return;
0189     }
0190 
0191     const QScreen *screen = window->screen();
0192     config.writeEntry(windowXPositionString(screen), window->x(), options);
0193     config.writeEntry(windowYPositionString(screen), window->y(), options);
0194     config.writeEntry(windowScreenPositionString(), screen->SCREENNAME(), options);
0195 }
0196 
0197 void KWindowConfig::restoreWindowPosition(QWindow *window, const KConfigGroup &config)
0198 {
0199     // On Wayland, the compositor is solely responsible for window positioning,
0200     // So this needs to be a no-op
0201     if (!window || QGuiApplication::platformName() == QLatin1String{"wayland"}) {
0202         return;
0203     }
0204 
0205     const QScreen *screen = window->screen();
0206     const bool isMaximized = config.readEntry(configFileString(screen, QStringLiteral("Window-Maximized")), false);
0207 
0208     // Don't need to restore position if the window was maximized
0209     if (isMaximized) {
0210         window->setWindowState(Qt::WindowMaximized);
0211         return;
0212     }
0213 
0214     // Move window to proper screen
0215     const QString screenName = config.readEntry(windowScreenPositionString(), screen->SCREENNAME());
0216     if (screenName != screen->SCREENNAME()) {
0217         QScreen *screenConf = findScreenByName(window, screenName);
0218         window->setScreen(screenConf);
0219         restoreWindowScreenPosition(window, screenConf, config);
0220         return;
0221     }
0222     restoreWindowScreenPosition(window, screen, config);
0223 }
0224 
0225 void KWindowConfig::restoreWindowScreenPosition(QWindow *window, const QScreen *screen, const KConfigGroup &config)
0226 {
0227     const QRect desk = window->screen()->geometry();
0228     // Fall back to non-per-resolution info if it's available but
0229     // per-resolution information is not
0230     // TODO: Remove in KF6 or maybe even KF5.85 or something. It really only needs
0231     // to be here to transition existing users once they upgrade from 5.78 -> 5.79
0232     const int fallbackXPosition = config.readEntry(QStringLiteral("%1 XPosition %2").arg(allConnectedScreens(), QString::number(desk.width())), -1);
0233     const int fallbackYPosition = config.readEntry(QStringLiteral("%1 YPosition %2").arg(allConnectedScreens(), QString::number(desk.height())), -1);
0234     const int xPos = config.readEntry(windowXPositionString(screen), fallbackXPosition);
0235     const int yPos = config.readEntry(windowYPositionString(screen), fallbackYPosition);
0236 
0237     if (xPos == -1 || yPos == -1) {
0238         return;
0239     }
0240 
0241     window->setX(xPos);
0242     window->setY(yPos);
0243 }