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 }