diff --git a/shell/dbus/org.kde.PlasmaShell.xml b/shell/dbus/org.kde.PlasmaShell.xml --- a/shell/dbus/org.kde.PlasmaShell.xml +++ b/shell/dbus/org.kde.PlasmaShell.xml @@ -21,5 +21,14 @@ + + + + + + + + + diff --git a/shell/scripting/containment.cpp b/shell/scripting/containment.cpp --- a/shell/scripting/containment.cpp +++ b/shell/scripting/containment.cpp @@ -20,13 +20,15 @@ #include "containment.h" #include +#include #include #include #include #include #include +#include #include "scriptengine.h" #include "widget.h" @@ -171,7 +173,7 @@ QScriptValue Containment::addWidget(QScriptContext *context, QScriptEngine *engine) { if (context->argumentCount() == 0) { - return context->throwError(i18n("widgetById requires a name of a widget or a widget object")); + return context->throwError(i18n("addWidget requires a name of a widget or a widget object")); } Containment *c = qobject_cast(context->thisObject().toQObject()); @@ -182,8 +184,44 @@ QScriptValue v = context->argument(0); Plasma::Applet *applet = 0; + QRectF geometry(-1, -1, -1, -1); + if (context->argumentCount() > 4) { + //The user provided a geometry as parameter + if (context->argument(1).isNumber() && + context->argument(2).isNumber() && + context->argument(3).isNumber() && + context->argument(4).isNumber()) { + //Try to reconstruct a rectangle from the object hat has been passed + //It's expected a js object such as + //addWidget("org.kde.plasma.analogclock", {"x": 0, "y": 100, "width": 300, "height": 400}); + const QVariantMap geom = context->argument(1).toVariant().value(); + geometry = QRectF(context->argument(1).toNumber(), + context->argument(2).toNumber(), + context->argument(3).toNumber(), + context->argument(4).toNumber()); + } + } if (v.isString()) { + //A position has been supplied: search for the containment's graphics object + QQuickItem *containmentItem = nullptr; + + if (geometry.x() >= 0 && geometry.y() >= 0) { + containmentItem = c->d->containment.data()->property("_plasma_graphicObject").value(); + Plasma::Applet *applet = nullptr; + if (containmentItem) { + QMetaObject::invokeMethod(containmentItem , "createApplet", Qt::DirectConnection, Q_RETURN_ARG(Plasma::Applet *, applet), Q_ARG(QString, v.toString()), Q_ARG(QVariantList, QVariantList()), Q_ARG(QRectF, geometry)); + } + if (applet) { + ScriptEngine *env = ScriptEngine::envFor(engine); + return env->wrap(applet); + } + } + + //Case in which either: + // * a geometry wasn't provided + // * containmentItem wasn't found applet = c->d->containment.data()->createApplet(v.toString()); + if (applet) { ScriptEngine *env = ScriptEngine::envFor(engine); return env->wrap(applet); diff --git a/shell/shellcorona.h b/shell/shellcorona.h --- a/shell/shellcorona.h +++ b/shell/shellcorona.h @@ -143,6 +143,21 @@ void loadKWinScriptInInteractiveConsole(const QString &script); void toggleActivityManager(); void evaluateScript(const QString &string); + QString dumpCurrentLayoutJS(); + + /** + * loads the shell layout from a look and feel package, + * restoring it at the last moment it was used, restoring + * any changes the user made + */ + void loadLookAndFeelLayout(const QString &layout); + + /** + * loads the shell layout from a look and feel package, + * resetting it to the default layout exported in the + * look and feel package + */ + void loadLookAndFeelDefaultLayout(const QString &layout); Plasma::Containment *addPanel(const QString &plugin); diff --git a/shell/shellcorona.cpp b/shell/shellcorona.cpp --- a/shell/shellcorona.cpp +++ b/shell/shellcorona.cpp @@ -43,6 +43,7 @@ #include #include #include +#include #include @@ -63,7 +64,7 @@ #include "waylanddialogfilter.h" #include "plasmashelladaptor.h" - +#include "debug.h" #include "futureutil.h" #ifndef NDEBUG @@ -128,14 +129,15 @@ dbus.registerObject(QStringLiteral("/PlasmaShell"), this); connect(this, &Plasma::Corona::startupCompleted, this, - []() { + [this]() { qDebug() << "Plasma Shell startup completed"; QDBusMessage ksplashProgressMessage = QDBusMessage::createMethodCall(QStringLiteral("org.kde.KSplash"), QStringLiteral("/KSplash"), QStringLiteral("org.kde.KSplash"), QStringLiteral("setStage")); ksplashProgressMessage.setArguments(QList() << QStringLiteral("desktop")); QDBusConnection::sessionBus().asyncCall(ksplashProgressMessage); + //TODO: remove }); // Look for theme config in plasmarc, if it isn't configured, take the theme from the @@ -311,6 +313,278 @@ } } + +QString dumpconfigGroupJS(const KConfigGroup &rootGroup, const QString &prefix) +{ + QString script; + QStringList hierarchy; + QList groups; + groups << rootGroup; + QSet visitedNodes; + + QSet forbiddenKeys; + forbiddenKeys << QStringLiteral("activityId") << QStringLiteral("ItemsGeometries") << QStringLiteral("AppletOrder") << QStringLiteral("SystrayContainmentId") << QStringLiteral("location") << QStringLiteral("plugin"); + + //perform a depth-first tree traversal for config groups + while (!groups.isEmpty()) { + KConfigGroup cg = groups.last(); + + KConfigGroup parentCg = cg; + //FIXME: name is not enoug + hierarchy.clear(); + while (parentCg.isValid() && parentCg.name() != rootGroup.name()) { + hierarchy.prepend(parentCg.name()); + parentCg = parentCg.parent(); + } + + visitedNodes.insert(hierarchy.join(QChar())); + groups.pop_back(); + + if (!cg.keyList().isEmpty()) { + script += "\n"; + //TODO: this is conditional if applet or containment + if (hierarchy.length() > 0) { + script += QStringLiteral(" //all config values in the \"%1\" group\n").arg(hierarchy.last()); + script += prefix + QStringLiteral(".currentConfigGroup = Array(\"") + hierarchy.join("\", \"") + "\");\n"; + } + + QMap::const_iterator i; + QMap map = cg.entryMap(); + for (i = map.constBegin(); i != map.constEnd(); ++i) { + //some blacklisted keys we don't want to save + if (!forbiddenKeys.contains(i.key())) { + script += prefix + ".writeConfig(\"" + i.key() + "\", \"" + i.value() + "\");\n"; + } + } + } + + foreach (const QString &groupName, cg.groupList()) { + if (groupName == QStringLiteral("Applets") || + visitedNodes.contains(hierarchy.join(QChar()) + groupName)) { + continue; + } + groups << KConfigGroup(&cg, groupName); + } + } + + return script; +} + +QString ShellCorona::dumpCurrentLayoutJS() +{ + QString script; + + //same gridUnit calculation as ScriptEngine + int gridUnit = QFontMetrics(QGuiApplication::font()).boundingRect(QStringLiteral("M")).height(); + if (gridUnit % 2 != 0) { + gridUnit++; + } + + int i = 0; + + //dump panels + script += "//////Panels\n"; + foreach (Plasma::Containment *cont, containments()) { + if ((cont->formFactor() == Plasma::Types::Horizontal || cont->formFactor() == Plasma::Types::Vertical) && + (cont->location() == Plasma::Types::TopEdge || cont->location() == Plasma::Types::BottomEdge || + cont->location() == Plasma::Types::LeftEdge || + cont->location() == Plasma::Types::RightEdge) && + cont->pluginInfo().pluginName() != QStringLiteral("org.kde.plasma.private.systemtray")) { + + const PanelView *view = m_panelViews.value(cont); + + script += "{\n"; + script += " var panel = new Panel;\n"; + + switch (cont->location()) { + case Plasma::Types::TopEdge: + script += " panel.location = \"top\";\n"; + break; + case Plasma::Types::LeftEdge: + script += " panel.location = \"left\";\n"; + break; + case Plasma::Types::RightEdge: + script += " panel.location = \"right\";\n"; + break; + case Plasma::Types::BottomEdge: + default: + script += " panel.location = \"bottom\";\n"; + break; + } + + //for panels we don't have view for, fallback to 4 units + qreal units = 4; + + //Repeated switch as one depends from view the other doesn't + if (view) { + switch (cont->location()) { + case Plasma::Types::TopEdge: + units = view->height() / gridUnit; + break; + case Plasma::Types::LeftEdge: + units = view->width() / gridUnit; + break; + case Plasma::Types::RightEdge: + units = view->width() / gridUnit; + break; + case Plasma::Types::BottomEdge: + default: + units = view->height() / gridUnit; + break; + } + } + + script += " panel.height = gridUnit * " + QString::number(units) + ";\n"; + + //enumerate config keys for containment + KConfigGroup contConfig = cont->config(); + script += " //Panel containment configuration\n"; + script += dumpconfigGroupJS(contConfig, QStringLiteral(" panel")); + script += "\n\n"; + + //try to parse the encoded applets order + KConfigGroup genericConf(&contConfig, QStringLiteral("General")); + const QStringList appletsOrderStrings = genericConf.readEntry(QStringLiteral("AppletOrder"), QString()).split(QChar(';')); + //consider the applet order to be valid only if there are as many entries as applets() + if (appletsOrderStrings.length() == cont->applets().length()) { + foreach (const QString &stringId, appletsOrderStrings) { + KConfigGroup appletConfig(&contConfig, QStringLiteral("Applets")); + appletConfig = KConfigGroup(&appletConfig, stringId); + const QString pluginName = appletConfig.readEntry(QStringLiteral("plugin"), QString()); + + if (pluginName.isEmpty()) { + continue; + } + + script += " {\n"; + script += " //Configuration of applet " + pluginName + "\n"; + script += " var applet = panel.addWidget(\"" + pluginName + "\");\n"; + + script += dumpconfigGroupJS(appletConfig, QStringLiteral(" applet")); + + script += " }\n"; + } + //applets order not found, just use order returned by applets() + } else { + foreach (Plasma::Applet *applet, cont->applets()) { + script += " {\n"; + script += " //Configuration of applet " + applet->pluginInfo().pluginName() + "\n"; + script += " var applet = panel.addWidget(\"" + applet->pluginInfo().pluginName() + "\", " + QString::number(applet->id()) + ");\n"; + + KConfigGroup appletConfig = applet->config(); + script += dumpconfigGroupJS(appletConfig, QStringLiteral(" applet")); + + script += " }\n"; + } + } + + script += "}\n"; + script += "\n\n"; + } + } + + //dump desktop containments + script += "//////Desktops\n"; + script += "var desktopsArray = desktopsForActivity(currentActivity());\n"; + + //two separate loops to have desktops and panels separated in scripts + foreach (Plasma::Containment *cont, containments()) { + if (!cont->activity().isEmpty()) { + script += "if (desktopsArray.length > " + QString::number(i) + ") {\n"; + script += " var cont = desktopsArray[" + QString::number(i) + "];\n\n"; + script += " cont.wallpaperPlugin = '" + cont->wallpaper() + "';\n"; + + //enumerate config keys for containment + KConfigGroup contConfig = cont->config(); + script += " //Containment configuration\n"; + script += dumpconfigGroupJS(contConfig, QStringLiteral(" cont")); + + //try to parse the items geometries + KConfigGroup genericConf(&contConfig, QStringLiteral("General")); + const QStringList appletsGeomStrings = genericConf.readEntry(QStringLiteral("ItemsGeometries"), QString()).split(QChar(';')); + + QHash appletGeometries; + for (const QString encoded : appletsGeomStrings) { + const QStringList keyValue = encoded.split(QChar(':')); + if (keyValue.length() != 2) { + continue; + } + QStringList rectPieces = keyValue.last().split(QChar(',')); + if (rectPieces.length() != 5) { + continue; + } + QRect rect(rectPieces[0].toInt(), rectPieces[1].toInt(), + rectPieces[2].toInt(), rectPieces[3].toInt()); + appletGeometries[keyValue.first()] = rect; + } + + script += "\n\n"; + foreach (Plasma::Applet *applet, cont->applets()) { + const QRect geometry = appletGeometries.value(QStringLiteral("Applet-") % QString::number(applet->id())); + script += " {\n"; + script += " //Configuration of applet " + applet->title() + "\n"; + script += " var applet = cont.addWidget(\"" + applet->pluginInfo().pluginName() + "\", gridUnit * " + QString::number(geometry.x()/gridUnit) + + ", gridUnit * " + QString::number(geometry.y()/gridUnit) + + ", gridUnit * " + QString::number(geometry.width()/gridUnit) + + ", gridUnit * " + QString::number(geometry.height()/gridUnit) + ");\n"; + + KConfigGroup appletConfig = applet->config(); + script += dumpconfigGroupJS(appletConfig, QStringLiteral(" applet")); + + script += " }\n"; + } + ++i; + script += "}\n\n"; + } + } + + + + return script; +} + +void ShellCorona::loadLookAndFeelDefaultLayout(const QString &packageName) +{ + KPackage::Package newPack = m_lookAndFeelPackage; + newPack.setPath(packageName); + + if (!newPack.isValid()) { + return; + } + + KSharedConfig::Ptr conf = KSharedConfig::openConfig(QStringLiteral("plasma-") + m_shell + QChar('-') + m_lookAndFeelPackage.metadata().pluginId() + QStringLiteral("-appletsrc"), KConfig::SimpleConfig); + + m_lookAndFeelPackage.setPath(packageName); + + //get rid of old config + for (const QString &group : conf->groupList()) { + conf->deleteGroup(group); + } + conf->sync(); + unload(); + load(); +} + +void ShellCorona::loadLookAndFeelLayout(const QString &packageName) +{ + if (packageName == m_lookAndFeelPackage.metadata().pluginId()) { + return; + } + + KPackage::Package newPack = m_lookAndFeelPackage; + newPack.setPath(packageName); + + if (!newPack.isValid()) { + return; + } + + m_lookAndFeelPackage = newPack; + + //NOTE: updateng the plasma theme should *not* be necessary here as the kcm is already doing this + unload(); + load(); +} + QString ShellCorona::shell() const { return m_shell; @@ -325,7 +599,18 @@ disconnect(m_activityController, &KActivities::Controller::serviceStatusChanged, this, &ShellCorona::load); - loadLayout("plasma-" + m_shell + "-appletsrc"); + //TODO: a kconf_update script is needed + QString configFileName(QStringLiteral("plasma-") + m_shell + QChar('-') + m_lookAndFeelPackage.metadata().pluginId() + QStringLiteral("-appletsrc")); + + //NOTE: this is just for the transition from Plasma 5.7 + { + QString oldConfigFilePath(QStandardPaths::writableLocation(QStandardPaths::ConfigLocation) + QStringLiteral("/plasma-") + m_shell + QStringLiteral("-appletsrc")); + if (QFile::exists(oldConfigFilePath)) { + qCDebug(PLASMASHELL) << "Old config file older than Plasma 5.8 found: moving to new location"; + QFile::rename(oldConfigFilePath, QStandardPaths::writableLocation(QStandardPaths::ConfigLocation) + QChar('/') + configFileName); + } + } + loadLayout(configFileName); checkActivities(); @@ -358,6 +643,9 @@ } } + //NOTE: this is needed in case loadLayout() did *not* call loadDefaultLayout() + //it needs to be after of loadLayout() as it would always create new + //containments on each startup otherwise for (QScreen* screen : qGuiApp->screens()) { addOutput(screen); } @@ -514,8 +802,19 @@ if (m_shell.isEmpty()) { return; } + qDeleteAll(m_desktopViewforId.values()); + m_desktopViewforId.clear(); + qDeleteAll(m_panelViews.values()); + m_panelViews.clear(); + m_desktopContainments.clear(); + m_waitingPanels.clear(); + m_activityContainmentPlugins.clear(); - qDeleteAll(containments()); + while (!containments().isEmpty()) { + //deleting a containment will remove it from the list due to QObject::destroyed connect in Corona + //this form doesn't crash, while qDeleteAll(containments()) does + delete containments().first(); + } } KSharedConfig::Ptr ShellCorona::applicationConfig() @@ -530,6 +829,13 @@ void ShellCorona::loadDefaultLayout() { + //NOTE: Is important the containments already exist for each screen + // at the moment of the script execution,the same loop in :load() + // is executed too late + for (QScreen* screen : qGuiApp->screens()) { + addOutput(screen); + } + QString script = ShellManager::s_testModeLayout; if (script.isEmpty()) {