QortalOS Brooklyn for Raspberry Pi 4
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

555 lines
17 KiB

/*
SPDX-FileCopyrightText: 2016 Eike Hein <[email protected]>
SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
*/
#include "launchertasksmodel.h"
#include "tasktools.h"
#include <KDesktopFile>
#include <KNotificationJobUiDelegate>
#include <KService>
#include <KStartupInfo>
#include <KSycoca>
#include <KWindowSystem>
#include <KActivities/Consumer>
#include <KActivities/ResourceInstance>
#include <KIO/ApplicationLauncherJob>
#include <config-X11.h>
#include <QHash>
#include <QIcon>
#include <QSet>
#include <QTimer>
#include <QUrlQuery>
#if HAVE_X11
#include <QX11Info>
#endif
#include "launchertasksmodel_p.h"
namespace TaskManager
{
typedef QSet<QString> ActivitiesSet;
template<typename ActivitiesCollection>
inline bool isOnAllActivities(const ActivitiesCollection &activities)
{
return activities.isEmpty() || activities.contains(NULL_UUID);
}
class Q_DECL_HIDDEN LauncherTasksModel::Private
{
public:
Private(LauncherTasksModel *q);
KActivities::Consumer activitiesConsumer;
QList<QUrl> launchersOrder;
QHash<QUrl, ActivitiesSet> activitiesForLauncher;
inline void setActivitiesForLauncher(const QUrl &url, const ActivitiesSet &activities)
{
if (activities.size() == activitiesConsumer.activities().size()) {
activitiesForLauncher[url] = {NULL_UUID};
} else {
activitiesForLauncher[url] = activities;
}
}
QHash<QUrl, AppData> appDataCache;
QTimer sycocaChangeTimer;
void init();
AppData appData(const QUrl &url);
bool requestAddLauncherToActivities(const QUrl &_url, const QStringList &activities);
bool requestRemoveLauncherFromActivities(const QUrl &_url, const QStringList &activities);
private:
LauncherTasksModel *q;
};
LauncherTasksModel::Private::Private(LauncherTasksModel *q)
: q(q)
{
}
void LauncherTasksModel::Private::init()
{
sycocaChangeTimer.setSingleShot(true);
sycocaChangeTimer.setInterval(100);
QObject::connect(&sycocaChangeTimer, &QTimer::timeout, q, [this]() {
if (!launchersOrder.count()) {
return;
}
appDataCache.clear();
// Emit changes of all roles satisfied from app data cache.
Q_EMIT q->dataChanged(q->index(0, 0),
q->index(launchersOrder.count() - 1, 0),
QVector<int>{Qt::DisplayRole,
Qt::DecorationRole,
AbstractTasksModel::AppId,
AbstractTasksModel::AppName,
AbstractTasksModel::GenericName,
AbstractTasksModel::LauncherUrl,
AbstractTasksModel::LauncherUrlWithoutIcon});
});
QObject::connect(KSycoca::self(), &KSycoca::databaseChanged, q, [this]() {
sycocaChangeTimer.start();
});
}
AppData LauncherTasksModel::Private::appData(const QUrl &url)
{
const auto &it = appDataCache.constFind(url);
if (it != appDataCache.constEnd()) {
return *it;
}
const AppData &data = appDataFromUrl(url, QIcon::fromTheme(QLatin1String("unknown")));
appDataCache.insert(url, data);
return data;
}
bool LauncherTasksModel::Private::requestAddLauncherToActivities(const QUrl &_url, const QStringList &_activities)
{
QUrl url(_url);
if (!isValidLauncherUrl(url)) {
return false;
}
const auto activities = ActivitiesSet(_activities.cbegin(), _activities.cend());
if (url.isLocalFile() && KDesktopFile::isDesktopFile(url.toLocalFile())) {
KDesktopFile f(url.toLocalFile());
const KService::Ptr service = KService::serviceByStorageId(f.fileName());
// Resolve to non-absolute menuId-based URL if possible.
if (service) {
const QString &menuId = service->menuId();
if (!menuId.isEmpty()) {
url = QUrl(QLatin1String("applications:") + menuId);
}
}
}
// Merge duplicates
int row = -1;
foreach (const QUrl &launcher, launchersOrder) {
++row;
if (launcherUrlsMatch(url, launcher, IgnoreQueryItems)) {
ActivitiesSet newActivities;
// Use the key we established equivalence to ('launcher').
if (!activitiesForLauncher.contains(launcher)) {
// If we don't have the activities assigned to this url
// for some reason
newActivities = activities;
} else {
if (isOnAllActivities(activities)) {
// If the new list is empty, or has a null uuid, this
// launcher should be on all activities
newActivities = ActivitiesSet{NULL_UUID};
} else if (isOnAllActivities(activitiesForLauncher[launcher])) {
// If we have been on all activities before, and we have
// been asked to be on a specific one, lets make an
// exception - we will set the activities to exactly
// what we have been asked
newActivities = activities;
} else {
newActivities += activities;
newActivities += activitiesForLauncher[launcher];
}
}
if (newActivities != activitiesForLauncher[launcher]) {
setActivitiesForLauncher(launcher, newActivities);
Q_EMIT q->dataChanged(q->index(row, 0), q->index(row, 0));
Q_EMIT q->launcherListChanged();
return true;
}
return false;
}
}
// This is a new one
const auto count = launchersOrder.count();
q->beginInsertRows(QModelIndex(), count, count);
setActivitiesForLauncher(url, activities);
launchersOrder.append(url);
q->endInsertRows();
Q_EMIT q->launcherListChanged();
return true;
}
bool LauncherTasksModel::Private::requestRemoveLauncherFromActivities(const QUrl &url, const QStringList &activities)
{
for (int row = 0; row < launchersOrder.count(); ++row) {
const QUrl &launcher = launchersOrder.at(row);
if (launcherUrlsMatch(url, launcher, IgnoreQueryItems) || launcherUrlsMatch(url, appData(launcher).url, IgnoreQueryItems)) {
const auto currentActivities = activitiesForLauncher[url];
ActivitiesSet newActivities;
bool remove = false;
bool update = false;
if (isOnAllActivities(currentActivities)) {
// We are currently on all activities.
// Should we go away, or just remove from the current one?
if (isOnAllActivities(activities)) {
remove = true;
} else {
const auto _activities = activitiesConsumer.activities();
for (const auto &activity : _activities) {
if (!activities.contains(activity)) {
newActivities << activity;
} else {
update = true;
}
}
}
} else if (isOnAllActivities(activities)) {
remove = true;
} else {
// We weren't on all activities, just remove those that
// we were on
for (const auto &activity : currentActivities) {
if (!activities.contains(activity)) {
newActivities << activity;
}
}
if (newActivities.isEmpty()) {
remove = true;
} else {
update = true;
}
}
if (remove) {
q->beginRemoveRows(QModelIndex(), row, row);
launchersOrder.removeAt(row);
activitiesForLauncher.remove(url);
appDataCache.remove(launcher);
q->endRemoveRows();
} else if (update) {
setActivitiesForLauncher(url, newActivities);
Q_EMIT q->dataChanged(q->index(row, 0), q->index(row, 0));
}
if (remove || update) {
Q_EMIT q->launcherListChanged();
return true;
}
}
}
return false;
}
LauncherTasksModel::LauncherTasksModel(QObject *parent)
: AbstractTasksModel(parent)
, d(new Private(this))
{
d->init();
}
LauncherTasksModel::~LauncherTasksModel()
{
}
QVariant LauncherTasksModel::data(const QModelIndex &index, int role) const
{
if (!index.isValid() || index.row() >= d->launchersOrder.count()) {
return QVariant();
}
const QUrl &url = d->launchersOrder.at(index.row());
const AppData &data = d->appData(url);
if (role == Qt::DisplayRole) {
return data.name;
} else if (role == Qt::DecorationRole) {
return data.icon;
} else if (role == AppId) {
return data.id;
} else if (role == AppName) {
return data.name;
} else if (role == GenericName) {
return data.genericName;
} else if (role == LauncherUrl) {
// Take resolved URL from cache.
return data.url;
} else if (role == LauncherUrlWithoutIcon) {
// Take resolved URL from cache.
QUrl url = data.url;
if (url.hasQuery()) {
QUrlQuery query(url);
query.removeQueryItem(QLatin1String("iconData"));
url.setQuery(query);
}
return url;
} else if (role == IsLauncher) {
return true;
} else if (role == IsVirtualDesktopsChangeable) {
return false;
} else if (role == IsOnAllVirtualDesktops) {
return true;
} else if (role == Activities) {
return QStringList(d->activitiesForLauncher[url].values());
} else if (role == CanLaunchNewInstance) {
return false;
}
return QVariant();
}
int LauncherTasksModel::rowCount(const QModelIndex &parent) const
{
return parent.isValid() ? 0 : d->launchersOrder.count();
}
QStringList LauncherTasksModel::launcherList() const
{
// Serializing the launchers
QStringList result;
for (const auto &launcher : qAsConst(d->launchersOrder)) {
const auto &activities = d->activitiesForLauncher[launcher];
QString serializedLauncher;
if (isOnAllActivities(activities)) {
serializedLauncher = launcher.toString();
} else {
serializedLauncher = "[" + d->activitiesForLauncher[launcher].values().join(",") + "]\n" + launcher.toString();
}
result << serializedLauncher;
}
return result;
}
void LauncherTasksModel::setLauncherList(const QStringList &serializedLaunchers)
{
// Clearing everything
QList<QUrl> newLaunchersOrder;
QHash<QUrl, ActivitiesSet> newActivitiesForLauncher;
// Loading the activity to launchers map
for (const auto &serializedLauncher : serializedLaunchers) {
QStringList _activities;
QUrl url;
std::tie(url, _activities) = deserializeLauncher(serializedLauncher);
auto activities = ActivitiesSet(_activities.cbegin(), _activities.cend());
// Is url is not valid, ignore it
if (!isValidLauncherUrl(url)) {
continue;
}
// If we have a null uuid, it means we are on all activities
// and we should contain only the null uuid
if (isOnAllActivities(activities)) {
activities = {NULL_UUID};
} else {
// Filter out invalid activities
const auto allActivities = d->activitiesConsumer.activities();
ActivitiesSet validActivities;
for (const auto &activity : qAsConst(activities)) {
if (allActivities.contains(activity)) {
validActivities << activity;
}
}
if (validActivities.isEmpty()) {
// If all activities that had this launcher are
// removed, we are killing the launcher as well
continue;
}
activities = validActivities;
}
// Is the url a duplicate?
const auto location = std::find_if(newLaunchersOrder.begin(), newLaunchersOrder.end(), [&url](const QUrl &item) {
return launcherUrlsMatch(url, item, IgnoreQueryItems);
});
if (location != newLaunchersOrder.end()) {
// It is a duplicate
url = *location;
} else {
// It is not a duplicate, we need to add it
// to the list of registered launchers
newLaunchersOrder << url;
}
if (!newActivitiesForLauncher.contains(url)) {
// This is the first time we got this url
newActivitiesForLauncher[url] = activities;
} else if (newActivitiesForLauncher[url].contains(NULL_UUID)) {
// Do nothing, we are already on all activities
} else if (activities.contains(NULL_UUID)) {
newActivitiesForLauncher[url] = {NULL_UUID};
} else {
// We are not on all activities, append the new ones
newActivitiesForLauncher[url] += activities;
}
}
if (newLaunchersOrder != d->launchersOrder || newActivitiesForLauncher != d->activitiesForLauncher) {
beginResetModel();
std::swap(newLaunchersOrder, d->launchersOrder);
std::swap(newActivitiesForLauncher, d->activitiesForLauncher);
d->appDataCache.clear();
endResetModel();
Q_EMIT launcherListChanged();
}
}
bool LauncherTasksModel::requestAddLauncher(const QUrl &url)
{
return d->requestAddLauncherToActivities(url, {NULL_UUID});
}
bool LauncherTasksModel::requestRemoveLauncher(const QUrl &url)
{
return d->requestRemoveLauncherFromActivities(url, {NULL_UUID});
}
bool LauncherTasksModel::requestAddLauncherToActivity(const QUrl &url, const QString &activity)
{
return d->requestAddLauncherToActivities(url, {activity});
}
bool LauncherTasksModel::requestRemoveLauncherFromActivity(const QUrl &url, const QString &activity)
{
return d->requestRemoveLauncherFromActivities(url, {activity});
}
QStringList LauncherTasksModel::launcherActivities(const QUrl &_url) const
{
const auto position = launcherPosition(_url);
if (position == -1) {
// If we do not have this launcher, return an empty list
return {};
} else {
const auto url = d->launchersOrder.at(position);
// If the launcher is on all activities, return a null uuid
return d->activitiesForLauncher.contains(url) ? d->activitiesForLauncher[url].values() : QStringList{NULL_UUID};
}
}
int LauncherTasksModel::launcherPosition(const QUrl &url) const
{
for (int i = 0; i < d->launchersOrder.count(); ++i) {
if (launcherUrlsMatch(url, d->appData(d->launchersOrder.at(i)).url, IgnoreQueryItems)) {
return i;
}
}
return -1;
}
void LauncherTasksModel::requestActivate(const QModelIndex &index)
{
requestNewInstance(index);
}
void LauncherTasksModel::requestNewInstance(const QModelIndex &index)
{
if (!index.isValid() || index.model() != this || index.row() < 0 || index.row() >= d->launchersOrder.count()) {
return;
}
runApp(d->appData(d->launchersOrder.at(index.row())));
}
void LauncherTasksModel::requestOpenUrls(const QModelIndex &index, const QList<QUrl> &urls)
{
if (!index.isValid() || index.model() != this || index.row() < 0 || index.row() >= d->launchersOrder.count() || urls.isEmpty()) {
return;
}
const QUrl &url = d->launchersOrder.at(index.row());
quint32 timeStamp = 0;
#if HAVE_X11
if (KWindowSystem::isPlatformX11()) {
timeStamp = QX11Info::appUserTime();
}
#endif
KService::Ptr service;
if (url.scheme() == QLatin1String("applications")) {
service = KService::serviceByMenuId(url.path());
} else if (url.scheme() == QLatin1String("preferred")) {
service = KService::serviceByStorageId(defaultApplication(url));
} else {
service = KService::serviceByDesktopPath(url.toLocalFile());
}
if (!service || !service->isApplication()) {
return;
}
auto *job = new KIO::ApplicationLauncherJob(service);
job->setUiDelegate(new KNotificationJobUiDelegate(KJobUiDelegate::AutoErrorHandlingEnabled));
job->setUrls(urls);
job->setStartupId(KStartupInfo::createNewStartupIdForTimestamp(timeStamp));
job->start();
KActivities::ResourceInstance::notifyAccessed(QUrl(QStringLiteral("applications:") + service->storageId()), QStringLiteral("org.kde.libtaskmanager"));
}
}