File indexing completed on 2025-01-26 05:06:22

0001 /*
0002     SPDX-FileCopyrightText: 2017 Klarälvdalens Datakonsult AB a KDAB Group company <info@kdab.com>
0003     SPDX-FileCopyrightText: 2023 Harald Sitter <sitter@kde.org>
0004 
0005     Work sponsored by the LiMux project of the city of Munich.
0006     SPDX-FileContributor: Andras Mantia <andras.mantia@kdab.com>
0007 
0008     SPDX-License-Identifier: GPL-2.0-or-later
0009 */
0010 #include <optional>
0011 
0012 #include "screenmapper.h"
0013 
0014 #include <QMap>
0015 #include <QScreen>
0016 #include <QTimer>
0017 
0018 #include <PlasmaActivities/Consumer>
0019 #include <KConfig>
0020 #include <KConfigGroup>
0021 #include <Plasma/Corona>
0022 #include <chrono>
0023 
0024 #include "debug.h"
0025 
0026 using namespace std::chrono_literals;
0027 
0028 namespace
0029 {
0030 // The maximum amount of mappings we allow. This prevents performance and memory exhaustion problems when too many
0031 // items are on the desktop.
0032 // https://bugs.kde.org/show_bug.cgi?id=469445
0033 constexpr auto MAX_MAPPING_COUNT = 4096;
0034 } // namespace
0035 
0036 ScreenMapper *ScreenMapper::instance()
0037 {
0038     static ScreenMapper *s_instance = new ScreenMapper();
0039     return s_instance;
0040 }
0041 
0042 ScreenMapper::ScreenMapper(QObject *parent)
0043     : QObject(parent)
0044     , m_screenMappingChangedTimer(new QTimer(this))
0045 
0046 {
0047     connect(m_screenMappingChangedTimer, &QTimer::timeout, this, &ScreenMapper::screenMappingChanged);
0048 
0049     connect(this, &ScreenMapper::screenMappingChanged, this, [this] {
0050         if (!m_corona)
0051             return;
0052 
0053         auto config = m_corona->config();
0054         KConfigGroup group(config, QStringLiteral("ScreenMapping"));
0055         group.writeEntry(QStringLiteral("screenMapping"), screenMapping());
0056         config->sync();
0057     });
0058 
0059     // used to compress screenMappingChanged signals when addMapping is called multiple times,
0060     // eg. from FolderModel::filterAcceptRows. The timer interval is an arbitrary number,
0061     // that doesn't delay too much the signal, but still compresses as much as possible
0062     m_screenMappingChangedTimer->setInterval(100ms);
0063     m_screenMappingChangedTimer->setSingleShot(true);
0064 }
0065 
0066 void ScreenMapper::removeScreen(int screenId, const QString &activity, const QUrl &screenUrl)
0067 {
0068     const std::pair<int, QString> pair = std::make_pair(screenId, activity);
0069 
0070     if (screenId < 0 || !m_availableScreens.contains(pair))
0071         return;
0072 
0073     const auto screenPathWithScheme = screenUrl.url();
0074     // store the original location for the items
0075     QList<QUrl> urlsToRemoveFromMapping;
0076     for (auto it = m_screenItemMap.constBegin(); it != m_screenItemMap.constEnd(); ++it) {
0077         const auto &name = it.key();
0078         if (it.value() == screenId && name.first.url().startsWith(screenPathWithScheme) && name.second == activity) {
0079             bool found = false;
0080             for (const auto &disabledUrls : std::as_const(m_itemsOnDisabledScreensMap)) {
0081                 found = disabledUrls.contains(name.first);
0082                 if (found)
0083                     break;
0084             }
0085             if (!found) {
0086                 auto urlVectorIt = m_itemsOnDisabledScreensMap.find(pair);
0087                 if (urlVectorIt == m_itemsOnDisabledScreensMap.end()) {
0088                     m_itemsOnDisabledScreensMap[pair] = {name.first};
0089                 } else {
0090                     urlVectorIt->insert(name.first);
0091                 }
0092             }
0093             urlsToRemoveFromMapping.append(name.first);
0094         }
0095     }
0096 
0097     saveDisabledScreensMap();
0098 
0099     for (const auto &url : urlsToRemoveFromMapping)
0100         removeFromMap(url, activity);
0101 
0102     m_availableScreens.removeAll(pair);
0103 
0104     auto pathIt = m_screensPerPath.find(screenUrl);
0105     if (pathIt != m_screensPerPath.end() && pathIt.value().size() > 0) {
0106         // remove the screen for a certain url, used when switching the URL for a screen
0107         pathIt->removeAll(pair);
0108     } else if (screenUrl.isEmpty()) {
0109         // the screen was indeed removed so all references to it needs to be cleaned up
0110         for (auto &pathIt : m_screensPerPath) {
0111             pathIt.removeAll(pair);
0112         }
0113     }
0114 
0115     Q_EMIT screensChanged();
0116 }
0117 
0118 void ScreenMapper::addScreen(int screenId, const QString &activity, const QUrl &screenUrl)
0119 {
0120     const std::pair<int, QString> pair = std::make_pair(screenId, activity);
0121 
0122     if (screenId < 0 || m_availableScreens.contains(pair))
0123         return;
0124 
0125     const auto screenPathWithScheme = screenUrl.url();
0126     // restore the stored locations
0127     auto it = m_itemsOnDisabledScreensMap.find(pair);
0128     if (it != m_itemsOnDisabledScreensMap.end()) {
0129         auto &items = it.value();
0130         auto itemIt = items.begin();
0131         while (itemIt != items.end()) {
0132             // add the items to the new screen, if they are on a disabled screen and their
0133             // location is below the new screen's path
0134             if (itemIt->url().startsWith(screenPathWithScheme)) {
0135                 addMapping(*itemIt, screenId, activity, DelayedSignal);
0136                 itemIt = items.erase(itemIt);
0137                 continue;
0138             }
0139             itemIt = std::next(itemIt);
0140         }
0141 
0142         if (items.empty()) {
0143             m_itemsOnDisabledScreensMap.erase(it);
0144         }
0145     }
0146     saveDisabledScreensMap();
0147 
0148     m_availableScreens.append(pair);
0149 
0150     // path is empty when a new screen appears that has no folderview base path associated with
0151     if (!screenUrl.isEmpty()) {
0152         auto it = m_screensPerPath.find(screenUrl);
0153         if (it == m_screensPerPath.end()) {
0154             m_screensPerPath[screenUrl] = {pair};
0155         } else {
0156             it->append(pair);
0157         }
0158     }
0159 
0160     Q_EMIT screensChanged();
0161 }
0162 
0163 void ScreenMapper::addMapping(const QUrl &url, int screen, const QString &activity, MappingSignalBehavior behavior)
0164 {
0165     if (m_screenItemMap.count() > MAX_MAPPING_COUNT) {
0166         // Don't spam this
0167         static auto reported = false;
0168         if (!reported) {
0169             qCCritical(FOLDER)
0170                 << "Greater than" << MAX_MAPPING_COUNT
0171                 << "files and folders on the desktop; this is too many to map their positions in a performant way! Not adding any more position mappings.";
0172             reported = true;
0173         }
0174         return;
0175     }
0176 
0177     m_screenItemMap[std::make_pair(url, activity)] = screen;
0178 
0179     if (behavior == DelayedSignal) {
0180         m_screenMappingChangedTimer->start();
0181     } else {
0182         Q_EMIT screenMappingChanged();
0183     }
0184 }
0185 
0186 void ScreenMapper::removeFromMap(const QUrl &url, const QString &activity)
0187 {
0188     m_screenItemMap.remove(std::make_pair(url, activity));
0189 
0190     m_screenMappingChangedTimer->start();
0191 }
0192 
0193 int ScreenMapper::firstAvailableScreen(const QUrl &screenUrl, const QString &activity) const
0194 {
0195     auto screens = m_screensPerPath[screenUrl];
0196     std::optional<int> newFirstScreen = std::nullopt;
0197 
0198     for (const std::pair<int, QString> &screen : std::as_const(screens)) {
0199         if (screen.second != activity) {
0200             continue;
0201         }
0202 
0203         if (newFirstScreen.has_value()) {
0204             if (newFirstScreen > screen.first) {
0205                 newFirstScreen = screen.first;
0206             }
0207         } else {
0208             newFirstScreen = screen.first;
0209         }
0210     }
0211 
0212     return newFirstScreen.value_or(-1);
0213 }
0214 
0215 void ScreenMapper::removeItemFromDisabledScreen(const QUrl &url)
0216 {
0217     for (auto it = m_itemsOnDisabledScreensMap.begin(); it != m_itemsOnDisabledScreensMap.end(); ++it) {
0218         auto urls = &(*it);
0219         urls->remove(url);
0220     }
0221 }
0222 
0223 void ScreenMapper::setSharedDesktop(bool sharedDesktops)
0224 {
0225     if (m_sharedDesktops != sharedDesktops) {
0226         m_sharedDesktops = true;
0227         if (!m_corona)
0228             return;
0229 
0230         auto config = m_corona->config();
0231         KConfigGroup group(config, QStringLiteral("ScreenMapping"));
0232         group.writeEntry(QStringLiteral("sharedDesktops"), m_sharedDesktops);
0233     }
0234 }
0235 
0236 #ifdef BUILD_TESTING
0237 void ScreenMapper::cleanup()
0238 {
0239     m_screenItemMap.clear();
0240     m_itemsOnDisabledScreensMap.clear();
0241     m_screensPerPath.clear();
0242     m_availableScreens.clear();
0243 }
0244 #endif
0245 
0246 void ScreenMapper::setCorona(Plasma::Corona *corona)
0247 {
0248     if (m_corona != corona) {
0249         Q_ASSERT(!m_corona);
0250 
0251         m_corona = corona;
0252         if (m_corona) {
0253             auto config = m_corona->config();
0254             KConfigGroup group(config, QStringLiteral("ScreenMapping"));
0255             const QStringList mapping = group.readEntry(QStringLiteral("screenMapping"), QStringList{});
0256             setScreenMapping(mapping);
0257             m_sharedDesktops = group.readEntry(QStringLiteral("sharedDesktops"), false);
0258 
0259             const QStringList serializedMap = group.readEntry(QStringLiteral("itemsOnDisabledScreens"), QStringList{});
0260             readDisabledScreensMap(serializedMap);
0261         }
0262     }
0263 }
0264 
0265 QStringList ScreenMapper::screenMapping() const
0266 {
0267     QStringList result;
0268     result.reserve(m_screenItemMap.count() * 3); // Match setScreenMapping()
0269     auto it = m_screenItemMap.constBegin();
0270     int i = 0;
0271     while (it != m_screenItemMap.constEnd()) {
0272         if (i >= MAX_MAPPING_COUNT) {
0273             qCCritical(FOLDER)
0274                 << "Greater than" << MAX_MAPPING_COUNT
0275                 << "disabled files and folders; this is too many to remember their position in a performant way! Not adding any more position mappings.";
0276             break;
0277         }
0278         result.append(it.key().first.toString());
0279         result.append(QString::number(it.value())); // Screen ID
0280         result.append(it.key().second); // Activity ID
0281         ++it;
0282         ++i;
0283     }
0284 
0285     return result;
0286 }
0287 
0288 void ScreenMapper::setScreenMapping(const QStringList &mapping)
0289 {
0290     decltype(m_screenItemMap) newMap;
0291     // <url>,<screen id>,<activity id>
0292     constexpr int sizeOfParamGroup = 3; // Match screenMapping()
0293     const int count = std::min<int>(mapping.size(), MAX_MAPPING_COUNT * sizeOfParamGroup);
0294     Q_ASSERT(count % sizeOfParamGroup == 0);
0295 
0296     newMap.reserve(count / sizeOfParamGroup);
0297     QMap<int, int> screenConsistencyMap;
0298     for (int i = 0; i < count - (sizeOfParamGroup - 1); i += sizeOfParamGroup) {
0299         if (i + (sizeOfParamGroup - 1) < count) {
0300             const QUrl url = QUrl::fromUserInput(mapping[i], {}, QUrl::AssumeLocalFile);
0301             const QString activity = mapping[i + 2];
0302             newMap[std::make_pair(url, activity)] = mapping[i + 1].toInt();
0303             screenConsistencyMap[mapping[i + 1].toInt()] = -1;
0304         }
0305     }
0306 
0307     int lastMappedScreen = 0;
0308     for (int key : screenConsistencyMap.keys()) {
0309         screenConsistencyMap[key] = lastMappedScreen++;
0310     }
0311 
0312     for (auto it = newMap.begin(); it != newMap.end(); it++) {
0313         newMap[it.key()] = screenConsistencyMap.value(it.value());
0314     }
0315 
0316     if (m_screenItemMap != newMap) {
0317         m_screenItemMap = newMap;
0318         Q_EMIT screenMappingChanged();
0319     }
0320 }
0321 
0322 int ScreenMapper::screenForItem(const QUrl &url, const QString &activity) const
0323 {
0324     const int screen = m_screenItemMap.value(std::make_pair(url, activity), -1);
0325 
0326     if (!m_availableScreens.contains(std::make_pair(screen, activity)))
0327         return -1;
0328 
0329     return screen;
0330 }
0331 
0332 QUrl ScreenMapper::stringToUrl(const QString &path)
0333 {
0334     return QUrl::fromUserInput(path, {}, QUrl::AssumeLocalFile);
0335 }
0336 
0337 QStringList ScreenMapper::disabledScreensMap() const
0338 {
0339     QStringList serializedMap;
0340     auto it = m_itemsOnDisabledScreensMap.constBegin();
0341     for (int i = 0; it != m_itemsOnDisabledScreensMap.constEnd(); it = std::next(it), ++i) {
0342         if (i >= MAX_MAPPING_COUNT) {
0343             qCCritical(FOLDER)
0344                 << "Greater than" << MAX_MAPPING_COUNT
0345                 << "files and folders on the desktop; this is too many to map their positions in a performant way! Not adding any more position mappings.";
0346             break;
0347         }
0348         serializedMap.append(QString::number(it.key().first)); // Screen ID
0349         serializedMap.append(it.key().second); // Activity ID
0350         const auto urls = it.value();
0351         serializedMap.append(QString::number(urls.size())); // Number of urls
0352         for (const auto &url : urls) {
0353             serializedMap.append(url.toString());
0354         }
0355     }
0356 
0357     return serializedMap;
0358 }
0359 
0360 void ScreenMapper::readDisabledScreensMap(const QStringList &serializedMap)
0361 {
0362     m_itemsOnDisabledScreensMap.clear();
0363     bool readingScreenId = true;
0364     bool readingActivityId = true;
0365     int vectorSize = -1;
0366     int screenId = -1;
0367     QString activityId;
0368     int vectorCounter = 0;
0369 
0370     // <screen id>,<activity id>,<number of urls>,<url 1>, ...
0371     for (const auto &entry : serializedMap) {
0372         if (readingScreenId) {
0373             screenId = entry.toInt();
0374             readingScreenId = false;
0375         } else if (readingActivityId) {
0376             activityId = entry;
0377             readingActivityId = false;
0378         } else if (vectorSize == -1 /*number of urls is not read*/) {
0379             vectorSize = entry.toInt();
0380         } else {
0381             const auto url = stringToUrl(entry);
0382             const auto pair = std::make_pair(screenId, activityId);
0383             auto urlVectorIt = m_itemsOnDisabledScreensMap.find(pair);
0384             if (urlVectorIt == m_itemsOnDisabledScreensMap.end()) {
0385                 m_itemsOnDisabledScreensMap[pair] = {url};
0386             } else {
0387                 urlVectorIt->insert(url);
0388             }
0389             vectorCounter++;
0390             if (vectorCounter == vectorSize) {
0391                 readingScreenId = true;
0392                 readingActivityId = true;
0393                 screenId = -1;
0394                 vectorCounter = 0;
0395                 vectorSize = -1;
0396             }
0397         }
0398     }
0399 }
0400 
0401 void ScreenMapper::saveDisabledScreensMap() const
0402 {
0403     if (!m_corona)
0404         return;
0405 
0406     auto config = m_corona->config();
0407     KConfigGroup group(config, QStringLiteral("ScreenMapping"));
0408     const auto serializedMap = disabledScreensMap();
0409 
0410     group.writeEntry(QStringLiteral("itemsOnDisabledScreens"), serializedMap);
0411 }