SPDX-FileCopyrightText: 2017 Klarälvdalens Datakonsult AB a KDAB Group company <info@kdab.com>
SPDX-FileCopyrightText: 2023 Harald Sitter <sitter@kde.org>
Work sponsored by the LiMux project of the city of Munich.
SPDX-FileContributor: Andras Mantia <andras.mantia@kdab.com>
SPDX-License-Identifier: GPL-2.0-or-later
*/
#include <optional>
#include "screenmapper.h"
#include <QMap>
#include <QScreen>
#include <QTimer>
#include <KConfig>
#include <KConfigGroup>
#include <Plasma/Corona>
#include <PlasmaActivities/Consumer>
#include <chrono>
#include "debug.h"
using namespace std::chrono_literals;
namespace
{
constexpr auto MAX_MAPPING_COUNT = 4096;
}
ScreenMapper *ScreenMapper::instance()
{
static ScreenMapper *s_instance = new ScreenMapper();
return s_instance;
}
ScreenMapper::ScreenMapper(QObject *parent)
: QObject(parent)
, m_screenMappingChangedTimer(new QTimer(this))
, m_screenTransitionTimer(new QTimer(this))
{
connect(m_screenMappingChangedTimer, &QTimer::timeout, this, &ScreenMapper::screenMappingChanged);
connect(this, &ScreenMapper::screenMappingChanged, this, [this] {
if (m_disabledScreensMapDirty) {
saveDisabledScreensMap();
m_disabledScreensMapDirty = false;
}
if (!m_corona)
return;
auto config = m_corona->config();
KConfigGroup group(config, QStringLiteral("ScreenMapping"));
group.writeEntry(QStringLiteral("screenMapping"), screenMapping());
config->sync();
});
m_screenMappingChangedTimer->setInterval(100ms);
m_screenMappingChangedTimer->setSingleShot(true);
m_screenTransitionTimer->setInterval(0);
m_screenTransitionTimer->setSingleShot(true);
m_screenTransitionTimer->callOnTimeout(this, &ScreenMapper::processScreenTransition);
}
void ScreenMapper::registerScreen(const QObject *owner, int screenId, const QString &activity, const QUrl &screenUrl)
{
if (screenId < 0) {
return;
}
m_screenOwners.insert(owner, {screenId, activity, screenUrl});
addScreen(screenId, activity, screenUrl);
}
void ScreenMapper::unregisterScreen(const QObject *owner)
{
const auto it = m_screenOwners.find(owner);
if (it == m_screenOwners.end()) {
return;
}
const auto registration = it.value();
m_screenOwners.erase(it);
if (m_screenOwners.values().contains(registration)) {
return;
}
const auto &[screen, activity, url] = registration;
removeScreen(screen, activity, url);
}
void ScreenMapper::removeScreen(int screenId, const QString &activity, const QUrl &screenUrl)
{
const std::pair<int, QString> pair = std::make_pair(screenId, activity);
if (screenId < 0 || !m_availableScreens.contains(pair))
return;
const auto screenPathWithScheme = screenUrl.url();
QList<QUrl> urlsToRemoveFromMapping;
for (auto it = m_screenItemMap.constBegin(); it != m_screenItemMap.constEnd(); ++it) {
const auto &name = it.key();
if (it.value() == screenId && name.first.url().startsWith(screenPathWithScheme) && name.second == activity) {
bool found = false;
for (const auto &disabledUrls : std::as_const(m_itemsOnDisabledScreensMap)) {
found = disabledUrls.contains(name.first);
if (found)
break;
}
if (!found) {
auto urlVectorIt = m_itemsOnDisabledScreensMap.find(pair);
if (urlVectorIt == m_itemsOnDisabledScreensMap.end()) {
m_itemsOnDisabledScreensMap[pair] = {name.first};
} else {
urlVectorIt->insert(name.first);
}
}
urlsToRemoveFromMapping.append(name.first);
}
}
saveDisabledScreensMap();
for (const auto &url : urlsToRemoveFromMapping)
removeFromMap(url, activity);
m_availableScreens.removeAll(pair);
auto pathIt = m_screensPerPath.find(screenUrl);
if (pathIt != m_screensPerPath.end() && pathIt.value().size() > 0) {
pathIt->removeAll(pair);
} else if (screenUrl.isEmpty()) {
for (auto &pathIt : m_screensPerPath) {
pathIt.removeAll(pair);
}
}
Q_EMIT screensChanged();
}
void ScreenMapper::addScreen(int screenId, const QString &activity, const QUrl &screenUrl)
{
const std::pair<int, QString> pair = std::make_pair(screenId, activity);
if (screenId < 0 || m_availableScreens.contains(pair))
return;
const auto screenPathWithScheme = screenUrl.url();
auto it = m_itemsOnDisabledScreensMap.find(pair);
if (it != m_itemsOnDisabledScreensMap.end()) {
auto &items = it.value();
auto itemIt = items.begin();
while (itemIt != items.end()) {
if (itemIt->url().startsWith(screenPathWithScheme)) {
addMapping(*itemIt, screenId, activity, DelayedSignal);
itemIt = items.erase(itemIt);
continue;
}
itemIt = std::next(itemIt);
}
if (items.empty()) {
m_itemsOnDisabledScreensMap.erase(it);
}
}
saveDisabledScreensMap();
m_availableScreens.append(pair);
if (!screenUrl.isEmpty()) {
auto it = m_screensPerPath.find(screenUrl);
if (it == m_screensPerPath.end()) {
m_screensPerPath[screenUrl] = {pair};
} else {
it->append(pair);
}
}
Q_EMIT screensChanged();
}
void ScreenMapper::maybeMoveToDisabledScreens(const QUrl &url, const QString &activity)
{
if (auto it = m_screenItemMap.find(std::make_pair(url, activity)); it != m_screenItemMap.end() && it.value() > -1) {
m_itemsOnDisabledScreensMap[std::make_pair(it.value(), activity)].insert(url);
m_screenItemMap.erase(it);
m_disabledScreensMapDirty = true;
m_screenMappingChangedTimer->start();
}
}
void ScreenMapper::addMapping(const QUrl &url, int screen, const QString &activity, MappingSignalBehavior behavior)
{
if (m_screenItemMap.count() > MAX_MAPPING_COUNT) {
static auto reported = false;
if (!reported) {
qCCritical(FOLDER)
<< "Greater than" << MAX_MAPPING_COUNT
<< "files and folders on the desktop; this is too many to map their positions in a performant way! Not adding any more position mappings.";
reported = true;
}
return;
}
m_screenItemMap[std::make_pair(url, activity)] = screen;
if (behavior == DelayedSignal) {
m_screenMappingChangedTimer->start();
} else {
Q_EMIT screenMappingChanged();
}
}
void ScreenMapper::removeFromMap(const QUrl &url, const QString &activity)
{
m_screenItemMap.remove(std::make_pair(url, activity));
m_screenMappingChangedTimer->start();
}
int ScreenMapper::firstAvailableScreen(const QUrl &screenUrl, const QString &activity) const
{
auto screens = m_screensPerPath[screenUrl];
std::optional<int> newFirstScreen = std::nullopt;
for (const std::pair<int, QString> &screen : std::as_const(screens)) {
if (screen.second != activity) {
continue;
}
if (newFirstScreen.has_value()) {
if (newFirstScreen > screen.first) {
newFirstScreen = screen.first;
}
} else {
newFirstScreen = screen.first;
}
}
return newFirstScreen.value_or(-1);
}
void ScreenMapper::removeItemFromDisabledScreen(const QUrl &url)
{
for (auto it = m_itemsOnDisabledScreensMap.begin(); it != m_itemsOnDisabledScreensMap.end(); ++it) {
auto urls = &(*it);
urls->remove(url);
}
}
void ScreenMapper::addScreenTransition(int screenSrc, int screenDst, const QString &activity)
{
Q_ASSERT(screenSrc > -1);
Q_ASSERT(screenDst > -1);
m_screenTransitions.push_back(std::make_tuple(screenSrc, screenDst, activity));
m_screenTransitionTimer->start();
}
void ScreenMapper::setSharedDesktop(bool sharedDesktops)
{
if (m_sharedDesktops != sharedDesktops) {
m_sharedDesktops = true;
if (!m_corona)
return;
auto config = m_corona->config();
KConfigGroup group(config, QStringLiteral("ScreenMapping"));
group.writeEntry(QStringLiteral("sharedDesktops"), m_sharedDesktops);
}
}
#ifdef BUILD_TESTING
void ScreenMapper::cleanup()
{
m_screenItemMap.clear();
m_itemsOnDisabledScreensMap.clear();
m_screensPerPath.clear();
m_availableScreens.clear();
m_screenOwners.clear();
}
#endif
void ScreenMapper::setCorona(Plasma::Corona *corona)
{
if (m_corona != corona) {
Q_ASSERT(!m_corona);
m_corona = corona;
if (m_corona) {
auto config = m_corona->config();
KConfigGroup group(config, QStringLiteral("ScreenMapping"));
const QStringList mapping = group.readEntry(QStringLiteral("screenMapping"), QStringList{});
setScreenMapping(mapping);
m_sharedDesktops = group.readEntry(QStringLiteral("sharedDesktops"), false);
const QStringList serializedMap = group.readEntry(QStringLiteral("itemsOnDisabledScreens"), QStringList{});
readDisabledScreensMap(serializedMap);
}
}
}
QStringList ScreenMapper::screenMapping() const
{
QStringList result;
result.reserve(m_screenItemMap.count() * 3);
auto it = m_screenItemMap.constBegin();
int i = 0;
while (it != m_screenItemMap.constEnd()) {
if (i >= MAX_MAPPING_COUNT) {
qCCritical(FOLDER)
<< "Greater than" << MAX_MAPPING_COUNT
<< "disabled files and folders; this is too many to remember their position in a performant way! Not adding any more position mappings.";
break;
}
result.append(it.key().first.toString());
result.append(QString::number(it.value()));
result.append(it.key().second);
++it;
++i;
}
return result;
}
void ScreenMapper::setScreenMapping(const QStringList &mapping)
{
decltype(m_screenItemMap) newMap;
constexpr int sizeOfParamGroup = 3;
const int count = std::min<int>(mapping.size(), MAX_MAPPING_COUNT * sizeOfParamGroup);
Q_ASSERT(count % sizeOfParamGroup == 0);
newMap.reserve(count / sizeOfParamGroup);
for (int i = 0; i < count - (sizeOfParamGroup - 1); i += sizeOfParamGroup) {
if (i + (sizeOfParamGroup - 1) < count) {
const QUrl url = QUrl::fromUserInput(mapping[i], {}, QUrl::AssumeLocalFile);
const QString activity = mapping[i + 2];
newMap[std::make_pair(url, activity)] = mapping[i + 1].toInt();
}
}
if (m_screenItemMap != newMap) {
m_screenItemMap = newMap;
Q_EMIT screenMappingChanged();
}
}
int ScreenMapper::screenForItem(const QUrl &url, const QString &activity) const
{
const int screen = m_screenItemMap.value(std::make_pair(url, activity), -1);
if (!m_availableScreens.contains(std::make_pair(screen, activity)))
return -1;
return screen;
}
QUrl ScreenMapper::stringToUrl(const QString &path)
{
return QUrl::fromUserInput(path, {}, QUrl::AssumeLocalFile);
}
void ScreenMapper::processScreenTransition()
{
auto cleaner = qScopeGuard([this] {
m_screenTransitions.clear();
});
if (m_screenTransitions.size() != 2) {
return;
}
auto &&[screen1, screen2, activity] = m_screenTransitions.constFirst();
auto &&[screenTmp2, screenTmp1, activityTmp] = m_screenTransitions.constLast();
if (screen1 != screenTmp1 || screen2 != screenTmp2 || activity != activityTmp) {
return;
}
for (auto &&[pair, screen] : m_screenItemMap.asKeyValueRange()) {
if (pair.second != activity) {
continue;
}
if (screen == screen1) {
screen = screen2;
} else if (screen == screen2) {
screen = screen1;
}
}
m_screenMappingChangedTimer->start();
}
QStringList ScreenMapper::disabledScreensMap() const
{
QStringList serializedMap;
auto it = m_itemsOnDisabledScreensMap.constBegin();
for (int i = 0; it != m_itemsOnDisabledScreensMap.constEnd(); it = std::next(it), ++i) {
if (i >= MAX_MAPPING_COUNT) {
qCCritical(FOLDER)
<< "Greater than" << MAX_MAPPING_COUNT
<< "files and folders on the desktop; this is too many to map their positions in a performant way! Not adding any more position mappings.";
break;
}
serializedMap.append(QString::number(it.key().first));
serializedMap.append(it.key().second);
const auto urls = it.value();
serializedMap.append(QString::number(urls.size()));
for (const auto &url : urls) {
serializedMap.append(url.toString());
}
}
return serializedMap;
}
void ScreenMapper::readDisabledScreensMap(const QStringList &serializedMap)
{
m_itemsOnDisabledScreensMap.clear();
bool readingScreenId = true;
bool readingActivityId = true;
int vectorSize = -1;
int screenId = -1;
QString activityId;
int vectorCounter = 0;
for (const auto &entry : serializedMap) {
if (readingScreenId) {
screenId = entry.toInt();
readingScreenId = false;
} else if (readingActivityId) {
activityId = entry;
readingActivityId = false;
} else if (vectorSize == -1 ) {
vectorSize = entry.toInt();
} else {
const auto url = stringToUrl(entry);
const auto pair = std::make_pair(screenId, activityId);
auto urlVectorIt = m_itemsOnDisabledScreensMap.find(pair);
if (urlVectorIt == m_itemsOnDisabledScreensMap.end()) {
m_itemsOnDisabledScreensMap[pair] = {url};
} else {
urlVectorIt->insert(url);
}
vectorCounter++;
if (vectorCounter == vectorSize) {
readingScreenId = true;
readingActivityId = true;
screenId = -1;
vectorCounter = 0;
vectorSize = -1;
}
}
}
}
void ScreenMapper::saveDisabledScreensMap() const
{
if (!m_corona)
return;
auto config = m_corona->config();
KConfigGroup group(config, QStringLiteral("ScreenMapping"));
const auto serializedMap = disabledScreensMap();
group.writeEntry(QStringLiteral("itemsOnDisabledScreens"), serializedMap);
}
#include "moc_screenmapper.cpp"