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