forked from Qortal/Brooklyn
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
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")); |
|
} |
|
|
|
}
|
|
|