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 }