Index: CMakeLists.txt =================================================================== --- CMakeLists.txt +++ CMakeLists.txt @@ -28,6 +28,7 @@ X11Extras Svg Concurrent + Test ) set(KF5_MIN_VERSION "5.34.0") Index: applets/kicker/CMakeLists.txt =================================================================== --- applets/kicker/CMakeLists.txt +++ applets/kicker/CMakeLists.txt @@ -22,9 +22,11 @@ plugin/contactentry.cpp plugin/containmentinterface.cpp plugin/draghelper.cpp - plugin/favoritesmodel.cpp + plugin/simplefavoritesmodel.cpp + plugin/kastatsfavoritesmodel.cpp plugin/fileentry.cpp plugin/forwardingmodel.cpp + plugin/placeholdermodel.cpp plugin/funnelmodel.cpp plugin/dashboardwindow.cpp plugin/kickerplugin.cpp @@ -41,6 +43,8 @@ plugin/systemsettings.cpp plugin/wheelinterceptor.cpp plugin/windowsystem.cpp + plugin/funnelmodel.cpp + plugin/modeltest.cpp ) qt5_add_dbus_interface(kickerplugin_SRCS ${KRUNNERAPP_INTERFACE} krunner_interface) @@ -56,6 +60,7 @@ Qt5::Qml Qt5::Quick Qt5::X11Extras + Qt5::Test KF5::Activities KF5::ActivitiesStats KF5::ConfigCore Index: applets/kicker/package/contents/code/tools.js =================================================================== --- applets/kicker/package/contents/code/tools.js +++ applets/kicker/package/contents/code/tools.js @@ -1,6 +1,7 @@ /*************************************************************************** * Copyright (C) 2013 by Aurélien Gâteau * * Copyright (C) 2013-2015 by Eike Hein * + * Copyright (C) 2017 by Ivan Cukic * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * @@ -22,42 +23,128 @@ // Accessing actionList can be a costly operation, so we don't // access it until we need the menu. - var action = createFavoriteAction(favoriteModel, favoriteId); + var actions = createFavoriteActions(favoriteModel, favoriteId); - if (action) { + if (actions) { if (actionList && actionList.length > 0) { var separator = { "type": "separator" }; - actionList.unshift(action, separator); + actionList.unshift(separator); + // actionList = actions.concat(actionList); // this crashes Qt O.o + actionList.unshift.apply(actionList, actions); } else { - actionList = [action]; + actionList = actions; } } actionMenu.actionList = actionList; } -function createFavoriteAction(favoriteModel, favoriteId) { +function createFavoriteActions(favoriteModel, favoriteId) { if (favoriteModel === null || !favoriteModel.enabled || favoriteId == null) { return null; } - var action = {}; + var activities = favoriteModel.activities.runningActivities; + + if (activities.length <= 1) { + var action = {}; + + if (favoriteModel.isFavorite(favoriteId)) { + action.text = i18n("Remove from Favorites"); + action.icon = "list-remove"; + action.actionId = "_kicker_favorite_remove"; + } else if (favoriteModel.maxFavorites == -1 || favoriteModel.count < favoriteModel.maxFavorites) { + action.text = i18n("Add to Favorites"); + action.icon = "bookmark-new"; + action.actionId = "_kicker_favorite_add"; + } else { + return null; + } + + action.actionArgument = { favoriteModel: favoriteModel, favoriteId: favoriteId }; + + return [action]; - if (favoriteModel.isFavorite(favoriteId)) { - action.text = i18n("Remove from Favorites"); - action.icon = "list-remove"; - action.actionId = "_kicker_favorite_remove"; - } else if (favoriteModel.maxFavorites == -1 || favoriteModel.count < favoriteModel.maxFavorites) { - action.text = i18n("Add to Favorites"); - action.icon = "bookmark-new"; - action.actionId = "_kicker_favorite_add"; } else { - return null; - } + var actions = []; + + var linkedActivities = favoriteModel.linkedActivitiesFor(favoriteId); + + // Adding the item to link/unlink to all activities + + var linkedToAllActivities = + !(linkedActivities.indexOf(":global") === -1); + + actions.push({ + text : i18n("On All Activities"), + checkable : true, + + actionId : linkedToAllActivities ? + "_kicker_favorite_remove_from_activity" : + "_kicker_favorite_set_to_activity", + checked : linkedToAllActivities, + + actionArgument : { + favoriteModel: favoriteModel, + favoriteId: favoriteId, + favoriteActivity: "" + } + }); + + + // Adding items for each activity separately + + var addActivityItem = function(activityId, activityName) { + var linkedToThisActivity = + !(linkedActivities.indexOf(activityId) === -1); - action.actionArgument = { favoriteModel: favoriteModel, favoriteId: favoriteId }; + actions.push({ + text : activityName, + checkable : true, + checked : linkedToThisActivity && !linkedToAllActivities, - return action; + actionId : + // If we are on all activities, and the user clicks just one + // specific activity, unlink from everything else + linkedToAllActivities ? "_kicker_favorite_set_to_activity" : + + // If we are linked to the current activity, just unlink from + // that single one + linkedToThisActivity ? "_kicker_favorite_remove_from_activity" : + + // Otherwise, link to this activity, but do not unlink from + // other ones + "_kicker_favorite_add_to_activity", + + actionArgument : { + favoriteModel : favoriteModel, + favoriteId : favoriteId, + favoriteActivity : activityId + } + }); + }; + + // Adding the item to link/unlink to the current activity + + addActivityItem(favoriteModel.activities.currentActivity, i18n("On The Current Activity")); + + actions.push({ + type: "separator", + actionId: "_kicker_favorite_separator" + }); + + // Adding the items for each activity + + activities.forEach(function(activityId) { + addActivityItem(activityId, favoriteModel.activityNameForId(activityId)); + }); + + return [{ + text : i18n("Show In Favorites"), + icon : "favorite", + subActions : actions + }]; + } } function triggerAction(model, index, actionId, actionArgument) { @@ -85,12 +172,31 @@ var favoriteId = actionArgument.favoriteId; var favoriteModel = actionArgument.favoriteModel; + console.log(actionId); + if (favoriteModel === null || favoriteId == null) { return null; } + if (actionId == "_kicker_favorite_remove") { - favoriteModel.removeFavorite(favoriteId); + console.log("Removing from all activities"); + favoriteModel.removeFavoriteFrom(favoriteId, ":any"); + } else if (actionId == "_kicker_favorite_add") { - favoriteModel.addFavorite(favoriteId); + console.log("Adding to global activity"); + favoriteModel.addFavoriteTo(favoriteId, ":global"); + + } else if (actionId == "_kicker_favorite_remove_from_activity") { + console.log("Removing from a specific activity"); + favoriteModel.removeFavoriteFrom(favoriteId, actionArgument.favoriteActivity); + + } else if (actionId == "_kicker_favorite_add_to_activity") { + console.log("Adding to another activity"); + favoriteModel.addFavoriteTo(favoriteId, actionArgument.favoriteActivity); + + } else if (actionId == "_kicker_favorite_set_to_activity") { + console.log("Removing the item from the favourites, and re-adding it just to be on a specific activity"); + favoriteModel.setFavoriteOn(favoriteId, actionArgument.favoriteActivity); + } } Index: applets/kicker/package/contents/config/main.xml =================================================================== --- applets/kicker/package/contents/config/main.xml +++ applets/kicker/package/contents/config/main.xml @@ -44,6 +44,10 @@ logout,reboot,shutdown + + + false + Index: applets/kicker/package/contents/ui/ActionMenu.qml =================================================================== --- applets/kicker/package/contents/ui/ActionMenu.qml +++ applets/kicker/package/contents/ui/ActionMenu.qml @@ -64,11 +64,28 @@ menu = contextMenuComponent.createObject(root); - actionList.forEach(function(actionItem) { - var item = contextMenuItemComponent.createObject(menu, { - "actionItem": actionItem, - }); + fillMenu(menu, actionList); + } + + function fillMenu(menu, items) { + items.forEach(function(actionItem) { + if (actionItem.subActions) { + // This is a menu + var submenuItem = contextSubmenuItemComponent.createObject( + menu, { "actionItem" : actionItem }); + + fillMenu(submenuItem.submenu, actionItem.subActions); + + } else { + var item = contextMenuItemComponent.createObject( + menu, + { + "actionItem": actionItem, + } + ); + } }); + } Component { @@ -80,17 +97,39 @@ } Component { - id: contextMenuItemComponent + id: contextSubmenuItemComponent PlasmaComponents.MenuItem { + id: submenuItem + property variant actionItem text: actionItem.text ? actionItem.text : "" - enabled: actionItem.type != "title" && ("enabled" in actionItem ? actionItem.enabled : true) - separator: actionItem.type == "separator" - section: actionItem.type == "title" icon: actionItem.icon ? actionItem.icon : null + property variant submenu : submenu_ + + PlasmaComponents.ContextMenu { + id: submenu_ + visualParent: submenuItem.action + } + } + } + + Component { + id: contextMenuItemComponent + + PlasmaComponents.MenuItem { + property variant actionItem + + text : actionItem.text ? actionItem.text : "" + enabled : actionItem.type != "title" && ("enabled" in actionItem ? actionItem.enabled : true) + separator : actionItem.type == "separator" + section : actionItem.type == "title" + icon : actionItem.icon ? actionItem.icon : null + checkable : actionItem.checkable ? actionItem.checkable : false + checked : actionItem.checked ? actionItem.checked : false + onClicked: { actionClicked(actionItem.actionId, actionItem.actionArgument); } Index: applets/kicker/package/contents/ui/main.qml =================================================================== --- applets/kicker/package/contents/ui/main.qml +++ applets/kicker/package/contents/ui/main.qml @@ -109,7 +109,13 @@ } Component.onCompleted: { - favoritesModel.favorites = plasmoid.configuration.favoriteApps; + favoritesModel.initForClient("org.kde.plasma.kicker.favorites.instance-" + plasmoid.id) + + if (!plasmoid.configuration.favoritesPortedToKAstats) { + favoritesModel.portOldFavorites(plasmoid.configuration.favoriteApps); + plasmoid.configuration.favoritesPortedToKAstats = true; + } + rootModel.refresh(); } } Index: applets/kicker/plugin/abstractentry.cpp =================================================================== --- applets/kicker/plugin/abstractentry.cpp +++ applets/kicker/plugin/abstractentry.cpp @@ -19,6 +19,8 @@ #include "abstractentry.h" +#include + AbstractEntry::AbstractEntry(AbstractModel *owner) : m_owner(owner) { Index: applets/kicker/plugin/appentry.h =================================================================== --- applets/kicker/plugin/appentry.h +++ applets/kicker/plugin/appentry.h @@ -58,6 +58,8 @@ bool run(const QString& actionId = QString(), const QVariant &argument = QVariant()) Q_DECL_OVERRIDE; + QString menuId() const; + static QString nameFromService(const KService::Ptr service, NameFormat nameFormat); static KService::Ptr defaultAppByName(const QString &name); Index: applets/kicker/plugin/appentry.cpp =================================================================== --- applets/kicker/plugin/appentry.cpp +++ applets/kicker/plugin/appentry.cpp @@ -118,6 +118,11 @@ return m_service->storageId(); } +QString AppEntry::menuId() const +{ + return m_service->menuId(); +} + QUrl AppEntry::url() const { return QUrl::fromLocalFile(m_service->entryPath()); Index: applets/kicker/plugin/computermodel.h =================================================================== --- applets/kicker/plugin/computermodel.h +++ applets/kicker/plugin/computermodel.h @@ -26,7 +26,7 @@ #include #include -class FavoritesModel; +class SimpleFavoritesModel; class KConcatenateRowsProxyModel; class KFilePlacesModel; @@ -110,7 +110,7 @@ private: KConcatenateRowsProxyModel *m_concatProxy; RunCommandModel *m_runCommandModel; - FavoritesModel *m_systemAppsModel; + SimpleFavoritesModel *m_systemAppsModel; FilteredPlacesModel *m_filteredPlacesModel; AppEntry::NameFormat m_appNameFormat; QObject *m_appletInterface; Index: applets/kicker/plugin/computermodel.h.orig =================================================================== --- /dev/null +++ applets/kicker/plugin/computermodel.h.orig @@ -0,0 +1,119 @@ +/*************************************************************************** + * Copyright (C) 2015 by Eike Hein * + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA . * + ***************************************************************************/ + +#ifndef COMPUTERMODEL_H +#define COMPUTERMODEL_H + +#include "forwardingmodel.h" +#include "appentry.h" + +#include +#include + +class FavoritesModel; + +class KConcatenateRowsProxyModel; +class KFilePlacesModel; + +namespace Solid { + class Device; +} + +class FilteredPlacesModel : public QSortFilterProxyModel +{ + Q_OBJECT + + public: + FilteredPlacesModel(QObject *parent = 0); + ~FilteredPlacesModel(); + + QUrl url(const QModelIndex &index) const; + bool isDevice(const QModelIndex &index) const; + Solid::Device deviceForIndex(const QModelIndex &index) const; + + protected: + bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const Q_DECL_OVERRIDE; + bool lessThan(const QModelIndex &left, const QModelIndex &right) const Q_DECL_OVERRIDE; + + private: + KFilePlacesModel *m_placesModel; +}; + +class RunCommandModel : public AbstractModel +{ + Q_OBJECT + + public: + RunCommandModel(QObject *parent = 0); + ~RunCommandModel(); + + QString description() const Q_DECL_OVERRIDE; + + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const Q_DECL_OVERRIDE; + + int rowCount(const QModelIndex &parent = QModelIndex()) const Q_DECL_OVERRIDE; + + Q_INVOKABLE bool trigger(int row, const QString &actionId, const QVariant &argument) Q_DECL_OVERRIDE; +}; + +class ComputerModel : public ForwardingModel +{ + Q_OBJECT + + Q_PROPERTY(int appNameFormat READ appNameFormat WRITE setAppNameFormat NOTIFY appNameFormatChanged) + Q_PROPERTY(QObject* appletInterface READ appletInterface WRITE setAppletInterface NOTIFY appletInterfaceChanged) + Q_PROPERTY(QStringList systemApplications READ systemApplications WRITE setSystemApplications NOTIFY systemApplicationsChanged) + + public: + explicit ComputerModel(QObject *parent = 0); + ~ComputerModel(); + + QString description() const Q_DECL_OVERRIDE; + + int appNameFormat() const; + void setAppNameFormat(int format); + + QObject *appletInterface() const; + void setAppletInterface(QObject *appletInterface); + + QStringList systemApplications() const; + void setSystemApplications(const QStringList &apps); + + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const Q_DECL_OVERRIDE; + + Q_INVOKABLE bool trigger(int row, const QString &actionId, const QVariant &argument) Q_DECL_OVERRIDE; + + Q_SIGNALS: + void appNameFormatChanged() const; + void appletInterfaceChanged() const; + void systemApplicationsChanged() const; + + private Q_SLOTS: + void onSetupDone(Solid::ErrorType error, QVariant errorData, const QString &udi); + + private: + KConcatenateRowsProxyModel *m_concatProxy; + RunCommandModel *m_runCommandModel; + FavoritesModel *m_systemAppsModel; + FilteredPlacesModel *m_filteredPlacesModel; + AppEntry::NameFormat m_appNameFormat; + QObject *m_appletInterface; +}; + +#endif Index: applets/kicker/plugin/computermodel.cpp =================================================================== --- applets/kicker/plugin/computermodel.cpp +++ applets/kicker/plugin/computermodel.cpp @@ -20,7 +20,7 @@ #include "computermodel.h" #include "actionlist.h" -#include "favoritesmodel.h" +#include "simplefavoritesmodel.h" #include @@ -137,12 +137,13 @@ ComputerModel::ComputerModel(QObject *parent) : ForwardingModel(parent) , m_concatProxy(new KConcatenateRowsProxyModel(this)) , m_runCommandModel(new RunCommandModel(this)) -, m_systemAppsModel(new FavoritesModel(this)) +, m_systemAppsModel(new SimpleFavoritesModel(this)) , m_filteredPlacesModel(new FilteredPlacesModel(this)) , m_appNameFormat(AppEntry::NameOnly) , m_appletInterface(nullptr) { - connect(m_systemAppsModel, &FavoritesModel::favoritesChanged, this, &ComputerModel::systemApplicationsChanged); + connect(m_systemAppsModel, &SimpleFavoritesModel::favoritesChanged, this, &ComputerModel::systemApplicationsChanged); + m_systemAppsModel->setFavorites(QStringList() << "systemsettings.desktop"); m_concatProxy->addSourceModel(m_runCommandModel); m_concatProxy->addSourceModel(m_systemAppsModel); Index: applets/kicker/plugin/kastatsfavoritesmodel.h =================================================================== --- /dev/null +++ applets/kicker/plugin/kastatsfavoritesmodel.h @@ -0,0 +1,117 @@ +/*************************************************************************** + * Copyright (C) 2014-2015 by Eike Hein * + * Copyright (C) 2016-2017 by Ivan Cukic * + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA . * + ***************************************************************************/ + +#ifndef FAVORITESMODEL_H +#define FAVORITESMODEL_H + +#include "placeholdermodel.h" + +#include + +#include +#include + +class PlaceholderModel; + +namespace KActivities { + class Consumer; +namespace Stats { + class ResultModel; +namespace Terms { + class Activity; +} // namespace Terms +} // namespace Stats +} // namespace KActivities + +class KAStatsFavoritesModel : public PlaceholderModel +{ + Q_OBJECT + + Q_PROPERTY(bool enabled READ enabled WRITE setEnabled NOTIFY enabledChanged) + Q_PROPERTY(QStringList favorites READ favorites WRITE setFavorites NOTIFY favoritesChanged) + Q_PROPERTY(int maxFavorites READ maxFavorites WRITE setMaxFavorites NOTIFY maxFavoritesChanged) + + Q_PROPERTY(QObject* activities READ activities CONSTANT) + + public: + explicit KAStatsFavoritesModel(QObject *parent = 0); + ~KAStatsFavoritesModel(); + + QString description() const; + + Q_INVOKABLE bool trigger(int row, const QString &actionId, const QVariant &argument); + + bool enabled() const; + void setEnabled(bool enable); + + QStringList favorites() const; + void setFavorites(const QStringList &favorites); + + int maxFavorites() const; + void setMaxFavorites(int max); + + Q_INVOKABLE bool isFavorite(const QString &id) const; + + Q_INVOKABLE void addFavorite(const QString &id, int index = -1); + Q_INVOKABLE void removeFavorite(const QString &id); + + Q_INVOKABLE void addFavoriteTo(const QString &id, const QString &activityId, int index = -1); + Q_INVOKABLE void removeFavoriteFrom(const QString &id, const QString &activityId); + + Q_INVOKABLE void setFavoriteOn(const QString &id, const QString &activityId); + + Q_INVOKABLE void portOldFavorites(const QStringList &ids); + + Q_INVOKABLE QStringList linkedActivitiesFor(const QString &id) const; + + Q_INVOKABLE void moveRow(int from, int to); + + Q_INVOKABLE void initForClient(const QString &client); + + QObject *activities() const; + Q_INVOKABLE QString activityNameForId(const QString &activityId) const; + + AbstractModel* favoritesModel(); + + public Q_SLOTS: + virtual void refresh(); + + Q_SIGNALS: + void enabledChanged() const; + void favoritesChanged() const; + void maxFavoritesChanged() const; + + private: + class Private; + Private * d; + + AbstractEntry *favoriteFromId(const QString &id) const; + + void addFavoriteTo(const QString &id, const KActivities::Stats::Terms::Activity &activityId, int index = -1); + void removeFavoriteFrom(const QString &id, const KActivities::Stats::Terms::Activity &activityId); + + bool m_enabled; + + int m_maxFavorites; + + KActivities::Consumer *m_activities; +}; + +#endif Index: applets/kicker/plugin/kastatsfavoritesmodel.cpp =================================================================== --- /dev/null +++ applets/kicker/plugin/kastatsfavoritesmodel.cpp @@ -0,0 +1,721 @@ +/*************************************************************************** + * Copyright (C) 2014-2015 by Eike Hein * + * Copyright (C) 2016-2017 by Ivan Cukic * + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA . * + ***************************************************************************/ + +#include "kastatsfavoritesmodel.h" +#include "appentry.h" +#include "contactentry.h" +#include "fileentry.h" +#include "actionlist.h" + +#include "modeltest.h" + +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace KAStats = KActivities::Stats; + +using namespace KAStats; +using namespace KAStats::Terms; + +#define AGENT_APPLICATIONS "org.kde.plasma.favorites.applications" +#define AGENT_CONTACTS "org.kde.plasma.favorites.contacts" +#define AGENT_DOCUMENTS "org.kde.plasma.favorites.documents" + +#define DEBUG_PREFIX "\033[0;31m[KASTATSFAVS]\033[0;34m " +#define DEBUG qDebug() << DEBUG_PREFIX << (void*)q << ((void*)this) << m_clientId << "\033[0;0m " + +QString agentForUrl(const QString &url) +{ + return url.startsWith("ktp:") + ? AGENT_CONTACTS + : url.startsWith("preferred:") + ? AGENT_APPLICATIONS + : url.startsWith("applications:") + ? AGENT_APPLICATIONS + : (url.startsWith("/") && !url.endsWith(".desktop")) + ? AGENT_DOCUMENTS + : (url.startsWith("file:/") && !url.endsWith(".desktop")) + ? AGENT_DOCUMENTS + // use applications as the default + : AGENT_APPLICATIONS; +} + +class KAStatsFavoritesModel::Private: public QAbstractListModel { +public: + class NormalizedId { + public: + NormalizedId() + { + } + + NormalizedId(const Private *parent, const QString &id) + { + if (id.isEmpty()) return; + + AbstractEntry *entry = nullptr; + QScopedPointer deleter; + + if (parent->m_itemEntries.contains(id)) { + entry = parent->m_itemEntries[id]; + } else { + // This entry is not cached - it is temporary, + // so let's clean up when we exit this function + entry = parent->entryForResource(id); + deleter.reset(entry); + } + + if (!entry || !entry->isValid()) { + qWarning() << "Entry is not valid" << id << entry; + m_id = id; + return; + } + + const auto url = entry->url(); + + qDebug() << "Original id is: " << id << ", and the url is" << url; + + // Preferred applications need special handling + if (entry->id().startsWith("preferred:")) { + m_id = entry->id(); + return; + } + + // If this is an application, use the applications:-format url + auto appEntry = dynamic_cast(entry); + if (appEntry && !appEntry->menuId().isEmpty()) { + m_id = "applications:" + appEntry->menuId(); + return; + } + + // We want to resolve symbolic links not to have two paths + // refer to the same .desktop file + if (url.isLocalFile()) { + QFileInfo file(url.toLocalFile()); + + if (file.exists()) { + m_id = QUrl::fromLocalFile(file.canonicalFilePath()).toString(); + return; + } + } + + // If this is a file, we should have already covered it + if (url.scheme() == "file") { + return; + } + + m_id = url.toString(); + } + + const QString& value() const + { + return m_id; + } + + bool operator==(const NormalizedId &other) const + { + return m_id == other.m_id; + } + + private: + QString m_id; + }; + + NormalizedId normalizedId(const QString &id) const + { + return NormalizedId(this, id); + } + + AbstractEntry *entryForResource(const QString &resource) const + { + const auto agent = + agentForUrl(resource); + + if (agent == AGENT_CONTACTS) { + return new ContactEntry(q, resource); + + } else if (agent == AGENT_DOCUMENTS) { + if (resource.startsWith("/")) { + return new FileEntry(q, QUrl::fromLocalFile(resource)); + } else { + return new FileEntry(q, QUrl(resource)); + } + + } else if (agent == AGENT_APPLICATIONS) { + if (resource.startsWith("applications:")) { + return new AppEntry(q, resource.mid(13)); + } else { + return new AppEntry(q, resource); + } + + } else { + return nullptr; + } + } + + Private(KAStatsFavoritesModel *parent, QString clientId) + : q(parent) + , m_query( + LinkedResources + | Agent { + AGENT_APPLICATIONS, + AGENT_CONTACTS, + AGENT_DOCUMENTS + } + | Type::any() + | Activity::current() + | Activity::global() + ) + , m_watcher(m_query) + , m_clientId(clientId) + { + // Connecting the watcher + connect(&m_watcher, &ResultWatcher::resultLinked, + [this] (const QString &resource) { + addResult(resource, -1); + }); + + connect(&m_watcher, &ResultWatcher::resultUnlinked, + [this] (const QString &resource) { + removeResult(resource); + }); + + // Loading the items order + const auto cfg = KSharedConfig::openConfig("kactivitymanagerd-statsrc"); + + // We want first to check whether we have an ordering for this activity. + // If not, we will try to get a global one for this applet + + const QString thisGroupName = + "Favorites-" + clientId + "-" + m_activities.currentActivity(); + const QString globalGroupName = + "Favorites-" + clientId + "-global"; + + KConfigGroup thisCfgGroup(cfg, thisGroupName); + KConfigGroup globalCfgGroup(cfg, globalGroupName); + + QStringList ordering = + thisCfgGroup.readEntry("ordering", QStringList()) + + globalCfgGroup.readEntry("ordering", QStringList()); + + DEBUG << "Loading the ordering " << ordering; + + // Loading the results without emitting any model signals + ResultSet results(m_query); + + for (const auto& result: results) { + addResult(result.resource(), -1, false); + } + + // Normalizing all the ids + std::transform(ordering.begin(), ordering.end(), ordering.begin(), + [&] (const QString &item) { + return normalizedId(item).value(); + }); + + // Sorting the items in the cache + std::sort(m_items.begin(), m_items.end(), + [&] (const NormalizedId &left, const NormalizedId &right) { + auto leftIndex = ordering.indexOf(left.value()); + auto rightIndex = ordering.indexOf(right.value()); + + return (leftIndex == -1 && rightIndex == -1) ? + left.value() < right.value() : + + (leftIndex == -1) ? + false : + + (rightIndex == -1) ? + true : + + // otherwise + leftIndex < rightIndex; + }); + + // Debugging: + QVector itemStrings(m_items.size()); + std::transform(m_items.cbegin(), m_items.cend(), itemStrings.begin(), + [] (const NormalizedId &item) { + return item.value(); + }); + DEBUG << "After ordering: " << itemStrings; + } + + void addResult(const QString &_resource, int index, bool notifyModel = true) + { + // We want even files to have a proper URL + const auto resource = + _resource.startsWith("/") ? QUrl::fromLocalFile(_resource).toString() : _resource; + + DEBUG << "Adding result" << resource << "already present?" << m_itemEntries.contains(resource); + + if (m_itemEntries.contains(resource)) return; + + auto entry = entryForResource(resource); + + if (!entry || !entry->isValid()) { + DEBUG << "Entry is not valid!"; + return; + } + + if (index == -1) { + index = m_items.count(); + } + + if (notifyModel) { + beginInsertRows(QModelIndex(), index, index); + } + + auto url = entry->url(); + + m_itemEntries[resource] + = m_itemEntries[entry->id()] + = m_itemEntries[url.toString()] + = m_itemEntries[url.toLocalFile()] + = entry; + + auto normalized = normalizedId(resource); + m_items.insert(index, normalized); + m_itemEntries[normalized.value()] = entry; + + if (notifyModel) { + endInsertRows(); + saveOrdering(); + } + } + + void removeResult(const QString &resource) + { + auto normalized = normalizedId(resource); + + // If we know this item will not really be removed, + // but only that activities it is on have changed, + // lets leave it + if (m_ignoredItems.contains(normalized.value())) { + m_ignoredItems.removeAll(normalized.value()); + return; + } + + DEBUG << "Removing result" << resource; + + auto index = m_items.indexOf(normalizedId(resource)); + + if (index == -1) return; + + beginRemoveRows(QModelIndex(), index, index); + auto entry = m_itemEntries[resource]; + m_items.removeAt(index); + + // Removing the entry from the cache + QMutableHashIterator i(m_itemEntries); + while (i.hasNext()) { + if (i.value() == entry) { + i.remove(); + } + i.next(); + } + delete entry; + + endRemoveRows(); + } + + + int rowCount(const QModelIndex &parent = QModelIndex()) const override + { + if (parent.isValid()) return 0; + + return m_items.count(); + } + + QVariant data(const QModelIndex &item, + int role = Qt::DisplayRole) const override + { + if (item.parent().isValid()) return QVariant(); + + const auto index = item.row(); + + const auto entry = m_itemEntries[m_items[index].value()]; + + return entry == nullptr ? QVariant() + : role == Qt::DisplayRole ? entry->name() + : role == Qt::DecorationRole ? entry->icon() + : role == Kicker::DescriptionRole ? entry->description() + : role == Kicker::FavoriteIdRole ? entry->id() + : role == Kicker::UrlRole ? entry->url() + : role == Kicker::HasActionListRole ? entry->hasActions() + : role == Kicker::ActionListRole ? entry->actions() + : QVariant(); + } + + bool trigger(int row, const QString &actionId, const QVariant &argument) + { + if (row < 0 || row >= rowCount()) { + return false; + } + + const QString id = data(index(row, 0), Kicker::UrlRole).toString(); + + return m_itemEntries.contains(id) + ? m_itemEntries[id]->run(actionId, argument) + : false; + } + + void move(int from, int to) + { + if (from < 0) return; + if (from >= m_items.count()) return; + if (to < 0) return; + if (to >= m_items.count()) return; + + if (from == to) return; + + const int modelTo = to + (to > from ? 1 : 0); + + if (q->beginMoveRows(QModelIndex(), from, from, + QModelIndex(), modelTo)) { + m_items.move(from, to); + q->endMoveRows(); + + DEBUG << "Save ordering (from Private::move) -->"; + saveOrdering(); + } + } + + void saveOrdering() + { + QStringList ids; + + for (const auto& item: m_items) { + ids << item.value(); + } + + DEBUG << "Save ordering (from Private::saveOrdering) -->"; + saveOrdering(ids, m_clientId, m_activities.currentActivity()); + } + + static void saveOrdering(const QStringList &ids, const QString &clientId, const QString ¤tActivity) + { + const auto cfg = KSharedConfig::openConfig("kactivitymanagerd-statsrc"); + + QStringList activities { + currentActivity, + "global" + }; + + qDebug() << "Saving ordering for" << currentActivity << "and global" << ids; + + for (const auto& activity: activities) { + const QString groupName = + "Favorites-" + clientId + "-" + activity; + + KConfigGroup cfgGroup(cfg, groupName); + + cfgGroup.writeEntry("ordering", ids); + } + + cfg->sync(); + } + + KAStatsFavoritesModel *const q; + KActivities::Consumer m_activities; + Query m_query; + ResultWatcher m_watcher; + QString m_clientId; + + QVector m_items; + QHash m_itemEntries; + QStringList m_ignoredItems; +}; + +#undef DEBUG +#define DEBUG qDebug() << DEBUG_PREFIX << ((void*)this) << ((void*)d) << (d ? d->m_clientId : QString("no client ID yet")) << "\033[0;0m " + +KAStatsFavoritesModel::KAStatsFavoritesModel(QObject *parent) +: PlaceholderModel(parent) +, d(nullptr) // we have no client id yet +, m_enabled(true) +, m_maxFavorites(-1) +, m_activities(new KActivities::Consumer(this)) +{ + connect(m_activities, &KActivities::Consumer::currentActivityChanged, + this, [&] (const QString ¤tActivity) { + DEBUG << "Activity just got changed"; + Q_UNUSED(currentActivity); + auto clientId = d->m_clientId; + initForClient(clientId); + }); +} + +KAStatsFavoritesModel::~KAStatsFavoritesModel() +{ + delete d; +} + +void KAStatsFavoritesModel::initForClient(const QString &clientId) +{ + DEBUG << "initForClient" << clientId; + + setSourceModel(nullptr); + delete d; + d = new Private( + this, + clientId + ); + + if (!qobject_cast(QObject::parent())) { + // creating a model test with a copy of this + DEBUG << "Creating the test model"; + new ModelTest(new KAStatsFavoritesModel(d), d); + } + + setSourceModel(d); +} + +QString KAStatsFavoritesModel::description() const +{ + return i18n("Favorites"); +} + +bool KAStatsFavoritesModel::trigger(int row, const QString &actionId, const QVariant &argument) +{ + return d->trigger(row, actionId, argument); +} + +bool KAStatsFavoritesModel::enabled() const +{ + return m_enabled; +} + +int KAStatsFavoritesModel::maxFavorites() const +{ + return m_maxFavorites; +} + +void KAStatsFavoritesModel::setMaxFavorites(int max) +{ + Q_UNUSED(max); +} + +void KAStatsFavoritesModel::setEnabled(bool enable) +{ + if (m_enabled != enable) { + m_enabled = enable; + + emit enabledChanged(); + } +} + +QStringList KAStatsFavoritesModel::favorites() const +{ + qWarning() << "KAStatsFavoritesModel::favorites returns nothing, it is here just to keep the API backwards-compatible"; + return QStringList(); +} + +void KAStatsFavoritesModel::setFavorites(const QStringList& favorites) +{ + Q_UNUSED(favorites); + qWarning() << "KAStatsFavoritesModel::setFavorites is ignored"; +} + +bool KAStatsFavoritesModel::isFavorite(const QString &id) const +{ + return d && d->m_itemEntries.contains(id); +} + +void KAStatsFavoritesModel::portOldFavorites(const QStringList &ids) +{ + DEBUG << "portOldFavorites" << ids; + + const auto activityId = ":global"; + std::for_each(ids.begin(), ids.end(), [&] (const QString &id) { + addFavoriteTo(id, activityId); + }); + + // Resetting the model + auto clientId = d->m_clientId; + setSourceModel(nullptr); + delete d; + d = nullptr; + + DEBUG << "Save ordering (from portOldFavorites) -->"; + Private::saveOrdering(ids, clientId, m_activities->currentActivity()); + + QTimer::singleShot(500, + std::bind(&KAStatsFavoritesModel::initForClient, this, clientId)); +} + +void KAStatsFavoritesModel::addFavorite(const QString &id, int index) +{ + DEBUG << "addFavorite" << id << index << " -->"; + addFavoriteTo(id, Activity::current(), index); +} + +void KAStatsFavoritesModel::removeFavorite(const QString &id) +{ + DEBUG << "removeFavorite" << id << " -->"; + removeFavoriteFrom(id, Activity::current()); +} + +void KAStatsFavoritesModel::addFavoriteTo(const QString &id, const QString &activityId, int index) +{ + DEBUG << "addFavoriteTo" << id << activityId << index << " -->"; + addFavoriteTo(id, Activity(activityId), index); +} + +void KAStatsFavoritesModel::removeFavoriteFrom(const QString &id, const QString &activityId) +{ + DEBUG << "removeFavoriteFrom" << id << activityId << " -->"; + removeFavoriteFrom(id, Activity(activityId)); +} + +void KAStatsFavoritesModel::addFavoriteTo(const QString &id, const Activity &activity, int index) +{ + if (!d || id.isEmpty()) return; + + Q_ASSERT(!activity.values.isEmpty()); + + setDropPlaceholderIndex(-1); + + QStringList matchers { d->m_activities.currentActivity(), ":global", ":current" }; + if (std::find_first_of(activity.values.cbegin(), activity.values.cend(), + matchers.cbegin(), matchers.cend()) != activity.values.cend()) { + d->addResult(id, index); + } + + const auto url = d->normalizedId(id).value(); + + DEBUG << "addFavoriteTo" << id << activity << index << url << " (actual)"; + + if (url.isEmpty()) return; + + d->m_watcher.linkToActivity(QUrl(url), activity, + Agent(agentForUrl(url))); +} + +void KAStatsFavoritesModel::removeFavoriteFrom(const QString &id, const Activity &activity) +{ + const auto url = d->normalizedId(id).value(); + + Q_ASSERT(!activity.values.isEmpty()); + + DEBUG << "addFavoriteTo" << id << activity << url << " (actual)"; + + if (url.isEmpty()) return; + + d->m_watcher.unlinkFromActivity(QUrl(url), activity, + Agent(agentForUrl(url))); +} + +void KAStatsFavoritesModel::setFavoriteOn(const QString &id, const QString &activityId) +{ + const auto url = d->normalizedId(id).value(); + + DEBUG << "setFavoriteOn" << id << activityId << url << " (actual)"; + + DEBUG << "%%%%%%%%%%% Activity is" << activityId; + if (activityId.isEmpty() || activityId == ":any" || + activityId == ":global" || + activityId == m_activities->currentActivity()) { + d->m_ignoredItems << url; + } + + d->m_watcher.unlinkFromActivity(QUrl(url), Activity::any(), + Agent(agentForUrl(url))); + d->m_watcher.linkToActivity(QUrl(url), activityId, + Agent(agentForUrl(url))); +} + +void KAStatsFavoritesModel::moveRow(int from, int to) +{ + d->move(from, to); +} + +AbstractModel *KAStatsFavoritesModel::favoritesModel() +{ + return this; +} + +void KAStatsFavoritesModel::refresh() +{ +} + +QObject *KAStatsFavoritesModel::activities() const +{ + return m_activities; +} + +QString KAStatsFavoritesModel::activityNameForId(const QString &activityId) const +{ + // It is safe to use a short-lived object here, + // we are always synced with KAMD in plasma + KActivities::Info info(activityId); + return info.name(); +} + +QStringList KAStatsFavoritesModel::linkedActivitiesFor(const QString &id) const +{ + if (!d) { + DEBUG << "Linked for" << id << "is empty, no Private instance"; + return {}; + } + + auto url = d->normalizedId(id).value(); + + if (url.startsWith("file:")) { + url = QUrl(url).toLocalFile(); + } + + if (url.isEmpty()) { + DEBUG << "The url for" << id << "is empty"; + return {}; + } + + auto query = LinkedResources + | Agent { + AGENT_APPLICATIONS, + AGENT_CONTACTS, + AGENT_DOCUMENTS + } + | Type::any() + | Activity::any() + | Url(url); + + ResultSet results(query); + + for (const auto &result: results) { + DEBUG << "Returning" << result.linkedActivities() << "for" << id << url; + return result.linkedActivities(); + } + + DEBUG << "Returning empty list of activities for" << id << url; + return {}; +} + Index: applets/kicker/plugin/kickerplugin.cpp =================================================================== --- applets/kicker/plugin/kickerplugin.cpp +++ applets/kicker/plugin/kickerplugin.cpp @@ -23,7 +23,8 @@ #include "computermodel.h" #include "containmentinterface.h" #include "draghelper.h" -#include "favoritesmodel.h" +#include "simplefavoritesmodel.h" +#include "kastatsfavoritesmodel.h" #include "dashboardwindow.h" #include "funnelmodel.h" #include "processrunner.h" @@ -48,7 +49,8 @@ qmlRegisterType(uri, 0, 1, "ComputerModel"); qmlRegisterType(uri, 0, 1, "ContainmentInterface"); qmlRegisterType(uri, 0, 1, "DragHelper"); - qmlRegisterType(uri, 0, 1, "FavoritesModel"); + qmlRegisterType(uri, 0, 1, "FavoritesModel"); + qmlRegisterType(uri, 0, 1, "KAStatsFavoritesModel"); qmlRegisterType(uri, 0, 1, "DashboardWindow"); qmlRegisterType(uri, 0, 1, "FunnelModel"); qmlRegisterType(uri, 0, 1, "ProcessRunner"); Index: applets/kicker/plugin/placeholdermodel.h =================================================================== --- /dev/null +++ applets/kicker/plugin/placeholdermodel.h @@ -0,0 +1,94 @@ +/*************************************************************************** + * Copyright (C) 2015 by Eike Hein * + * Copyright (C) 2017 by Ivan Cukic * + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA . * + ***************************************************************************/ + +#ifndef PLACEHOLDERMODEL_H +#define PLACEHOLDERMODEL_H + +#include "abstractmodel.h" + +#include +#include + +class PlaceholderModel : public AbstractModel +{ + Q_OBJECT + + Q_PROPERTY(QAbstractItemModel* sourceModel READ sourceModel WRITE setSourceModel NOTIFY sourceModelChanged); + Q_PROPERTY(int dropPlaceholderIndex READ dropPlaceholderIndex WRITE setDropPlaceholderIndex NOTIFY dropPlaceholderIndexChanged) + + public: + explicit PlaceholderModel(QObject *parent = 0); + ~PlaceholderModel(); + + QString description() const; + + QAbstractItemModel *sourceModel() const; + virtual void setSourceModel(QAbstractItemModel *sourceModel); + + bool canFetchMore(const QModelIndex &parent) const; + void fetchMore(const QModelIndex &parent); + + QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const; + QModelIndex parent(const QModelIndex &index) const; + + virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const; + + int rowCount(const QModelIndex &parent = QModelIndex()) const; + + Q_INVOKABLE bool trigger(int row, const QString &actionId, const QVariant &argument); + + Q_INVOKABLE QString labelForRow(int row); + + Q_INVOKABLE AbstractModel *modelForRow(int row); + + AbstractModel* favoritesModel(); + + int separatorCount() const; + + int dropPlaceholderIndex() const; + void setDropPlaceholderIndex(int index); + + public Q_SLOTS: + void reset(); + + Q_SIGNALS: + void sourceModelChanged() const; + void dropPlaceholderIndexChanged(); + + protected: + void inhibitTriggering(); + + private: + QModelIndex indexToSourceIndex(const QModelIndex &index) const; + QModelIndex sourceIndexToIndex(const QModelIndex &index) const; + int sourceRowToRow(int sourceRow) const; + int rowToSourceRow(int row) const; + + void connectSignals(); + void disconnectSignals(); + + QPointer m_sourceModel; + + int m_dropPlaceholderIndex; + bool m_isTriggerInhibited; + QTimer m_triggerInhibitor; +}; + +#endif Index: applets/kicker/plugin/placeholdermodel.cpp =================================================================== --- /dev/null +++ applets/kicker/plugin/placeholdermodel.cpp @@ -0,0 +1,407 @@ +/*************************************************************************** + * Copyright (C) 2015 by Eike Hein * + * Copyright (C) 2017 by Ivan Cukic * + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA . * + ***************************************************************************/ + +#include "placeholdermodel.h" +#include "actionlist.h" + +#include +#include + +PlaceholderModel::PlaceholderModel(QObject *parent) + : AbstractModel(parent) + , m_dropPlaceholderIndex(-1) + , m_isTriggerInhibited(false) +{ + connect(&m_triggerInhibitor, &QTimer::timeout, + this, [&] { + qDebug() << "%%% Inhibit stopped"; + m_isTriggerInhibited = false; + }); + + m_triggerInhibitor.setInterval(500); + m_triggerInhibitor.setSingleShot(true); +} + +void PlaceholderModel::inhibitTriggering() +{ + qDebug() << "%%% Inhibit started"; + m_isTriggerInhibited = true; + m_triggerInhibitor.start(); +} + +PlaceholderModel::~PlaceholderModel() +{ +} + +QString PlaceholderModel::description() const +{ + if (auto abstractModel = qobject_cast(m_sourceModel)) { + return abstractModel->description(); + + } else { + return QString(); + } +} + +QAbstractItemModel *PlaceholderModel::sourceModel() const +{ + return m_sourceModel; +} + +void PlaceholderModel::setSourceModel(QAbstractItemModel *sourceModel) +{ + disconnectSignals(); + + beginResetModel(); + + m_sourceModel = sourceModel; + + connectSignals(); + + endResetModel(); + + emit countChanged(); + emit sourceModelChanged(); + emit descriptionChanged(); +} + +bool PlaceholderModel::canFetchMore(const QModelIndex &parent) const +{ + return m_sourceModel && m_sourceModel->canFetchMore(indexToSourceIndex(parent)); +} + +void PlaceholderModel::fetchMore(const QModelIndex &parent) +{ + if (m_sourceModel) { + m_sourceModel->fetchMore(indexToSourceIndex(parent)); + } +} + +QModelIndex PlaceholderModel::index(int row, int column, const QModelIndex &parent) const +{ + Q_UNUSED(parent) + + return m_sourceModel ? createIndex(row, column) + : QModelIndex(); +} + +QModelIndex PlaceholderModel::parent(const QModelIndex &index) const +{ + Q_UNUSED(index) + + return QModelIndex(); +} + +QVariant PlaceholderModel::data(const QModelIndex &index, int role) const +{ + const auto row = index.row(); + + if (m_dropPlaceholderIndex == row) { + switch (role) { + case Kicker::IsDropPlaceholderRole: + return true; + + // TODO: Maybe it would be nice to show something here? + // case Qt::DisplayRole: + // return "placeholder"; + // + // case Qt::DecorationRole: + // return "select"; + + default: + return QVariant(); + + } + } + + return m_sourceModel ? m_sourceModel->data(indexToSourceIndex(index), role) + : QVariant(); +} + +int PlaceholderModel::rowCount(const QModelIndex &parent) const +{ + if (!m_sourceModel || parent.isValid()) { + return 0; + } + + return m_sourceModel->rowCount() + + (m_dropPlaceholderIndex != -1 ? 1 : 0); +} + +QModelIndex PlaceholderModel::indexToSourceIndex(const QModelIndex& index) const +{ + if (!m_sourceModel || !index.isValid()) { + return QModelIndex(); + } + + const auto row = index.row(); + const auto column = index.column(); + + return index.parent().isValid() ? + // We do not support tree models + QModelIndex() : + + // If we are on top-level, lets add a placeholder + m_sourceModel->index( + row - (m_dropPlaceholderIndex != -1 && row > m_dropPlaceholderIndex ? 1 : 0), + column, + QModelIndex() + ); +} + +int PlaceholderModel::sourceRowToRow(int sourceRow) const +{ + return sourceRow + + (m_dropPlaceholderIndex != -1 && sourceRow >= m_dropPlaceholderIndex ? 1 : 0); +} + +int PlaceholderModel::rowToSourceRow(int row) const +{ + return row == m_dropPlaceholderIndex ? -1 : + row - (m_dropPlaceholderIndex != -1 && row > m_dropPlaceholderIndex ? 1 : 0); +} + +QModelIndex PlaceholderModel::sourceIndexToIndex(const QModelIndex& sourceIndex) const +{ + if (!m_sourceModel || !sourceIndex.isValid()) { + return QModelIndex(); + } + + const auto sourceRow = sourceIndex.row(); + const auto sourceColumn = sourceIndex.column(); + + return sourceIndex.parent().isValid() ? + // We do not support tree-models + QModelIndex() : + + // If we are on top-level, lets add a placeholder + index( + sourceRowToRow(sourceRow), + sourceColumn, + QModelIndex() + ); +} + +bool PlaceholderModel::trigger(int row, const QString &actionId, const QVariant &argument) +{ + if (m_isTriggerInhibited) return false; + + if (auto abstractModel = qobject_cast(m_sourceModel)) { + return abstractModel->trigger(rowToSourceRow(row), actionId, argument); + + } else { + return false; + } +} + +QString PlaceholderModel::labelForRow(int row) +{ + if (auto abstractModel = qobject_cast(m_sourceModel)) { + return abstractModel->labelForRow(rowToSourceRow(row)); + + } else { + return QString(); + } + +} + +AbstractModel* PlaceholderModel::modelForRow(int row) +{ + if (auto abstractModel = qobject_cast(m_sourceModel)) { + return abstractModel->modelForRow(rowToSourceRow(row)); + + } else { + return 0; + } +} + +AbstractModel* PlaceholderModel::favoritesModel() +{ + if (auto abstractModel = qobject_cast(m_sourceModel)) { + return abstractModel->favoritesModel(); + + } else { + return AbstractModel::favoritesModel(); + } +} + +int PlaceholderModel::separatorCount() const +{ + if (auto abstractModel = qobject_cast(m_sourceModel)) { + return abstractModel->separatorCount(); + + } else { + return 0; + } +} + +void PlaceholderModel::reset() +{ + emit beginResetModel(); + emit endResetModel(); + emit countChanged(); + emit separatorCountChanged(); +} + +void PlaceholderModel::connectSignals() +{ + if (!m_sourceModel) { + return; + } + + const auto sourceModelPtr = m_sourceModel.data(); + + connect(sourceModelPtr, SIGNAL(destroyed()), this, SLOT(reset())); + + connect(sourceModelPtr, &QAbstractItemModel::dataChanged, + this, [this] (const QModelIndex &from, const QModelIndex &to, const QVector &roles) { + emit dataChanged(sourceIndexToIndex(from), + sourceIndexToIndex(to), + roles); + }); + + connect(sourceModelPtr, &QAbstractItemModel::rowsAboutToBeInserted, + this, [this] (const QModelIndex &parent, int from, int to) { + if (parent.isValid()) { + qWarning() << "We do not support tree models"; + + } else { + beginInsertRows(QModelIndex(), + sourceRowToRow(from), + sourceRowToRow(to)); + + } + }); + + connect(sourceModelPtr, &QAbstractItemModel::rowsInserted, + this, [this] { + endInsertRows(); + emit countChanged(); + }); + + + connect(sourceModelPtr, &QAbstractItemModel::rowsAboutToBeMoved, + this, [this] (const QModelIndex &source, int from, int to, const QModelIndex &dest, int destRow) { + if (source.isValid() || dest.isValid()) { + qWarning() << "We do not support tree models"; + + } else { + beginMoveRows(QModelIndex(), + sourceRowToRow(from), + sourceRowToRow(to), + QModelIndex(), + sourceRowToRow(destRow)); + } + }); + + connect(sourceModelPtr, &QAbstractItemModel::rowsMoved, + this, [this] { + endMoveRows(); + }); + + + connect(sourceModelPtr, &QAbstractItemModel::rowsAboutToBeRemoved, + this, [this] (const QModelIndex &parent, int from, int to) { + if (parent.isValid()) { + qWarning() << "We do not support tree models"; + + } else { + beginRemoveRows(QModelIndex(), + sourceRowToRow(from), + sourceRowToRow(to)); + } + }); + + connect(sourceModelPtr, &QAbstractItemModel::rowsRemoved, + this, [this] { + endRemoveRows(); + emit countChanged(); + }); + + + connect(sourceModelPtr, &QAbstractItemModel::modelAboutToBeReset, + this, [this] { + beginResetModel(); + }); + + connect(sourceModelPtr, &QAbstractItemModel::modelReset, + this, [this] { + endResetModel(); + emit countChanged(); + }); + + // We do not have persistant indices + // connect(sourceModelPtr, &QAbstractItemModel::layoutAboutToBeChanged), + // this, &PlaceholderModel::layoutAboutToBeChanged); + // connect(sourceModelPtr, &QAbstractItemModel::layoutChanged), + // this, SIGNAL(layoutChanged(QList,QAbstractItemModel::LayoutChangeHint)), + // Qt::UniqueConnection); +} + +void PlaceholderModel::disconnectSignals() +{ + if (!m_sourceModel) { + return; + } + + disconnect(m_sourceModel, 0, this, 0); +} + +int PlaceholderModel::dropPlaceholderIndex() const +{ + return m_dropPlaceholderIndex; +} + +void PlaceholderModel::setDropPlaceholderIndex(int index) +{ + if (index == m_dropPlaceholderIndex) return; + + inhibitTriggering(); + + if (index == -1 && m_dropPlaceholderIndex != -1) { + // Removing the placeholder + beginRemoveRows(QModelIndex(), m_dropPlaceholderIndex, m_dropPlaceholderIndex); + m_dropPlaceholderIndex = index; + endRemoveRows(); + + emit countChanged(); + + } else if (index != -1 && m_dropPlaceholderIndex == -1) { + // Creating the placeholder + beginInsertRows(QModelIndex(), index, index); + m_dropPlaceholderIndex = index; + endInsertRows(); + + emit countChanged(); + + } else if (m_dropPlaceholderIndex != index) { + // Moving the placeholder + int modelTo = index + (index > m_dropPlaceholderIndex ? 1 : 0); + + if (beginMoveRows( + QModelIndex(), m_dropPlaceholderIndex, m_dropPlaceholderIndex, + QModelIndex(), modelTo)) { + m_dropPlaceholderIndex = index; + endMoveRows(); + } + } + + emit dropPlaceholderIndexChanged(); +} Index: applets/kicker/plugin/recentusagemodel.cpp =================================================================== --- applets/kicker/plugin/recentusagemodel.cpp +++ applets/kicker/plugin/recentusagemodel.cpp @@ -21,7 +21,7 @@ #include "actionlist.h" #include "appsmodel.h" #include "appentry.h" -#include "favoritesmodel.h" +#include "kastatsfavoritesmodel.h" #include @@ -74,8 +74,8 @@ void InvalidAppsFilterProxy::connectNewFavoritesModel() { - FavoritesModel* favoritesModel = static_cast(m_parentModel->favoritesModel()); - connect(favoritesModel, &FavoritesModel::favoritesChanged, this, &QSortFilterProxyModel::invalidate); + KAStatsFavoritesModel* favoritesModel = static_cast(m_parentModel->favoritesModel()); + connect(favoritesModel, &KAStatsFavoritesModel::favoritesChanged, this, &QSortFilterProxyModel::invalidate); invalidate(); } @@ -89,7 +89,7 @@ if (resource.startsWith(QLatin1String("applications:"))) { KService::Ptr service = KService::serviceByStorageId(resource.section(':', 1)); - FavoritesModel* favoritesModel = m_parentModel ? static_cast(m_parentModel->favoritesModel()) : nullptr; + KAStatsFavoritesModel* favoritesModel = m_parentModel ? static_cast(m_parentModel->favoritesModel()) : nullptr; return (service && (!favoritesModel || !favoritesModel->isFavorite(service->storageId()))); } Index: applets/kicker/plugin/rootmodel.h =================================================================== --- applets/kicker/plugin/rootmodel.h +++ applets/kicker/plugin/rootmodel.h @@ -24,7 +24,7 @@ #include -class FavoritesModel; +class KAStatsFavoritesModel; class RecentContactsModel; class RecentUsageModel; class SystemModel; @@ -117,7 +117,7 @@ private: bool m_complete; - FavoritesModel *m_favorites; + KAStatsFavoritesModel *m_favorites; SystemModel *m_systemModel; bool m_autoPopulate; Index: applets/kicker/plugin/rootmodel.cpp =================================================================== --- applets/kicker/plugin/rootmodel.cpp +++ applets/kicker/plugin/rootmodel.cpp @@ -19,7 +19,7 @@ #include "rootmodel.h" #include "actionlist.h" -#include "favoritesmodel.h" +#include "kastatsfavoritesmodel.h" #include "recentcontactsmodel.h" #include "recentusagemodel.h" #include "systemmodel.h" @@ -64,7 +64,7 @@ RootModel::RootModel(QObject *parent) : AppsModel(QString(), parent) , m_complete(false) -, m_favorites(new FavoritesModel(this)) +, m_favorites(new KAStatsFavoritesModel(this)) , m_systemModel(nullptr) , m_autoPopulate(true) , m_showAllApps(false) @@ -301,7 +301,7 @@ QList groups; if (m_paginate) { - m_favorites = new FavoritesModel(this); + m_favorites = new KAStatsFavoritesModel(this); emit favoritesModelChanged(); QHash appsHash; Index: applets/kicker/plugin/simplefavoritesmodel.h =================================================================== --- applets/kicker/plugin/simplefavoritesmodel.h +++ applets/kicker/plugin/simplefavoritesmodel.h @@ -17,8 +17,8 @@ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA . * ***************************************************************************/ -#ifndef FAVORITESMODEL_H -#define FAVORITESMODEL_H +#ifndef SIMPLEFAVORITESMODEL_H +#define SIMPLEFAVORITESMODEL_H #include "abstractmodel.h" @@ -26,7 +26,7 @@ #include -class FavoritesModel : public AbstractModel +class SimpleFavoritesModel : public AbstractModel { Q_OBJECT @@ -36,8 +36,8 @@ Q_PROPERTY(int dropPlaceholderIndex READ dropPlaceholderIndex WRITE setDropPlaceholderIndex NOTIFY dropPlaceholderIndexChanged) public: - explicit FavoritesModel(QObject *parent = 0); - ~FavoritesModel(); + explicit SimpleFavoritesModel(QObject *parent = 0); + ~SimpleFavoritesModel(); QString description() const Q_DECL_OVERRIDE; Index: applets/kicker/plugin/simplefavoritesmodel.cpp =================================================================== --- applets/kicker/plugin/simplefavoritesmodel.cpp +++ applets/kicker/plugin/simplefavoritesmodel.cpp @@ -17,7 +17,7 @@ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA . * ***************************************************************************/ -#include "favoritesmodel.h" +#include "simplefavoritesmodel.h" #include "appentry.h" #include "contactentry.h" #include "fileentry.h" @@ -26,24 +26,24 @@ #include -FavoritesModel::FavoritesModel(QObject *parent) : AbstractModel(parent) +SimpleFavoritesModel::SimpleFavoritesModel(QObject *parent) : AbstractModel(parent) , m_enabled(true) , m_maxFavorites(-1) , m_dropPlaceholderIndex(-1) { } -FavoritesModel::~FavoritesModel() +SimpleFavoritesModel::~SimpleFavoritesModel() { qDeleteAll(m_entryList); } -QString FavoritesModel::description() const +QString SimpleFavoritesModel::description() const { return i18n("Favorites"); } -QVariant FavoritesModel::data(const QModelIndex& index, int role) const +QVariant SimpleFavoritesModel::data(const QModelIndex& index, int role) const { if (!index.isValid() || index.row() >= rowCount()) { return QVariant(); @@ -84,12 +84,12 @@ return QVariant(); } -int FavoritesModel::rowCount(const QModelIndex& parent) const +int SimpleFavoritesModel::rowCount(const QModelIndex& parent) const { return parent.isValid() ? 0 : m_entryList.count() + (m_dropPlaceholderIndex != -1 ? 1 : 0); } -bool FavoritesModel::trigger(int row, const QString &actionId, const QVariant &argument) +bool SimpleFavoritesModel::trigger(int row, const QString &actionId, const QVariant &argument) { if (row < 0 || row >= m_entryList.count()) { return false; @@ -98,12 +98,12 @@ return m_entryList.at(row)->run(actionId, argument); } -bool FavoritesModel::enabled() const +bool SimpleFavoritesModel::enabled() const { return m_enabled; } -void FavoritesModel::setEnabled(bool enable) +void SimpleFavoritesModel::setEnabled(bool enable) { if (m_enabled != enable) { m_enabled = enable; @@ -112,12 +112,12 @@ } } -QStringList FavoritesModel::favorites() const +QStringList SimpleFavoritesModel::favorites() const { return m_favorites; } -void FavoritesModel::setFavorites(const QStringList& favorites) +void SimpleFavoritesModel::setFavorites(const QStringList& favorites) { QStringList _favorites(favorites); _favorites.removeDuplicates(); @@ -128,12 +128,12 @@ } } -int FavoritesModel::maxFavorites() const +int SimpleFavoritesModel::maxFavorites() const { return m_maxFavorites; } -void FavoritesModel::setMaxFavorites(int max) +void SimpleFavoritesModel::setMaxFavorites(int max) { if (m_maxFavorites != max) { @@ -147,12 +147,12 @@ } } -bool FavoritesModel::isFavorite(const QString &id) const +bool SimpleFavoritesModel::isFavorite(const QString &id) const { return m_favorites.contains(id); } -void FavoritesModel::addFavorite(const QString &id, int index) +void SimpleFavoritesModel::addFavorite(const QString &id, int index) { if (!m_enabled || id.isEmpty()) { return; @@ -184,7 +184,7 @@ emit favoritesChanged(); } -void FavoritesModel::removeFavorite(const QString &id) +void SimpleFavoritesModel::removeFavorite(const QString &id) { if (!m_enabled || id.isEmpty()) { return; @@ -208,7 +208,7 @@ } } -void FavoritesModel::moveRow(int from, int to) +void SimpleFavoritesModel::moveRow(int from, int to) { if (from >= m_favorites.count() || to >= m_favorites.count()) { return; @@ -234,12 +234,12 @@ } } -int FavoritesModel::dropPlaceholderIndex() const +int SimpleFavoritesModel::dropPlaceholderIndex() const { return m_dropPlaceholderIndex; } -void FavoritesModel::setDropPlaceholderIndex(int index) +void SimpleFavoritesModel::setDropPlaceholderIndex(int index) { if (index == -1 && m_dropPlaceholderIndex != -1) { beginRemoveRows(QModelIndex(), m_dropPlaceholderIndex, m_dropPlaceholderIndex); @@ -270,12 +270,12 @@ } } -AbstractModel *FavoritesModel::favoritesModel() +AbstractModel *SimpleFavoritesModel::favoritesModel() { return this; } -void FavoritesModel::refresh() +void SimpleFavoritesModel::refresh() { beginResetModel(); @@ -314,7 +314,7 @@ emit favoritesChanged(); } -AbstractEntry *FavoritesModel::favoriteFromId(const QString &id) +AbstractEntry *SimpleFavoritesModel::favoriteFromId(const QString &id) { const QUrl url(id); const QString &s = url.scheme(); Index: applets/kicker/plugin/systemmodel.cpp =================================================================== --- applets/kicker/plugin/systemmodel.cpp +++ applets/kicker/plugin/systemmodel.cpp @@ -19,7 +19,7 @@ #include "systemmodel.h" #include "actionlist.h" -#include "favoritesmodel.h" +#include "simplefavoritesmodel.h" #include "systementry.h" #include @@ -31,7 +31,7 @@ { init(); - m_favoritesModel = new FavoritesModel(this); + m_favoritesModel = new SimpleFavoritesModel(this); const QString configFile = QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation) + "/ksmserverrc"; Index: applets/kickoff/package/contents/code/tools.js =================================================================== --- applets/kickoff/package/contents/code/tools.js +++ applets/kickoff/package/contents/code/tools.js @@ -1,6 +1,7 @@ /*************************************************************************** * Copyright (C) 2013 by Aurélien Gâteau * * Copyright (C) 2013-2015 by Eike Hein * + * Copyright (C) 2017 by Ivan Cukic * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * @@ -22,21 +23,22 @@ // Accessing actionList can be a costly operation, so we don't // access it until we need the menu. - var action = createFavoriteAction(favoriteModel, favoriteId); + var actions = createFavoriteActions(favoriteModel, favoriteId); - if (action) { + if (actions) { if (actionList && actionList.length > 0) { var separator = { "type": "separator" }; - actionList.push(separator, action); + actionList.unshift(separator); + actionList.unshift.apply(actionList, actions); } else { - actionList = [action]; + actionList = actions; } } actionMenu.actionList = actionList; } -function createFavoriteAction(favoriteModel, favoriteId) { +function createFavoriteActions(favoriteModel, favoriteId) { // Don't allow changes to favorites when system is immutable. if (plasmoid.immutability === PlasmaCore.Types.SystemImmutable) { return null; @@ -46,23 +48,107 @@ return null; } - var action = {}; + var activities = favoriteModel.activities.runningActivities; + + if (activities.length <= 1) { + var action = {}; + + if (favoriteModel.isFavorite(favoriteId)) { + action.text = i18n("Remove from Favorites"); + action.icon = "list-remove"; + action.actionId = "_kicker_favorite_remove"; + } else if (favoriteModel.maxFavorites == -1 || favoriteModel.count < favoriteModel.maxFavorites) { + action.text = i18n("Add to Favorites"); + action.icon = "bookmark-new"; + action.actionId = "_kicker_favorite_add"; + } else { + return null; + } + + action.actionArgument = { favoriteModel: favoriteModel, favoriteId: favoriteId }; + + return [action]; - if (favoriteModel.isFavorite(favoriteId)) { - action.text = i18n("Remove from Favorites"); - action.icon = "list-remove"; - action.actionId = "_kicker_favorite_remove"; - } else if (favoriteModel.maxFavorites == -1 || favoriteModel.count < favoriteModel.maxFavorites) { - action.text = i18n("Add to Favorites"); - action.icon = "bookmark-new"; - action.actionId = "_kicker_favorite_add"; } else { - return null; - } + var actions = []; + + var linkedActivities = favoriteModel.linkedActivitiesFor(favoriteId); + + // Adding the item to link/unlink to all activities + + var linkedToAllActivities = + !(linkedActivities.indexOf(":global") === -1); + + actions.push({ + text : i18n("On All Activities"), + checkable : true, + + actionId : linkedToAllActivities ? + "_kicker_favorite_remove_from_activity" : + "_kicker_favorite_set_to_activity", + checked : linkedToAllActivities, + + actionArgument : { + favoriteModel: favoriteModel, + favoriteId: favoriteId, + favoriteActivity: "" + } + }); + + + // Adding items for each activity separately + + var addActivityItem = function(activityId, activityName) { + var linkedToThisActivity = + !(linkedActivities.indexOf(activityId) === -1); - action.actionArgument = { favoriteModel: favoriteModel, favoriteId: favoriteId }; + actions.push({ + text : activityName, + checkable : true, + checked : linkedToThisActivity && !linkedToAllActivities, - return action; + actionId : + // If we are on all activities, and the user clicks just one + // specific activity, unlink from everything else + linkedToAllActivities ? "_kicker_favorite_set_to_activity" : + + // If we are linked to the current activity, just unlink from + // that single one + linkedToThisActivity ? "_kicker_favorite_remove_from_activity" : + + // Otherwise, link to this activity, but do not unlink from + // other ones + "_kicker_favorite_add_to_activity", + + actionArgument : { + favoriteModel : favoriteModel, + favoriteId : favoriteId, + favoriteActivity : activityId + } + }); + }; + + // Adding the item to link/unlink to the current activity + + addActivityItem(favoriteModel.activities.currentActivity, i18n("On The Current Activity")); + + actions.push({ + type: "separator", + actionId: "_kicker_favorite_separator" + }); + + // Adding the items for each activity + + activities.forEach(function(activityId) { + addActivityItem(activityId, favoriteModel.activityNameForId(activityId)); + }); + + return [{ + text : i18n("Show In Favorites"), + icon : "favorite", + subActions : actions + }]; + } } function triggerAction(model, index, actionId, actionArgument) { @@ -89,9 +175,26 @@ if (favoriteModel === null || favoriteId == null) { return null; } + if (actionId == "_kicker_favorite_remove") { - favoriteModel.removeFavorite(favoriteId); + console.log("Removing from all activities"); + favoriteModel.removeFavoriteFrom(favoriteId, ":any"); + } else if (actionId == "_kicker_favorite_add") { - favoriteModel.addFavorite(favoriteId); + console.log("Adding to global activity"); + favoriteModel.addFavoriteTo(favoriteId, ":global"); + + } else if (actionId == "_kicker_favorite_remove_from_activity") { + console.log("Removing from a specific activity"); + favoriteModel.removeFavoriteFrom(favoriteId, actionArgument.favoriteActivity); + + } else if (actionId == "_kicker_favorite_add_to_activity") { + console.log("Adding to another activity"); + favoriteModel.addFavoriteTo(favoriteId, actionArgument.favoriteActivity); + + } else if (actionId == "_kicker_favorite_set_to_activity") { + console.log("Removing the item from the favourites, and re-adding it just to be on a specific activity"); + favoriteModel.setFavoriteOn(favoriteId, actionArgument.favoriteActivity); + } } Index: applets/kickoff/package/contents/config/main.xml =================================================================== --- applets/kickoff/package/contents/config/main.xml +++ applets/kickoff/package/contents/config/main.xml @@ -22,6 +22,10 @@ preferred://browser,kontact.desktop,systemsettings.desktop,org.kde.dolphin.desktop,ktp-contactlist.desktop,org.kde.kate.desktop,org.kde.discover.desktop + + + false + systemsettings.desktop,org.kde.kinfocenter.desktop,org.kde.discover.desktop Index: applets/kickoff/package/contents/ui/ActionMenu.qml =================================================================== --- applets/kickoff/package/contents/ui/ActionMenu.qml +++ applets/kickoff/package/contents/ui/ActionMenu.qml @@ -64,11 +64,34 @@ menu = contextMenuComponent.createObject(root); - actionList.forEach(function(actionItem) { - var item = contextMenuItemComponent.createObject(menu, { - "actionItem": actionItem, - }); + // actionList.forEach(function(actionItem) { + // var item = contextMenuItemComponent.createObject(menu, { + // "actionItem": actionItem, + // }); + // }); + + fillMenu(menu, actionList); + } + + function fillMenu(menu, items) { + items.forEach(function(actionItem) { + if (actionItem.subActions) { + // This is a menu + var submenuItem = contextSubmenuItemComponent.createObject( + menu, { "actionItem" : actionItem }); + + fillMenu(submenuItem.submenu, actionItem.subActions); + + } else { + var item = contextMenuItemComponent.createObject( + menu, + { + "actionItem": actionItem, + } + ); + } }); + } Component { @@ -80,17 +103,39 @@ } Component { - id: contextMenuItemComponent + id: contextSubmenuItemComponent PlasmaComponents.MenuItem { + id: submenuItem + property variant actionItem text: actionItem.text ? actionItem.text : "" - enabled: actionItem.type != "title" && ("enabled" in actionItem ? actionItem.enabled : true) - separator: actionItem.type == "separator" - section: actionItem.type == "title" icon: actionItem.icon ? actionItem.icon : null + property variant submenu : submenu_ + + PlasmaComponents.ContextMenu { + id: submenu_ + visualParent: submenuItem.action + } + } + } + + Component { + id: contextMenuItemComponent + + PlasmaComponents.MenuItem { + property variant actionItem + + text : actionItem.text ? actionItem.text : "" + enabled : actionItem.type != "title" && ("enabled" in actionItem ? actionItem.enabled : true) + separator : actionItem.type == "separator" + section : actionItem.type == "title" + icon : actionItem.icon ? actionItem.icon : null + checkable : actionItem.checkable ? actionItem.checkable : false + checked : actionItem.checked ? actionItem.checked : false + onClicked: { actionClicked(actionItem.actionId, actionItem.actionArgument); } Index: applets/kickoff/package/contents/ui/FullRepresentation.qml =================================================================== --- applets/kickoff/package/contents/ui/FullRepresentation.qml +++ applets/kickoff/package/contents/ui/FullRepresentation.qml @@ -65,7 +65,7 @@ sorted: plasmoid.configuration.alphaSort showSeparators: false - favoritesModel: Kicker.FavoritesModel { + favoritesModel: Kicker.KAStatsFavoritesModel { id: rootModelFavorites favorites: plasmoid.configuration.favorites @@ -73,6 +73,17 @@ plasmoid.configuration.favorites = favorites; } } + + Component.onCompleted: { + favoritesModel.initForClient("org.kde.plasma.kickoff.favorites.instance-" + plasmoid.id) + + if (!plasmoid.configuration.favoritesPortedToKAstats) { + favoritesModel.portOldFavorites(plasmoid.configuration.favorites); + plasmoid.configuration.favoritesPortedToKAstats = true; + } + + rootModel.refresh(); + } } Kicker.RunnerModel {