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 }