🏡 index : quine-global-labs/mirror-kde-plasma-desktop.git

/*
    SPDX-FileCopyrightText: 2016 Ivan Cukic <ivan.cukic(at)kde.org>

    SPDX-License-Identifier: GPL-2.0-or-later
*/

// Self
#include "sortedactivitiesmodel.h"

// C++
#include <abstracttasksmodel.h>
#include <windowtasksmodel.h>

// Qt
#include <QColor>
#include <QDir>
#include <QFileInfo>
#include <QObject>
#include <QTimer>
#include <QUrl>

// KDE
#include <KConfigGroup>
#include <KDirWatch>
#include <KLocalizedString>
#include <KSharedConfig>

static const char *s_plasma_config = "plasma-org.kde.plasma.desktop-appletsrc";

using namespace Qt::Literals;

namespace
{
class BackgroundCache : public QObject
{
public:
    BackgroundCache()
        : initialized(false)
        , plasmaConfig(KSharedConfig::openConfig(QString::fromLatin1(s_plasma_config)))
    {
        using namespace std::placeholders;

        const QString configFile = QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation) + QLatin1Char{'/'} + QLatin1String{s_plasma_config};

        KDirWatch::self()->addFile(configFile);

        QObject::connect(KDirWatch::self(), &KDirWatch::dirty, this, &BackgroundCache::settingsFileChanged, Qt::QueuedConnection);
        QObject::connect(KDirWatch::self(), &KDirWatch::created, this, &BackgroundCache::settingsFileChanged, Qt::QueuedConnection);
    }

    void settingsFileChanged(const QString &file)
    {
        if (!file.endsWith(QLatin1String{s_plasma_config})) {
            return;
        }

        if (initialized) {
            plasmaConfig->reparseConfiguration();
            reload();
        }
    }

    void subscribe(SortedActivitiesModel *model)
    {
        if (!initialized) {
            reload();
        }

        models << model;
    }

    void unsubscribe(SortedActivitiesModel *model)
    {
        models.removeAll(model);

        if (models.isEmpty()) {
            initialized = false;
            forActivity.clear();
        }
    }

    QSize parseSize(QStringView str) const
    {
        const int index = str.indexOf(QLatin1Char('x'));

        if (index != -1) {
            return QSize(str.left(index).toInt(), str.mid(index + 1).toInt());
        }

        return QSize();
    }

    double distance(const QSize &size, const QSize &desired) const
    {
        const double desiredAspectRatio = (desired.height() > 0) ? desired.width() / static_cast<double>(desired.height()) : 0;
        const double candidateAspectRatio = (size.height() > 0) ? size.width() / static_cast<double>(size.height()) : std::numeric_limits<double>::max();

        double delta = size.width() - desired.width();
        delta = delta >= 0.0 ? delta : -delta * 2; // Penalize for scaling up

        return std::abs(candidateAspectRatio - desiredAspectRatio) * 25000 + delta;
    }

    QString findImageInWallpaperPackage(const QString &folderName) const
    {
        QString preferred;
        const QStringList images = QDir(folderName).entryList();

        if (images.empty()) {
            return preferred;
        }

        double best = std::numeric_limits<double>::max();

        for (const QString &entry : images) {
            QSize candidate = parseSize(QFileInfo(entry).baseName());

            if (candidate.isEmpty()) {
                continue;
            }

            const double dist = distance(candidate, QSize(1920, 1080));

            if (preferred.isEmpty() || dist < best) {
                preferred = entry;
                best = dist;
            }
        }

        return folderName + u"/" + preferred;
    }

