diff --git a/src/common/dbus/org.kde.ActivityManager.Activities.xml b/src/common/dbus/org.kde.ActivityManager.Activities.xml index 8d3e074..83fa6de 100644 --- a/src/common/dbus/org.kde.ActivityManager.Activities.xml +++ b/src/common/dbus/org.kde.ActivityManager.Activities.xml @@ -1,115 +1,121 @@ + + + + + + diff --git a/src/service/Activities.cpp b/src/service/Activities.cpp index 27f7d90..188f71c 100644 --- a/src/service/Activities.cpp +++ b/src/service/Activities.cpp @@ -1,599 +1,684 @@ /* * Copyright (C) 2010 - 2016 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) 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 14 of version 3 of the license. * * 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, see . */ // Self #include #include "Activities.h" #include "Activities_p.h" // Qt #include #include #include #include #include #include // KDE #include #include #include #include #include // Utils #include #include // Local #include "DebugActivities.h" #include "activitiesadaptor.h" #include "ksmserver/KSMServer.h" #include "common/dbus/common.h" // Private #define ACTIVITY_MANAGER_CONFIG_FILE_NAME QStringLiteral("kactivitymanagerdrc") +namespace { + inline + bool nameBasedOrdering(const ActivityInfo &info, const ActivityInfo &other) + { + const auto comp = + QString::compare(info.name, other.name, Qt::CaseInsensitive); + return comp < 0 || (comp == 0 && info.id < other.id); + } +} + Activities::Private::KDE4ConfigurationTransitionChecker::KDE4ConfigurationTransitionChecker() { // Checking whether we need to transfer the KActivities/KDE4 // configuration file to the new location. const QString newConfigLocation = QStandardPaths::writableLocation(QStandardPaths::ConfigLocation) + QLatin1Char('/') + ACTIVITY_MANAGER_CONFIG_FILE_NAME; if (QFile(newConfigLocation).exists()) { return; } // Testing for kdehome Kdelibs4Migration migration; if (!migration.kdeHomeFound()) { return; } QString oldConfigFile(migration.locateLocal("config", QStringLiteral("activitymanagerrc"))); if (!oldConfigFile.isEmpty()) { QFile(oldConfigFile).copy(newConfigLocation); } } Activities::Private::Private(Activities *parent) : kde4ConfigurationTransitionChecker() , config(QStringLiteral("kactivitymanagerdrc")) , q(parent) { // qCDebug(KAMD_ACTIVITIES) << "Using this configuration file:" // << config.name() // << config.locationType() // << QStandardPaths::standardLocations(config.locationType()) // ; // Reading activities from the config file. // Saving only the running activities means that if we have any // errors in the config, we might end up with all activities // stopped const auto defaultState = !mainConfig().hasKey("runningActivities") ? Activities::Running : !mainConfig().hasKey("stoppedActivities") ? Activities::Stopped : Activities::Running; const auto runningActivities = mainConfig().readEntry("runningActivities", QStringList()).toSet(); const auto stoppedActivities = mainConfig().readEntry("stoppedActivities", QStringList()).toSet(); // Do we have a running activity? bool atLeastOneRunning = false; for (const auto &activity: activityNameConfig().keyList()) { auto state = runningActivities.contains(activity) ? Activities::Running : stoppedActivities.contains(activity) ? Activities::Stopped : defaultState; activities[activity] = state; if (state == Activities::Running) { atLeastOneRunning = true; } } // Is this our first start? if (activities.isEmpty()) { // We need to add this only after the service has been properly started KConfigGroup cg(KSharedConfig::openConfig(QStringLiteral("kdeglobals")), QStringLiteral("Activities")); //NOTE: config key still singular for retrocompatibility const QStringList names = cg.readEntry("defaultActivityName", QStringList{i18n("Default")}); for (const auto &name : names) { QMetaObject::invokeMethod( q, "AddActivity", Qt::QueuedConnection, Q_ARG(QString, name)); } } else if (!atLeastOneRunning) { // If we have no running activities, but we have activities, // we are in a problem. This should not happen unless the // configuration file is in a big problem and told us there // are no running activities, and enlists all of them as stopped. // In that case, we will pretend all of them are running qCWarning(KAMD_LOG_ACTIVITIES) << "The config file enlisted all activities as stopped"; for (const auto &keys: activities.keys()) { activities[keys] = Activities::Running; } } + + QMetaObject::invokeMethod( + this, + "updateSortedActivityList", + Qt::QueuedConnection); +} + +void Activities::Private::updateSortedActivityList() { + QVector a; + for (const auto &activity : activities.keys()) { + a.append(q->ActivityInformation(activity)); + } + + std::sort(a.begin(), a.end(), &nameBasedOrdering); + + QWriteLocker lock(&activitiesLock); + sortedActivities = a; } void Activities::Private::loadLastActivity() { // This is called from constructor, no need for locking // If there are no public activities, try to load the last used activity const auto lastUsedActivity = mainConfig().readEntry("currentActivity", QString()); setCurrentActivity( (lastUsedActivity.isEmpty() && activities.size() > 0) ? activities.keys().at(0) : lastUsedActivity); } Activities::Private::~Private() { configSync(); } bool Activities::Private::setCurrentActivity(const QString &activity) { { // There is nothing expensive in this block, not a problem to lock QWriteLocker lock(&activitiesLock); // Should we change the activity at all? if (currentActivity == activity) { return true; } // If the activity is empty, this means we are entering a limbo state if (activity.isEmpty()) { currentActivity.clear(); emit q->CurrentActivityChanged(currentActivity); return true; } // Does the requested activity exist? if (!activities.contains(activity)) { return false; } } // Start activity q->StartActivity(activity); // Saving the current activity, and notifying // clients of the change currentActivity = activity; mainConfig().writeEntry("currentActivity", activity); scheduleConfigSync(); emit q->CurrentActivityChanged(activity); return true; } +bool Activities::Private::previousActivity() +{ + const auto a = q->ListActivities(Activities::Running); + + for (int i = 0; i < a.count(); ++i) { + if (a[i] == currentActivity) { + return setCurrentActivity(a[(i + a.size() - 1) % a.size()]); + } + } + + return false; +} + +bool Activities::Private::nextActivity() +{ + const auto a = q->ListActivities(Activities::Running); + + for (int i = 0; i < a.count(); ++i) { + if (a[i] == currentActivity) { + return setCurrentActivity(a[(i + 1) % a.size()]); + } + } + + return false; +} + QString Activities::Private::addActivity(const QString &name) { QString activity; if (name.isEmpty()) { Q_ASSERT(!name.isEmpty()); return activity; } int activitiesCount = 0; { QWriteLocker lock(&activitiesLock); // Ensuring a new Uuid. The loop should usually end after only // one iteration while (activity.isEmpty() || activities.contains(activity)) { activity = QUuid::createUuid().toString().mid(1, 36); } // Saves the activity info to the config activities[activity] = Invalid; activitiesCount = activities.size(); } setActivityState(activity, Running); q->SetActivityName(activity, name); + updateSortedActivityList(); + emit q->ActivityAdded(activity); scheduleConfigSync(); if (activitiesCount == 1) { q->SetCurrentActivity(activity); } return activity; } void Activities::Private::removeActivity(const QString &activity) { Q_ASSERT(!activity.isEmpty()); // Sanity checks { QWriteLocker lock(&activitiesLock); if (!activities.contains(activity)) { return; } // Is somebody trying to remove the last activity? if (activities.size() == 1) { return; } } // If the activity is running, stash it q->StopActivity(activity); setActivityState(activity, Activities::Invalid); bool currentActivityDeleted = false; { QWriteLocker lock(&activitiesLock); // Removing the activity activities.remove(activity); + for (int i = 0; i < sortedActivities.count(); ++i) { + if (sortedActivities[i].id == activity) { + sortedActivities.remove(i); + break; + } + } + // If the removed activity was the current one, // set another activity as current currentActivityDeleted = (currentActivity == activity); } activityNameConfig().deleteEntry(activity); activityDescriptionConfig().deleteEntry(activity); activityIconConfig().deleteEntry(activity); if (currentActivityDeleted) { ensureCurrentActivityIsRunning(); } emit q->ActivityRemoved(activity); QMetaObject::invokeMethod(q, "ActivityRemoved", Qt::QueuedConnection, Q_ARG(QString, activity)); QMetaObject::invokeMethod(this, "configSync", Qt::QueuedConnection); } void Activities::Private::scheduleConfigSync() { static const auto shortInterval = 1000; // If the timer is not running, or has a longer interval than we need, // start it // Note: If you want to add multiple different delays for different // events based on the importance of how quickly something needs // to be synced to the config, don't. Since the QTimer lives in a // separate thread, we have to communicate with it in via // queued connections, which means that we don't know whether // the timer was already started when this method was invoked, // we do not know whether the interval is properly set etc. if (!configSyncTimer.isActive()) { QMetaObject::invokeMethod( &configSyncTimer, "start", Qt::QueuedConnection, Q_ARG(int, shortInterval)); } } void Activities::Private::configSync() { // Stop the timer and reset the interval to zero QMetaObject::invokeMethod(&configSyncTimer, "stop", Qt::QueuedConnection); config.sync(); } void Activities::Private::setActivityState(const QString &activity, Activities::State state) { bool configNeedsUpdating = false; { QWriteLocker lock(&activitiesLock); Q_ASSERT(activities.contains(activity)); if (activities.value(activity) == state) { return; } // Treating 'Starting' as 'Running', and 'Stopping' as 'Stopped' // as far as the config file is concerned configNeedsUpdating = ((activities[activity] & 4) != (state & 4)); activities[activity] = state; } switch (state) { case Activities::Running: emit q->ActivityStarted(activity); break; case Activities::Stopped: emit q->ActivityStopped(activity); break; default: break; } emit q->ActivityStateChanged(activity, state); if (configNeedsUpdating) { QReadLocker lock(&activitiesLock); mainConfig().writeEntry("runningActivities", activities.keys(Activities::Running) + activities.keys(Activities::Starting)); mainConfig().writeEntry("stoppedActivities", activities.keys(Activities::Stopped) + activities.keys(Activities::Stopping)); scheduleConfigSync(); } + + updateSortedActivityList(); } void Activities::Private::ensureCurrentActivityIsRunning() { // If the current activity is not running, // make some other activity current const auto runningActivities = q->ListActivities(Activities::Running); if (!runningActivities.contains(currentActivity) && runningActivities.size() > 0) { setCurrentActivity(runningActivities.first()); } } void Activities::Private::activitySessionStateChanged(const QString &activity, int status) { QString currentActivity = this->currentActivity; { QReadLocker lock(&activitiesLock); if (!activities.contains(activity)) { return; } } switch (status) { case KSMServer::Started: case KSMServer::FailedToStop: setActivityState(activity, Activities::Running); break; case KSMServer::Stopped: setActivityState(activity, Activities::Stopped); if (currentActivity == activity) { ensureCurrentActivityIsRunning(); } break; } QMetaObject::invokeMethod(this, "configSync", Qt::QueuedConnection); } // Main Activities::Activities(QObject *parent) : Module(QStringLiteral("activities"), parent) , d(this) { qCDebug(KAMD_LOG_ACTIVITIES) << "Starting the KDE Activity Manager daemon" << QDateTime::currentDateTime(); // Basic initialization //////////////////////////////////////////////////// // Initializing D-Bus service new ActivitiesAdaptor(this); KDBusConnectionPool::threadConnection().registerObject( KAMD_DBUS_OBJECT_PATH(Activities), this); // Initializing config qCDebug(KAMD_LOG_ACTIVITIES) << "Config timer connecting..."; d->connect(&d->configSyncTimer, SIGNAL(timeout()), SLOT(configSync()), Qt::QueuedConnection); d->configSyncTimer.setSingleShot(true); d->ksmserver = new KSMServer(this); d->connect(d->ksmserver, SIGNAL(activitySessionStateChanged(QString, int)), SLOT(activitySessionStateChanged(QString, int))); // Loading the last used activity, if possible d->loadLastActivity(); } Activities::~Activities() { } QString Activities::CurrentActivity() const { QReadLocker lock(&d->activitiesLock); return d->currentActivity; } bool Activities::SetCurrentActivity(const QString &activity) { // Public method can not put us in a limbo state if (activity.isEmpty()) { return false; } return d->setCurrentActivity(activity); } +bool Activities::PreviousActivity() +{ + return d->previousActivity(); +} + +bool Activities::NextActivity() +{ + return d->nextActivity(); +} + QString Activities::AddActivity(const QString &name) { // We do not care about authorization if this is the first start if (!d->activities.isEmpty() && !KAuthorized::authorize(QStringLiteral("plasma-desktop/add_activities"))) { return QString(); } return d->addActivity(name); } void Activities::RemoveActivity(const QString &activity) { if (!KAuthorized::authorize(QStringLiteral("plasma-desktop/add_activities"))) { return; } d->removeActivity(activity); } QStringList Activities::ListActivities() const { QReadLocker lock(&d->activitiesLock); - return d->activities.keys(); + + QStringList s; + for (const auto &a : d->sortedActivities) { + s << a.id; + } + return s; } QStringList Activities::ListActivities(int state) const { QReadLocker lock(&d->activitiesLock); - return d->activities.keys((State)state); + + QStringList s; + for (const auto &a : d->sortedActivities) { + if (a.state == (State)state) { + s << a.id; + } + } + return s; } QList Activities::ListActivitiesWithInformation() const { using namespace kamd::utils; // Mapping activity ids to info return as_collection>( ListActivities() | transformed(&Activities::ActivityInformation, this) ); } ActivityInfo Activities::ActivityInformation(const QString &activity) const { return ActivityInfo { activity, ActivityName(activity), ActivityDescription(activity), ActivityIcon(activity), ActivityState(activity) }; } #define CREATE_GETTER_AND_SETTER(What) \ QString Activities::Activity##What(const QString &activity) const \ { \ QReadLocker lock(&d->activitiesLock); \ return d->activities.contains(activity) ? d->activity##What(activity) \ : QString(); \ } \ \ void Activities::SetActivity##What(const QString &activity, \ const QString &value) \ { \ { \ QReadLocker lock(&d->activitiesLock); \ if (value == d->activity##What(activity) \ || !d->activities.contains(activity)) { \ return; \ } \ } \ \ d->activity##What##Config().writeEntry(activity, value); \ d->scheduleConfigSync(); \ \ emit Activity##What##Changed(activity, value); \ emit ActivityChanged(activity); \ } CREATE_GETTER_AND_SETTER(Name) CREATE_GETTER_AND_SETTER(Description) CREATE_GETTER_AND_SETTER(Icon) #undef CREATE_GETTER_AND_SETTER // Main void Activities::StartActivity(const QString &activity) { { QReadLocker lock(&d->activitiesLock); if (!d->activities.contains(activity) || d->activities[activity] != Stopped) { return; } } d->setActivityState(activity, Starting); d->ksmserver->startActivitySession(activity); } void Activities::StopActivity(const QString &activity) { { QReadLocker lock(&d->activitiesLock); if (!d->activities.contains(activity) || d->activities[activity] == Stopped || d->activities.size() == 1 || d->activities.keys(Activities::Running).size() <= 1 ) { return; } } d->setActivityState(activity, Stopping); d->ksmserver->stopActivitySession(activity); } int Activities::ActivityState(const QString &activity) const { QReadLocker lock(&d->activitiesLock); return d->activities.contains(activity) ? d->activities[activity] : Invalid; } - diff --git a/src/service/Activities.h b/src/service/Activities.h index 0b84925..0896e6e 100644 --- a/src/service/Activities.h +++ b/src/service/Activities.h @@ -1,242 +1,252 @@ /* * Copyright (C) 2010 - 2016 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) 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 14 of version 3 of the license. * * 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, see . */ #ifndef ACTIVITIES_H #define ACTIVITIES_H // Qt #include #include // Utils #include // Local #include "Module.h" #include /** * Service for tracking the user actions and managing the * activities */ class Activities : public Module { Q_OBJECT Q_CLASSINFO("D-Bus Interface", "org.kde.ActivityManager.Activities") Q_PROPERTY(QString CurrentActivity READ CurrentActivity WRITE SetCurrentActivity NOTIFY CurrentActivityChanged) public: /** * Activity state * @note: Do not change the values, needed for bit-operations */ enum State { Invalid = 0, Running = 2, Starting = 3, Stopped = 4, Stopping = 5 }; /** * Creates new Activities object */ explicit Activities(QObject *parent = nullptr); /** * Destroys this interface */ ~Activities() override; // workspace activities control public Q_SLOTS: /** * @returns the id of the current activity, empty string if none */ QString CurrentActivity() const; /** * Sets the current activity * @param activity id of the activity to make current */ bool SetCurrentActivity(const QString &activity); + /** + * Switches to the previous activity + */ + bool PreviousActivity(); + + /** + * Switches to the next activity + */ + bool NextActivity(); + /** * Adds a new activity * @param name name of the activity * @returns id of the newly created activity */ QString AddActivity(const QString &name); /** * Starts the specified activity * @param activity id of the activity to stash */ void StartActivity(const QString &activity); /** * Stops the specified activity * @param activity id of the activity to stash */ void StopActivity(const QString &activity); /** * @returns the state of the activity * @param activity id of the activity */ int ActivityState(const QString &activity) const; /** * Removes the specified activity * @param activity id of the activity to delete */ void RemoveActivity(const QString &activity); /** * @returns the list of all existing activities */ QStringList ListActivities() const; /** * @returns the list of activities with the specified state * @param state state */ QStringList ListActivities(int state) const; /** * @returns the name of the specified activity * @param activity id of the activity */ QString ActivityName(const QString &activity) const; /** * Sets the name of the specified activity * @param activity id of the activity * @param name name to be set */ void SetActivityName(const QString &activity, const QString &name); /** * @returns the description of the specified activity * @param activity id of the activity */ QString ActivityDescription(const QString &activity) const; /** * Sets the description of the specified activity * @param activity id of the activity * @param description description to be set */ void SetActivityDescription(const QString &activity, const QString &description); /** * @returns the icon of the specified activity * @param activity id of the activity */ QString ActivityIcon(const QString &activity) const; /** * Sets the icon of the specified activity * @param activity id of the activity * @param icon icon to be set */ void SetActivityIcon(const QString &activity, const QString &icon); public Q_SLOTS: /** * @returns a list of activities with basic info about them */ ActivityInfoList ListActivitiesWithInformation() const; /** * @returns the info about an activity */ ActivityInfo ActivityInformation(const QString &activity) const; Q_SIGNALS: /** * This signal is emitted when the global * activity is changed * @param activity id of the new current activity */ void CurrentActivityChanged(const QString &activity); /** * This signal is emitted when a new activity is created * @param activity id of the activity */ void ActivityAdded(const QString &activity); /** * This signal is emitted when an activity is started * @param activity id of the activity */ void ActivityStarted(const QString &activity); /** * This signal is emitted when an activity is stashed * @param activity id of the activity */ void ActivityStopped(const QString &activity); /** * This signal is emitted when an activity is deleted * @param activity id of the activity */ void ActivityRemoved(const QString &activity); /** * Emitted when an activity name is changed * @param activity id of the changed activity * @param name name of the changed activity */ void ActivityNameChanged(const QString &activity, const QString &name); /** * Emitted when an activity description is changed * @param activity id of the changed activity * @param description description of the changed activity */ void ActivityDescriptionChanged(const QString &activity, const QString &description); /** * Emitted when an activity icon is changed * @param activity id of the changed activity * @param icon name of the changed activity */ void ActivityIconChanged(const QString &activity, const QString &icon); /** * Emitted when an activity is changed (name, icon, or some other property) * @param activity id of the changed activity */ void ActivityChanged(const QString &activity); /** * Emitted when the state of activity is changed */ void ActivityStateChanged(const QString &activity, int state); private: D_PTR; }; #endif // ACTIVITIES_H diff --git a/src/service/Activities_p.h b/src/service/Activities_p.h index dae44f4..4a1e969 100644 --- a/src/service/Activities_p.h +++ b/src/service/Activities_p.h @@ -1,128 +1,132 @@ /* * Copyright (C) 2010 - 2016 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) 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 14 of version 3 of the license. * * 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, see . */ #ifndef ACTIVITIES_P_H #define ACTIVITIES_P_H // Self #include "Activities.h" // Qt #include #include #include // KDE #include #include class KSMServer; class Activities::Private : public QObject { Q_OBJECT public: Private(Activities *parent); ~Private() override; // Loads the last activity // the user has used void loadLastActivity(); // If the current activity is not running, // make some other activity current void ensureCurrentActivityIsRunning(); public Q_SLOTS: bool setCurrentActivity(const QString &activity); + bool previousActivity(); + bool nextActivity(); + void updateSortedActivityList(); public: void setActivityState(const QString &activity, Activities::State state); // Configuration class KDE4ConfigurationTransitionChecker { public: KDE4ConfigurationTransitionChecker(); } kde4ConfigurationTransitionChecker; QTimer configSyncTimer; KConfig config; // Interface to the session management KSMServer *ksmserver; QHash activities; + QVector sortedActivities; QReadWriteLock activitiesLock; QString currentActivity; public: inline KConfigGroup activityNameConfig() { return KConfigGroup(&config, "activities"); } inline KConfigGroup activityDescriptionConfig() { return KConfigGroup(&config, "activities-descriptions"); } inline KConfigGroup activityIconConfig() { return KConfigGroup(&config, "activities-icons"); } inline KConfigGroup mainConfig() { return KConfigGroup(&config, "main"); } inline QString activityName(const QString &activity) { return activityNameConfig().readEntry(activity, QString()); } inline QString activityDescription(const QString &activity) { return activityDescriptionConfig().readEntry(activity, QString()); } inline QString activityIcon(const QString &activity) { return activityIconConfig().readEntry(activity, QString()); } public Q_SLOTS: // Schedules config syncing to be done after // a predefined time interval void scheduleConfigSync(); // Immediately syncs the configuration file void configSync(); QString addActivity(const QString &name); void removeActivity(const QString &activity); void activitySessionStateChanged(const QString &activity, int state); private: Activities *const q; }; #endif // ACTIVITIES_P_H