File indexing completed on 2024-04-28 05:36:00

0001 /*
0002     SPDX-FileCopyrightText: 2021 Cyril Rossi <cyril.rossi@enioka.com>
0003 
0004     SPDX-License-Identifier: LGPL-2.0-or-later
0005 */
0006 
0007 #include "shellcontainmentconfig.h"
0008 
0009 #include <KLocalizedContext>
0010 #include <KLocalizedString>
0011 #include <KPackage/Package>
0012 #include <PlasmaActivities/Consumer>
0013 #include <PlasmaActivities/Info>
0014 
0015 #include <QQmlContext>
0016 #include <QQuickItem>
0017 #include <QScreen>
0018 
0019 #include "screenpool.h"
0020 #include "shellcorona.h"
0021 #include <chrono>
0022 
0023 using namespace std::chrono_literals;
0024 using namespace Qt::StringLiterals;
0025 
0026 ScreenPoolModel::ScreenPoolModel(ShellCorona *corona, QObject *parent)
0027     : QAbstractListModel(parent)
0028     , m_corona(corona)
0029 {
0030     m_reloadTimer = new QTimer(this);
0031     m_reloadTimer->setSingleShot(true);
0032     m_reloadTimer->setInterval(200ms);
0033 
0034     connect(m_reloadTimer, &QTimer::timeout, this, &ScreenPoolModel::load);
0035 
0036     connect(m_corona, &Plasma::Corona::screenAdded, m_reloadTimer, static_cast<void (QTimer::*)()>(&QTimer::start));
0037     connect(m_corona, &Plasma::Corona::screenRemoved, m_reloadTimer, static_cast<void (QTimer::*)()>(&QTimer::start));
0038 }
0039 
0040 ScreenPoolModel::~ScreenPoolModel() = default;
0041 
0042 QVariant ScreenPoolModel::data(const QModelIndex &index, int role) const
0043 {
0044     if (!index.isValid() || index.column() != 0 || index.row() < 0 || index.row() >= int(m_screens.size())) {
0045         return QVariant();
0046     }
0047     const Data &d = m_screens.at(index.row());
0048     switch (role) {
0049     case ScreenIdRole:
0050         return d.id;
0051     case ScreenNameRole:
0052         return d.name;
0053     case ContainmentsRole: {
0054         auto *cont = m_containments.at(index.row());
0055         return QVariant::fromValue<QObject *>(cont);
0056     }
0057     case PrimaryRole:
0058         return d.primary;
0059     case EnabledRole:
0060         return d.enabled;
0061     }
0062     return QVariant();
0063 }
0064 
0065 int ScreenPoolModel::rowCount(const QModelIndex &parent) const
0066 {
0067     if (parent.isValid()) {
0068         return 0;
0069     }
0070     return m_screens.size();
0071 }
0072 
0073 QHash<int, QByteArray> ScreenPoolModel::roleNames() const
0074 {
0075     QHash<int, QByteArray> roles({
0076         {ScreenIdRole, QByteArrayLiteral("screenId")},
0077         {ScreenNameRole, QByteArrayLiteral("screenName")},
0078         {ContainmentsRole, QByteArrayLiteral("containments")},
0079         {EnabledRole, QByteArrayLiteral("isEnabled")},
0080         {PrimaryRole, QByteArrayLiteral("isPrimary")},
0081     });
0082     return roles;
0083 }
0084 
0085 void ScreenPoolModel::load()
0086 {
0087     beginResetModel();
0088     m_screens.clear();
0089     qDeleteAll(m_containments);
0090     m_containments.clear();
0091 
0092     QSet<int> unknownScreenIds;
0093     for (auto *cont : m_corona->containments()) {
0094         connect(cont, &Plasma::Containment::destroyedChanged, this, &ScreenPoolModel::load, Qt::UniqueConnection);
0095         if (!cont->destroyed()) {
0096             unknownScreenIds.insert(cont->lastScreen());
0097         }
0098     }
0099     int knownId = 0;
0100     for (QScreen *screen : m_corona->screenPool()->screenOrder()) {
0101         Data d;
0102         unknownScreenIds.remove(knownId);
0103         d.id = knownId;
0104         if (screen->name().contains(u"eDP"_s)) {
0105             d.name = i18n("Internal Screen on %1", screen->name());
0106         } else if (screen->model().contains(screen->name())) {
0107             d.name = screen->model();
0108         } else {
0109             d.name = i18nc("Screen manufacturer and model on connector", "%1 %2 on %3", screen->manufacturer(), screen->model(), screen->name());
0110         }
0111         d.primary = knownId == 0;
0112         d.enabled = true;
0113 
0114         auto *conts = new ShellContainmentModel(m_corona, knownId, this);
0115         conts->load();
0116 
0117         // Exclude screens which don't have any containemnt assigned
0118         if (conts->rowCount() > 0) {
0119             m_containments.push_back(conts);
0120             m_screens.push_back(d);
0121         } else {
0122             delete conts;
0123         }
0124         ++knownId;
0125     }
0126 
0127     QList sortedIds = unknownScreenIds.values();
0128     std::sort(sortedIds.begin(), sortedIds.end());
0129     for (int id : sortedIds) {
0130         Data d;
0131         d.id = id;
0132         d.name = i18n("Disconnected Screen %1", id + 1);
0133         d.primary = id == 0;
0134         d.enabled = false;
0135 
0136         auto *conts = new ShellContainmentModel(m_corona, id, this);
0137         conts->load();
0138         m_containments.push_back(conts);
0139         m_screens.push_back(d);
0140     }
0141     endResetModel();
0142 }
0143 
0144 void ScreenPoolModel::remove(int screenId)
0145 {
0146     // Don't allow to remove currently used containemnts
0147     if (m_corona->screenPool()->screenForId(screenId)) {
0148         return;
0149     }
0150 
0151     // remove containments of *all* activities
0152     auto conts = m_corona->containmentsForScreen(screenId);
0153     for (auto *cont : std::as_const(conts)) {
0154         // Don't call destroy directly, so we can have the undo action notification
0155         auto *destroyAction = cont->internalAction("remove");
0156         if (destroyAction) {
0157             destroyAction->trigger();
0158         }
0159     }
0160 }
0161 
0162 // ---
0163 
0164 ShellContainmentModel::ShellContainmentModel(ShellCorona *corona, int screenId, ScreenPoolModel *parent)
0165     : QAbstractListModel(parent)
0166     , m_screenId(screenId)
0167     , m_corona(corona)
0168     , m_screenPoolModel(parent)
0169     , m_activityConsumer(new KActivities::Consumer(this))
0170 {
0171     m_reloadTimer = new QTimer(this);
0172     m_reloadTimer->setSingleShot(true);
0173     m_reloadTimer->setInterval(200ms);
0174 
0175     connect(m_reloadTimer, &QTimer::timeout, this, &ShellContainmentModel::load);
0176 
0177     connect(m_corona, &ShellCorona::startupCompleted, this, &ShellContainmentModel::load);
0178 
0179     connect(m_corona, &Plasma::Corona::containmentAdded, m_reloadTimer, static_cast<void (QTimer::*)()>(&QTimer::start));
0180     connect(m_corona, &Plasma::Corona::screenOwnerChanged, m_reloadTimer, static_cast<void (QTimer::*)()>(&QTimer::start));
0181 
0182     connect(m_corona, &ShellCorona::containmentPreviewReady, this, [this](Plasma::Containment *containment, const QString &path) {
0183         int i = 0;
0184         for (auto &d : m_containments) {
0185             if (d.containment == containment) {
0186                 d.image = path;
0187                 emit dataChanged(index(i, 0), index(i, 0));
0188                 break;
0189             }
0190             ++i;
0191         }
0192     });
0193 }
0194 
0195 ShellContainmentModel::~ShellContainmentModel() = default;
0196 
0197 QVariant ShellContainmentModel::data(const QModelIndex &index, int role) const
0198 {
0199     if (!index.isValid() || index.column() != 0 || index.row() < 0 || index.row() >= int(m_containments.size())) {
0200         return QVariant();
0201     }
0202     const Data &d = m_containments.at(index.row());
0203     switch (role) {
0204     case ContainmentIdRole:
0205         return d.id;
0206     case ScreenRole:
0207         return d.screen;
0208     case EdgeRole:
0209         return ShellContainmentModel::plasmaLocationToString(d.edge);
0210     case EdgePositionRole:
0211         return qMax(0, m_edgeCount.value(d.screen).value(d.edge).indexOf(d.id));
0212     case PanelCountAtRightRole:
0213         return qMax(0, m_edgeCount.value(d.screen).value(Plasma::Types::RightEdge).count());
0214     case PanelCountAtTopRole:
0215         return qMax(0, m_edgeCount.value(d.screen).value(Plasma::Types::TopEdge).count());
0216     case PanelCountAtLeftRole:
0217         return qMax(0, m_edgeCount.value(d.screen).value(Plasma::Types::LeftEdge).count());
0218     case PanelCountAtBottomRole:
0219         return qMax(0, m_edgeCount.value(d.screen).value(Plasma::Types::BottomEdge).count());
0220     case ActivityRole: {
0221         const auto *activityInfo = m_activitiesInfos.value(d.activity);
0222         if (activityInfo) {
0223             return activityInfo->name();
0224         }
0225         break;
0226     }
0227     case IsActiveRole:
0228         return d.isActive;
0229     case ImageSourceRole:
0230         return d.image;
0231     case DestroyedRole:
0232         return d.containment->destroyed();
0233     }
0234     return QVariant();
0235 }
0236 
0237 int ShellContainmentModel::rowCount(const QModelIndex &parent) const
0238 {
0239     if (parent.isValid()) {
0240         return 0;
0241     }
0242     return m_containments.size();
0243 }
0244 
0245 QHash<int, QByteArray> ShellContainmentModel::roleNames() const
0246 {
0247     QHash<int, QByteArray> roles({
0248         {ContainmentIdRole, QByteArrayLiteral("containmentId")},
0249         {ScreenRole, QByteArrayLiteral("screen")},
0250         {EdgeRole, QByteArrayLiteral("edge")},
0251         {EdgePositionRole, QByteArrayLiteral("edgePosition")},
0252         {PanelCountAtRightRole, QByteArrayLiteral("panelCountAtRight")},
0253         {PanelCountAtTopRole, QByteArrayLiteral("panelCountAtTop")},
0254         {PanelCountAtLeftRole, QByteArrayLiteral("panelCountAtLeft")},
0255         {PanelCountAtBottomRole, QByteArrayLiteral("panelCountAtBottom")},
0256         {ActivityRole, QByteArrayLiteral("activity")},
0257         {IsActiveRole, QByteArrayLiteral("active")},
0258         {ImageSourceRole, QByteArrayLiteral("imageSource")},
0259         {DestroyedRole, QByteArrayLiteral("isDestroyed")},
0260     });
0261     return roles;
0262 }
0263 
0264 ScreenPoolModel *ShellContainmentModel::screenPoolModel() const
0265 {
0266     return m_screenPoolModel;
0267 }
0268 
0269 void ShellContainmentModel::remove(int contId)
0270 {
0271     if (contId < 0) {
0272         return;
0273     }
0274 
0275     auto *cont = containmentById(contId);
0276     if (cont) {
0277         disconnect(cont, nullptr, this, nullptr);
0278         // Don't call destroy directly, so we can have the undo action notification
0279         auto *destroyAction = cont->internalAction("remove");
0280         if (destroyAction) {
0281             destroyAction->trigger();
0282         }
0283     }
0284     // Don't call load() there as is already triggered by destroyedChanged signal of the containemnt
0285 }
0286 
0287 void ShellContainmentModel::moveContainementToScreen(unsigned int contId, int newScreen)
0288 {
0289     if (contId == 0 || newScreen < 0) {
0290         return;
0291     }
0292 
0293     auto containmentIt = std::find_if(m_containments.begin(), m_containments.end(), [contId](Data &d) {
0294         return d.id == contId;
0295     });
0296     if (containmentIt == m_containments.end()) {
0297         return;
0298     }
0299     if (containmentIt->screen == newScreen) {
0300         return;
0301     }
0302 
0303     auto *cont = containmentById(contId);
0304     if (cont == nullptr) {
0305         return;
0306     }
0307 
0308     // If it's a panel, only move that one
0309     if (cont->containmentType() == Plasma::Containment::Panel || cont->containmentType() == Plasma::Containment::CustomPanel) {
0310         m_corona->setScreenForContainment(cont, newScreen);
0311     } else {
0312         // If it's a desktop, for now move all desktops for all activities
0313         const int oldScreen = cont->screen() >= 0 ? cont->screen() : cont->lastScreen();
0314         m_corona->swapDesktopScreens(oldScreen, newScreen);
0315     }
0316 }
0317 
0318 bool ShellContainmentModel::findContainment(unsigned int containmentId) const
0319 {
0320     return m_containments.cend() != std::find_if(m_containments.cbegin(), m_containments.cend(), [containmentId](const Data &d) {
0321                return d.id == containmentId;
0322            });
0323 }
0324 
0325 void ShellContainmentModel::load()
0326 {
0327     beginResetModel();
0328 
0329     for (auto &d : m_containments) {
0330         disconnect(d.containment, nullptr, this, nullptr);
0331     }
0332     m_containments.clear();
0333     m_edgeCount.clear();
0334 
0335     for (const auto *cont : m_corona->containments()) {
0336         // Skip the systray
0337         if (qobject_cast<Plasma::Applet *>(cont->parent())) {
0338             continue;
0339         }
0340         // Only allow current activity for now (panels always go in)
0341         if (cont->containmentType() != Plasma::Containment::Panel && cont->containmentType() != Plasma::Containment::CustomPanel
0342             && cont->activity() != m_activityConsumer->currentActivity()) {
0343             continue;
0344         }
0345         if (!m_edgeCount.contains(cont->lastScreen())) {
0346             m_edgeCount[cont->lastScreen()] = QHash<Plasma::Types::Location, QList<int>>();
0347             m_edgeCount[cont->lastScreen()][cont->location()] = QList<int>();
0348         }
0349         m_edgeCount[cont->lastScreen()][cont->location()].append(cont->id());
0350         m_corona->grabContainmentPreview(const_cast<Plasma::Containment *>(cont));
0351         Data d;
0352         d.id = cont->id();
0353         d.screen = cont->lastScreen();
0354         d.edge = cont->location();
0355         d.activity = cont->activity();
0356         d.isActive = cont->screen() != -1;
0357         d.containment = cont;
0358         d.image = containmentPreview(const_cast<Plasma::Containment *>(cont));
0359 
0360         if (cont->lastScreen() == m_screenId || (cont->lastScreen() == -1 && cont->screen() == m_screenId)) {
0361             m_containments.push_back(d);
0362             connect(cont, &QObject::destroyed, this, &ShellContainmentModel::load);
0363             connect(cont, &Plasma::Containment::destroyedChanged, this, &ShellContainmentModel::load);
0364             connect(cont, &Plasma::Containment::locationChanged, this, &ShellContainmentModel::load);
0365         }
0366     }
0367     endResetModel();
0368 }
0369 
0370 void ShellContainmentModel::loadActivitiesInfos()
0371 {
0372     beginResetModel();
0373     for (const auto &cont : m_containments) {
0374         const auto activitId = cont.activity;
0375         if (activitId.isEmpty()) {
0376             continue;
0377         }
0378         auto *activityInfo = new KActivities::Info(cont.activity, this);
0379         if (activityInfo) {
0380             if (!m_activitiesInfos.value(cont.activity)) {
0381                 m_activitiesInfos[cont.activity] = activityInfo;
0382             }
0383         }
0384     }
0385     endResetModel();
0386 }
0387 
0388 QString ShellContainmentModel::plasmaLocationToString(Plasma::Types::Location location)
0389 {
0390     switch (location) {
0391     case Plasma::Types::Floating:
0392         return u"floating"_s;
0393     case Plasma::Types::Desktop:
0394         return u"desktop"_s;
0395     case Plasma::Types::FullScreen:
0396         return u"Full Screen"_s;
0397     case Plasma::Types::TopEdge:
0398         return u"top"_s;
0399     case Plasma::Types::BottomEdge:
0400         return u"bottom"_s;
0401     case Plasma::Types::LeftEdge:
0402         return u"left"_s;
0403     case Plasma::Types::RightEdge:
0404         return u"right"_s;
0405     default:
0406         return QString("unknown");
0407     }
0408 }
0409 
0410 Plasma::Containment *ShellContainmentModel::containmentById(unsigned int id)
0411 {
0412     for (auto *cont : m_corona->containments()) {
0413         if (cont->id() == id) {
0414             return cont;
0415         }
0416     }
0417     return nullptr;
0418 }
0419 
0420 QString ShellContainmentModel::containmentPreview(Plasma::Containment *containment)
0421 {
0422     QString savedThumbnail = m_corona->containmentPreviewPath(containment);
0423 
0424     if (!savedThumbnail.isEmpty()) {
0425         return savedThumbnail;
0426     }
0427 
0428     m_corona->grabContainmentPreview(containment);
0429 
0430     // If not found, try to understand the configured wallpaper for the containment, assuming is using the Image plugin
0431     KSharedConfig::Ptr conf = KSharedConfig::openConfig(QLatin1String("plasma-") + m_corona->shell() + QLatin1String("-appletsrc"), KConfig::SimpleConfig);
0432     KConfigGroup containmentsGroup(conf, u"Containments"_s);
0433     KConfigGroup config = containmentsGroup.group(QString::number(containment->id()));
0434     auto wallpaperPlugin = config.readEntry("wallpaperplugin");
0435     auto wallpaperConfig = config.group(u"Wallpaper"_s).group(wallpaperPlugin).group(u"General"_s);
0436 
0437     if (wallpaperConfig.hasKey("Image")) {
0438         // Trying for the wallpaper
0439         auto wallpaper = wallpaperConfig.readEntry("Image", QString());
0440         if (!wallpaper.isEmpty()) {
0441             return wallpaper;
0442         }
0443     }
0444     if (wallpaperConfig.hasKey("Color")) {
0445         auto backgroundColor = wallpaperConfig.readEntry("Color", QColor(0, 0, 0));
0446         return backgroundColor.name();
0447     }
0448 
0449     return QString();
0450 }
0451 
0452 // ---
0453 
0454 ShellContainmentConfig::ShellContainmentConfig(ShellCorona *corona, QWindow *parent)
0455     : QQmlApplicationEngine(parent)
0456     , m_corona(corona)
0457     , m_model(nullptr)
0458 {
0459 }
0460 
0461 ShellContainmentConfig::~ShellContainmentConfig() = default;
0462 
0463 void ShellContainmentConfig::init()
0464 {
0465     m_model = new ScreenPoolModel(m_corona, this);
0466     m_model->load();
0467 
0468     auto *localizedContext = new KLocalizedContext(this);
0469     localizedContext->setTranslationDomain(u"plasma_shell_"_s + m_corona->shell());
0470 
0471     rootContext()->setContextObject(localizedContext);
0472     rootContext()->setContextProperty(u"ShellContainmentModel"_s, m_model);
0473     load(m_corona->kPackage().fileUrl("containmentmanagementui"));
0474 
0475     if (!rootObjects().isEmpty()) {
0476         auto *obj = qobject_cast<QWindow *>(rootObjects().first());
0477         connect(obj, &QWindow::visibleChanged, this, [this] {
0478             deleteLater();
0479         });
0480     }
0481 }