File indexing completed on 2024-05-05 05:00:15

0001 /* This file is part of the KDE project
0002     SPDX-FileCopyrightText: 2023 Stefano Crocco <stefano.crocco@alice.it>
0003 
0004     SPDX-License-Identifier: LGPL-2.0-or-later
0005 */
0006 
0007 #include "activitymanager.h"
0008 
0009 #include "konqviewmanager.h"
0010 #include "konqmainwindow.h"
0011 #include "konqapplication.h"
0012 
0013 #include <QStandardPaths>
0014 
0015 #if QT_VERSION_MAJOR < 6
0016 #include <KActivities/Consumer>
0017 #else //QT_VERSION_MAJOR
0018 #include <PlasmaActivities/Consumer>
0019 #endif //QT_VERSION_MAJOR
0020 
0021 #include <KX11Extras>
0022 #include <KWindowInfo>
0023 #include <KSharedConfig>
0024 #include <KConfigGroup>
0025 
0026 #include <QTimer>
0027 
0028 ActivityManager::ActivityManager(QObject* parent) : QObject(parent), m_activitiesConsumer(new KActivities::Consumer(this))
0029 {
0030     connect(m_activitiesConsumer, &KActivities::Consumer::runningActivitiesChanged, this, &ActivityManager::handleRunningActivitiesChange);
0031     connect(m_activitiesConsumer, &KActivities::Consumer::activityRemoved, this, &ActivityManager::removeActivityState);
0032     connect(KX11Extras::self(), &KX11Extras::windowChanged, this, &ActivityManager::handleWindowChanged);
0033 }
0034 
0035 ActivityManager::~ActivityManager()
0036 {
0037 }
0038 
0039 QString ActivityManager::activitiesConfigPath()
0040 {
0041     static QString s_actitivitiesConfigPath = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QLatin1String("/activitiesrc");
0042     return s_actitivitiesConfigPath;
0043 }
0044 
0045 QString ActivityManager::activitiesGroupName()
0046 {
0047     static QString s_activitiesGroupName = QLatin1String("Activities");
0048     return s_activitiesGroupName;
0049 }
0050 
0051 void ActivityManager::closeWindowBecauseNotInRunningActivities(KonqMainWindow* window)
0052 {
0053     disconnect(window, &KonqMainWindow::closing, this, &ActivityManager::removeWindowFromActivities);
0054 
0055     //If this is the last window, don't close it, because it would interfere with activities management.
0056     //Closing the last window would close the whole application, so the global activity manager wouldn't
0057     //restart Konqueror when switching to the activity the window was moved to.
0058     //TODO activities: check what happens with preloaded windows
0059     QList<KonqMainWindow*>* allWindows = KonqMainWindow::mainWindowList();
0060     if (allWindows && allWindows->length() > 1) {
0061         window->close();
0062     }
0063 }
0064 
0065 void ActivityManager::handleRunningActivitiesChange(const QStringList& runningActivities)
0066 {
0067     QList<KonqMainWindow*> *windows = KonqMainWindow::mainWindowList();
0068     if (!windows) {
0069         return;
0070     }
0071 
0072     auto inRunningActivities = [runningActivities](const QStringList &activities) {
0073         //If activities is empty, it means that the window should be shown in all activities
0074         return activities.isEmpty() ||
0075             std::any_of(activities.constBegin(), activities.constEnd(), [runningActivities](const QString &a){return runningActivities.contains(a);});
0076     };
0077 
0078     QHash<KonqMainWindow*, QStringList> toClose;
0079     for (KonqMainWindow *w : *windows) {
0080         KWindowInfo info(w->winId(), NET::Properties(), NET::WM2Activities);
0081         QStringList activities = info.activities();
0082         if (!inRunningActivities(activities)) {
0083             if (!w->isPreloaded()) {
0084                 toClose.insert(w, activities);
0085             }
0086         }
0087     }
0088 
0089     QStringList titlesToClose;
0090     QList<KonqMainWindow*> keys = toClose.keys();
0091     std::transform(keys.constBegin(), keys.constEnd(), std::back_inserter(titlesToClose), [](KonqMainWindow *mw){return mw->windowTitle();});
0092 
0093     saveWindowsActivityInfo(toClose);
0094 
0095     QStringList existingUuidsList;
0096     std::transform(windows->constBegin(), windows->constEnd(), std::back_inserter(existingUuidsList), [](KonqMainWindow *w){return w->uuid();});
0097     QSet<QString> existingUuids(existingUuidsList.constBegin(), existingUuidsList.constEnd());
0098 
0099     QStringList expUuids;
0100     KConfigGroup grp = KSharedConfig::openConfig(activitiesConfigPath())->group(activitiesGroupName());
0101     for (const QString &act : runningActivities) {
0102         QStringList uuids = grp.readEntry(act, QStringList{});
0103         expUuids.append(uuids);
0104     }
0105     makeUnique(expUuids);
0106 
0107     QStringList uuidsToRestore;
0108     std::copy_if(expUuids.constBegin(), expUuids.constEnd(), std::back_inserter(uuidsToRestore), [existingUuids](const QString &u){return !existingUuids.contains(u);});
0109 
0110     if (!uuidsToRestore.isEmpty()) {
0111         m_restoringStoppedActivity = true;
0112         // In theory, this should be set by KonquerorApplication::performStart. This is just for safety, to avoid the risk that m_restoringStoppedActivity remains set to true
0113         QTimer::singleShot(2000, [this](){m_restoringStoppedActivity = false;});
0114     }
0115 
0116     for (const QString &uuid : existingUuids) {
0117         restoreWindowFromActivityState(uuid);
0118     }
0119 
0120     for (auto it = toClose.begin(); it != toClose.end(); ++it) {
0121         closeWindowBecauseNotInRunningActivities(it.key());
0122     }
0123 }
0124 
0125 void ActivityManager::saveWindowsActivityInfo(const QHash<KonqMainWindow *, QStringList>& windowsWithActivities)
0126 {
0127     KSharedConfig::Ptr config = KSharedConfig::openConfig(activitiesConfigPath());
0128 
0129     QStringList windowsUuids;
0130 
0131     QMultiHash <QString, QString> activities;
0132     for (auto it = windowsWithActivities.constBegin(); it != windowsWithActivities.constEnd(); ++it) {
0133         KonqMainWindow *w = it.key();
0134         windowsUuids.append(w->uuid());
0135         if (w->isPreloaded()) {
0136             continue;
0137         }
0138         KConfigGroup grp = config->group(w->uuid());
0139         w->saveProperties(grp);
0140         for (const QString & act : it.value()) {
0141             activities.insert(act, w->uuid());
0142         }
0143     }
0144 
0145     //Saving the windows belonging to each activity requires care:
0146     // - we can't just write the value of the entry in activities corresponding to each activity because there may
0147     //  be windows belonging to that activity which aren't included in windowsWithActivities and which must be preserved
0148     // - we can't just append the existing entries for each activity to the corresponding entry in activities because if
0149     //  one of the windows in windowsWithActivities used to belong to an activity but doesn't belong to it anymore, it
0150     //  wouldn't be removed (since it was in the existing entry and thus would remain)
0151     // What we do is the following:
0152     // - read the existing windows for each activity from the configuration file
0153     // - remove from this list all the windows in windowsWithActivities (whose uuids are in windowsUuids)
0154     // - append the remaining existing entries to the new ones (in uuids)
0155     KConfigGroup grp = config->group(activitiesGroupName());
0156     for (const QString & act : activities.uniqueKeys()) {
0157         QStringList uuids = activities.values(act);
0158         QStringList existingUuids = grp.readEntry(act, QStringList{});
0159         for (const QString &u : windowsUuids) {
0160             existingUuids.removeOne(u);
0161         }
0162         uuids.append(existingUuids);
0163         makeUnique(uuids);
0164         grp.writeEntry(act, uuids);
0165     }
0166 
0167     // grp = config->group("Windows UUID");
0168     // for (KonqMainWindow *w : windowsWithActivities.keys()) {
0169     //     grp.writeEntry(w->uuid(), w->windowTitle());
0170     // }
0171 
0172     config->sync();
0173 }
0174 
0175 void ActivityManager::handleWindowChanged(WId id, NET::Properties, NET::Properties2 prop2)
0176 {
0177     if (!(prop2 & NET::WM2Activities)) {
0178         return;
0179     }
0180     KonqMainWindow *w = qobject_cast<KonqMainWindow*>(QWidget::find(id));
0181     if (!w) {
0182         return;
0183     }
0184 
0185     KWindowInfo info(id, NET::Properties(), NET::WM2Activities);
0186     QStringList activities = info.activities();
0187     //activities will be empty if the window should be shown in all activities. In this case,
0188     //there's nothing we need to do
0189     if (activities.isEmpty()) {
0190         return;
0191     }
0192     QStringList runningActivities = m_activitiesConsumer->runningActivities();
0193     auto isRunning = [runningActivities](const QString &act){return runningActivities.contains(act);};
0194     if (std::any_of(activities.constBegin(), activities.constEnd(), isRunning)) {
0195         return;
0196     }
0197 
0198     QHash<KonqMainWindow*, QStringList> hash;
0199     hash.insert(w, activities);
0200     saveWindowsActivityInfo(hash);
0201     closeWindowBecauseNotInRunningActivities(w);
0202 }
0203 
0204 void ActivityManager::removeWindowFromActivities(KonqMainWindow* window)
0205 {
0206     QString uuid = window->uuid();
0207     KSharedConfig::Ptr config = KSharedConfig::openConfig(activitiesConfigPath());
0208 
0209     config->deleteGroup(uuid);
0210 
0211     KConfigGroup grp = config->group(activitiesGroupName());
0212     QStringList activities = grp.keyList();
0213     for (const QString &act : activities) {
0214         QStringList windows = grp.readEntry(act, QStringList{});
0215         //Remove the window uuid from the list of windows for the activity. If removeOne returns false, it means
0216         //the uuid wasn't in the the list, so there's no need to write it back
0217         if (windows.removeOne(uuid)) {
0218             grp.writeEntry(act, windows);
0219         }
0220     }
0221     config->sync();
0222 }
0223 
0224 void ActivityManager::removeActivityState(const QString& id)
0225 {
0226     KSharedConfig::Ptr config = KSharedConfig::openConfig(activitiesConfigPath());
0227 
0228     KConfigGroup activitiesGrp = config->group(activitiesGroupName());
0229 
0230     //We need to find out all windows which only belongs to the activity id
0231     //and remove all information about them
0232     QStringList activities = activitiesGrp.keyList();
0233     QStringList uuids = activitiesGrp.readEntry(id, QStringList{});
0234     QStringList otherActivitiesUuids;
0235     for (const QString &act : activities) {
0236         if (act != id) {
0237             otherActivitiesUuids.append(activitiesGrp.readEntry(act, QStringList{}));
0238         }
0239     }
0240     makeUnique(otherActivitiesUuids);
0241     for (const QString &uuid : uuids) {
0242         if (!otherActivitiesUuids.contains(uuid)) {
0243             config->deleteGroup(uuid);
0244         }
0245     }
0246 
0247     activitiesGrp.deleteEntry(id);
0248 
0249     config->sync();
0250 }
0251 
0252 KonqMainWindow* ActivityManager::restoreWindowFromActivityState(const QString& uuid)
0253 {
0254     //WARNING: for efficiency reasons, this method assumes no window with the given uuid exists. It's up to the caller to make sure of that
0255 
0256     KSharedConfig::Ptr conf = KSharedConfig::openConfig(activitiesConfigPath());
0257     KConfigGroup windowGrp = conf->group(uuid);
0258     KConfigGroup activitiesGrp = conf->group(activitiesGroupName());
0259 
0260     if (!windowGrp.exists()) {
0261         return nullptr;
0262     }
0263     KonqMainWindow *w = KonqViewManager::openSavedWindow(windowGrp);
0264     if (!w) {
0265         return nullptr;
0266     }
0267 
0268     // QStringList activities;
0269     // QStringList activitiesEntries = activitiesGrp.keyList();
0270     // auto activityHasWindow = [uuid, activitiesGrp] (const QString &act) {
0271     //     return activitiesGrp.readEntry(act, QStringList{}).contains(uuid);
0272     // };
0273     // std::copy_if(activitiesEntries.constBegin(), activitiesEntries.constEnd(), std::back_inserter(activities), activityHasWindow);
0274     // KX11Extras::setOnActivities(w->winId(), activities);
0275     w->show();
0276 
0277     //Don't keep information about the window, since they would become outdated
0278     conf->deleteGroup(uuid);
0279     conf->sync();
0280 
0281     return w;
0282 }
0283 
0284 void ActivityManager::registerMainWindow(KonqMainWindow* window)
0285 {
0286     connect(window, &KonqMainWindow::closing, this, &ActivityManager::removeWindowFromActivities);
0287 }
0288 
0289 void ActivityManager::makeUnique(QStringList& lst)
0290 {
0291     if (lst.isEmpty()) {
0292         return;
0293     }
0294     lst.sort();
0295     auto last = std::unique(lst.begin(), lst.end());
0296     lst.erase(last, lst.end());
0297 }