    QString backgroundFromConfig(const KConfigGroup &config) const
    {
        auto wallpaperPlugin = config.readEntry("wallpaperplugin");
        auto wallpaperConfig = config.group(QStringLiteral("Wallpaper")).group(wallpaperPlugin).group(QStringLiteral("General"));

        if (wallpaperPlugin == u"org.kde.image") {
            // we can have three cases:
            // - Image is a URL to a specific image -> use that
            // - Image is a URL to a wallpaper package folder -> pick one image from that folder
            // - Image is empty -> use default wallpaper

            const QUrl defaultWallpaper =
                QUrl::fromLocalFile(QStandardPaths::locate(QStandardPaths::GenericDataLocation, u"wallpapers/Next"_s, QStandardPaths::LocateDirectory));

            const QUrl wallpaper = wallpaperConfig.readEntry("Image", defaultWallpaper);

            if (QFileInfo(wallpaper.toLocalFile()).isDir()) {
                return findImageInWallpaperPackage(wallpaper.toLocalFile() + u"/contents/images");
            } else {
                return wallpaper.toString();
            }
        } else if (wallpaperPlugin == u"org.kde.color") {
            auto backgroundColor = wallpaperConfig.readEntry("Color", QColor(29, 153, 243));
            return backgroundColor.name();
        }

        return QString();
    }

    void reload()
    {
        auto newForActivity = forActivity;
        QHash<QString, int> lastScreenForActivity;

        // contains activities for which the wallpaper
        // has updated
        QStringList changedActivities;

        // Contains activities not covered by any containment
        QStringList ghostActivities = forActivity.keys();

        // Traversing through all containments in search for
        // containments that define activities in plasma
        for (const auto &containmentId : plasmaConfigContainments().groupList()) {
            const auto containment = plasmaConfigContainments().group(containmentId);
            const auto lastScreen = containment.readEntry("lastScreen", 0);
            const auto activity = containment.readEntry("activityId", QString());

            // Ignore the containment if the activity is not defined
            if (activity.isEmpty())
                continue;

            // If we have already found the same activity from another
            // containment, we are using the new one only if
            // the previous one was a color and not a proper wallpaper,
            // or if the screen ID is closer to zero
            const bool processed = !ghostActivities.contains(activity) && newForActivity.contains(activity) && (lastScreenForActivity[activity] <= lastScreen);

            // qDebug() << "GREPME Searching containment " << containmentId
            //          << "for the wallpaper of the " << activity << " activity - "
            //          << "currently, we think that the wallpaper is " << processed << (processed ? newForActivity[activity] : QString())
            //          << "last screen is" << lastScreen
            //          ;

            if (processed && !newForActivity[activity].startsWith(QLatin1Char{'#'}))
                continue;

            // Marking the current activity as processed
            ghostActivities.removeAll(activity);

            const auto background = backgroundFromConfig(containment);

            // qDebug() << "        GREPME Found wallpaper: " << background;

            if (background.isEmpty())
                continue;

            // If we got this far and we already had a new wallpaper for
            // this activity, it means we now have a better one
            bool foundBetterWallpaper = changedActivities.contains(activity);

            if (foundBetterWallpaper || newForActivity[activity] != background) {
                if (!foundBetterWallpaper) {
                    changedActivities << activity;
                }

                // qDebug() << "        GREPME Setting: " << activity << " = " << background << "," << lastScreen;
                newForActivity[activity] = background;
                lastScreenForActivity[activity] = lastScreen;
            }
        }

        initialized = true;

        // Removing the activities from the list if we haven't found them
        // while traversing through the containments
        for (const auto &activity : std::as_const(ghostActivities)) {
            newForActivity.remove(activity);
        }

        // If we have detected the changes, lets notify everyone
        if (!changedActivities.isEmpty()) {
            forActivity = newForActivity;

            for (auto model : std::as_const(models)) {
                model->onBackgroundsUpdated(changedActivities);
            }
        }
    }

    KConfigGroup plasmaConfigContainments()
    {
        return plasmaConfig->group(QStringLiteral("Containments"));
    }

    QHash<QString, QString> forActivity;
    QList<SortedActivitiesModel *> models;

    bool initialized;
    KSharedConfig::Ptr plasmaConfig;
};

static BackgroundCache &backgrounds()
{
    // If you convert this to a shared pointer,
    // fix the connections to KDirWatcher
    static BackgroundCache cache;
    return cache;
}

}

