Commit e66349f7 authored by Cyril Rossi's avatar Cyril Rossi Committed by Marco Martin
Browse files

Plasmashell : add dialog to manage containments when in edit mode

An UI to manage Screen assignment of Containments, the main use case is to recover lost or inactive panels from a display that you can no longer access.

qml part: plasma-desktop!618

related to https://phabricator.kde.org/T14346

CCBUG: 447044

Access the dialog from Toolbar in Edit Mode (Manage containments)
![Screenshot_20211222_114140](/uploads/31379781e3315a612d695220e623353b/Screenshot_20211222_114140.png)

![Screenshot_20211217_095945](/uploads/f890d5ac9f3ee6e82991d325b3078f5b/Screenshot_20211217_095945.png)


To show the dialog, simply enter in `edit mode`, then you can :
* See known displays and their panels and desktops, the dialog automatically update when adding/moving/removing a panel.
* Delete a panel or a desktop
* Recover a panel by moving it to an active display (click on move icon, then select the proper action in drop menu)
* Recover a desktop by moving it to an active display, in fact, this will swap it with th targeted desktop
* Modifications are automatically applied

What I've tested:
* Adding a panel, adding some applet on it. Then disabling its display, open the dialog and move it to an active display, restarting session, the panel is here with its applet.
* On another display, add applets (sticky note) on the desktop. Disable this display, open the dialog and move it to an active display, restart session, both desktop were swapped and you see the background (if different) and sticky note available.
parent d1f29843
Pipeline #126640 passed with stage
in 6 minutes and 25 seconds
......@@ -41,6 +41,7 @@ set (plasma_shell_SRCS
debug.cpp
screenpool.cpp
softwarerendernotifier.cpp
shellcontainmentconfig.cpp
${scripting_SRC}
)
......
......@@ -58,6 +58,9 @@ void ShellPackage::initPackage(KPackage::Package *package)
package->addFileDefinition("appletalternativesui",
QStringLiteral("explorer/AppletAlternatives.qml"),
i18n("QML component for choosing an alternate applet"));
package->addFileDefinition("containmentmanagementui",
QStringLiteral("configuration/ShellContainmentConfiguration.qml"),
i18n("QML component for the configuration dialog of containments"));
// Widget explorer
package->addFileDefinition("widgetexplorer", QStringLiteral("explorer/WidgetExplorer.qml"), i18n("Widgets explorer UI"));
......
/*
SPDX-FileCopyrightText: 2021 Cyril Rossi <cyril.rossi@enioka.com>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#include "shellcontainmentconfig.h"
#include <KActionCollection>
#include <KActivities/Consumer>
#include <KActivities/Info>
#include <KLocalizedContext>
#include <KLocalizedString>
#include <KPackage/Package>
#include <QQmlContext>
#include <QQuickItem>
#include <QScreen>
#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<void (QTimer::*)()>(&QTimer::start));
connect(m_corona, &Plasma::Corona::screenRemoved, m_reloadTimer, static_cast<void (QTimer::*)()>(&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<QObject *>(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<int, QByteArray> ScreenPoolModel::roleNames() const
{
QHash<int, QByteArray> 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();
for (auto &knownId : m_corona->screenPool()->knownIds()) {
Data d;
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;
}
}
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<void (QTimer::*)()>(&QTimer::start));
connect(m_corona, &Plasma::Corona::screenOwnerChanged, m_reloadTimer, static_cast<void (QTimer::*)()>(&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<int, QByteArray> ShellContainmentModel::roleNames() const
{
QHash<int, QByteArray> 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<Plasma::Applet *>(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<Plasma::Types::Location, QList<int>>();
m_edgeCount[cont->lastScreen()][cont->location()] = QList<int>();
}
m_edgeCount[cont->lastScreen()][cont->location()].append(cont->id());
m_corona->grabContainmentPreview(const_cast<Plasma::Containment *>(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<Plasma::Containment *>(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<QWindow *>(rootObjects().first());
connect(obj, &QWindow::visibleChanged, this, [this, obj]() {
deleteLater();
});
}
}
/*
SPDX-FileCopyrightText: 2021 Cyril Rossi <cyril.rossi@enioka.com>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#ifndef SHELLCONTAINMENTCONFIG_H
#define SHELLCONTAINMENTCONFIG_H
#include <QAbstractTableModel>
#include <QHash>
#include <QObject>
#include <QQmlApplicationEngine>
#include <QQuickView>
#include <plasma/plasma.h>
#include <plasma/containment.h>
namespace KActivities {
class Consumer;
class Info;
}
class ShellCorona;
class ShellContainmentModel;
class ScreenPoolModel : public QAbstractListModel
{
Q_OBJECT
public:
enum ScreenPoolModelRoles { ScreenIdRole = Qt::UserRole + 1, ScreenNameRole, ContainmentsRole, PrimaryRole, EnabledRole };
public:
explicit ScreenPoolModel(ShellCorona *corona, QObject *parent = nullptr);
~ScreenPoolModel() override;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
QHash<int, QByteArray> roleNames() const override;
public Q_SLOTS:
void load();
private:
ShellCorona *m_corona;
struct Data {
int id;
QString name;
bool primary;
bool enabled;
};
QTimer *m_reloadTimer = nullptr;
QVector<Data> m_screens;
QVector<ShellContainmentModel *> m_containments;
};
class ShellContainmentModel : public QAbstractListModel
{
Q_OBJECT
Q_PROPERTY(ScreenPoolModel *screenPoolModel READ screenPoolModel CONSTANT)
public:
enum ShellContainmentModelRoles {
ContainmentIdRole = Qt::UserRole + 1,
NameRole,
ScreenRole,
EdgeRole,
EdgePositionRole,
PanelCountAtRightRole,
PanelCountAtTopRole,
PanelCountAtLeftRole,
PanelCountAtBottomRole,
ActivityRole,
IsActiveRole,
ImageSourceRole,
DestroyedRole
};
public:
explicit ShellContainmentModel(ShellCorona *corona, int screenId, ScreenPoolModel *parent = nullptr);
~ShellContainmentModel() override;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
QHash<int, QByteArray> roleNames() const override;
ScreenPoolModel *screenPoolModel() const;
Q_INVOKABLE void remove(int contId);
Q_INVOKABLE void moveContainementToScreen(unsigned int contId, int newScreen);
bool findContainment(unsigned int containmentId) const;
void loadActivitiesInfos();
public Q_SLOTS:
void load();
private:
static QString plasmaLocationToString(const Plasma::Types::Location location);
static QString containmentTypeToString(const Plasma::Types::ContainmentType containmentType);
Plasma::Containment *containmentById(unsigned int id);
QString containmentPreview(Plasma::Containment *containment);
private:
int m_screenId = -1;
ShellCorona *m_corona;
struct Data {
unsigned int id;
QString name;
int screen;
Plasma::Types::Location edge;
QString activity;
bool changed = false;