diff --git a/CMakeLists.txt b/CMakeLists.txt --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -29,7 +29,7 @@ Concurrent ) -set(KF5_MIN_VERSION "5.12.0") +set(KF5_MIN_VERSION "5.19.0") find_package(KF5 ${KF5_MIN_VERSION} REQUIRED COMPONENTS Auth Plasma @@ -64,7 +64,7 @@ PURPOSE "Provides package management integration to the application launcher." ) -find_package(KF5Activities ${KF5_VERSION}) +find_package(KF5Activities ${KF5_MIN_VERSION}) set_package_properties(KF5Activities PROPERTIES DESCRIPTION "management of Plasma activities" TYPE OPTIONAL PURPOSE "Needed by activity related plasmoids and the pager." diff --git a/desktoppackage/contents/activitymanager/ActivityItem.qml b/desktoppackage/contents/activitymanager/ActivityItem.qml --- a/desktoppackage/contents/activitymanager/ActivityItem.qml +++ b/desktoppackage/contents/activitymanager/ActivityItem.qml @@ -230,9 +230,12 @@ elide : Text.ElideRight opacity : .6 + // text: root.current ? + // i18nd("plasma_shell_org.kde.plasma.desktop", "Currently being used") : + // ActivitySwitcher.Backend.lastTimeUsedString(root.activityId) text: root.current ? i18nd("plasma_shell_org.kde.plasma.desktop", "Currently being used") : - ActivitySwitcher.Backend.lastTimeUsedString(root.activityId) + model.lastTimeUsedString anchors { top : parent.top @@ -333,7 +336,7 @@ iconSource: "process-stop" tooltip: i18nd("plasma_shell_org.kde.plasma.desktop", "Stop") - onClicked: activitiesModel.stopActivity(activityId, function () {}); + onClicked: ActivitySwitcher.Backend.stopActivity(activityId); anchors { right : parent.right diff --git a/desktoppackage/contents/activitymanager/ActivityList.qml b/desktoppackage/contents/activitymanager/ActivityList.qml --- a/desktoppackage/contents/activitymanager/ActivityList.qml +++ b/desktoppackage/contents/activitymanager/ActivityList.qml @@ -22,19 +22,21 @@ import org.kde.plasma.extras 2.0 as PlasmaExtras import org.kde.activities 0.1 as Activities +import org.kde.plasma.activityswitcher 1.0 as ActivitySwitcher + Flickable { id: root // contentWidth: content.width contentHeight: content.height - property var model: activitiesModel + property var model: ActivitySwitcher.Backend.runningActivitiesModel() property string filterString: "" property bool showSwitcherOnly: false property int itemsWidth: 0 - property int selectedIndex: -1 + property int selectedIndex: -1 function _selectRelativeToCurrent(distance) { @@ -53,7 +55,8 @@ // Searching for the first item that is visible, or back to the one // that we started with - } while (!activitiesList.itemAt(selectedIndex).visible && startingWithSelected != selectedIndex); + } while (!activitiesList.itemAt(selectedIndex).visible + && startingWithSelected != selectedIndex); _updateSelectedItem(); @@ -98,24 +101,10 @@ } if (selectedItem != null) { - activitiesModel.setCurrentActivity( - selectedItem.activityId, function () {} - ); + ActivitySwitcher.Backend.setCurrentActivity(selectedItem.activityId); } } - Activities.ActivityModel { - id: activitiesModel - - shownStates: "Running,Stopping" - } - - Activities.ActivityModel { - id: stoppedActivitiesModel - - shownStates: "Stopped,Starting" - } - Column { id: content @@ -128,7 +117,7 @@ Repeater { id: activitiesList - model: activitiesModel + model: ActivitySwitcher.Backend.runningActivitiesModel() ActivityItem { @@ -141,12 +130,12 @@ title : model.name icon : model.iconSource background : model.background - current : model.current + current : model.isCurrent innerPadding : 2 * units.smallSpacing stoppable : activitiesList.count > 1 - onClicked : { - activitiesModel.setCurrentActivity(model.id, function () {}) + onClicked : { + ActivitySwitcher.Backend.setCurrentActivity(model.id); } } } @@ -170,7 +159,7 @@ Repeater { id: stoppedActivitiesList - model: root.showSwitcherOnly ? null : stoppedActivitiesModel + model: root.showSwitcherOnly ? null : ActivitySwitcher.Backend.stoppedActivitiesModel() delegate: StoppedActivityItem { id: stoppedActivityItem @@ -186,7 +175,7 @@ innerPadding : 2 * units.smallSpacing onClicked: { - stoppedActivitiesModel.setCurrentActivity(model.id, function () {}) + ActivitySwitcher.Backend.setCurrentActivity(model.id) } } } diff --git a/imports/activitymanager/CMakeLists.txt b/imports/activitymanager/CMakeLists.txt --- a/imports/activitymanager/CMakeLists.txt +++ b/imports/activitymanager/CMakeLists.txt @@ -21,6 +21,9 @@ activityswitcher_imports_LIB_SRCS activityswitcherextensionplugin.cpp switcherbackend.cpp + sortedactivitiesmodel.cpp + + backport/switcheractivitiesmodel.cpp ) add_library (activityswitcherextensionplugin SHARED ${activityswitcher_imports_LIB_SRCS}) diff --git a/imports/activitymanager/activityswitcherextensionplugin.cpp b/imports/activitymanager/activityswitcherextensionplugin.cpp --- a/imports/activitymanager/activityswitcherextensionplugin.cpp +++ b/imports/activitymanager/activityswitcherextensionplugin.cpp @@ -20,7 +20,6 @@ #include "activityswitcherextensionplugin.h" #include -#include #include "switcherbackend.h" diff --git a/imports/activitymanager/backport/model_updaters.h b/imports/activitymanager/backport/model_updaters.h new file mode 100644 --- /dev/null +++ b/imports/activitymanager/backport/model_updaters.h @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2012 Ivan Cukic + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * or (at your option) any later version, as published by the Free + * Software Foundation + * + * 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 KACTIVITIES_IMPORTS_UTILS_P_H +#define KACTIVITIES_IMPORTS_UTILS_P_H + +// ----------------------------------------- +// RAII classes for model updates ---------- +// ----------------------------------------- + +#define DECLARE_RAII_MODEL_UPDATERS(Class) \ + template class _model_reset { \ + T *model; \ + \ + public: \ + _model_reset(T *m) : model(m) \ + { \ + model->beginResetModel(); \ + } \ + ~_model_reset() \ + { \ + model->endResetModel(); \ + } \ + }; \ + template class _model_insert { \ + T *model; \ + \ + public: \ + _model_insert(T *m, const QModelIndex &parent, int first, int last) \ + : model(m) \ + { \ + model->beginInsertRows(parent, first, last); \ + } \ + ~_model_insert() \ + { \ + model->endInsertRows(); \ + } \ + }; \ + template class _model_remove { \ + T *model; \ + \ + public: \ + _model_remove(T *m, const QModelIndex &parent, int first, int last) \ + : model(m) \ + { \ + model->beginRemoveRows(parent, first, last); \ + } \ + ~_model_remove() \ + { \ + model->endRemoveRows(); \ + } \ + }; \ + typedef _model_reset model_reset; \ + typedef _model_remove model_remove; \ + typedef _model_insert model_insert; + +// ----------------------------------------- + +#endif // KACTIVITIES_IMPORTS_UTILS_P_H + diff --git a/imports/activitymanager/backport/qflatset.h b/imports/activitymanager/backport/qflatset.h new file mode 100644 --- /dev/null +++ b/imports/activitymanager/backport/qflatset.h @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2016 by Ivan Čukić + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) version 3, or any + * later version accepted by the membership of KDE e.V. (or its + * successor approved by the membership of KDE e.V.), which shall + * act as a proxy defined in Section 6 of version 3 of the license. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. + * If not, see . + */ + +#ifndef KACTIVITIES_STATS_QFLATSET_H +#define KACTIVITIES_STATS_QFLATSET_H + +#include +#include + +namespace KActivities { + +template +class QFlatSet: public QVector { +public: + inline + QPair::iterator, bool> insert(const T &value) + { + auto comparator = Comparator(); + auto begin = this->begin(); + auto end = this->end(); + + // We want small sets, so a binary search + // will be slower than a serial search + auto iterator = std::find_if(begin, end, + [&] (const T ¤t) { + return comparator(value, current); + }); + + if (iterator != end) { + if (comparator(*iterator, value)) { + // Already present + return { iterator, false }; + } + } + + QVector::insert(iterator, value); + + return { iterator, true }; + } +}; + + +} // namespace KActivities + +#endif // KACTIVITIES_STATS_QFLATSET_H + diff --git a/imports/activitymanager/backport/switcheractivitiesmodel.h b/imports/activitymanager/backport/switcheractivitiesmodel.h new file mode 100644 --- /dev/null +++ b/imports/activitymanager/backport/switcheractivitiesmodel.h @@ -0,0 +1,105 @@ +/* + * Copyright (C) 2012, 2013, 2014 Ivan Cukic + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * or (at your option) any later version, as published by the Free + * Software Foundation + * + * 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 ACTIVITIES_ACTIVITIESMODEL_H +#define ACTIVITIES_ACTIVITIESMODEL_H + +// Qt +#include +#include + +// STL +#include + +#include + +class QModelIndex; +class QDBusPendingCallWatcher; + +namespace KActivitiesBackport { + +using namespace KActivities; + +class ActivitiesModelPrivate; + +/** + * Data model that shows existing activities + */ +class KACTIVITIES_EXPORT ActivitiesModel : public QAbstractListModel { + Q_OBJECT + + Q_PROPERTY(QVector shownStates READ shownStates WRITE setShownStates NOTIFY shownStatesChanged) + +public: + ActivitiesModel(QObject *parent = 0); + + /** + * Constructs the model and sets the shownStates + */ + ActivitiesModel(QVector shownStates, QObject *parent = 0); + virtual ~ActivitiesModel(); + + int rowCount(const QModelIndex &parent = QModelIndex()) const + Q_DECL_OVERRIDE; + + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const + Q_DECL_OVERRIDE; + + QVariant headerData(int section, Qt::Orientation orientation, + int role = Qt::DisplayRole) const Q_DECL_OVERRIDE; + + QHash roleNames() const Q_DECL_OVERRIDE; + + enum Roles { + ActivityId = Qt::UserRole, ///< UUID of the activity + ActivityName = Qt::UserRole + 1, ///< Activity name + ActivityDescription = Qt::UserRole + 2, ///< Activity description + ActivityIconSource = Qt::UserRole + 3, ///< Activity icon source name + ActivityState = Qt::UserRole + 4, ///< The current state of the activity @see Info::State + ActivityBackground = Qt::UserRole + 5, ///< Activity wallpaper (currently unsupported) + ActivityIsCurrent = Qt::UserRole + 6, ///< Is this activity the current one current + + UserRole = Qt::UserRole + 32 ///< To be used by models that inherit this one + }; + +public Q_SLOTS: + /** + * The model can filter the list of activities based on their state. + * This method sets which states should be shown. + */ + void setShownStates(const QVector &shownStates); + + /** + * The model can filter the list of activities based on their state. + * This method returns which states are currently shown. + */ + QVector shownStates() const; + +Q_SIGNALS: + void shownStatesChanged(const QVector &state); + +private: + friend class ActivitiesModelPrivate; + ActivitiesModelPrivate * const d; +}; + +} // namespace KActivitiesBackport + +#endif // ACTIVITIES_ACTIVITIESMODEL_H + diff --git a/imports/activitymanager/backport/switcheractivitiesmodel.cpp b/imports/activitymanager/backport/switcheractivitiesmodel.cpp new file mode 100644 --- /dev/null +++ b/imports/activitymanager/backport/switcheractivitiesmodel.cpp @@ -0,0 +1,430 @@ +/* + * Copyright (C) 2012 - 2016 Ivan Cukic + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * or (at your option) any later version, as published by the Free + * Software Foundation + * + * 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. + */ + +// Self +#include "switcheractivitiesmodel.h" +#include "switcheractivitiesmodel_p.h" + +// Qt +#include +#include +#include +#include + +namespace KActivitiesBackport { + +namespace Private { + + template + struct ActivityPosition { + ActivityPosition() + : isValid(false) + , index(0) + , iterator() + { + } + + ActivityPosition(unsigned int index, + typename _Container::const_iterator iterator) + : isValid(true) + , index(index) + , iterator(iterator) + { + } + + operator bool() const + { + return isValid; + } + + const bool isValid; + const unsigned int index; + const typename _Container::const_iterator iterator; + + typedef typename _Container::value_type ContainerElement; + }; + + /** + * Returns whether the the activity has a desired state. + * If the state is 0, returns true + */ + template + inline bool matchingState(ActivitiesModelPrivate::InfoPtr activity, + const T &states) + { + return states.empty() || states.contains(activity->state()); + } + + /** + * Searches for the activity. + * Returns an option(index, iterator) for the found activity. + */ + template + inline + ActivityPosition<_Container> + activityPosition(const _Container &container, const QString &activityId) + { + auto position = std::find_if(container.begin(), container.end(), + [&] (const typename ActivityPosition<_Container>::ContainerElement &activity) { + return activity->id() == activityId; + } + ); + + return (position != container.end()) ? + ActivityPosition<_Container>(position - container.begin(), position) : + ActivityPosition<_Container>(); + } + + /** + * Notifies the model that an activity was updated + */ + template + inline + void emitActivityUpdated(_Model *model, + const _Container &container, + const QString &activity, int role) + { + auto position = Private::activityPosition(container, activity); + + if (position) { + emit model->q->dataChanged( + model->q->index(position.index), + model->q->index(position.index), + role == Qt::DecorationRole ? + QVector {role, ActivitiesModel::ActivityIconSource} : + QVector {role} + ); + } + } + + /** + * Notifies the model that an activity was updated + */ + template + inline + void emitActivityUpdated(_Model *model, + const _Container &container, + QObject *activityInfo, int role) + { + const auto activity = static_cast (activityInfo); + emitActivityUpdated(model, container, activity->id(), role); + } + +} + +ActivitiesModelPrivate::ActivitiesModelPrivate(ActivitiesModel *parent) + : q(parent) +{ +} + +ActivitiesModel::ActivitiesModel(QObject *parent) + : QAbstractListModel(parent) + , d(new ActivitiesModelPrivate(this)) +{ + // Initializing role names for qml + connect(&d->activities, &Consumer::serviceStatusChanged, + this, [this] (Consumer::ServiceStatus status) { d->setServiceStatus(status); }); + + connect(&d->activities, &Consumer::activityAdded, + this, [this] (const QString &activity) { d->onActivityAdded(activity); }); + connect(&d->activities, &Consumer::activityRemoved, + this, [this] (const QString &activity) { d->onActivityRemoved(activity); }); + connect(&d->activities, &Consumer::currentActivityChanged, + this, [this] (const QString &activity) { d->onCurrentActivityChanged(activity); }); + + d->setServiceStatus(d->activities.serviceStatus()); +} + +ActivitiesModel::ActivitiesModel(QVector shownStates, QObject *parent) + : QAbstractListModel(parent) + , d(new ActivitiesModelPrivate(this)) +{ + d->shownStates = shownStates; + + // Initializing role names for qml + connect(&d->activities, &Consumer::serviceStatusChanged, + this, [this] (Consumer::ServiceStatus status) { d->setServiceStatus(status); }); + + connect(&d->activities, &Consumer::activityAdded, + this, [this] (const QString &activity) { d->onActivityAdded(activity); }); + connect(&d->activities, &Consumer::activityRemoved, + this, [this] (const QString &activity) { d->onActivityRemoved(activity); }); + connect(&d->activities, &Consumer::currentActivityChanged, + this, [this] (const QString &activity) { d->onCurrentActivityChanged(activity); }); + + d->setServiceStatus(d->activities.serviceStatus()); +} + +ActivitiesModel::~ActivitiesModel() +{ + delete d; +} + +QHash ActivitiesModel::roleNames() const +{ + return { + {ActivityName, "name"}, + {ActivityState, "state"}, + {ActivityId, "id"}, + {ActivityIconSource, "iconSource"}, + {ActivityDescription, "description"}, + {ActivityBackground, "background"}, + {ActivityIsCurrent, "isCurrent"} + }; +} + + +void ActivitiesModelPrivate::setServiceStatus(Consumer::ServiceStatus) +{ + replaceActivities(activities.activities()); +} + +void ActivitiesModelPrivate::replaceActivities(const QStringList &activities) +{ + model_reset m(q); + + knownActivities.clear(); + shownActivities.clear(); + + for (const QString &activity: activities) { + onActivityAdded(activity, false); + } +} + +void ActivitiesModelPrivate::onActivityAdded(const QString &id, bool notifyClients) +{ + auto info = registerActivity(id); + + showActivity(info, notifyClients); +} + +void ActivitiesModelPrivate::onActivityRemoved(const QString &id) +{ + hideActivity(id); + unregisterActivity(id); +} + +void ActivitiesModelPrivate::onCurrentActivityChanged(const QString &id) +{ + Q_UNUSED(id); + + for (const auto &activity: shownActivities) { + Private::emitActivityUpdated(this, shownActivities, activity->id(), + ActivitiesModel::ActivityIsCurrent); + } +} + +ActivitiesModelPrivate::InfoPtr ActivitiesModelPrivate::registerActivity(const QString &id) +{ + auto position = Private::activityPosition(knownActivities, id); + + if (position) { + return *(position.iterator); + + } else { + auto activityInfo = std::make_shared(id); + + auto ptr = activityInfo.get(); + + connect(ptr, &Info::nameChanged, + this, &ActivitiesModelPrivate::onActivityNameChanged); + connect(ptr, &Info::descriptionChanged, + this, &ActivitiesModelPrivate::onActivityDescriptionChanged); + connect(ptr, &Info::iconChanged, + this, &ActivitiesModelPrivate::onActivityIconChanged); + connect(ptr, &Info::stateChanged, + this, &ActivitiesModelPrivate::onActivityStateChanged); + + knownActivities.insert(InfoPtr(activityInfo)); + + return activityInfo; + } +} + +void ActivitiesModelPrivate::unregisterActivity(const QString &id) +{ + auto position = Private::activityPosition(knownActivities, id); + + if (position) { + if (auto shown = Private::activityPosition(shownActivities, id)) { + model_remove m(q, QModelIndex(), shown.index, shown.index); + shownActivities.removeAt(shown.index); + } + + knownActivities.removeAt(position.index); + } +} + +void ActivitiesModelPrivate::showActivity(InfoPtr activityInfo, bool notifyClients) +{ + // Should it really be shown? + if (!Private::matchingState(activityInfo, shownStates)) return; + + // Is it already shown? + if (std::binary_search(shownActivities.cbegin(), shownActivities.cend(), + activityInfo, InfoPtrComparator())) return; + + auto registeredPosition + = Private::activityPosition(knownActivities, activityInfo->id()); + + if (!registeredPosition) { + qDebug() << "Got a request to show an unknown activity, ignoring"; + return; + } + + auto activityInfoPtr = *(registeredPosition.iterator); + + auto position = shownActivities.insert(activityInfoPtr); + + if (notifyClients) { + unsigned int index = + (position.second ? position.first : shownActivities.end()) + - shownActivities.begin(); + + model_insert m(q, QModelIndex(), index, index); + } +} + +void ActivitiesModelPrivate::hideActivity(const QString &id) +{ + auto position = Private::activityPosition(shownActivities, id); + + if (position) { + model_remove m(q, QModelIndex(), position.index, position.index); + shownActivities.removeAt(position.index); + } +} + +#define CREATE_SIGNAL_EMITTER(What, Role) \ + void ActivitiesModelPrivate::onActivity##What##Changed(const QString &) \ + { \ + Private::emitActivityUpdated(this, shownActivities, sender(), Role); \ + } + +CREATE_SIGNAL_EMITTER(Name, Qt::DisplayRole) +CREATE_SIGNAL_EMITTER(Description, ActivitiesModel::ActivityDescription) +CREATE_SIGNAL_EMITTER(Icon, Qt::DecorationRole) + +#undef CREATE_SIGNAL_EMITTER + +void ActivitiesModelPrivate::onActivityStateChanged(Info::State state) +{ + if (shownStates.empty()) { + Private::emitActivityUpdated(this, shownActivities, sender(), + ActivitiesModel::ActivityState); + + } else { + auto info = findActivity(sender()); + + if (!info) { + return; + } + + if (shownStates.contains(state)) { + showActivity(info, true); + } else { + hideActivity(info->id()); + } + } +} + +void ActivitiesModel::setShownStates(const QVector &states) +{ + d->shownStates = states; + + d->replaceActivities(d->activities.activities()); + + emit shownStatesChanged(states); +} + +QVector ActivitiesModel::shownStates() const +{ + return d->shownStates; +} + +int ActivitiesModel::rowCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent); + + return d->shownActivities.size(); +} + +QVariant ActivitiesModel::data(const QModelIndex &index, int role) const +{ + const int row = index.row(); + const auto &item = *(d->shownActivities.cbegin() + row); + + switch (role) { + case Qt::DisplayRole: + case ActivityName: + return item->name(); + + case ActivityId: + return item->id(); + + case ActivityState: + return item->state(); + + case Qt::DecorationRole: + case ActivityIconSource: + { + const QString &icon = item->icon(); + + // We need a default icon for activities + return icon.isEmpty() ? "preferences-activities" : icon; + } + + case ActivityDescription: + return item->description(); + + case ActivityIsCurrent: + return d->activities.currentActivity() == item->id(); + + default: + return QVariant(); + } +} + +QVariant ActivitiesModel::headerData(int section, Qt::Orientation orientation, + int role) const +{ + Q_UNUSED(section); + Q_UNUSED(orientation); + Q_UNUSED(role); + + return QVariant(); +} + +ActivitiesModelPrivate::InfoPtr ActivitiesModelPrivate::findActivity(QObject *ptr) const +{ + auto info = std::find_if(knownActivities.cbegin(), knownActivities.cend(), + [ptr] (const InfoPtr &info) { + return ptr == info.get(); + } + ); + + if (info == knownActivities.end()) { + return nullptr; + } else { + return *info; + } +} + +} // namespace KActivitiesBackport + diff --git a/imports/activitymanager/backport/switcheractivitiesmodel_p.h b/imports/activitymanager/backport/switcheractivitiesmodel_p.h new file mode 100644 --- /dev/null +++ b/imports/activitymanager/backport/switcheractivitiesmodel_p.h @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2012, 2013, 2014 Ivan Cukic + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * or (at your option) any later version, as published by the Free + * Software Foundation + * + * 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 ACTIVITIES_ACTIVITYMODEL_P_H +#define ACTIVITIES_ACTIVITYMODEL_P_H + +#include +#include + +#include "model_updaters.h" +#include "switcheractivitiesmodel.h" +#include "qflatset.h" + +namespace KActivitiesBackport { + +class ActivitiesModelPrivate : public QObject { + Q_OBJECT +public: + ActivitiesModelPrivate(ActivitiesModel *parent); + +public Q_SLOTS: + void onActivityNameChanged(const QString &name); + void onActivityDescriptionChanged(const QString &description); + void onActivityIconChanged(const QString &icon); + void onActivityStateChanged(KActivities::Info::State state); + + void replaceActivities(const QStringList &activities); + void onActivityAdded(const QString &id, bool notifyClients = true); + void onActivityRemoved(const QString &id); + void onCurrentActivityChanged(const QString &id); + + void setServiceStatus(KActivities::Consumer::ServiceStatus status); + +public: + KActivities::Consumer activities; + QVector shownStates; + + typedef std::shared_ptr InfoPtr; + + struct InfoPtrComparator { + bool operator() (const InfoPtr& left, const InfoPtr& right) const + { + const QString &leftName = left->name().toLower(); + const QString &rightName = right->name().toLower(); + + return + (leftName < rightName) || + (leftName == rightName && left->id() < right->id()); + } + }; + + QFlatSet knownActivities; + QFlatSet shownActivities; + + InfoPtr registerActivity(const QString &id); + void unregisterActivity(const QString &id); + void showActivity(InfoPtr activityInfo, bool notifyClients); + void hideActivity(const QString &id); + void backgroundsUpdated(const QStringList &activities); + + InfoPtr findActivity(QObject *ptr) const; + + ActivitiesModel *const q; + + DECLARE_RAII_MODEL_UPDATERS(ActivitiesModel) +}; + +} // namespace KActivitiesBackport + +#endif // ACTIVITIES_ACTIVITYMODEL_P_H + diff --git a/imports/activitymanager/sortedactivitiesmodel.h b/imports/activitymanager/sortedactivitiesmodel.h new file mode 100644 --- /dev/null +++ b/imports/activitymanager/sortedactivitiesmodel.h @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2016 Ivan Cukic + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * or (at your option) any later version, as published by the Free + * Software Foundation + * + * 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 SORTED_ACTIVITY_MODEL +#define SORTED_ACTIVITY_MODEL + +// Qt +#include + +#include +#include + +#include "backport/switcheractivitiesmodel.h" + +class SortedActivitiesModel : public QSortFilterProxyModel { + Q_OBJECT + + Q_PROPERTY(bool sortByLastUsedTime READ sortByLastUsedTime WRITE setSortByLastUsedTime NOTIFY sortByLastUsedTimeChanged) + Q_PROPERTY(bool inhibitUpdates READ inhibitUpdates WRITE setInhibitUpdates NOTIFY inhibitUpdatesChanged) + +public: + SortedActivitiesModel(QVector states, QObject *parent = 0); + virtual ~SortedActivitiesModel(); + + QVariant data(const QModelIndex & index, int role = Qt::DisplayRole) const; + + QHash roleNames() const; + + QString relativeActivity(int relative) const; + +protected: + uint lastUsedTime(const QString &activity) const; + bool lessThan(const QModelIndex & source_left, const QModelIndex & source_right) const; + + enum AdditionalRoles { + LastTimeUsed = KActivitiesBackport::ActivitiesModel::UserRole, + LastTimeUsedString = KActivitiesBackport::ActivitiesModel::UserRole + 1 + }; + +public Q_SLOTS: + bool sortByLastUsedTime() const; + void setSortByLastUsedTime(bool sortByLastUsedTime); + + bool inhibitUpdates() const; + void setInhibitUpdates(bool sortByLastUsedTime); + + void onBackgroundsUpdated(const QStringList &changedBackgrounds); + void onCurrentActivityChanged(const QString ¤tActivity); + + QString activityIdForRow(int row) const; + int rowForActivityId(const QString &activity) const; + + void rowChanged(int row, const QVector &roles); + +Q_SIGNALS: + void sortByLastUsedTimeChanged(bool sortByLastUsedTime); + void inhibitUpdatesChanged(bool inhibitUpdates); + +private: + bool m_sortByLastUsedTime; + bool m_inhibitUpdates; + + QString m_previousActivity; + + KActivitiesBackport::ActivitiesModel *m_activitiesModel; + KActivities::Consumer *m_activities; +}; + +#endif // SORTED_ACTIVITY_MODEL + diff --git a/imports/activitymanager/sortedactivitiesmodel.cpp b/imports/activitymanager/sortedactivitiesmodel.cpp new file mode 100644 --- /dev/null +++ b/imports/activitymanager/sortedactivitiesmodel.cpp @@ -0,0 +1,375 @@ +/* + * Copyright (C) 2016 + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * or (at your option) any later version, as published by the Free + * Software Foundation + * + * 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. + */ + +// Self +#include "sortedactivitiesmodel.h" + +// Qt +#include + +// KDE +#include +#include +#include +#include +#include + +namespace { + + class BackgroundCache { + public: + BackgroundCache() + : initialized(false) + , plasmaConfig("plasma-org.kde.plasma.desktop-appletsrc") + { + using namespace std::placeholders; + + const auto configFile = QStandardPaths::writableLocation( + QStandardPaths::GenericConfigLocation) + + QLatin1Char('/') + plasmaConfig.name(); + + KDirWatch::self()->addFile(configFile); + + QObject::connect(KDirWatch::self(), &KDirWatch::dirty, + [this] (const QString &file) { settingsFileChanged(file); }); + QObject::connect(KDirWatch::self(), &KDirWatch::created, + [this] (const QString &file) { settingsFileChanged(file); }); + } + + void settingsFileChanged(const QString &file) + { + if (!file.endsWith(plasmaConfig.name())) return; + + plasmaConfig.reparseConfiguration(); + + if (initialized) { + reload(false); + } + } + + void subscribe(SortedActivitiesModel *model) + { + if (!initialized) { + reload(true); + } + + models << model; + } + + void unsubscribe(SortedActivitiesModel *model) + { + models.removeAll(model); + + if (models.isEmpty()) { + initialized = false; + forActivity.clear(); + } + } + + QString backgroundFromConfig(const KConfigGroup &config) const + { + 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(); + } + + void reload(bool fullReload) + { + QHash newBackgrounds; + + if (fullReload) { + forActivity.clear(); + } + + QStringList changedBackgrounds; + + for (const auto &cont: plasmaConfigContainments().groupList()) { + + auto config = plasmaConfigContainments().group(cont); + auto activityId = config.readEntry("activityId", QString()); + + // Ignore if it has no assigned activity + if (activityId.isEmpty()) continue; + + // Ignore if we have already found the background + if (newBackgrounds.contains(activityId) && + newBackgrounds[activityId][0] != '#') continue; + + auto newBackground = backgroundFromConfig(config); + + if (forActivity[activityId] != newBackground) { + changedBackgrounds << activityId; + if (!newBackground.isEmpty()) { + newBackgrounds[activityId] = newBackground; + } + } + } + + initialized = true; + + if (!changedBackgrounds.isEmpty()) { + forActivity = newBackgrounds; + + for (auto model: models) { + model->onBackgroundsUpdated(changedBackgrounds); + } + } + } + + KConfigGroup plasmaConfigContainments() { + return plasmaConfig.group("Containments"); + } + + QHash forActivity; + QList models; + + bool initialized; + KConfig plasmaConfig; + + }; + + static BackgroundCache &backgrounds() + { + // If you convert this to a shared pointer, + // fix the connections to KDirWatcher + static BackgroundCache cache; + return cache; + } +} + +SortedActivitiesModel::SortedActivitiesModel(QVector states, QObject *parent) + : QSortFilterProxyModel(parent) + , m_sortByLastUsedTime(true) + , m_activitiesModel(new KActivitiesBackport::ActivitiesModel(states, this)) + , m_activities(new KActivities::Consumer(this)) +{ + setSourceModel(m_activitiesModel); + + setDynamicSortFilter(true); + setSortRole(LastTimeUsed); + sort(0, Qt::DescendingOrder); + + backgrounds().subscribe(this); +} + +SortedActivitiesModel::~SortedActivitiesModel() +{ + backgrounds().unsubscribe(this); +} + +bool SortedActivitiesModel::sortByLastUsedTime() const +{ + return m_sortByLastUsedTime; +} + +void SortedActivitiesModel::setSortByLastUsedTime(bool sortByLastUsedTime) +{ + if (m_sortByLastUsedTime != sortByLastUsedTime) { + m_sortByLastUsedTime = sortByLastUsedTime; + + if (m_sortByLastUsedTime) { + setSortRole(LastTimeUsed); + } else { + setSortRole(Qt::DisplayRole); + } + } +} + +bool SortedActivitiesModel::inhibitUpdates() const +{ + return m_inhibitUpdates; +} + +void SortedActivitiesModel::setInhibitUpdates(bool inhibitUpdates) +{ + if (m_inhibitUpdates != inhibitUpdates) { + m_inhibitUpdates = inhibitUpdates; + emit inhibitUpdatesChanged(m_inhibitUpdates); + + setDynamicSortFilter(!inhibitUpdates); + } +} + +uint SortedActivitiesModel::lastUsedTime(const QString &activity) const +{ + if (m_activities->currentActivity() == activity) { + return ~(uint)0; + + } else { + KConfig config("kactivitymanagerd-switcher"); + KConfigGroup times(&config, "LastUsed"); + + return times.readEntry(activity, (uint)0); + } +} + +bool SortedActivitiesModel::lessThan(const QModelIndex &sourceLeft, + const QModelIndex &sourceRight) const +{ + if (m_sortByLastUsedTime) { + const auto activityLeft = sourceModel()->data(sourceLeft, KActivitiesBackport::ActivitiesModel::ActivityId); + const auto activityRight = sourceModel()->data(sourceRight, KActivitiesBackport::ActivitiesModel::ActivityId); + + const auto timeLeft = lastUsedTime(activityLeft.toString()); + const auto timeRight = lastUsedTime(activityRight.toString()); + + return timeLeft < timeRight; + + } else { + const auto titleLeft = sourceModel()->data(sourceLeft, KActivitiesBackport::ActivitiesModel::ActivityName); + const auto titleRight = sourceModel()->data(sourceRight, KActivitiesBackport::ActivitiesModel::ActivityName); + + return titleLeft < titleRight; + } +} + +QHash SortedActivitiesModel::roleNames() const +{ + if (!sourceModel()) return QHash(); + + auto roleNames = sourceModel()->roleNames(); + + roleNames[LastTimeUsed] = "lastTimeUsed"; + roleNames[LastTimeUsedString] = "lastTimeUsedString"; + + return roleNames; +} + +QVariant SortedActivitiesModel::data(const QModelIndex &index, int role) const +{ + if (role == KActivitiesBackport::ActivitiesModel::ActivityBackground) { + const auto activity = + QSortFilterProxyModel::data(index, Qt::UserRole).toString(); + + return backgrounds().forActivity[activity]; + + } else if (role == LastTimeUsed || role == LastTimeUsedString) { + const auto activity = + QSortFilterProxyModel::data(index, Qt::UserRole).toString(); + + const auto time = lastUsedTime(activity); + + if (role == LastTimeUsed) { + return QVariant(time); + + } else { + const auto now = QDateTime::currentDateTime().toTime_t(); + + if (time == 0) return i18n("Used some time ago"); + + auto diff = now - time; + + // We do not need to be precise + diff /= 60; + const auto minutes = diff % 60; diff /= 60; + const auto hours = diff % 24; diff /= 24; + const auto days = diff % 30; diff /= 30; + const auto months = diff % 12; diff /= 12; + const auto years = diff; + + return (years > 0) ? i18n("Used a long time ago") + : (months > 0) ? i18ncp("amount in months", "Used a month ago", "Used %1 months ago", months) + : (days > 0) ? i18ncp("amount in days", "Used a day ago", "Used %1 days ago", days) + : (hours > 0) ? i18ncp("amount in hours", "Used an hour ago", "Used %1 hours ago", hours) + : (minutes > 0) ? i18ncp("amount in minutes", "Used a minute ago", "Used %1 minutes ago", minutes) + : i18n("Used a moment ago"); + + } + + } else { + return QSortFilterProxyModel::data(index, role); + } +} + +QString SortedActivitiesModel::activityIdForRow(int row) const +{ + return data(index(row, 0), KActivitiesBackport::ActivitiesModel::ActivityId).toString(); +} + +int SortedActivitiesModel::rowForActivityId(const QString &activity) const +{ + int position = -1; + + for (int row = 0; row < rowCount(); ++row) { + if (activity == activityIdForRow(row)) { + position = row; + } + } + + return position; +} + +QString SortedActivitiesModel::relativeActivity(int relative) const +{ + const auto currentActivity = m_activities->currentActivity(); + + if (!sourceModel()) return QString(); + + const auto currentRowCount = sourceModel()->rowCount(); + + int currentActivityRow = 0; + + for (; currentActivityRow < currentRowCount; currentActivityRow++) { + if (activityIdForRow(currentActivityRow) == currentActivity) break; + } + + currentActivityRow = (currentActivityRow + relative) % currentRowCount; + + return activityIdForRow(currentActivityRow); +} + +void SortedActivitiesModel::onCurrentActivityChanged(const QString ¤tActivity) +{ + if (m_previousActivity == currentActivity) return; + + const int previousActivityRow = rowForActivityId(m_previousActivity); + emit rowChanged(previousActivityRow, { LastTimeUsed, LastTimeUsedString }); + + m_previousActivity = currentActivity; + + const int currentActivityRow = rowForActivityId(m_previousActivity); + emit rowChanged(currentActivityRow, { LastTimeUsed, LastTimeUsedString }); +} + +void SortedActivitiesModel::onBackgroundsUpdated(const QStringList &activities) +{ + for (const auto &activity: activities) { + const int row = rowForActivityId(activity); + emit rowChanged(row, { KActivitiesBackport::ActivitiesModel::ActivityBackground }); + } +} + +void SortedActivitiesModel::rowChanged(int row, const QVector &roles) +{ + if (row == -1) return; + emit dataChanged(index(row, 0), index(row, 0), roles); +} diff --git a/imports/activitymanager/switcherbackend.h b/imports/activitymanager/switcherbackend.h --- a/imports/activitymanager/switcherbackend.h +++ b/imports/activitymanager/switcherbackend.h @@ -29,9 +29,12 @@ #include // KDE -#include +#include #include +// Local +#include "sortedactivitiesmodel.h" + class QAction; class QQmlEngine; class QJSEngine; @@ -63,7 +66,11 @@ QPixmap wallpaperThumbnail(const QString &path, int width, int height, const QJSValue &callback); - QString lastTimeUsedString(const QString &activity); + QAbstractItemModel *runningActivitiesModel() const; + QAbstractItemModel *stoppedActivitiesModel() const; + + void setCurrentActivity(const QString &activity); + void stopActivity(const QString &activity); private: template @@ -85,7 +92,7 @@ void showActivitySwitcherIfNeeded(); - void currentActivityChangedSlot(const QString &id); + void onCurrentActivityChanged(const QString &id); private: QHash m_actionShortcut; @@ -98,6 +105,9 @@ KImageCache *m_wallpaperCache; QSet m_previewJobs; + SortedActivitiesModel *m_runningActivitiesModel; + SortedActivitiesModel *m_stoppedActivitiesModel; + }; #endif // SWITCHER_BACKEND_H diff --git a/imports/activitymanager/switcherbackend.cpp b/imports/activitymanager/switcherbackend.cpp --- a/imports/activitymanager/switcherbackend.cpp +++ b/imports/activitymanager/switcherbackend.cpp @@ -25,7 +25,6 @@ // Qt #include -// #include #include #include #include @@ -156,6 +155,8 @@ : QObject(parent) , m_lastInvokedAction(Q_NULLPTR) , m_shouldShowSwitcher(false) + , m_runningActivitiesModel(new SortedActivitiesModel({KActivities::Info::Running, KActivities::Info::Stopping}, this)) + , m_stoppedActivitiesModel(new SortedActivitiesModel({KActivities::Info::Stopped, KActivities::Info::Starting}, this)) { m_wallpaperCache = new KImageCache("activityswitcher_wallpaper_preview", 10485760); @@ -169,10 +170,13 @@ Qt::META + Qt::SHIFT + Qt::Key_Tab, &SwitcherBackend::keybdSwitchToPreviousActivity); + connect(this, &SwitcherBackend::shouldShowSwitcherChanged, + m_runningActivitiesModel, &SortedActivitiesModel::setInhibitUpdates); + connect(&m_modKeyPollingTimer, &QTimer::timeout, this, &SwitcherBackend::showActivitySwitcherIfNeeded); connect(&m_activities, &KActivities::Controller::currentActivityChanged, - this, &SwitcherBackend::currentActivityChangedSlot); + this, &SwitcherBackend::onCurrentActivityChanged); m_previousActivity = m_activities.currentActivity(); } @@ -204,47 +208,16 @@ void SwitcherBackend::switchToActivity(Direction direction) { - auto runningActivities - = m_activities.activities(KActivities::Info::Running); - - if (runningActivities.count() == 0) { - return; - } + const auto activityToSet = + m_runningActivitiesModel->relativeActivity(direction == Next ? 1 : -1); - // Sorting this every time is not really (or at all) efficient, - // but at least we do not need to connect to too many Info objects - std::sort(runningActivities.begin(), runningActivities.end(), - [] (const QString &left, const QString &right) { - using KActivities::Info; - const QString &leftName = Info(left).name().toLower(); - const QString &rightName = Info(right).name().toLower(); + if (activityToSet.isEmpty()) return; - return - (leftName < rightName) || - (leftName == rightName && left < right); - }); - - auto index = std::max( - 0, runningActivities.indexOf(m_activities.currentActivity())); - - index += direction == Next ? 1 : -1; - - if (index < 0) { - index = runningActivities.count() - 1; - } else if (index >= runningActivities.count()) { - index = 0; - } - - // TODO: This is evil, but plasmashell goes into a dead-lock if - // the activity is changed while one tries to open the switcher O.o - // m_activities.setCurrentActivity(runningActivities[index]); - const auto activityToSet = runningActivities[index]; QTimer::singleShot(150, this, [this,activityToSet] () { - m_activities.setCurrentActivity(activityToSet); + setCurrentActivity(activityToSet); }); keybdSwitchedToAnotherActivity(); - } void SwitcherBackend::keybdSwitchedToAnotherActivity() @@ -281,8 +254,17 @@ // nothing } -void SwitcherBackend::currentActivityChangedSlot(const QString &id) +void SwitcherBackend::onCurrentActivityChanged(const QString &id) { + if (m_shouldShowSwitcher) { + // If we are showing the switcher because the user is + // pressing Meta+Tab, we are not ready to commit the + // activity change to memory + return; + } + + if (m_previousActivity == id) return; + // Safe, we have a long-lived Consumer object KActivities::Info activity(id); emit showSwitchNotification(id, activity.name(), activity.icon()); @@ -299,7 +281,7 @@ if (!m_previousActivity.isEmpty()) { // When leaving an activity, say goodbye and fondly remember - // the time we saw it + // the last time we saw it times.writeEntry(m_previousActivity, now); } @@ -324,6 +306,9 @@ m_modKeyPollingTimer.start(100); } else { m_modKeyPollingTimer.stop(); + + // We might have an unprocessed onCurrentActivityChanged + onCurrentActivityChanged(m_activities.currentActivity()); } emit shouldShowSwitcherChanged(m_shouldShowSwitcher); @@ -341,8 +326,6 @@ return preview; } - // qDebug() << "SwitcherBackend: Requesting wallpaper: " << path << width << height; - if (width == 0) { width = 320; } @@ -356,16 +339,12 @@ + QString::number(width) + "x" + QString::number(height); - // qDebug() << "SwitcherBackend: Wallpaper cache id is: " << pixmapKey; - if (m_wallpaperCache->findPixmap(pixmapKey, &preview)) { return preview; } QUrl file(path); - // qDebug() << "SwitcherBackend: Cache miss. We need to generate the thumbnail: " << file; - if (!m_previewJobs.contains(file) && file.isValid()) { m_previewJobs.insert(file); @@ -383,7 +362,6 @@ m_wallpaperCache->insertPixmap(pixmapKey, pixmap); m_previewJobs.remove(path); - // qDebug() << "SwitcherBackend: Got the thumbnail for " << path << "saving under" << pixmapKey; callback.call({true}); }); @@ -402,33 +380,24 @@ return preview; } -QString SwitcherBackend::lastTimeUsedString(const QString &activity) +QAbstractItemModel *SwitcherBackend::runningActivitiesModel() const { - KConfig config("kactivitymanagerd-switcher"); - KConfigGroup times(&config, "LastUsed"); + return m_runningActivitiesModel; +} - const auto now = QDateTime::currentDateTime().toTime_t(); - const auto time = times.readEntry(activity, 0); - - if (time == 0) return i18n("Used some time ago"); - - auto diff = now - time; - - // We do not need to be precise - const auto seconds = diff % 60; diff /= 60; - const auto minutes = diff % 60; diff /= 60; - const auto hours = diff % 24; diff /= 24; - const auto days = diff % 30; diff /= 30; - const auto months = diff % 12; diff /= 12; - const auto years = diff; - - return (years > 0) ? i18n("Used a long time ago") - : (months > 0) ? i18ncp("amount in months", "Used a month ago", "Used %1 months ago", months) - : (days > 0) ? i18ncp("amount in days", "Used a day ago", "Used %1 days ago", days) - : (hours > 0) ? i18ncp("amount in hours", "Used an hour ago", "Used %1 hours ago", hours) - : (minutes > 0) ? i18ncp("amount in minutes", "Used a minute ago", "Used %1 minutes ago", minutes) - : i18n("Used a moment ago"); +QAbstractItemModel *SwitcherBackend::stoppedActivitiesModel() const +{ + return m_stoppedActivitiesModel; } +void SwitcherBackend::setCurrentActivity(const QString &activity) +{ + m_activities.setCurrentActivity(activity); +} + +void SwitcherBackend::stopActivity(const QString &activity) +{ + m_activities.stopActivity(activity); +} #include "switcherbackend.moc"