SortedActivitiesModel::SortedActivitiesModel(QObject *parent)
    : QSortFilterProxyModel(parent)
    , m_windowTasksModel(new TaskManager::WindowTasksModel(this))
    , m_activitiesModel(new KActivities::ActivitiesModel(this))
    , m_activities(new KActivities::Consumer(this))
{
    setSourceModel(m_activitiesModel);

    setDynamicSortFilter(true);
    setSortRole(LastTimeUsed);
    sort(0, Qt::DescendingOrder);

    backgrounds().subscribe(this);

    connect(m_windowTasksModel, &TaskManager::WindowTasksModel::rowsInserted, this, &SortedActivitiesModel::onWindowAdded);
    // Using rowsAboutToBeRemoved because we can't fetch data from already removed rows
    connect(m_windowTasksModel, &TaskManager::WindowTasksModel::rowsAboutToBeRemoved, this, &SortedActivitiesModel::onWindowRemoved);
    connect(m_windowTasksModel, &TaskManager::WindowTasksModel::dataChanged, this, &SortedActivitiesModel::onWindowChanged);

    // Update windows at start
    onWindowAdded(QModelIndex(), 0, m_windowTasksModel->rowCount());
}

SortedActivitiesModel::~SortedActivitiesModel()
{
    backgrounds().unsubscribe(this);
}

bool SortedActivitiesModel::inhibitUpdates() const
{
    return m_inhibitUpdates;
}

void SortedActivitiesModel::setInhibitUpdates(bool inhibitUpdates)
{
    if (m_inhibitUpdates != inhibitUpdates) {
        m_inhibitUpdates = inhibitUpdates;
        Q_EMIT inhibitUpdatesChanged(m_inhibitUpdates);

        setDynamicSortFilter(!inhibitUpdates);
    }
}

uint SortedActivitiesModel::lastUsedTime(const QString &activity) const
{
    if (m_activities->currentActivity() == activity) {
        return ~(uint)0;

    } else {
        KConfig config(QStringLiteral("plasma/activitiesstaterc"), KConfig::SimpleConfig, QStandardPaths::GenericStateLocation);
        KConfigGroup times(&config, QStringLiteral("LastUsed"));

        return times.readEntry(activity, (uint)0);
    }
}

bool SortedActivitiesModel::lessThan(const QModelIndex &sourceLeft, const QModelIndex &sourceRight) const
{
    const auto activityLeft = sourceModel()->data(sourceLeft, KActivities::ActivitiesModel::ActivityId).toString();
    const auto activityRight = sourceModel()->data(sourceRight, KActivities::ActivitiesModel::ActivityId).toString();

    const auto timeLeft = lastUsedTime(activityLeft);
    const auto timeRight = lastUsedTime(activityRight);

    return (timeLeft < timeRight) || (timeLeft == timeRight && activityLeft < activityRight);
}

QHash<int, QByteArray> SortedActivitiesModel::roleNames() const
{
    if (!sourceModel())
        return QHash<int, QByteArray>();

    auto roleNames = sourceModel()->roleNames();

    roleNames[LastTimeUsed] = QByteArrayLiteral("lastTimeUsed");
    roleNames[LastTimeUsedString] = QByteArrayLiteral("lastTimeUsedString");
    roleNames[WindowCount] = QByteArrayLiteral("windowCount");
    roleNames[HasWindows] = QByteArrayLiteral("hasWindows");

    return roleNames;
}

