/* 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 #include "shellcorona.h" #include "screenpool.h" #include "panelview.h" ScreenPoolModel::ScreenPoolModel(ShellCorona *corona, QObject *parent) : QAbstractListModel(parent) , m_corona(corona) { m_reloadTimer = new QTimer(this); m_reloadTimer->setSingleShot(true); m_reloadTimer->setInterval(200); 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()) { unknownScreenIds.insert(cont->lastScreen()); } for (auto &knownId : m_corona->screenPool()->knownIds()) { Data d; unknownScreenIds.remove(knownId); d.id = knownId; d.name = m_corona->screenPool()->connector(knownId); d.primary = knownId == 0; d.enabled = false; // TODO: add a m_corona->screenPool()->screenForId() method instead of this loop, but needs the whole primary screen tracking be moved from shellcorona // to screenpool for (QScreen *screen : qGuiApp->screens()) { if (screen->name() == d.name) { d.enabled = true; break; } } 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; } } QList sortedIds = unknownScreenIds.values(); std::sort(sortedIds.begin(), sortedIds.end()); int i = 1; for (int id : sortedIds) { Data d; d.id = id; d.name = i18n("Unknown %1", i); 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); i++; } endResetModel(); } // --- 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(200); 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; 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 Qt::DisplayRole: return d.name; case ContainmentIdRole: return d.id; case NameRole: return d.name; 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")}, {NameRole, QByteArrayLiteral("name")}, {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->actions()->action("remove"); if (destroyAction) { destroyAction->trigger(); } } load(); } 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::Types::PanelContainment || cont->containmentType() == Plasma::Types::CustomPanelContainment) { 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::Types::PanelContainment && cont->containmentType() != Plasma::Types::CustomPanelContainment && 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.name = cont->title() + " (" + ShellContainmentModel::containmentTypeToString(cont->containmentType()) + ")"; 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 QStringLiteral("floating"); case Plasma::Types::Desktop: return QStringLiteral("desktop"); case Plasma::Types::FullScreen: return QStringLiteral("Full Screen"); case Plasma::Types::TopEdge: return QStringLiteral("top"); case Plasma::Types::BottomEdge: return QStringLiteral("bottom"); case Plasma::Types::LeftEdge: return QStringLiteral("left"); case Plasma::Types::RightEdge: return QStringLiteral("right"); default: return QString("unknown"); } } QString ShellContainmentModel::containmentTypeToString(Plasma::Types::ContainmentType containmentType) { switch (containmentType) { case Plasma::Types::DesktopContainment: /**< A desktop containment */ return QStringLiteral("Desktop"); case Plasma::Types::PanelContainment: /**< A desktop panel */ return QStringLiteral("Panel"); case Plasma::Types::CustomContainment: /**< A containment that is neither a desktop nor a panel but something application specific */ return QStringLiteral("Custom"); case Plasma::Types::CustomPanelContainment: /**< A customized desktop panel */ return QStringLiteral("Custom Desktop"); case Plasma::Types::CustomEmbeddedContainment: /**< A customized containment embedded in another applet */ return QStringLiteral("Embedded"); 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, "Containments"); KConfigGroup config = containmentsGroup.group(QString::number(containment->id())); auto wallpaperPlugin = config.readEntry("wallpaperplugin"); auto wallpaperConfig = config.group("Wallpaper").group(wallpaperPlugin).group("General"); 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(QStringLiteral("plasma_shell_") + m_corona->shell()); rootContext()->setContextObject(localizedContext); rootContext()->setContextProperty(QStringLiteral("ShellContainmentModel"), m_model); load(m_corona->kPackage().fileUrl("containmentmanagementui")); if (!rootObjects().isEmpty()) { auto *obj = qobject_cast(rootObjects().first()); connect(obj, &QWindow::visibleChanged, this, [this, obj]() { deleteLater(); }); } }