/* SPDX-FileCopyrightText: 2021 Cyril Rossi SPDX-License-Identifier: LGPL-2.0-or-later */ #include "shellcontainmentconfig.h" #include #include #include #include #include #include #include #include #include "screenpool.h" #include "shellcorona.h" #include using namespace std::chrono_literals; using namespace Qt::StringLiterals; ScreenPoolModel::ScreenPoolModel(ShellCorona *corona, QObject *parent) : QAbstractListModel(parent) , m_corona(corona) { m_reloadTimer = new QTimer(this); m_reloadTimer->setSingleShot(true); m_reloadTimer->setInterval(200ms); connect(m_reloadTimer, &QTimer::timeout, this, &ScreenPoolModel::load); connect(m_corona, &Plasma::Corona::screenAdded, m_reloadTimer, static_cast(&QTimer::start)); connect(m_corona, &Plasma::Corona::screenRemoved, m_reloadTimer, static_cast(&QTimer::start)); } ScreenPoolModel::~ScreenPoolModel() = default; QVariant ScreenPoolModel::data(const QModelIndex &index, int role) const { if (!index.isValid() || index.column() != 0 || index.row() < 0 || index.row() >= int(m_screens.size())) { return QVariant(); } const Data &d = m_screens.at(index.row()); switch (role) { case ScreenIdRole: return d.id; case ScreenNameRole: return d.name; case ContainmentsRole: { auto *cont = m_containments.at(index.row()); return QVariant::fromValue(cont); } case PrimaryRole: return d.primary; case EnabledRole: return d.enabled; } return QVariant(); } int ScreenPoolModel::rowCount(const QModelIndex &parent) const { if (parent.isValid()) { return 0; } return m_screens.size(); } QHash ScreenPoolModel::roleNames() const { QHash roles({ {ScreenIdRole, QByteArrayLiteral("screenId")}, {ScreenNameRole, QByteArrayLiteral("screenName")}, {ContainmentsRole, QByteArrayLiteral("containments")}, {EnabledRole, QByteArrayLiteral("isEnabled")}, {PrimaryRole, QByteArrayLiteral("isPrimary")}, }); return roles; } void ScreenPoolModel::load() { beginResetModel(); m_screens.clear(); qDeleteAll(m_containments); m_containments.clear(); QSet unknownScreenIds; for (auto *cont : m_corona->containments()) { connect(cont, &Plasma::Containment::destroyedChanged, this, &ScreenPoolModel::load, Qt::UniqueConnection); if (!cont->destroyed()) { unknownScreenIds.insert(cont->lastScreen()); } } int knownId = 0; for (QScreen *screen : m_corona->screenPool()->screenOrder()) { Data d; unknownScreenIds.remove(knownId); d.id = knownId; if (screen->name().contains(u"eDP"_s)) { d.name = i18n("Internal Screen on %1", screen->name()); } else if (screen->model().contains(screen->name())) { d.name = screen->model(); } else { d.name = i18nc("Screen manufacturer and model on connector", "%1 %2 on %3", screen->manufacturer(), screen->model(), screen->name()); } d.primary = knownId == 0; d.enabled = true; auto *conts = new ShellContainmentModel(m_corona, knownId, this); conts->load(); // Exclude screens which don't have any containemnt assigned if (conts->rowCount() > 0) { m_containments.push_back(conts); m_screens.push_back(d); } else { delete conts; } ++knownId; } QList sortedIds = unknownScreenIds.values(); std::sort(sortedIds.begin(), sortedIds.end()); for (int id : sortedIds) { Data d; d.id = id; d.name = i18n("Disconnected Screen %1", id + 1); d.primary = id == 0; d.enabled = false; auto *conts = new ShellContainmentModel(m_corona, id, this); conts->load(); m_containments.push_back(conts); m_screens.push_back(d); } endResetModel(); } void ScreenPoolModel::remove(int screenId) { // Don't allow to remove currently used containemnts if (m_corona->screenPool()->screenForId(screenId)) { return; } // remove containments of *all* activities auto conts = m_corona->containmentsForScreen(screenId); for (auto *cont : std::as_const(conts)) { // Don't call destroy directly, so we can have the undo action notification auto *destroyAction = cont->internalAction(u"remove"_s); if (destroyAction) { destroyAction->trigger(); } } } // --- ShellContainmentModel::ShellContainmentModel(ShellCorona *corona, int screenId, ScreenPoolModel *parent) : QAbstractListModel(parent) , m_screenId(screenId) , m_corona(corona) , m_screenPoolModel(parent) , m_activityConsumer(new KActivities::Consumer(this)) { m_reloadTimer = new QTimer(this); m_reloadTimer->setSingleShot(true); m_reloadTimer->setInterval(200ms); connect(m_reloadTimer, &QTimer::timeout, this, &ShellContainmentModel::load); connect(m_corona, &ShellCorona::startupCompleted, this, &ShellContainmentModel::load); connect(m_corona, &Plasma::Corona::containmentAdded, m_reloadTimer, static_cast(&QTimer::start)); connect(m_corona, &Plasma::Corona::screenOwnerChanged, m_reloadTimer, static_cast(&QTimer::start)); connect(m_corona, &ShellCorona::containmentPreviewReady, this, [this](Plasma::Containment *containment, const QString &path) { int i = 0; for (auto &d : m_containments) { if (d.containment == containment) { d.image = path; Q_EMIT dataChanged(index(i, 0), index(i, 0)); break; } ++i; } }); } ShellContainmentModel::~ShellContainmentModel() = default; QVariant ShellContainmentModel::data(const QModelIndex &index, int role) const { if (!index.isValid() || index.column() != 0 || index.row() < 0 || index.row() >= int(m_containments.size())) { return QVariant(); } const Data &d = m_containments.at(index.row()); switch (role) { case ContainmentIdRole: return d.id; case ScreenRole: return d.screen; case EdgeRole: return ShellContainmentModel::plasmaLocationToString(d.edge); case EdgePositionRole: return qMax(0, m_edgeCount.value(d.screen).value(d.edge).indexOf(d.id)); case PanelCountAtRightRole: return qMax(0, m_edgeCount.value(d.screen).value(Plasma::Types::RightEdge).count()); case PanelCountAtTopRole: return qMax(0, m_edgeCount.value(d.screen).value(Plasma::Types::TopEdge).count()); case PanelCountAtLeftRole: return qMax(0, m_edgeCount.value(d.screen).value(Plasma::Types::LeftEdge).count()); case PanelCountAtBottomRole: return qMax(0, m_edgeCount.value(d.screen).value(Plasma::Types::BottomEdge).count()); case ActivityRole: { const auto *activityInfo = m_activitiesInfos.value(d.activity); if (activityInfo) { return activityInfo->name(); } break; } case IsActiveRole: return d.isActive; case ImageSourceRole: return d.image; case DestroyedRole: return d.containment->destroyed(); } return QVariant(); } int ShellContainmentModel::rowCount(const QModelIndex &parent) const { if (parent.isValid()) { return 0; } return m_containments.size(); } QHash ShellContainmentModel::roleNames() const { QHash roles({ {ContainmentIdRole, QByteArrayLiteral("containmentId")}, {ScreenRole, QByteArrayLiteral("screen")}, {EdgeRole, QByteArrayLiteral("edge")}, {EdgePositionRole, QByteArrayLiteral("edgePosition")}, {PanelCountAtRightRole, QByteArrayLiteral("panelCountAtRight")}, {PanelCountAtTopRole, QByteArrayLiteral("panelCountAtTop")}, {PanelCountAtLeftRole, QByteArrayLiteral("panelCountAtLeft")}, {PanelCountAtBottomRole, QByteArrayLiteral("panelCountAtBottom")}, {ActivityRole, QByteArrayLiteral("activity")}, {IsActiveRole, QByteArrayLiteral("active")}, {ImageSourceRole, QByteArrayLiteral("imageSource")}, {DestroyedRole, QByteArrayLiteral("isDestroyed")}, }); return roles; } ScreenPoolModel *ShellContainmentModel::screenPoolModel() const { return m_screenPoolModel; } void ShellContainmentModel::remove(int contId) { if (contId < 0) { return; } auto *cont = containmentById(contId); if (cont) { disconnect(cont, nullptr, this, nullptr); // Don't call destroy directly, so we can have the undo action notification auto *destroyAction = cont->internalAction(u"remove"_s); if (destroyAction) { destroyAction->trigger(); } } // Don't call load() there as is already triggered by destroyedChanged signal of the containemnt } void ShellContainmentModel::moveContainementToScreen(unsigned int contId, int newScreen) { if (contId == 0 || newScreen < 0) { return; } auto containmentIt = std::find_if(m_containments.begin(), m_containments.end(), [contId](Data &d) { return d.id == contId; }); if (containmentIt == m_containments.end()) { return; } if (containmentIt->screen == newScreen) { return; } auto *cont = containmentById(contId); if (cont == nullptr) { return; } // If it's a panel, only move that one if (cont->containmentType() == Plasma::Containment::Panel || cont->containmentType() == Plasma::Containment::CustomPanel) { m_corona->setScreenForContainment(cont, newScreen); } else { // If it's a desktop, for now move all desktops for all activities const int oldScreen = cont->screen() >= 0 ? cont->screen() : cont->lastScreen(); m_corona->swapDesktopScreens(oldScreen, newScreen); } } bool ShellContainmentModel::findContainment(unsigned int containmentId) const { return m_containments.cend() != std::find_if(m_containments.cbegin(), m_containments.cend(), [containmentId](const Data &d) { return d.id == containmentId; }); } void ShellContainmentModel::load() { beginResetModel(); for (auto &d : m_containments) { disconnect(d.containment, nullptr, this, nullptr); } m_containments.clear(); m_edgeCount.clear(); for (const auto *cont : m_corona->containments()) { // Skip the systray if (qobject_cast(cont->parent())) { continue; } // Only allow current activity for now (panels always go in) if (cont->containmentType() != Plasma::Containment::Panel && cont->containmentType() != Plasma::Containment::CustomPanel && cont->activity() != m_activityConsumer->currentActivity()) { continue; } if (!m_edgeCount.contains(cont->lastScreen())) { m_edgeCount[cont->lastScreen()] = QHash>(); m_edgeCount[cont->lastScreen()][cont->location()] = QList(); } m_edgeCount[cont->lastScreen()][cont->location()].append(cont->id()); m_corona->grabContainmentPreview(const_cast(cont)); Data d; d.id = cont->id(); d.screen = cont->lastScreen(); d.edge = cont->location(); d.activity = cont->activity(); d.isActive = cont->screen() != -1; d.containment = cont; d.image = containmentPreview(const_cast(cont)); if (cont->lastScreen() == m_screenId || (cont->lastScreen() == -1 && cont->screen() == m_screenId)) { m_containments.push_back(d); connect(cont, &QObject::destroyed, this, &ShellContainmentModel::load); connect(cont, &Plasma::Containment::destroyedChanged, this, &ShellContainmentModel::load); connect(cont, &Plasma::Containment::locationChanged, this, &ShellContainmentModel::load); } } endResetModel(); } void ShellContainmentModel::loadActivitiesInfos() { beginResetModel(); for (const auto &cont : m_containments) { const auto activitId = cont.activity; if (activitId.isEmpty()) { continue; } auto *activityInfo = new KActivities::Info(cont.activity, this); if (activityInfo) { if (!m_activitiesInfos.value(cont.activity)) { m_activitiesInfos[cont.activity] = activityInfo; } } } endResetModel(); } QString ShellContainmentModel::plasmaLocationToString(Plasma::Types::Location location) { switch (location) { case Plasma::Types::Floating: return u"floating"_s; case Plasma::Types::Desktop: return u"desktop"_s; case Plasma::Types::FullScreen: return u"Full Screen"_s; case Plasma::Types::TopEdge: return u"top"_s; case Plasma::Types::BottomEdge: return u"bottom"_s; case Plasma::Types::LeftEdge: return u"left"_s; case Plasma::Types::RightEdge: return u"right"_s; default: return QStringLiteral("unknown"); } } Plasma::Containment *ShellContainmentModel::containmentById(unsigned int id) { for (auto *cont : m_corona->containments()) { if (cont->id() == id) { return cont; } } return nullptr; } QString ShellContainmentModel::containmentPreview(Plasma::Containment *containment) { QString savedThumbnail = m_corona->containmentPreviewPath(containment); if (!savedThumbnail.isEmpty()) { return savedThumbnail; } m_corona->grabContainmentPreview(containment); // If not found, try to understand the configured wallpaper for the containment, assuming is using the Image plugin KSharedConfig::Ptr conf = KSharedConfig::openConfig(QLatin1String("plasma-") + m_corona->shell() + QLatin1String("-appletsrc"), KConfig::SimpleConfig); KConfigGroup containmentsGroup(conf, u"Containments"_s); KConfigGroup config = containmentsGroup.group(QString::number(containment->id())); auto wallpaperPlugin = config.readEntry("wallpaperplugin"); auto wallpaperConfig = config.group(u"Wallpaper"_s).group(wallpaperPlugin).group(u"General"_s); if (wallpaperConfig.hasKey("Image")) { // Trying for the wallpaper auto wallpaper = wallpaperConfig.readEntry("Image", QString()); if (!wallpaper.isEmpty()) { return wallpaper; } } if (wallpaperConfig.hasKey("Color")) { auto backgroundColor = wallpaperConfig.readEntry("Color", QColor(0, 0, 0)); return backgroundColor.name(); } return QString(); } // --- ShellContainmentConfig::ShellContainmentConfig(ShellCorona *corona, QWindow *parent) : QQmlApplicationEngine(parent) , m_corona(corona) , m_model(nullptr) { } ShellContainmentConfig::~ShellContainmentConfig() = default; void ShellContainmentConfig::init() { m_model = new ScreenPoolModel(m_corona, this); m_model->load(); auto *localizedContext = new KLocalizedContext(this); localizedContext->setTranslationDomain(u"plasma_shell_"_s + m_corona->shell()); rootContext()->setContextObject(localizedContext); rootContext()->setContextProperty(u"ShellContainmentModel"_s, m_model); load(m_corona->kPackage().fileUrl("containmentmanagementui")); if (!rootObjects().isEmpty()) { auto *obj = qobject_cast(rootObjects().first()); connect(obj, &QWindow::visibleChanged, this, [this] { deleteLater(); }); } }