QVariant SortedActivitiesModel::data(const QModelIndex &index, int role) const
{
    if (role == KActivities::ActivitiesModel::ActivityBackground) {
        const auto activity = activityIdForIndex(index);

        return backgrounds().forActivity[activity];

    } else if (role == LastTimeUsed || role == LastTimeUsedString) {
        const auto activity = activityIdForIndex(index);

        const auto time = lastUsedTime(activity);

        if (role == LastTimeUsed) {
            return QVariant(time);

        } else {
            const auto now = QDateTime::currentSecsSinceEpoch();

            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 more than a year 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 if (role == HasWindows || role == WindowCount) {
        const auto activity = activityIdForIndex(index);

        if (role == HasWindows) {
            return (m_activitiesWindows[activity].size() > 0);
        } else {
            return m_activitiesWindows[activity].size();
        }

    } else {
        return QSortFilterProxyModel::data(index, role);
    }
}

QString SortedActivitiesModel::activityIdForIndex(const QModelIndex &index) const
{
    return data(index, KActivities::ActivitiesModel::ActivityId).toString();
}

QString SortedActivitiesModel::activityIdForRow(int row) const
{
    return activityIdForIndex(index(row, 0));
}

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();

    // x % 0 is undefined in c++
    if (currentRowCount == 0) {
        return QString();
    }

    int currentActivityRow = 0;

    for (; currentActivityRow < currentRowCount; currentActivityRow++) {
        if (activityIdForRow(currentActivityRow) == currentActivity)
            break;
    }

    currentActivityRow = currentActivityRow + relative;

    // wrap to within bounds for both positive and negative currentActivityRows
    currentActivityRow = (currentRowCount + (currentActivityRow % currentRowCount)) % currentRowCount;

    return activityIdForRow(currentActivityRow);
}

void SortedActivitiesModel::onCurrentActivityChanged(const QString &currentActivity)
{
    if (m_previousActivity == currentActivity)
        return;

    const int previousActivityRow = rowForActivityId(m_previousActivity);
    rowChanged(previousActivityRow, {LastTimeUsed, LastTimeUsedString});

    m_previousActivity = currentActivity;

    const int currentActivityRow = rowForActivityId(m_previousActivity);
    rowChanged(currentActivityRow, {LastTimeUsed, LastTimeUsedString});
}

void SortedActivitiesModel::onBackgroundsUpdated(const QStringList &activities)
{
    for (const auto &activity : activities) {
        const int row = rowForActivityId(activity);
        rowChanged(row, {KActivities::ActivitiesModel::ActivityBackground});
    }
}

void SortedActivitiesModel::onWindowAdded(const QModelIndex &parent, int first, int last)
{
    for (int row = first; row <= last; row++) {
        auto window = m_windowTasksModel->index(row, 0, parent);
        const QStringList activities = window.data(TaskManager::AbstractTasksModel::Activities).toStringList();
        auto winIds = getWinIdList(parent, row);

        for (const auto &activity : activities) {
            if (!m_activitiesWindows[activity].contains(winIds)) {
                m_activitiesWindows[activity].append(winIds);

                rowChanged(rowForActivityId(activity),
                           m_activitiesWindows[activity].size() == 1 //
                               ? QList<int>{WindowCount, HasWindows}
                               : QList<int>{WindowCount});
            }
        }
    }
}

void SortedActivitiesModel::onWindowRemoved(const QModelIndex &parent, int first, int last)
{
    for (int row = first; row <= last; row++) {
        auto winIds = getWinIdList(parent, row);

        for (const auto &activity : m_activitiesWindows.keys()) {
            if (m_activitiesWindows[activity].contains(winIds)) {
                m_activitiesWindows[activity].removeAll(winIds);

                rowChanged(rowForActivityId(activity),
                           m_activitiesWindows[activity].size() == 0 //
                               ? QList<int>{WindowCount, HasWindows}
                               : QList<int>{WindowCount});
            }
        }
    }
}

void SortedActivitiesModel::onWindowChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QList<int> &roles)
{
    // If Activities are changed, remove and add the window again to correct activity
    if (roles.contains(TaskManager::AbstractTasksModel::Activities) || roles.isEmpty()) {
        onWindowRemoved(topLeft.parent(), topLeft.row(), bottomRight.row());
        onWindowAdded(topLeft.parent(), topLeft.row(), bottomRight.row());
    }
}

QVariant SortedActivitiesModel::getWinIdList(const QModelIndex &parent, int row)
{
    return m_windowTasksModel->index(row, 0, parent).data(TaskManager::AbstractTasksModel::WinIdList);
}

void SortedActivitiesModel::rowChanged(int row, const QList<int> &roles)
{
    if (row == -1)
        return;
    Q_EMIT dataChanged(index(row, 0), index(row, 0), roles);
}

#include "moc_sortedactivitiesmodel.cpp"