diff --git a/CMakeLists.txt b/CMakeLists.txt --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -132,6 +132,7 @@ add_subdirectory(doc) add_subdirectory(libkworkspace) +add_subdirectory(libtaskmanager) add_subdirectory(liblegacytaskmanager) add_subdirectory(components) diff --git a/libtaskmanager/CMakeLists.txt b/libtaskmanager/CMakeLists.txt new file mode 100644 --- /dev/null +++ b/libtaskmanager/CMakeLists.txt @@ -0,0 +1,105 @@ +add_subdirectory(declarative) +add_subdirectory(autotests) + +set(taskmanager_LIB_SRCS + abstracttasksmodel.cpp + activityinfo.cpp + concatenatetasksproxymodel.cpp + launchertasksmodel.cpp + startuptasksmodel.cpp + taskfilterproxymodel.cpp + taskgroupingproxymodel.cpp + tasksmodel.cpp + tasktools.cpp + virtualdesktopinfo.cpp + waylandtasksmodel.cpp +) + +if (X11_FOUND) + set(taskmanager_LIB_SRCS + ${taskmanager_LIB_SRCS} + xwindowtasksmodel.cpp + ) +endif() + +add_library(taskmanager ${taskmanager_LIB_SRCS}) +add_library(PW::LibTaskManager ALIAS taskmanager) + +generate_export_header(taskmanager) + +target_include_directories(taskmanager PUBLIC "$" "$") + +target_link_libraries(taskmanager + PUBLIC + Qt5::Core + Qt5::Gui + Qt5::Quick + KF5::WaylandClient + PRIVATE + KF5::Activities + KF5::ItemModels + KF5::ConfigCore + KF5::KIOCore + KF5::KIOWidgets + KF5::ProcessCore + KF5::WindowSystem +) + +if (X11_FOUND) + target_link_libraries(taskmanager + PRIVATE + Qt5::X11Extras + KF5::IconThemes) +endif() + +set_target_properties(taskmanager PROPERTIES + VERSION ${PROJECT_VERSION} + SOVERSION ${PROJECT_VERSION_MAJOR} + EXPORT_NAME LibTaskManager) + +install(TARGETS taskmanager EXPORT libtaskmanagerLibraryTargets ${KDE_INSTALL_TARGETS_DEFAULT_ARGS} ) + +install(FILES + abstracttasksmodel.h + abstracttasksmodeliface.h + activityinfo.h + concatenatetasksproxymodel.h + launchertasksmodel.h + startuptasksmodel.h + taskfilterproxymodel.h + taskgroupingproxymodel.h + tasksmodel.h + tasktools.h + virtualdesktopinfo.h + waylandtasksmodel.h + ${CMAKE_CURRENT_BINARY_DIR}/taskmanager_export.h + DESTINATION ${KDE_INSTALL_INCLUDEDIR}/taskmanager COMPONENT Devel +) + +if (X11_FOUND) + install(FILES + xwindowtasksmodel.h + ${CMAKE_CURRENT_BINARY_DIR}/taskmanager_export.h + DESTINATION ${KDE_INSTALL_INCLUDEDIR}/taskmanager COMPONENT Devel + ) +endif() + +write_basic_config_version_file(${CMAKE_CURRENT_BINARY_DIR}/LibTaskManagerConfigVersion.cmake VERSION "${PROJECT_VERSION}" COMPATIBILITY AnyNewerVersion) + +set(CMAKECONFIG_INSTALL_DIR ${KDE_INSTALL_LIBDIR}/cmake/LibTaskManager) + +ecm_configure_package_config_file(LibTaskManagerConfig.cmake.in + "${CMAKE_CURRENT_BINARY_DIR}/LibTaskManagerConfig.cmake" + INSTALL_DESTINATION ${CMAKECONFIG_INSTALL_DIR}) + +install(FILES ${CMAKE_CURRENT_BINARY_DIR}/LibTaskManagerConfig.cmake + ${CMAKE_CURRENT_BINARY_DIR}/LibTaskManagerConfigVersion.cmake + DESTINATION ${CMAKECONFIG_INSTALL_DIR}) + +install(EXPORT libtaskmanagerLibraryTargets + NAMESPACE PW:: + DESTINATION ${CMAKECONFIG_INSTALL_DIR} + FILE LibTaskManagerLibraryTargets.cmake ) + +install(FILES taskmanagerrulesrc + DESTINATION ${KDE_INSTALL_CONFDIR}) diff --git a/libtaskmanager/LibTaskManagerConfig.cmake.in b/libtaskmanager/LibTaskManagerConfig.cmake.in new file mode 100644 --- /dev/null +++ b/libtaskmanager/LibTaskManagerConfig.cmake.in @@ -0,0 +1,3 @@ +@PACKAGE_INIT@ + +include("${CMAKE_CURRENT_LIST_DIR}/LibTaskManagerLibraryTargets.cmake") diff --git a/libtaskmanager/TODO.txt b/libtaskmanager/TODO.txt new file mode 100644 --- /dev/null +++ b/libtaskmanager/TODO.txt @@ -0,0 +1,8 @@ +Larger outstanding tasks: +- Implement missing kwayland bits (e.g. transient handling, virtual desktop logic). + +Other: +- Old lib compressed window changes over 200ms before grabbing new data from the server, might need to be added back to avoid a DoS vector. +- Tests tests tests. +- Code cleanup and addressing FIXMEs. +- Reconsider the library exports. diff --git a/libtaskmanager/abstracttasksmodel.h b/libtaskmanager/abstracttasksmodel.h new file mode 100644 --- /dev/null +++ b/libtaskmanager/abstracttasksmodel.h @@ -0,0 +1,253 @@ +/******************************************************************** +Copyright 2016 Eike Hein + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 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 6 of version 3 of the license. + +This library 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 +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library. If not, see . +*********************************************************************/ + +#ifndef ABSTRACTTASKSMODEL_H +#define ABSTRACTTASKSMODEL_H + +#include "abstracttasksmodeliface.h" + +#include + +#include "taskmanager_export.h" + +namespace TaskManager +{ + +/** + * @short An abstract base class for (flat) tasks models. + * + * This class serves as abstract base class for flat tasks model implementations. + * It provides data roles and no-op default implementations of methods in the + * AbstractTasksModelIface interface. + * + * @author Eike Hein + **/ + +class TASKMANAGER_EXPORT AbstractTasksModel : public QAbstractListModel, + public AbstractTasksModelIface +{ + Q_OBJECT + + Q_ENUMS(AdditionalRoles) + +public: + enum AdditionalRoles { + AppId = Qt::UserRole + 1, /**< KService storage id (.desktop name sans extension). */ + AppName, /**< Application name. */ + GenericName, /**< Generic application name. */ + LauncherUrl, /**< URL that can be used to launch this application (.desktop or executable). */ + LegacyWinIdList, /**< X11 window ids. Stopgap until we have something better. */ + MimeType, /**< MIME type for this task (window, window group), needed for DND. */ + MimeData, /**< Data for MimeType. */ + IsWindow, /**< This is a window task. */ + IsStartup, /**< This is a startup task. */ + IsLauncher, /**< This is a launcher task. */ + IsGroupParent, /**< This is a parent item for a group of child tasks. */ + IsGroupable, /**< Whether this task is being ignored by grouping or not. */ + IsActive, /**< This is the currently active task. */ + IsClosable, /**< requestClose (see below) available. */ + IsMovable, /**< requestMove (see below) available. */ + IsResizable, /**< requestResize (see below) available. */ + IsMaximizable, /**< requestToggleMaximize (see below) available. */ + IsMaximized, /**< Task (i.e. window) is maximized. */ + IsMinimizable, /**< requestToggleMinimize (see below) available. */ + IsMinimized, /**< Task (i.e. window) is minimized. */ + IsKeepAbove, /**< Task (i.e. window) is keep-above. */ + IsKeepBelow, /**< Task (i.e. window) is keep-below. */ + IsFullScreenable, /**< requestToggleFullScreen (see below) available. */ + IsFullScreen, /**< Task (i.e. window) is fullscreen. */ + IsShadeable, /**< requestToggleShade (see below) available. */ + IsShaded, /**< Task (i.e. window) is shaded. */ + IsVirtualDesktopChangeable, /**< requestVirtualDesktop (see below) available. */ + VirtualDesktop, /**< Virtual desktop for the task (i.e. window). */ + IsOnAllVirtualDesktops, /**< Task is on all virtual desktops. */ + Screen, /**< Screen for the task (i.e. window). */ + Activities, /**< Activities for the task (i.e. window). */ + IsDemandingAttention, /**< Task is demanding attention. */ + SkipTaskbar /**< Task desires not to be shown in a user interface. */ + }; + + explicit AbstractTasksModel(QObject *parent = nullptr); + virtual ~AbstractTasksModel(); + + QHash roleNames() const override; + + virtual QModelIndex index(int row, int column = 0, const QModelIndex &parent = QModelIndex()) const; + + /** + * Request activation of the task at the given index. Derived classes are + * free to interpret the meaning of "activate" themselves depending on + * the nature and state of the task, e.g. launch or raise a window task. + * + * This base implementation does nothing. + * + * @param index An index in this tasks model. + **/ + virtual void requestActivate(const QModelIndex &index); + + /** + * Request an additional instance of the application backing the task + * at the given index. + * + * This base implementation does nothing. + * + * @param index An index in this tasks model. + **/ + virtual void requestNewInstance(const QModelIndex &index); + + /** + * Request the task at the given index be closed. + * + * This base implementation does nothing. + * + * @param index An index in this tasks model. + **/ + virtual void requestClose(const QModelIndex &index); + + /** + * Request starting an interactive move for the task at the given index. + * + * This is meant for tasks that have an associated window, and may be + * a no-op when there is no window. + * + * This base implementation does nothing. + * + * @param index An index in this tasks model. + **/ + virtual void requestMove(const QModelIndex &index); + + /** + * Request starting an interactive resize for the task at the given index. + * + * This is meant for tasks that have an associated window, and may be a + * no-op when there is no window. + * + * This base implementation does nothing. + * + * @param index An index in this tasks model. + **/ + virtual void requestResize(const QModelIndex &index); + + /** + * Request toggling the minimized state of the task at the given index. + * + * This is meant for tasks that have an associated window, and may be + * a no-op when there is no window. + * + * This base implementation does nothing. + * + * @param index An index in this tasks model. + **/ + virtual void requestToggleMinimized(const QModelIndex &index); + + /** + * Request toggling the maximized state of the task at the given index. + * + * This is meant for tasks that have an associated window, and may be + * a no-op when there is no window. + * + * This base implementation does nothing. + * + * @param index An index in this tasks model. + **/ + virtual void requestToggleMaximized(const QModelIndex &index); + + /** + * Request toggling the keep-above state of the task at the given index. + * + * This is meant for tasks that have an associated window, and may be + * a no-op when there is no window. + * + * This base implementation does nothing. + * + * @param index An index in this tasks model. + **/ + virtual void requestToggleKeepAbove(const QModelIndex &index); + + /** + * Request toggling the keep-below state of the task at the given index. + * + * This is meant for tasks that have an associated window, and may be + * a no-op when there is no window. + * + * This base implementation does nothing. + * + * @param index An index in this tasks model. + **/ + virtual void requestToggleKeepBelow(const QModelIndex &index); + + /** + * Request toggling the fullscreen state of the task at the given index. + * + * This is meant for tasks that have an associated window, and may be + * a no-op when there is no window. + * + * This base implementation does nothing. + * + * @param index An index in this tasks model. + **/ + virtual void requestToggleFullScreen(const QModelIndex &index); + + /** + * Request toggling the shaded state of the task at the given index. + * + * This is meant for tasks that have an associated window, and may be + * a no-op when there is no window. + * + * This base implementation does nothing. + * + * @param index An index in this tasks model. + **/ + virtual void requestToggleShaded(const QModelIndex &index); + + /** + * Request moving the task at the given index to the specified virtual + * desktop. + * + * This is meant for tasks that have an associated window, and may be + * a no-op when there is no window. + * + * This base implementation does nothing. + * + * @param index An index in this tasks model. + * @param desktop A virtual desktop number. + **/ + virtual void requestVirtualDesktop(const QModelIndex &index, qint32 desktop); + + /** + * Request informing the window manager of new geometry for a visual + * delegate for the task at the given index. The geometry should be in + * screen coordinates. + * + * This base implementation does nothing. + * + * @param index An index in this tasks model. + * @param geometry Visual delegate geometry in screen coordinates. + * @param delegate The delegate. Implementations are on their own with + * regard to extracting information from this, and should take care to + * reject invalid objects. + **/ + virtual void requestPublishDelegateGeometry(const QModelIndex &index, const QRect &geometry, + QObject *delegate = nullptr); +}; + +} + +#endif diff --git a/libtaskmanager/abstracttasksmodel.cpp b/libtaskmanager/abstracttasksmodel.cpp new file mode 100644 --- /dev/null +++ b/libtaskmanager/abstracttasksmodel.cpp @@ -0,0 +1,123 @@ +/******************************************************************** +Copyright 2016 Eike Hein + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 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 6 of version 3 of the license. + +This library 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 +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library. If not, see . +*********************************************************************/ + +#include "abstracttasksmodel.h" + +#include + +namespace TaskManager +{ + +AbstractTasksModel::AbstractTasksModel(QObject *parent) + : QAbstractListModel(parent) +{ +} + +AbstractTasksModel::~AbstractTasksModel() +{ +} + +QHash AbstractTasksModel::roleNames() const +{ + QHash roles = QAbstractItemModel::roleNames(); + + QMetaEnum e = metaObject()->enumerator(metaObject()->indexOfEnumerator("AdditionalRoles")); + + for (int i = 0; i < e.keyCount(); ++i) { + roles.insert(e.value(i), e.key(i)); + } + + return roles; +} + +QModelIndex AbstractTasksModel::index(int row, int column, const QModelIndex &parent) const +{ + return hasIndex(row, column, parent) ? createIndex(row, column, nullptr) : QModelIndex(); +} + +void AbstractTasksModel::requestActivate(const QModelIndex &index) +{ + Q_UNUSED(index) +} + +void AbstractTasksModel::requestNewInstance(const QModelIndex &index) +{ + Q_UNUSED(index) +} + +void AbstractTasksModel::requestClose(const QModelIndex &index) +{ + Q_UNUSED(index) +} + +void AbstractTasksModel::requestMove(const QModelIndex &index) +{ + Q_UNUSED(index) +} + +void AbstractTasksModel::requestResize(const QModelIndex &index) +{ + Q_UNUSED(index) +} + +void AbstractTasksModel::requestToggleMinimized(const QModelIndex &index) +{ + Q_UNUSED(index) +} + +void AbstractTasksModel::requestToggleMaximized(const QModelIndex &index) +{ + Q_UNUSED(index) +} + +void AbstractTasksModel::requestToggleKeepAbove(const QModelIndex &index) +{ + Q_UNUSED(index) +} + +void AbstractTasksModel::requestToggleKeepBelow(const QModelIndex &index) +{ + Q_UNUSED(index) +} + +void AbstractTasksModel::requestToggleFullScreen(const QModelIndex &index) +{ + Q_UNUSED(index) +} + +void AbstractTasksModel::requestToggleShaded(const QModelIndex &index) +{ + Q_UNUSED(index) +} + +void AbstractTasksModel::requestVirtualDesktop(const QModelIndex &index, qint32 desktop) +{ + Q_UNUSED(index) + Q_UNUSED(desktop) +} + +void AbstractTasksModel::requestPublishDelegateGeometry(const QModelIndex &index, const QRect &geometry, QObject *delegate) +{ + Q_UNUSED(index) + Q_UNUSED(geometry) + Q_UNUSED(delegate) +} + +} diff --git a/libtaskmanager/abstracttasksmodeliface.h b/libtaskmanager/abstracttasksmodeliface.h new file mode 100644 --- /dev/null +++ b/libtaskmanager/abstracttasksmodeliface.h @@ -0,0 +1,182 @@ +/******************************************************************** +Copyright 2016 Eike Hein + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 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 6 of version 3 of the license. + +This library 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 +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library. If not, see . +*********************************************************************/ + +#ifndef ABSTRACTASKSMODELIFACE_H +#define ABSTRACTASKSMODELIFACE_H + +#include + +#include "taskmanager_export.h" + +namespace TaskManager +{ + +/** + * @short Pure virtual method interface for tasks model implementations. + * + * This is the pure virtual method interface implemented by AbstractTasksModel, + * as well as other model classes in this library which cannot inherit from + * AbstractTasksModel. + * + * @author Eike Hein + **/ + +class TASKMANAGER_EXPORT AbstractTasksModelIface +{ +public: + virtual ~AbstractTasksModelIface() {} + + /** + * Request activation of the task at the given index. Implementing classes + * are free to interpret the meaning of "activate" themselves depending on + * the nature and state of the task, e.g. launch or raise a window task. + * + * @param index An index in this tasks model. + **/ + virtual void requestActivate(const QModelIndex &index) = 0; + + /** + * Request an additional instance of the application backing the task at + * the given index. + * + * @param index An index in this tasks model. + **/ + virtual void requestNewInstance(const QModelIndex &index) = 0; + + /** + * Request the task at the given index be closed. + * + * @param index An index in this tasks model. + **/ + virtual void requestClose(const QModelIndex &index) = 0; + + /** + * Request starting an interactive move for the task at the given index. + * + * This is meant for tasks that have an associated window, and may be + * a no-op when there is no window. + * + * @param index An index in this tasks model. + **/ + virtual void requestMove(const QModelIndex &index) = 0; + + /** + * Request starting an interactive resize for the task at the given index. + * + * This is meant for tasks that have an associated window, and may be a + * no-op when there is no window. + * + * @param index An index in this tasks model. + **/ + virtual void requestResize(const QModelIndex &index) = 0; + + /** + * Request toggling the minimized state of the task at the given index. + * + * This is meant for tasks that have an associated window, and may be + * a no-op when there is no window. + * + * @param index An index in this tasks model. + **/ + virtual void requestToggleMinimized(const QModelIndex &index) = 0; + + /** + * Request toggling the maximized state of the task at the given index. + * + * This is meant for tasks that have an associated window, and may be + * a no-op when there is no window. + * + * @param index An index in this tasks model. + **/ + virtual void requestToggleMaximized(const QModelIndex &index) = 0; + + /** + * Request toggling the keep-above state of the task at the given index. + * + * This is meant for tasks that have an associated window, and may be + * a no-op when there is no window. + * + * @param index An index in this tasks model. + **/ + virtual void requestToggleKeepAbove(const QModelIndex &index) = 0; + + /** + * Request toggling the keep-below state of the task at the given index. + * + * This is meant for tasks that have an associated window, and may be + * a no-op when there is no window. + * + * @param index An index in this tasks model. + **/ + virtual void requestToggleKeepBelow(const QModelIndex &index) = 0; + + /** + * Request toggling the fullscreen state of the task at the given index. + * + * This is meant for tasks that have an associated window, and may be + * a no-op when there is no window. + * + * @param index An index in this tasks model. + **/ + virtual void requestToggleFullScreen(const QModelIndex &index) = 0; + + /** + * Request toggling the shaded state of the task at the given index. + * + * This is meant for tasks that have an associated window, and may be + * a no-op when there is no window. + * + * @param index An index in this tasks model. + **/ + virtual void requestToggleShaded(const QModelIndex &index) = 0; + + /** + * Request moving the task at the given index to the specified virtual + * desktop. + * + * This is meant for tasks that have an associated window, and may be + * a no-op when there is no window. + * + * @param index An index in this tasks model. + * @param desktop A virtual desktop number. + **/ + virtual void requestVirtualDesktop(const QModelIndex &index, qint32 desktop = -1) = 0; + + /** + * Request informing the window manager of new geometry for a visual + * delegate for the task at the given index. The geometry should be in + * screen coordinates. + * + * FIXME: Doesn't deal with the long-standing problem of multiple + * delegates in multiple applets. + * + * @param index An index in this tasks model. + * @param geometry Visual delegate geometry in screen coordinates. + * @param delegate The delegate. Implementations are on their own with + * regard to extracting information from this, and should take care to + * reject invalid objects. + **/ + virtual void requestPublishDelegateGeometry(const QModelIndex &index, const QRect &geometry, + QObject *delegate = nullptr) = 0; +}; + +} + +#endif diff --git a/libtaskmanager/activityinfo.h b/libtaskmanager/activityinfo.h new file mode 100644 --- /dev/null +++ b/libtaskmanager/activityinfo.h @@ -0,0 +1,98 @@ +/******************************************************************** +Copyright 2016 Eike Hein + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 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 6 of version 3 of the license. + +This library 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 +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library. If not, see . +*********************************************************************/ + +#ifndef ACTIVITYINFO_H +#define ACTIVITYINFO_H + +#include + +#include "taskmanager_export.h" + +namespace TaskManager +{ + +/** + * @short Provides basic activity information. + * + * This class provides basic information about the activities defined in + * the system. + * + * @NOTE: This is a placeholder, to be moved into KActivities (which it + * wraps) or the Task Manager applet backend. + * + * @see KActivities + * + * @author Eike Hein + **/ + +class TASKMANAGER_EXPORT ActivityInfo : public QObject +{ + Q_OBJECT + + Q_PROPERTY(QString currentActivity READ currentActivity NOTIFY currentActivityChanged) + Q_PROPERTY(int numberOfRunningActivities READ numberOfRunningActivities NOTIFY numberOfRunningActivitiesChanged) + +public: + explicit ActivityInfo(QObject *parent = 0); + virtual ~ActivityInfo(); + + /** + * The currently active virtual desktop. + * + * @returns the number of the currently active virtual desktop. + **/ + QString currentActivity() const; + + /** + * The number of currently-running activities defined in the session. + * + * @returns the number of activities defined in the session. + **/ + int numberOfRunningActivities() const; + + /** + * The list of currently-running activities defined in the session. + * + * @returns the list of currently-running activities defined in the session. + **/ + QStringList runningActivities() const; + + /** + * The name of the activity of the given id. + * + * @param id An activity id string. + * @returns the name of the activity of the given id. + **/ + Q_INVOKABLE QString activityName(const QString &id); + +Q_SIGNALS: + void currentActivityChanged() const; + void numberOfRunningActivitiesChanged() const; + +private: + class Private; + QScopedPointer d; + + Q_PRIVATE_SLOT(d, void refreshActivityInfos()) +}; + +} + +#endif diff --git a/libtaskmanager/activityinfo.cpp b/libtaskmanager/activityinfo.cpp new file mode 100644 --- /dev/null +++ b/libtaskmanager/activityinfo.cpp @@ -0,0 +1,143 @@ +/******************************************************************** +Copyright 2016 Eike Hein + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 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 6 of version 3 of the license. + +This library 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 +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library. If not, see . +*********************************************************************/ + +#include "activityinfo.h" + +#include + +namespace TaskManager +{ + +class ActivityInfo::Private +{ +public: + Private(ActivityInfo *q); + ~Private(); + + QHash activityInfos; + + static int instanceCount; + static KActivities::Consumer* activityConsumer; + + void refreshActivityInfos(); + +private: + ActivityInfo *q; +}; + +int ActivityInfo::Private::instanceCount = 0; +KActivities::Consumer* ActivityInfo::Private::activityConsumer = nullptr; + +ActivityInfo::Private::Private(ActivityInfo *q) + : q(q) +{ + ++instanceCount; +} + +ActivityInfo::Private::~Private() +{ + --instanceCount; + + if (!instanceCount) { + delete activityConsumer; + } +} + +void ActivityInfo::Private::refreshActivityInfos() +{ + QMutableHashIterator it(activityInfos); + + // Cull invalid activities. + while (it.hasNext()) { + it.next(); + + if (!it.value()->isValid()) { + delete it.value(); + it.remove(); + } + } + + // Find new activities and start listening for changes in their state. + foreach(const QString &activity, activityConsumer->activities()) { + if (!activityInfos.contains(activity)) { + KActivities::Info *info = new KActivities::Info(activity, q); + + // By connecting to ourselves, we will immediately clean up when an + // activity's state transitions to Invalid. + connect(info, SIGNAL(stateChanged(KActivities::Info::State)), + q, SLOT(refreshActivityInfos())); + + activityInfos.insert(activity, info); + } + } + + // Activity list or activity state changes -> number of running + // activities may have changed. + q->numberOfRunningActivitiesChanged(); +} + +ActivityInfo::ActivityInfo(QObject *parent) : QObject(parent) + , d(new Private(this)) +{ + if (!d->activityConsumer) { + d->activityConsumer = new KActivities::Consumer(); + } + + d->refreshActivityInfos(); + + connect(d->activityConsumer, &KActivities::Consumer::currentActivityChanged, + this, &ActivityInfo::currentActivityChanged); + connect(d->activityConsumer, SIGNAL(activitiesChanged(QStringList)), + this, SLOT(refreshActivityInfos())); +} + +ActivityInfo::~ActivityInfo() +{ +} + +QString ActivityInfo::currentActivity() const +{ + return d->activityConsumer->currentActivity(); +} + +int ActivityInfo::numberOfRunningActivities() const +{ + return d->activityConsumer->activities(KActivities::Info::State::Running).count(); +} + +QStringList ActivityInfo::runningActivities() const +{ + return d->activityConsumer->activities(KActivities::Info::State::Running); +} + +QString ActivityInfo::activityName(const QString &id) +{ + KActivities::Info info(id); + + if (info.state() != KActivities::Info::Invalid) { + return info.name(); + } + + return QString(); +} + +} + +#include "moc_activityinfo.cpp" diff --git a/libtaskmanager/autotests/CMakeLists.txt b/libtaskmanager/autotests/CMakeLists.txt new file mode 100644 --- /dev/null +++ b/libtaskmanager/autotests/CMakeLists.txt @@ -0,0 +1,7 @@ +include(ECMAddTests) + +ecm_add_tests( + launchertasksmodeltest.cpp + tasktoolstest.cpp + LINK_LIBRARIES taskmanager Qt5::Test KF5::Service KF5::IconThemes +) diff --git a/libtaskmanager/autotests/launchertasksmodeltest.cpp b/libtaskmanager/autotests/launchertasksmodeltest.cpp new file mode 100644 --- /dev/null +++ b/libtaskmanager/autotests/launchertasksmodeltest.cpp @@ -0,0 +1,163 @@ +/******************************************************************** +Copyright 2016 Eike Hein + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 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 6 of version 3 of the license. + +This library 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 +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library. If not, see . +*********************************************************************/ + +#include +#include +#include + +#include "launchertasksmodel.h" + +using namespace TaskManager; + +class LauncherTasksModelTest : public QObject +{ + Q_OBJECT + + private Q_SLOTS: + void initTestCase(); + + void shouldRoundTripLauncherUrlList(); + void shouldIgnoreInvalidUrls(); + void shouldRejectDuplicates(); + void shouldAddRemoveLauncher(); + void shouldReturnValidLauncherPositions(); + void shouldReturnValidData(); + + private: + QStringList m_urlStrings; +}; + +void LauncherTasksModelTest::initTestCase() +{ + m_urlStrings << QLatin1String("file:///usr/share/applications/org.kde.dolphin.desktop"); + m_urlStrings << QLatin1String("file:///usr/share/applications/org.kde.konsole.desktop"); +} + +void LauncherTasksModelTest::shouldRoundTripLauncherUrlList() +{ + LauncherTasksModel m; + + QSignalSpy launcherListChangedSpy(&m, &LauncherTasksModel::launcherListChanged); + QVERIFY(launcherListChangedSpy.isValid()); + + m.setLauncherList(m_urlStrings); + + QCOMPARE(launcherListChangedSpy.count(), 1); + + QCOMPARE(m.launcherList(), m_urlStrings); + + QCOMPARE(m.data(m.index(0, 0), AbstractTasksModel::LauncherUrl).toString(), m_urlStrings.at(0)); + QCOMPARE(m.data(m.index(1, 0), AbstractTasksModel::LauncherUrl).toString(), m_urlStrings.at(1)); +} + +void LauncherTasksModelTest::shouldIgnoreInvalidUrls() +{ + LauncherTasksModel m; + + QStringList urlStrings; + urlStrings << QLatin1String("GARBAGE URL"); + + QSignalSpy launcherListChangedSpy(&m, &LauncherTasksModel::launcherListChanged); + QVERIFY(launcherListChangedSpy.isValid()); + + m.setLauncherList(urlStrings); + + QCOMPARE(launcherListChangedSpy.count(), 0); + + bool added = m.requestAddLauncher(QUrl(urlStrings.at(0))); + + QVERIFY(!added); + QCOMPARE(launcherListChangedSpy.count(), 0); + + QCOMPARE(m.launcherList(), QStringList()); +} + +void LauncherTasksModelTest::shouldRejectDuplicates() +{ + LauncherTasksModel m; + + QStringList urlStrings; + urlStrings << QLatin1String("file:///usr/share/applications/org.kde.dolphin.desktop"); + urlStrings << QLatin1String("file:///usr/share/applications/org.kde.dolphin.desktop"); + + QSignalSpy launcherListChangedSpy(&m, &LauncherTasksModel::launcherListChanged); + QVERIFY(launcherListChangedSpy.isValid()); + + m.setLauncherList(urlStrings); + + QCOMPARE(launcherListChangedSpy.count(), 1); + + bool added = m.requestAddLauncher(urlStrings.at(0)); + + QVERIFY(!added); + QCOMPARE(launcherListChangedSpy.count(), 1); + + QCOMPARE(m.launcherList(), QStringList() << urlStrings.at(0)); +} + +void LauncherTasksModelTest::shouldAddRemoveLauncher() +{ + LauncherTasksModel m; + + QSignalSpy launcherListChangedSpy(&m, &LauncherTasksModel::launcherListChanged); + QVERIFY(launcherListChangedSpy.isValid()); + + bool added = m.requestAddLauncher(m_urlStrings.at(0)); + + QVERIFY(added); + QCOMPARE(launcherListChangedSpy.count(), 1); + + QCOMPARE(m.launcherList().at(0), m_urlStrings.at(0)); + + bool removed = m.requestRemoveLauncher(m_urlStrings.at(0)); + + QVERIFY(removed); + QCOMPARE(launcherListChangedSpy.count(), 2); + + removed = m.requestRemoveLauncher(m_urlStrings.at(0)); + + QVERIFY(!removed); + + QCOMPARE(m.launcherList(), QStringList()); +} + +void LauncherTasksModelTest::shouldReturnValidLauncherPositions() +{ + LauncherTasksModel m; + + QSignalSpy launcherListChangedSpy(&m, &LauncherTasksModel::launcherListChanged); + QVERIFY(launcherListChangedSpy.isValid()); + + m.setLauncherList(m_urlStrings); + + QCOMPARE(launcherListChangedSpy.count(), 1); + + QCOMPARE(m.launcherPosition(m_urlStrings.at(0)), 0); + QCOMPARE(m.launcherPosition(m_urlStrings.at(1)), 1); +} + +void LauncherTasksModelTest::shouldReturnValidData() +{ + // FIXME Reuse TaskToolsTest app link setup, then run URLs through model. +} + +QTEST_MAIN(LauncherTasksModelTest) + +#include "launchertasksmodeltest.moc" diff --git a/libtaskmanager/autotests/tasktoolstest.cpp b/libtaskmanager/autotests/tasktoolstest.cpp new file mode 100644 --- /dev/null +++ b/libtaskmanager/autotests/tasktoolstest.cpp @@ -0,0 +1,214 @@ +/******************************************************************** +Copyright 2016 Eike Hein + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 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 6 of version 3 of the license. + +This library 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 +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library. If not, see . +*********************************************************************/ + +#include + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "tasktools.h" + +// Taken from tst_qstandardpaths. +#if defined(Q_OS_UNIX) && !defined(Q_OS_MAC) && !defined(Q_OS_BLACKBERRY) && !defined(Q_OS_ANDROID) +#define Q_XDG_PLATFORM +#endif + +using namespace TaskManager; + +class TaskToolsTest : public QObject +{ + Q_OBJECT + + private Q_SLOTS: + void initTestCase(); + void cleanupTestCase(); + + void shouldFindApp(); + void shouldFindDefaultApp(); + void shouldCompareLauncherUrls(); + + private: + QString appLinkPath(); + void fillReferenceAppData(); + void createAppLink(); + void createIcon(); + + AppData m_referenceAppData; + QTemporaryDir m_tempDir; +}; + +void TaskToolsTest::initTestCase() +{ + QStandardPaths::enableTestMode(true); + + QVERIFY(m_tempDir.isValid()); + QVERIFY(QDir().mkpath(m_tempDir.path() + QLatin1String("/config"))); + QVERIFY(QDir().mkpath(m_tempDir.path() + QLatin1String("/cache"))); + QVERIFY(QDir().mkpath(m_tempDir.path() + QLatin1String("/data/applications"))); + +#ifdef Q_XDG_PLATFORM + qputenv("XDG_CONFIG_HOME", QFile::encodeName(m_tempDir.path() + QLatin1String("/config"))); + qputenv("XDG_CACHE_HOME", QFile::encodeName(m_tempDir.path() + QLatin1String("/cache"))); + qputenv("XDG_DATA_DIRS", QFile::encodeName(m_tempDir.path() + QLatin1String("/data"))); +#else + QSKIP("This test requires XDG."); +#endif + + createIcon(); + fillReferenceAppData(); + createAppLink(); + + QFile::remove(KSycoca::absoluteFilePath()); + KSycoca::self()->ensureCacheValid(); + QVERIFY(QFile::exists(KSycoca::absoluteFilePath())); +} + +void TaskToolsTest::cleanupTestCase() +{ + QFile::remove(KSycoca::absoluteFilePath()); +} + +void TaskToolsTest::shouldFindApp() +{ + // FIXME Test icon. + + const AppData &data = appDataFromUrl(QUrl::fromLocalFile(appLinkPath())); + + QCOMPARE(data.id, m_referenceAppData.id); + QCOMPARE(data.name, m_referenceAppData.name); + QCOMPARE(data.genericName, m_referenceAppData.genericName); + QCOMPARE(data.url, m_referenceAppData.url); +} + +void TaskToolsTest::shouldFindDefaultApp() +{ + // FIXME Test other recognized default app types. + + KConfigGroup config(KSharedConfig::openConfig(), "General"); + config.writePathEntry("BrowserApplication", QLatin1String("konqueror")); + + QVERIFY(defaultApplication(QUrl("wrong://url")).isEmpty()); + QCOMPARE(defaultApplication(QUrl("preferred://browser")), QLatin1String("konqueror")); +} + +void TaskToolsTest::shouldCompareLauncherUrls() +{ + QUrl a(QLatin1String("file:///usr/share/applications/org.kde.dolphin.desktop")); + QUrl b(QLatin1String("file:///usr/share/applications/org.kde.konsole.desktop")); + QUrl c(QLatin1String("file:///usr/share/applications/org.kde.dolphin.desktop?iconData=foo")); + QUrl d(QLatin1String("file:///usr/share/applications/org.kde.konsole.desktop?iconData=bar")); + + QVERIFY(launcherUrlsMatch(QUrl(a), QUrl(a))); + QVERIFY(launcherUrlsMatch(QUrl(a), QUrl(a), Strict)); + QVERIFY(launcherUrlsMatch(QUrl(a), QUrl(a), IgnoreQueryItems));; + + QVERIFY(!launcherUrlsMatch(QUrl(a), QUrl(b))); + QVERIFY(!launcherUrlsMatch(QUrl(a), QUrl(b), Strict)); + QVERIFY(!launcherUrlsMatch(QUrl(a), QUrl(b), IgnoreQueryItems)); + + QVERIFY(!launcherUrlsMatch(QUrl(b), QUrl(c), Strict)); + QVERIFY(!launcherUrlsMatch(QUrl(c), QUrl(d), Strict)); + + QVERIFY(launcherUrlsMatch(QUrl(a), QUrl(c), IgnoreQueryItems)); + QVERIFY(!launcherUrlsMatch(QUrl(c), QUrl(d), IgnoreQueryItems)); +} + +QString TaskToolsTest::appLinkPath() +{ + return QString(m_tempDir.path() + QLatin1String("/data/applications/org.kde.konversation.desktop")); +} + +void TaskToolsTest::fillReferenceAppData() +{ + // FIXME Add icon. + + m_referenceAppData.id = QLatin1String("org.kde.konversation"); + m_referenceAppData.name = QLatin1String("Konversation"); + m_referenceAppData.genericName = QLatin1String("IRC Client"); + m_referenceAppData.url = QUrl::fromLocalFile(appLinkPath()); +} + +void TaskToolsTest::createAppLink() +{ + KDesktopFile file(appLinkPath()); + KConfigGroup group = file.desktopGroup(); + group.writeEntry(QLatin1String("Type"), QString("Application")); + group.writeEntry(QLatin1String("Name"), m_referenceAppData.name); + group.writeEntry(QLatin1String("GenericName"), m_referenceAppData.genericName); + group.writeEntry(QLatin1String("Icon"), QString("konversation")); + group.writeEntry(QLatin1String("Exec"), QString("konversation")); + file.sync(); + + QVERIFY(file.hasApplicationType()); + + QVERIFY(QFile::exists(appLinkPath())); + QVERIFY(KDesktopFile::isDesktopFile(appLinkPath())); +} + +void TaskToolsTest::createIcon() +{ + // FIXME KIconLoaderPrivate::initIconThemes: Error: standard icon theme "oxygen" not found! + + QString iconDir = m_tempDir.path() + QLatin1String("/data/icons/"); + + QVERIFY(QDir().mkpath(iconDir)); + + QIcon::setThemeSearchPaths(QStringList() << m_tempDir.path() + QLatin1String("/data/icons/")); + QCOMPARE(QIcon::themeSearchPaths(), QStringList() << iconDir); + + iconDir = iconDir + QLatin1String("/") + KIconTheme::defaultThemeName(); + + QVERIFY(QDir().mkpath(iconDir)); + + const QString &themeFile = iconDir + QLatin1String("/index.theme"); + + KConfig config(themeFile); + KConfigGroup group(config.group(QLatin1String("Icon Theme"))); + group.writeEntry(QLatin1String("Name"), KIconTheme::defaultThemeName()); + group.writeEntry(QLatin1String("Inherits"), QString("hicolor")); + config.sync(); + + QVERIFY(QFile::exists(themeFile)); + + iconDir = iconDir + QLatin1String("/64x64/apps"); + + QVERIFY(QDir().mkpath(iconDir)); + + const QString &iconPath = iconDir + QLatin1String("/konversation.png"); + + QImage image(64, 64, QImage::Format_Mono); + image.save(iconPath); + + QVERIFY(QFile::exists(iconPath)); +} + +QTEST_MAIN(TaskToolsTest) + +#include "tasktoolstest.moc" diff --git a/libtaskmanager/concatenatetasksproxymodel.h b/libtaskmanager/concatenatetasksproxymodel.h new file mode 100644 --- /dev/null +++ b/libtaskmanager/concatenatetasksproxymodel.h @@ -0,0 +1,184 @@ +/******************************************************************** +Copyright 2016 Eike Hein + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 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 6 of version 3 of the license. + +This library 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 +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library. If not, see . +*********************************************************************/ + +#ifndef CONCATENATETASKSPROXYMODEL_H +#define CONCATENATETASKSPROXYMODEL_H + +#include "abstracttasksmodeliface.h" + +#include + +#include "taskmanager_export.h" + +namespace TaskManager +{ + +/** + * @short A proxy tasks model for concatenating multiple source tasks models. + * + * This proxy model is a subclass of KConcatenateRowsProxyModel implementing + * AbstractTasksModelIface, forwarding calls to the correct source model. + * + * @author Eike Hein + **/ + +class TASKMANAGER_EXPORT ConcatenateTasksProxyModel : public KConcatenateRowsProxyModel, + public AbstractTasksModelIface +{ + Q_OBJECT + +public: + explicit ConcatenateTasksProxyModel(QObject *parent = 0); + virtual ~ConcatenateTasksProxyModel(); + + /** + * Request activation of the task at the given index. Derived classes are + * free to interpret the meaning of "activate" themselves depending on + * the nature and state of the task, e.g. launch or raise a window task. + * + * @param index An index in this tasks model. + **/ + void requestActivate(const QModelIndex &index); + + /** + * Request an additional instance of the application backing the task + * at the given index. + * + * @param index An index in this tasks model. + **/ + void requestNewInstance(const QModelIndex &index); + + /** + * Request the task at the given index be closed. + * + * @param index An index in this tasks model. + **/ + void requestClose(const QModelIndex &index); + + /** + * Request starting an interactive move for the task at the given index. + * + * This is meant for tasks that have an associated window, and may be + * a no-op when there is no window. + * + * @param index An index in this tasks model. + **/ + void requestMove(const QModelIndex &index); + + /** + * Request starting an interactive resize for the task at the given index. + * + * This is meant for tasks that have an associated window, and may be a + * no-op when there is no window. + * + * @param index An index in this tasks model. + **/ + void requestResize(const QModelIndex &index); + + /** + * Request toggling the minimized state of the task at the given index. + * + * This is meant for tasks that have an associated window, and may be + * a no-op when there is no window. + * + * @param index An index in this tasks model. + **/ + void requestToggleMinimized(const QModelIndex &index); + + /** + * Request toggling the maximized state of the task at the given index. + * + * This is meant for tasks that have an associated window, and may be + * a no-op when there is no window. + * + * @param index An index in this tasks model. + **/ + void requestToggleMaximized(const QModelIndex &index); + + /** + * Request toggling the keep-above state of the task at the given index. + * + * This is meant for tasks that have an associated window, and may be + * a no-op when there is no window. + * + * @param index An index in this tasks model. + **/ + void requestToggleKeepAbove(const QModelIndex &index); + + /** + * Request toggling the keep-below state of the task at the given index. + * + * This is meant for tasks that have an associated window, and may be + * a no-op when there is no window. + * + * @param index An index in this tasks model. + **/ + void requestToggleKeepBelow(const QModelIndex &index); + + /** + * Request toggling the fullscreen state of the task at the given index. + * + * This is meant for tasks that have an associated window, and may be + * a no-op when there is no window. + * + * @param index An index in this tasks model. + **/ + void requestToggleFullScreen(const QModelIndex &index); + + /** + * Request toggling the shaded state of the task at the given index. + * + * This is meant for tasks that have an associated window, and may be + * a no-op when there is no window. + * + * @param index An index in this tasks model. + **/ + void requestToggleShaded(const QModelIndex &index); + + /** + * Request moving the task at the given index to the specified virtual + * desktop. + * + * This is meant for tasks that have an associated window, and may be + * a no-op when there is no window. + * + * @param index An index in this tasks model. + * @param desktop A virtual desktop number. + **/ + void requestVirtualDesktop(const QModelIndex &index, qint32 desktop); + + /** + * Request informing the window manager of new geometry for a visual + * delegate for the task at the given index. The geometry should be in + * screen coordinates. + * + * @param index An index in this tasks model. + * @param geometry Visual delegate geometry in screen coordinates. + * @param delegate The delegate. Implementations are on their own with + * regard to extracting information from this, and should take care to + * reject invalid objects. + **/ + void requestPublishDelegateGeometry(const QModelIndex &index, const QRect &geometry, + QObject *delegate = nullptr); +}; + +} + +#endif diff --git a/libtaskmanager/concatenatetasksproxymodel.cpp b/libtaskmanager/concatenatetasksproxymodel.cpp new file mode 100644 --- /dev/null +++ b/libtaskmanager/concatenatetasksproxymodel.cpp @@ -0,0 +1,242 @@ +/******************************************************************** +Copyright 2016 Eike Hein + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 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 6 of version 3 of the license. + +This library 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 +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library. If not, see . +*********************************************************************/ + +#include "concatenatetasksproxymodel.h" + +namespace TaskManager +{ + +ConcatenateTasksProxyModel::ConcatenateTasksProxyModel(QObject *parent) + : KConcatenateRowsProxyModel(parent) +{ +} + +ConcatenateTasksProxyModel::~ConcatenateTasksProxyModel() +{ +} + +void ConcatenateTasksProxyModel::requestActivate(const QModelIndex &index) +{ + if (!index.isValid() || index.model() != this) { + return; + } + + const QModelIndex &sourceIndex = mapToSource(index); + const AbstractTasksModelIface *m = dynamic_cast(sourceIndex.model()); + + if (m) { + // NOTE: KConcatenateRowsProxyModel offers no way to get a non-const pointer + // to one of the source models, so we have to go through a mapped index. + const_cast(m)->requestActivate(sourceIndex); + } +} + +void ConcatenateTasksProxyModel::requestNewInstance(const QModelIndex &index) +{ + if (!index.isValid() || index.model() != this) { + return; + } + + const QModelIndex &sourceIndex = mapToSource(index); + const AbstractTasksModelIface *m = dynamic_cast(sourceIndex.model()); + + if (m) { + // NOTE: KConcatenateRowsProxyModel offers no way to get a non-const pointer + // to one of the source models, so we have to go through a mapped index. + const_cast(m)->requestNewInstance(sourceIndex); + } +} + +void ConcatenateTasksProxyModel::requestClose(const QModelIndex &index) +{ + if (!index.isValid() || index.model() != this) { + return; + } + + const QModelIndex &sourceIndex = mapToSource(index); + const AbstractTasksModelIface *m = dynamic_cast(sourceIndex.model()); + + if (m) { + // NOTE: KConcatenateRowsProxyModel offers no way to get a non-const pointer + // to one of the source models, so we have to go through a mapped index. + const_cast(m)->requestClose(sourceIndex); + } +} +void ConcatenateTasksProxyModel::requestMove(const QModelIndex &index) +{ + if (!index.isValid() || index.model() != this) { + return; + } + + const QModelIndex &sourceIndex = mapToSource(index); + const AbstractTasksModelIface *m = dynamic_cast(sourceIndex.model()); + + if (m) { + // NOTE: KConcatenateRowsProxyModel offers no way to get a non-const pointer + // to one of the source models, so we have to go through a mapped index. + const_cast(m)->requestMove(sourceIndex); + } +} + +void ConcatenateTasksProxyModel::requestResize(const QModelIndex &index) +{ + if (!index.isValid() || index.model() != this) { + return; + } + + const QModelIndex &sourceIndex = mapToSource(index); + const AbstractTasksModelIface *m = dynamic_cast(sourceIndex.model()); + + if (m) { + // NOTE: KConcatenateRowsProxyModel offers no way to get a non-const pointer + // to one of the source models, so we have to go through a mapped index. + const_cast(m)->requestResize(sourceIndex); + } +} + +void ConcatenateTasksProxyModel::requestToggleMinimized(const QModelIndex &index) +{ + if (!index.isValid() || index.model() != this) { + return; + } + + const QModelIndex &sourceIndex = mapToSource(index); + const AbstractTasksModelIface *m = dynamic_cast(sourceIndex.model()); + + if (m) { + // NOTE: KConcatenateRowsProxyModel offers no way to get a non-const pointer + // to one of the source models, so we have to go through a mapped index. + const_cast(m)->requestToggleMinimized(sourceIndex); + } +} + +void ConcatenateTasksProxyModel::requestToggleMaximized(const QModelIndex &index) +{ + if (!index.isValid() || index.model() != this) { + return; + } + + const QModelIndex &sourceIndex = mapToSource(index); + const AbstractTasksModelIface *m = dynamic_cast(sourceIndex.model()); + + if (m) { + // NOTE: KConcatenateRowsProxyModel offers no way to get a non-const pointer + // to one of the source models, so we have to go through a mapped index. + const_cast(m)->requestToggleMaximized(sourceIndex); + } +} + +void ConcatenateTasksProxyModel::requestToggleKeepAbove(const QModelIndex &index) +{ + if (!index.isValid() || index.model() != this) { + return; + } + + const QModelIndex &sourceIndex = mapToSource(index); + const AbstractTasksModelIface *m = dynamic_cast(sourceIndex.model()); + + if (m) { + // NOTE: KConcatenateRowsProxyModel offers no way to get a non-const pointer + // to one of the source models, so we have to go through a mapped index. + const_cast(m)->requestToggleKeepAbove(sourceIndex); + } +} + +void ConcatenateTasksProxyModel::requestToggleKeepBelow(const QModelIndex &index) +{ + if (!index.isValid() || index.model() != this) { + return; + } + + const QModelIndex &sourceIndex = mapToSource(index); + const AbstractTasksModelIface *m = dynamic_cast(sourceIndex.model()); + + if (m) { + // NOTE: KConcatenateRowsProxyModel offers no way to get a non-const pointer + // to one of the source models, so we have to go through a mapped index. + const_cast(m)->requestToggleKeepBelow(sourceIndex); + } +} + +void ConcatenateTasksProxyModel::requestToggleFullScreen(const QModelIndex &index) +{ + if (!index.isValid() || index.model() != this) { + return; + } + + const QModelIndex &sourceIndex = mapToSource(index); + const AbstractTasksModelIface *m = dynamic_cast(sourceIndex.model()); + + if (m) { + // NOTE: KConcatenateRowsProxyModel offers no way to get a non-const pointer + // to one of the source models, so we have to go through a mapped index. + const_cast(m)->requestToggleFullScreen(sourceIndex); + } +} + +void ConcatenateTasksProxyModel::requestToggleShaded(const QModelIndex &index) +{ + if (!index.isValid() || index.model() != this) { + return; + } + + const QModelIndex &sourceIndex = mapToSource(index); + const AbstractTasksModelIface *m = dynamic_cast(sourceIndex.model()); + + if (m) { + // NOTE: KConcatenateRowsProxyModel offers no way to get a non-const pointer + // to one of the source models, so we have to go through a mapped index. + const_cast(m)->requestToggleShaded(sourceIndex); + } +} + +void ConcatenateTasksProxyModel::requestVirtualDesktop(const QModelIndex &index, qint32 desktop) +{ + if (!index.isValid() || index.model() != this) { + return; + } + + const QModelIndex &sourceIndex = mapToSource(index); + const AbstractTasksModelIface *m = dynamic_cast(sourceIndex.model()); + + if (m) { + // NOTE: KConcatenateRowsProxyModel offers no way to get a non-const pointer + // to one of the source models, so we have to go through a mapped index. + const_cast(m)->requestVirtualDesktop(sourceIndex, desktop); + } +} + +void ConcatenateTasksProxyModel::requestPublishDelegateGeometry(const QModelIndex &index, const QRect &geometry, QObject *delegate) +{ + if (!index.isValid() || index.model() != this) { + return; + } + + const QModelIndex &sourceIndex = mapToSource(index); + const AbstractTasksModelIface *m = dynamic_cast(sourceIndex.model()); + + if (m) { + // NOTE: KConcatenateRowsProxyModel offers no way to get a non-const pointer + // to one of the source models, so we have to go through a mapped index. + const_cast(m)->requestPublishDelegateGeometry(sourceIndex, geometry, delegate); + } +} + +} diff --git a/libtaskmanager/declarative/CMakeLists.txt b/libtaskmanager/declarative/CMakeLists.txt new file mode 100644 --- /dev/null +++ b/libtaskmanager/declarative/CMakeLists.txt @@ -0,0 +1,10 @@ +include_directories(${CMAKE_CURRENT_BINARY_DIR}/.. ${CMAKE_CURRENT_SOURCE_DIR}/..) + +add_library(taskmanagerplugin SHARED taskmanagerplugin.cpp) + +target_link_libraries(taskmanagerplugin Qt5::Qml + taskmanager) + +install(TARGETS taskmanagerplugin DESTINATION ${KDE_INSTALL_QMLDIR}/org/kde/taskmanager) +install(FILES qmldir DESTINATION ${KDE_INSTALL_QMLDIR}/org/kde/taskmanager) + diff --git a/libtaskmanager/declarative/qmldir b/libtaskmanager/declarative/qmldir new file mode 100644 --- /dev/null +++ b/libtaskmanager/declarative/qmldir @@ -0,0 +1,2 @@ +module org.kde.taskmanager +plugin taskmanagerplugin diff --git a/libtaskmanager/declarative/taskmanagerplugin.h b/libtaskmanager/declarative/taskmanagerplugin.h new file mode 100644 --- /dev/null +++ b/libtaskmanager/declarative/taskmanagerplugin.h @@ -0,0 +1,41 @@ +/******************************************************************** +Copyright 2015-2016 Eike Hein + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 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 6 of version 3 of the license. + +This library 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 +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library. If not, see . +*********************************************************************/ + +#ifndef TASKMANAGERPLUGIN_H +#define TASKMANAGERPLUGIN_H + +#include +#include + +namespace TaskManager +{ + +class TaskManagerPlugin : public QQmlExtensionPlugin +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QQmlExtensionInterface") + + public: + virtual void registerTypes(const char *uri); +}; + +} + +#endif diff --git a/libtaskmanager/declarative/taskmanagerplugin.cpp b/libtaskmanager/declarative/taskmanagerplugin.cpp new file mode 100644 --- /dev/null +++ b/libtaskmanager/declarative/taskmanagerplugin.cpp @@ -0,0 +1,41 @@ +/******************************************************************** +Copyright 2015-2016 Eike Hein + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 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 6 of version 3 of the license. + +This library 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 +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library. If not, see . +*********************************************************************/ + +#include "taskmanagerplugin.h" + +#include "tasksmodel.h" +#include "activityinfo.h" +#include "virtualdesktopinfo.h" + +#include + +namespace TaskManager +{ + +void TaskManagerPlugin::registerTypes(const char *uri) +{ + Q_ASSERT(uri == QLatin1String("org.kde.taskmanager")); + + qmlRegisterType(uri, 0, 1, "TasksModel"); + qmlRegisterType(uri, 0, 1, "ActivityInfo"); + qmlRegisterType(uri, 0, 1, "VirtualDesktopInfo"); +} + +} diff --git a/libtaskmanager/launchertasksmodel.h b/libtaskmanager/launchertasksmodel.h new file mode 100644 --- /dev/null +++ b/libtaskmanager/launchertasksmodel.h @@ -0,0 +1,145 @@ +/******************************************************************** +Copyright 2016 Eike Hein + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 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 6 of version 3 of the license. + +This library 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 +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library. If not, see . +*********************************************************************/ + +#ifndef LAUNCHERTASKSMODEL_H +#define LAUNCHERTASKSMODEL_H + +#include "abstracttasksmodel.h" + +#include "taskmanager_export.h" + +#include + +namespace TaskManager +{ + +/** + * @short A tasks model for launchers. + * + * This model presents tasks sourced from list of launcher URLs. The + * list can be read from and written to from a notifiable prop, enabling + * storage outside the instance (e.g. in config). + * + * Extends AbstractTasksModel API with API for adding, removing, checking + * for and moving launchers by URL or row index. + * + * Launcher URLs can use the preferred:// protocol to request system + * default applications such as "browser" and "mailer". + * + * @see defaultApplication + * + * @author Eike Hein + */ + +class TASKMANAGER_EXPORT LauncherTasksModel : public AbstractTasksModel +{ + Q_OBJECT + + Q_PROPERTY(QStringList launcherList READ launcherList WRITE setLauncherList NOTIFY launcherListChanged) + +public: + explicit LauncherTasksModel(QObject *parent = 0); + virtual ~LauncherTasksModel(); + + virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + + /** + * The list of launcher URLs serialized to strings. + * + * @see setLauncherList + * @returns the list of launcher URLs serialized to strings. + **/ + QStringList launcherList() const; + + /** + * Replace the list of launcher URL strings. + * + * Invalid or empty URLs will be rejected. Duplicate URLs will be + * collapsed. + * + * @see launcherList + * @param launchers A list of launcher URL strings. + **/ + void setLauncherList(const QStringList &launchers); + + /** + * Request adding a launcher with the given URL. + * + * If this URL is already in the list, the request will fail. URLs are + * compared for equality after removing the query string used to hold + * metadata. + * + * @see launcherUrlsMatch + * @param url A launcher URL. + * @returns @c true if a launcher was added. + */ + bool requestAddLauncher(const QUrl &url); + + /** + * Request removing the launcher with the given URL. + * + * If this URL is already in the list, the request will fail. URLs are + * compared for equality after removing the query string used to hold + * metadata. + * + * @see launcherUrlsMatch + * @param url A launcher URL. + * @returns @c true if the launcher was removed. + */ + bool requestRemoveLauncher(const QUrl &url); + + /** + * Return the position of the launcher with the given URL. + * + * URLs are compared for equality after removing the query string used + * to hold metadata. + * + * @see launcherUrlsMatch + * @param url A launcher URL. + * @returns @c -1 if no launcher exists for the given URL. + */ + int launcherPosition(const QUrl &url) const; + + /** + * Runs the URL (i.e. launches the application) at the given index. + * + * @param index An index in this launcher tasks model. + */ + void requestActivate(const QModelIndex &index) override; + + /** + * Runs the URL (i.e. launches the application) at the given index. + * + * @param index An index in this launcher tasks model. + */ + void requestNewInstance(const QModelIndex &index) override; + +Q_SIGNALS: + void launcherListChanged() const; + +private: + class Private; + QScopedPointer d; +}; + +} + +#endif diff --git a/libtaskmanager/launchertasksmodel.cpp b/libtaskmanager/launchertasksmodel.cpp new file mode 100644 --- /dev/null +++ b/libtaskmanager/launchertasksmodel.cpp @@ -0,0 +1,286 @@ +/******************************************************************** +Copyright 2016 Eike Hein + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 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 6 of version 3 of the license. + +This library 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 +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library. If not, see . +*********************************************************************/ + +#include "launchertasksmodel.h" +#include "tasktools.h" + +#include +#include +#include +#include + +#include + +#include +#include +#if HAVE_X11 +#include +#endif + +namespace TaskManager +{ + +class LauncherTasksModel::Private +{ +public: + Private(LauncherTasksModel *q); + QList launchers; + QHash appDataCache; + QTimer sycocaChangeTimer; + + void init(); + AppData appData(const QUrl &url); + +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 (!launchers.count()) { + return; + } + + appDataCache.clear(); + + // Emit changes of all roles satisfied from app data cache. + q->dataChanged(q->index(0, 0), q->index(launchers.count() - 1, 0), + QVector{Qt::DisplayRole, Qt::DecorationRole, + AbstractTasksModel::AppId, AbstractTasksModel::AppName, + AbstractTasksModel::GenericName, AbstractTasksModel::LauncherUrl}); + } + ); + + void (KSycoca::*myDatabaseChangeSignal)(const QStringList &) = &KSycoca::databaseChanged; + QObject::connect(KSycoca::self(), myDatabaseChangeSignal, q, + [this](const QStringList &changedResources) { + if (changedResources.contains(QLatin1String("services")) + || changedResources.contains(QLatin1String("apps")) + || changedResources.contains(QLatin1String("xdgdata-apps"))) { + sycocaChangeTimer.start(); + } + } + ); +} + +AppData LauncherTasksModel::Private::appData(const QUrl &url) +{ + if (!appDataCache.contains(url)) { + const AppData &data = appDataFromUrl(url, QIcon::fromTheme(QLatin1String("unknown"))); + appDataCache.insert(url, data); + + return data; + } + + return appDataCache.value(url); +} + +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->launchers.count()) { + return QVariant(); + } + + const QUrl &url = d->launchers.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 == IsLauncher) { + return true; + } else if (role == IsOnAllVirtualDesktops) { + return true; + } + + return QVariant(); +} + +int LauncherTasksModel::rowCount(const QModelIndex &parent) const +{ + return parent.isValid() ? 0 : d->launchers.count(); +} + +QStringList LauncherTasksModel::launcherList() const +{ + return QUrl::toStringList(d->launchers); +} + +void LauncherTasksModel::setLauncherList(const QStringList &launchers) +{ + const QList &_urls = QUrl::fromStringList(launchers, QUrl::StrictMode); + QList urls; + + // Reject invalid urls and duplicates. + foreach(const QUrl &url, _urls) { + if (url.isValid()) { + bool dupe = false; + + foreach(const QUrl &addedUrl, urls) { + dupe = launcherUrlsMatch(url, addedUrl, IgnoreQueryItems); + } + + if (!dupe) { + urls.append(url); + } + } + } + + if (d->launchers != urls) { + beginResetModel(); + + d->launchers.clear(); + d->appDataCache.clear(); + + d->launchers = urls; + + endResetModel(); + + emit launcherListChanged(); + } +} + +bool LauncherTasksModel::requestAddLauncher(const QUrl &_url) +{ + // isValid() for the passed-in URL might return true if it was + // constructed in TolerantMode, but we want to reject invalid URLs. + QUrl url(_url.toString(), QUrl::StrictMode); + + if (url.isEmpty() || !url.isValid()) { + return false; + } + + // Reject duplicates. + foreach(const QUrl &launcher, d->launchers) { + if (launcherUrlsMatch(url, launcher, IgnoreQueryItems)) { + return false; + } + } + + const int count = d->launchers.count(); + beginInsertRows(QModelIndex(), count, count); + d->launchers.append(url); + endInsertRows(); + + emit launcherListChanged(); + + return true; +} + +bool LauncherTasksModel::requestRemoveLauncher(const QUrl &url) +{ + for (int i = 0; i < d->launchers.count(); ++i) { + const QUrl &launcher = d->launchers.at(i); + + if (launcherUrlsMatch(url, launcher, IgnoreQueryItems) + || launcherUrlsMatch(url, d->appData(launcher).url, IgnoreQueryItems)) { + beginRemoveRows(QModelIndex(), i, i); + d->launchers.removeAt(i); + d->appDataCache.remove(launcher); + endRemoveRows(); + + emit launcherListChanged(); + + return true; + } + } + + return false; +} + +int LauncherTasksModel::launcherPosition(const QUrl &url) const +{ + for (int i = 0; i < d->launchers.count(); ++i) { + if (launcherUrlsMatch(url, d->launchers.at(i), 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->launchers.count()) { + return; + } + + const QUrl &url = d->launchers.at(index.row()); + + quint32 timeStamp = 0; + +#if HAVE_X11 + if (QX11Info::isPlatformX11()) { + timeStamp = QX11Info::appUserTime(); + } +#endif + + if (url.scheme() == QLatin1String("preferred")) { + KService::Ptr service = KService::serviceByStorageId(defaultApplication(url)); + + if (!service) { + return; + } + + new KRun(QUrl::fromLocalFile(service->entryPath()), 0, false, + KStartupInfo::createNewStartupIdForTimestamp(timeStamp)); + } else { + new KRun(url, 0, false, KStartupInfo::createNewStartupIdForTimestamp(timeStamp)); + } +} + +} diff --git a/libtaskmanager/startuptasksmodel.h b/libtaskmanager/startuptasksmodel.h new file mode 100644 --- /dev/null +++ b/libtaskmanager/startuptasksmodel.h @@ -0,0 +1,70 @@ +/******************************************************************** +Copyright 2016 Eike Hein + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 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 6 of version 3 of the license. + +This library 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 +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library. If not, see . +*********************************************************************/ + +#ifndef STARTUPTASKSMODEL_H +#define STARTUPTASKSMODEL_H + +#include "abstracttasksmodel.h" +#include "taskmanager_export.h" + +namespace TaskManager +{ + +/** + * @short A tasks model for startup notifications. + * + * This model presents tasks sourced from startup notifications. + * + * Startup notifications are given a timeout sourced from klaunchrc, falling + * back to a default of 5 seconds if no configuration is present. + * + * Startup tasks are removed 500 msec after cancellation or timeout to + * overlap with window tasks appearing in window task models. + * + * @author Eike Hein + */ + +class TASKMANAGER_EXPORT StartupTasksModel : public AbstractTasksModel +{ + Q_OBJECT + +public: + explicit StartupTasksModel(QObject *parent = 0); + virtual ~StartupTasksModel(); + + virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + virtual int rowCount(const QModelIndex &parent = QModelIndex()) const override; + + /** + * Request an additional instance of the application backing the + * startup notification at the given index. + * + * @param index An index in this startup tasks model. + **/ + void requestNewInstance(const QModelIndex &index) override; + +private: + class Private; + QScopedPointer d; +}; + +} + +#endif diff --git a/libtaskmanager/startuptasksmodel.cpp b/libtaskmanager/startuptasksmodel.cpp new file mode 100644 --- /dev/null +++ b/libtaskmanager/startuptasksmodel.cpp @@ -0,0 +1,278 @@ +/******************************************************************** +Copyright 2016 Eike Hein + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 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 6 of version 3 of the license. + +This library 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 +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library. If not, see . +*********************************************************************/ + +#include "startuptasksmodel.h" + +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace TaskManager +{ + +class StartupTasksModel::Private +{ +public: + Private(StartupTasksModel *q); + KDirWatch* configWatcher = nullptr; + KStartupInfo *startupInfo = nullptr; + QVector startups; + QHash startupData; + QHash launcherUrls; + + void init(); + void loadConfig(); + QUrl launcherUrl(const KStartupInfoData &data); + +private: + StartupTasksModel *q; +}; + +StartupTasksModel::Private::Private(StartupTasksModel *q) + : q(q) +{ +} + +void StartupTasksModel::Private::init() +{ + configWatcher = new KDirWatch(q); + configWatcher->addFile(QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation) + + QLatin1String("/klaunchrc")); + + QObject::connect(configWatcher, &KDirWatch::dirty, [this] { loadConfig(); }); + QObject::connect(configWatcher, &KDirWatch::created, [this] { loadConfig(); }); + QObject::connect(configWatcher, &KDirWatch::deleted, [this] { loadConfig(); }); + + loadConfig(); +} + +void StartupTasksModel::Private::loadConfig() +{ + const KConfig _c("klaunchrc"); + KConfigGroup c(&_c, "FeedbackStyle"); + + if (!c.readEntry("TaskbarButton", true)) { + delete startupInfo; + startupInfo = nullptr; + + q->beginResetModel(); + startups.clear(); + startupData.clear(); + q->endResetModel(); + + return; + } + + if (!startupInfo) { + startupInfo = new KStartupInfo(KStartupInfo::CleanOnCantDetect, q); + + QObject::connect(startupInfo, &KStartupInfo::gotNewStartup, q, + [this](const KStartupInfoId &id, const KStartupInfoData &data) { + if (startups.contains(id)) { + return; + } + + const QString appId = data.applicationId(); + const QString bin = data.bin(); + + foreach(const KStartupInfoData &known, startupData) { + // Reject if we already have a startup notification for this app. + if (known.applicationId() == appId && known.bin() == bin) { + return; + } + } + + const int count = startups.count(); + q->beginInsertRows(QModelIndex(), count, count); + startups.append(id); + startupData.insert(id.id(), data); + launcherUrls.insert(id.id(), launcherUrl(data)); + q->endInsertRows(); + } + ); + + QObject::connect(startupInfo, &KStartupInfo::gotRemoveStartup, q, + [this](const KStartupInfoId &id) { + // The order in which startups are cancelled and corresponding + // windows appear is not reliable. Add some grace time to make + // an overlap more likely, giving a proxy some time to arbitrate + // between the two. + QTimer::singleShot(500, + [this, id]() { + const int row = startups.indexOf(id); + + if (row != -1) { + q->beginRemoveRows(QModelIndex(), row, row); + startups.removeAt(row); + startupData.remove(id.id()); + launcherUrls.remove(id.id()); + q->endRemoveRows(); + } + } + ); + } + ); + + QObject::connect(startupInfo, &KStartupInfo::gotStartupChange, q, + [this](const KStartupInfoId &id, const KStartupInfoData &data) { + const int row = startups.indexOf(id); + if (row != -1) { + startupData.insert(id.id(), data); + launcherUrls.insert(id.id(), launcherUrl(data)); + QModelIndex idx = q->index(row); + emit q->dataChanged(idx, idx); + } + } + ); + } + + c = KConfigGroup(&_c, "TaskbarButtonSettings"); + startupInfo->setTimeout(c.readEntry("Timeout", 5)); +} + +QUrl StartupTasksModel::Private::launcherUrl(const KStartupInfoData &data) +{ + QUrl launcherUrl; + KService::List services; + + QString appId = data.applicationId(); + + // Try to match via desktop filename ... + if (!appId.isEmpty() && appId.endsWith(QLatin1String(".desktop"))) { + if (appId.startsWith(QLatin1String("/"))) { + launcherUrl = QUrl::fromLocalFile(appId); + return launcherUrl; + } else { + if (appId.endsWith(QLatin1String(".desktop"))) { + appId = appId.mid(appId.length() - 8); + } + + services = KServiceTypeTrader::self()->query(QStringLiteral("Application"), + QStringLiteral("exist Exec and ('%1' =~ DesktopEntryName)").arg(appId)); + } + } + + const QString wmClass = data.WMClass(); + + // Try StartupWMClass. + if (services.empty() && !wmClass.isEmpty()) { + services = KServiceTypeTrader::self()->query(QStringLiteral("Application"), + QStringLiteral("exist Exec and ('%1' =~ StartupWMClass)").arg(wmClass)); + } + + const QString name = data.findName(); + + // Try via name ... + if (services.empty() && !name.isEmpty()) { + services = KServiceTypeTrader::self()->query(QStringLiteral("Application"), + QStringLiteral("exist Exec and ('%1' =~ Name)").arg(name)); + } + + if (!services.empty()) { + QString path = services.at(0)->entryPath(); + + if (path.isEmpty()) { + path = services.at(0)->exec(); + } + + if (!path.isEmpty()) { + launcherUrl = QUrl::fromLocalFile(path); + } + } + + return launcherUrl; +} + +StartupTasksModel::StartupTasksModel(QObject *parent) + : AbstractTasksModel(parent) + , d(new Private(this)) +{ + d->init(); +} + +StartupTasksModel::~StartupTasksModel() +{ +} + +QVariant StartupTasksModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid() || index.row() >= d->startups.count()) { + return QVariant(); + } + + const QByteArray &id = d->startups.at(index.row()).id(); + + if (!d->startupData.contains(id)) { + return QVariant(); + } + + const KStartupInfoData &data = d->startupData.value(id); + + if (role == Qt::DisplayRole) { + return data.findName(); + } else if (role == Qt::DecorationRole) { + return QIcon::fromTheme(data.findIcon(), QIcon::fromTheme(QLatin1String("unknown"))); + } else if (role == AppId) { + QString idFromPath = QUrl::fromLocalFile(data.applicationId()).fileName(); + + if (idFromPath.endsWith(QLatin1String(".desktop"))) { + idFromPath = idFromPath.left(idFromPath.length() - 8); + } + + return idFromPath; + } else if (role == AppName) { + return data.findName(); + } else if (role == LauncherUrl) { + return d->launcherUrls.value(id); + } else if (role == IsStartup) { + return true; + } else if (role == VirtualDesktop) { + return data.desktop(); + } else if (role == IsOnAllVirtualDesktops) { + return (data.desktop() == 0); + } /* else if (role == Screen) { + // You might be tempted to do this, but KStartupInfoData::screen() + // is actually the X11 screen. + return (data.screen() == 0); + } */ + + return QVariant(); +} + +int StartupTasksModel::rowCount(const QModelIndex &parent) const +{ + return parent.isValid() ? 0 : d->startups.count(); +} + +void StartupTasksModel::requestNewInstance(const QModelIndex &index) +{ + Q_UNUSED(index) + + // FIXME Implement. +} + +} diff --git a/libtaskmanager/taskfilterproxymodel.h b/libtaskmanager/taskfilterproxymodel.h new file mode 100644 --- /dev/null +++ b/libtaskmanager/taskfilterproxymodel.h @@ -0,0 +1,360 @@ +/******************************************************************** +Copyright 2016 Eike Hein + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 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 6 of version 3 of the license. + +This library 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 +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library. If not, see . +*********************************************************************/ + +#ifndef TASKFILTERPROXYMODEL_H +#define TASKFILTERPROXYMODEL_H + +#include + +#include "abstracttasksmodeliface.h" + +#include "taskmanager_export.h" + +namespace TaskManager +{ + +/** + * @short A proxy tasks model filtering its source model by various properties. + * + * This proxy model class filters its source tasks model by properties such as + * virtual desktop or minimixed state. The values to filter for or by are set + * as properties on the proxy model instance. + * + * @author Eike Hein + **/ + +class TASKMANAGER_EXPORT TaskFilterProxyModel : public QSortFilterProxyModel, public AbstractTasksModelIface +{ + Q_OBJECT + + Q_PROPERTY(int virtualDesktop READ virtualDesktop WRITE setVirtualDesktop NOTIFY virtualDesktopChanged) + Q_PROPERTY(int screen READ screen WRITE setScreen NOTIFY screenChanged) + Q_PROPERTY(QString activity READ activity WRITE setActivity NOTIFY activityChanged) + + Q_PROPERTY(bool filterByVirtualDesktop READ filterByVirtualDesktop WRITE setFilterByVirtualDesktop NOTIFY filterByVirtualDesktopChanged) + Q_PROPERTY(bool filterByScreen READ filterByScreen WRITE setFilterByScreen NOTIFY filterByScreenChanged) + Q_PROPERTY(bool filterByActivity READ filterByActivity WRITE setFilterByActivity NOTIFY filterByActivityChanged) + Q_PROPERTY(bool filterNotMinimized READ filterNotMinimized WRITE setFilterNotMinimized NOTIFY filterNotMinimizedChanged) + +public: + explicit TaskFilterProxyModel(QObject *parent = 0); + virtual ~TaskFilterProxyModel();; + + void setSourceModel(QAbstractItemModel *sourceModel) override; + + /** + * The number of the virtual desktop used in filtering by virtual + * desktop. Usually set to the number of the current virtual desktop. + * Defaults to @c 0 (virtual desktop numbers start at 1). + * + * @see setVirtualDesktop + * @returns the number of the virtual desktop used in filtering. + **/ + uint virtualDesktop() const; + + /** + * Set the number of the virtual desktop to use in filtering by virtual + * desktop. + * + * If set to 0 (virtual desktop numbers start at 1), filtering by virtual + * desktop is disabled. + * + * @see virtualDesktop + * @param virtualDesktop A virtual desktop number. + **/ + void setVirtualDesktop(uint virtualDesktop); + + /** + * The number of the screen used in filtering by screen. Usually + * set to the number of the current screen. Defaults to @c -1. + * + * @see setScreen + * @returns the number of the screen used in filtering. + **/ + int screen() const; + + /** + * Set the number of the screen to use in filtering by screen. + * + * If set to @c -1, filtering by screen is disabled. + * + * @see screen + * @param screen A screen number. + **/ + void setScreen(int screen); + + /** + * The id of the activity used in filtering by activity. Usually + * set to the id of the current activity. Defaults to an empty id. + * + * @see setActivity + * @returns the id of the activity used in filtering. + **/ + QString activity() const; + + /** + * Set the id of the activity to use in filtering by activity. + * + * @see activity + * @param activity An activity id. + **/ + void setActivity(const QString &activity); + + /** + * Whether tasks should be filtered by virtual desktop. Defaults to + * @c false. + * + * Filtering by virtual desktop only happens if a virtual desktop + * number greater than -1 is set, even if this returns @c true. + * + * @see setFilterByVirtualDesktop + * @see setVirtualDesktop + * @returns @c true if tasks should be filtered by virtual desktop. + **/ + bool filterByVirtualDesktop() const; + + /** + * Set whether tasks should be filtered by virtual desktop. + * + * Filtering by virtual desktop only happens if a virtual desktop + * number is set, even if this is set to @c true. + * + * @see filterByVirtualDesktop + * @see setVirtualDesktop + * @param filter Whether tasks should be filtered by virtual desktop. + **/ + void setFilterByVirtualDesktop(bool filter); + + /** + * Whether tasks should be filtered by screen. Defaults to @c false. + * + * Filtering by screen only happens if a screen number is set, even + * if this returns @c true. + * + * @see setFilterByScreen + * @see setScreen + * @returns @c true if tasks should be filtered by screen. + **/ + bool filterByScreen() const; + + /** + * Set whether tasks should be filtered by screen. + * + * Filtering by screen only happens if a screen number is set, even + * if this is set to @c true. + * + * @see filterByScreen + * @see setScreen + * @param filter Whether tasks should be filtered by screen. + **/ + void setFilterByScreen(bool filter); + + /** + * Whether tasks should be filtered by activity. Defaults to @c false. + * + * Filtering by activity only happens if an activity id is set, even + * if this returns @c true. + * + * @see setFilterByActivity + * @see setActivity + * @returns @ctrue if tasks should be filtered by activity. + **/ + bool filterByActivity() const; + + /** + * Set whether tasks should be filtered by activity. Defaults to + * @c false. + * + * Filtering by virtual desktop only happens if an activity id is set, + * even if this is set to @c true. + * + * @see filterByActivity + * @see setActivity + * @param filter Whether tasks should be filtered by activity. + **/ + void setFilterByActivity(bool filter); + + /** + * Whether non-minimized tasks should be filtered. Defaults to + * @c false. + * + * @see setFilterNotMinimized + * @returns @c true if non-minimized tasks should be filtered. + **/ + bool filterNotMinimized() const; + + /** + * Set whether non-minimized tasks should be filtered. + * + * @see filterNotMinimized + * @param filter Whether non-minimized tasks should be filtered. + **/ + void setFilterNotMinimized(bool filter); + + /** + * Request activation of the task at the given index. Derived classes are + * free to interpret the meaning of "activate" themselves depending on + * the nature and state of the task, e.g. launch or raise a window task. + * + * @param index An index in this tasks model. + **/ + void requestActivate(const QModelIndex &index); + + /** + * Request an additional instance of the application backing the task + * at the given index. + * + * @param index An index in this tasks model. + **/ + void requestNewInstance(const QModelIndex &index); + + /** + * Request the task at the given index be closed. + * + * @param index An index in this tasks model. + **/ + void requestClose(const QModelIndex &index); + + /** + * Request starting an interactive move for the task at the given index. + * + * This is meant for tasks that have an associated window, and may be + * a no-op when there is no window. + * + * @param index An index in this tasks model. + **/ + void requestMove(const QModelIndex &index); + + /** + * Request starting an interactive resize for the task at the given index. + * + * This is meant for tasks that have an associated window, and may be a + * no-op when there is no window. + * + * @param index An index in this tasks model. + **/ + void requestResize(const QModelIndex &index); + + /** + * Request toggling the minimized state of the task at the given index. + * + * This is meant for tasks that have an associated window, and may be + * a no-op when there is no window. + * + * @param index An index in this tasks model. + **/ + void requestToggleMinimized(const QModelIndex &index); + + /** + * Request toggling the maximized state of the task at the given index. + * + * This is meant for tasks that have an associated window, and may be + * a no-op when there is no window. + * + * @param index An index in this tasks model. + **/ + void requestToggleMaximized(const QModelIndex &index); + + /** + * Request toggling the keep-above state of the task at the given index. + * + * This is meant for tasks that have an associated window, and may be + * a no-op when there is no window. + * + * @param index An index in this tasks model. + **/ + void requestToggleKeepAbove(const QModelIndex &index); + + /** + * Request toggling the keep-below state of the task at the given index. + * + * This is meant for tasks that have an associated window, and may be + * a no-op when there is no window. + * + * @param index An index in this tasks model. + **/ + void requestToggleKeepBelow(const QModelIndex &index); + + /** + * Request toggling the fullscreen state of the task at the given index. + * + * This is meant for tasks that have an associated window, and may be + * a no-op when there is no window. + * + * @param index An index in this tasks model. + **/ + void requestToggleFullScreen(const QModelIndex &index); + + /** + * Request toggling the shaded state of the task at the given index. + * + * This is meant for tasks that have an associated window, and may be + * a no-op when there is no window. + * + * @param index An index in this tasks model. + **/ + void requestToggleShaded(const QModelIndex &index); + + /** + * Request moving the task at the given index to the specified virtual + * desktop. + * + * This is meant for tasks that have an associated window, and may be + * a no-op when there is no window. + * + * @param index An index in this tasks model. + * @param desktop A virtual desktop number. + **/ + void requestVirtualDesktop(const QModelIndex &index, qint32 desktop); + + /** + * Request informing the window manager of new geometry for a visual + * delegate for the task at the given index. The geometry should be in + * screen coordinates. + * + * @param index An index in this tasks model. + * @param geometry Visual delegate geometry in screen coordinates. + * @param delegate The delegate. Implementations are on their own with + * regard to extracting information from this, and should take care to + * reject invalid objects. + **/ + void requestPublishDelegateGeometry(const QModelIndex &index, const QRect &geometry, + QObject *delegate = nullptr); + +Q_SIGNALS: + void virtualDesktopChanged() const; + void screenChanged() const; + void activityChanged() const; + void filterByVirtualDesktopChanged() const; + void filterByScreenChanged() const; + void filterByActivityChanged() const; + void filterNotMinimizedChanged() const; + +protected: + bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const; + +private: + class Private; + QScopedPointer d; +}; + +} + +#endif diff --git a/libtaskmanager/taskfilterproxymodel.cpp b/libtaskmanager/taskfilterproxymodel.cpp new file mode 100644 --- /dev/null +++ b/libtaskmanager/taskfilterproxymodel.cpp @@ -0,0 +1,344 @@ +/******************************************************************** +Copyright 2016 Eike Hein + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 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 6 of version 3 of the license. + +This library 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 +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library. If not, see . +*********************************************************************/ + +#include "taskfilterproxymodel.h" +#include "abstracttasksmodel.h" + +namespace TaskManager +{ + +class TaskFilterProxyModel::Private +{ +public: + Private(TaskFilterProxyModel *q); + + AbstractTasksModelIface *sourceTasksModel = nullptr; + + uint virtualDesktop = 0; + int screen = -1; + QString activity; + + bool filterByVirtualDesktop = false; + bool filterByScreen = false; + bool filterByActivity = false; + bool filterNotMinimized = false; + +private: + TaskFilterProxyModel *q; +}; + +TaskFilterProxyModel::Private::Private(TaskFilterProxyModel *q) + : q(q) +{ +} + +TaskFilterProxyModel::TaskFilterProxyModel(QObject *parent) + : QSortFilterProxyModel(parent) + , d(new Private(this)) +{ +} + +TaskFilterProxyModel::~TaskFilterProxyModel() +{ +} + +void TaskFilterProxyModel::setSourceModel(QAbstractItemModel *sourceModel) +{ + d->sourceTasksModel = dynamic_cast(sourceModel); + + QSortFilterProxyModel::setSourceModel(sourceModel); +} + +uint TaskFilterProxyModel::virtualDesktop() const +{ + return d->virtualDesktop; +} + +void TaskFilterProxyModel::setVirtualDesktop(uint virtualDesktop) +{ + if (d->virtualDesktop != virtualDesktop) { + d->virtualDesktop = virtualDesktop; + + if (d->filterByVirtualDesktop) { + invalidateFilter(); + } + + emit virtualDesktopChanged(); + } +} + +int TaskFilterProxyModel::screen() const +{ + return d->screen; +} + +void TaskFilterProxyModel::setScreen(int screen) +{ + if (d->screen != screen) { + d->screen = screen; + + if (d->filterByScreen) { + invalidateFilter(); + } + + emit screenChanged(); + } +} + +QString TaskFilterProxyModel::activity() const +{ + return d->activity; +} + +void TaskFilterProxyModel::setActivity(const QString &activity) +{ + if (d->activity != activity) { + d->activity = activity; + + if (d->filterByActivity) { + invalidateFilter(); + } + + emit activityChanged(); + } +} + +bool TaskFilterProxyModel::filterByVirtualDesktop() const +{ + return d->filterByVirtualDesktop; +} + +void TaskFilterProxyModel::setFilterByVirtualDesktop(bool filter) +{ + if (d->filterByVirtualDesktop != filter) { + d->filterByVirtualDesktop = filter; + + invalidateFilter(); + + emit filterByVirtualDesktopChanged(); + } +} + +bool TaskFilterProxyModel::filterByScreen() const +{ + return d->filterByScreen; +} + +void TaskFilterProxyModel::setFilterByScreen(bool filter) +{ + if (d->filterByScreen != filter) { + d->filterByScreen = filter; + + invalidateFilter(); + + emit filterByScreenChanged(); + } +} + +bool TaskFilterProxyModel::filterByActivity() const +{ + return d->filterByActivity; +} + +void TaskFilterProxyModel::setFilterByActivity(bool filter) +{ + if (d->filterByActivity != filter) { + d->filterByActivity = filter; + + invalidateFilter(); + + emit filterByActivityChanged(); + } +} + +bool TaskFilterProxyModel::filterNotMinimized() const +{ + return d->filterNotMinimized; +} + +void TaskFilterProxyModel::setFilterNotMinimized(bool filter) +{ + if (d->filterNotMinimized != filter) { + d->filterNotMinimized = filter; + + invalidateFilter(); + + emit filterNotMinimizedChanged(); + } +} + +void TaskFilterProxyModel::requestActivate(const QModelIndex &index) +{ + if (d->sourceTasksModel && index.isValid() && index.model() == this) { + d->sourceTasksModel->requestActivate(mapToSource(index)); + } +} + +void TaskFilterProxyModel::requestNewInstance(const QModelIndex &index) +{ + if (d->sourceTasksModel && index.isValid() && index.model() == this) { + d->sourceTasksModel->requestNewInstance(mapToSource(index)); + } +} + +void TaskFilterProxyModel::requestClose(const QModelIndex &index) +{ + if (d->sourceTasksModel && index.isValid() && index.model() == this) { + d->sourceTasksModel->requestClose(mapToSource(index)); + } +} + +void TaskFilterProxyModel::requestMove(const QModelIndex &index) +{ + if (d->sourceTasksModel && index.isValid() && index.model() == this) { + d->sourceTasksModel->requestMove(mapToSource(index)); + } +} + +void TaskFilterProxyModel::requestResize(const QModelIndex &index) +{ + if (d->sourceTasksModel && index.isValid() && index.model() == this) { + d->sourceTasksModel->requestResize(mapToSource(index)); + } +} + +void TaskFilterProxyModel::requestToggleMinimized(const QModelIndex &index) +{ + if (d->sourceTasksModel && index.isValid() && index.model() == this) { + d->sourceTasksModel->requestToggleMinimized(mapToSource(index)); + } +} + +void TaskFilterProxyModel::requestToggleMaximized(const QModelIndex &index) +{ + if (d->sourceTasksModel && index.isValid() && index.model() == this) { + d->sourceTasksModel->requestToggleMaximized(mapToSource(index)); + } +} + +void TaskFilterProxyModel::requestToggleKeepAbove(const QModelIndex &index) +{ + if (d->sourceTasksModel && index.isValid() && index.model() == this) { + d->sourceTasksModel->requestToggleKeepAbove(mapToSource(index)); + } +} + +void TaskFilterProxyModel::requestToggleKeepBelow(const QModelIndex &index) +{ + if (d->sourceTasksModel && index.isValid() && index.model() == this) { + d->sourceTasksModel->requestToggleKeepBelow(mapToSource(index)); + } +} + +void TaskFilterProxyModel::requestToggleFullScreen(const QModelIndex &index) +{ + if (d->sourceTasksModel && index.isValid() && index.model() == this) { + d->sourceTasksModel->requestToggleFullScreen(mapToSource(index)); + } +} + +void TaskFilterProxyModel::requestToggleShaded(const QModelIndex &index) +{ + if (d->sourceTasksModel && index.isValid() && index.model() == this) { + d->sourceTasksModel->requestToggleShaded(mapToSource(index)); + } +} + +void TaskFilterProxyModel::requestVirtualDesktop(const QModelIndex &index, qint32 desktop) +{ + if (d->sourceTasksModel && index.isValid() && index.model() == this) { + d->sourceTasksModel->requestVirtualDesktop(mapToSource(index), desktop); + } +} + +void TaskFilterProxyModel::requestPublishDelegateGeometry(const QModelIndex &index, const QRect &geometry, QObject *delegate) +{ + if (index.isValid() && index.model() == this) { + d->sourceTasksModel->requestPublishDelegateGeometry(mapToSource(index), geometry, delegate); + } +} + +bool TaskFilterProxyModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const +{ + Q_UNUSED(sourceParent) + + const QModelIndex &sourceIdx = sourceModel()->index(sourceRow, 0); + + // Filter tasks that are not to be shown on the task bar. + if (sourceIdx.data(AbstractTasksModel::SkipTaskbar).toBool()) { + return false; + } + + // Filter by virtual desktop. + if (d->filterByVirtualDesktop && d->virtualDesktop != 0) { + if (!sourceIdx.data(AbstractTasksModel::IsOnAllVirtualDesktops).toBool()) { + const QVariant &virtualDesktop = sourceIdx.data(AbstractTasksModel::VirtualDesktop); + + if (!virtualDesktop.isNull()) { + bool ok = false; + const uint i = virtualDesktop.toUInt(&ok); + + if (ok && i != d->virtualDesktop) { + return false; + } + } + } + } + + // Filter by screen. + if (d->filterByScreen && d->screen != -1) { + const QVariant &screen = sourceIdx.data(AbstractTasksModel::Screen); + + if (!screen.isNull()) { + bool ok = false; + const int i = screen.toInt(&ok); + + if (ok && i != -1 && i != d->screen) { + return false; + } + } + } + + // Filter by activity. + if (d->filterByActivity && !d->activity.isEmpty()) { + const QVariant &activities = sourceIdx.data(AbstractTasksModel::Activities); + + if (!activities.isNull()) { + const QStringList l = activities.toStringList(); + + if (!l.isEmpty() && !l.contains(d->activity)) { + return false; + } + } + } + + // Filter not minimized. + if (d->filterNotMinimized) { + bool isMinimized = sourceIdx.data(AbstractTasksModel::IsMinimized).toBool(); + + if (!isMinimized) { + return false; + } + } + + return true; +} + +} diff --git a/libtaskmanager/taskgroupingproxymodel.h b/libtaskmanager/taskgroupingproxymodel.h new file mode 100644 --- /dev/null +++ b/libtaskmanager/taskgroupingproxymodel.h @@ -0,0 +1,351 @@ +/******************************************************************** +Copyright 2016 Eike Hein + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 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 6 of version 3 of the license. + +This library 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 +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library. If not, see . +*********************************************************************/ + +#ifndef TASKGROUPINGPROXYMODEL_H +#define TASKGROUPINGPROXYMODEL_H + +#include + +#include "abstracttasksmodeliface.h" +#include "tasksmodel.h" + +#include "taskmanager_export.h" + +namespace TaskManager +{ + +/** + * @short A proxy tasks model for grouping tasks, forming a tree. + * + * This proxy model groups tasks in its source tasks model, forming a tree + * of tasks. Gouping behavior is influenced by various properties set on + * the proxy model instance. + * + * @author Eike Hein + **/ + +class TASKMANAGER_EXPORT TaskGroupingProxyModel : public QAbstractProxyModel, public AbstractTasksModelIface +{ + Q_OBJECT + + Q_PROPERTY(TasksModel::GroupMode groupMode READ groupMode WRITE setGroupMode NOTIFY groupModeChanged) + Q_PROPERTY(int windowTasksThreshold READ windowTasksThreshold WRITE setWindowTasksThreshold NOTIFY windowTasksThresholdChanged) + Q_PROPERTY(QStringList blacklistedAppIds READ blacklistedAppIds WRITE setBlacklistedAppIds NOTIFY blacklistedAppIdsChanged) + Q_PROPERTY(QStringList blacklistedLauncherUrls READ blacklistedLauncherUrls WRITE setBlacklistedLauncherUrls + NOTIFY blacklistedLauncherUrlsChanged) + +public: + explicit TaskGroupingProxyModel(QObject *parent = 0); + virtual ~TaskGroupingProxyModel(); + + QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override; + QModelIndex parent(const QModelIndex &child) const override; + + QModelIndex mapFromSource(const QModelIndex &sourceIndex) const override; + QModelIndex mapToSource(const QModelIndex &proxyIndex) const override; + + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + int columnCount(const QModelIndex &parent = QModelIndex()) const override; + + QVariant data(const QModelIndex &proxyIndex, int role) const override; + + void setSourceModel(QAbstractItemModel *sourceModel) override; + + /** + * Returns the current group mode, i.e. the criteria by which tasks should + * be grouped. + * + * Defaults to TasksModel::GroupApplication, which groups tasks backed by + * the same application. + * + * If the group mode is TasksModel::GroupDisabled, no grouping is done. + * + * @see TasksModel + * @see setGroupMode + * @returns the active group mode. + **/ + TasksModel::GroupMode groupMode() const; + + /** + * Sets the group mode, i.e. the criteria by which tasks should be grouped. + * + * The group mode can be set to TasksModel::GroupDisabled to disable grouping + * entirely, breaking apart any existing groups. + * + * @see TasksModel + * @see groupMode + * @param mode A TasksModel group mode. + **/ + void setGroupMode(TasksModel::GroupMode mode); + + /** + * As window tasks (AbstractTasksModel::IsWindow) come and go in the source + * model, groups will be formed when this threshold value is exceeded, and + * broken apart when it matches or falls below. + * + * Defaults to @c -1, which means grouping is done regardless of the number + * of window tasks in the source model. + * + * @see setWindowTasksThreshold + * @return the threshold number of source window tasks used in grouping + * decisions. + **/ + int windowTasksThreshold() const; + + /** + * Sets the number of source model window tasks (AbstractTasksModel::IsWindow) + * above which groups will be formed, and at or below which groups will be broken + * apart. + * + * If set to -1, grouping will be done regardless of the number of window tasks + * in the source model. + * + * @see windowTasksThreshold + * @param threshold A threshold number of source window tasks used in grouping + * decisions. + **/ + void setWindowTasksThreshold(int threshold); + + /** + * A blacklist of app ids (AbstractTasksModel::AppId) that is consulted before + * grouping a task. If a task's app id is found on the blacklist, it is not + * grouped. + * + * The default app id blacklist is empty. + * + * @see setBlacklistedAppIds + * @returns the blacklist of app ids consulted before grouping a task. + **/ + QStringList blacklistedAppIds() const; + + /** + * Sets the blacklist of app ids (AbstractTasksModel::AppId) that is consulted + * before grouping a task. If a task's app id is found on the blacklist, it is + * not grouped. + * + * When set, groups will be formed and broken apart as necessary. + * + * @see blacklistedAppIds + * @param list a blacklist of app ids to be consulted before grouping a task. + **/ + void setBlacklistedAppIds(const QStringList &list); + + /** + * A blacklist of launcher URLs (AbstractTasksModel::LauncherUrl) that is + * consulted before grouping a task. If a task's launcher URL is found on the + * blacklist, it is not grouped. + * + * The default launcher URL blacklist is empty. + * + * @see setBlacklistedLauncherUrls + * @returns the blacklist of launcher URLs consulted before grouping a task. + **/ + QStringList blacklistedLauncherUrls() const; + + /** + * Sets the blacklist of launcher URLs (AbstractTasksModel::LauncherUrl) that + * is consulted before grouping a task. If a task's launcher URL is found on + * the blacklist, it is not grouped. + * + * When set, groups will be formed and broken apart as necessary. + * + * @see blacklistedLauncherUrls + * @param list a blacklist of launcher URLs to be consulted before grouping a task. + **/ + void setBlacklistedLauncherUrls(const QStringList &list); + + /** + * Request activation of the task at the given index. Derived classes are + * free to interpret the meaning of "activate" themselves depending on + * the nature and state of the task, e.g. launch or raise a window task. + * + * @param index An index in this tasks model. + **/ + void requestActivate(const QModelIndex &index); + + /** + * Request an additional instance of the application backing the task + * at the given index. + * + * @param index An index in this tasks model. + **/ + void requestNewInstance(const QModelIndex &index); + + /** + * Request the task at the given index be closed. + * + * @param index An index in this tasks model. + **/ + void requestClose(const QModelIndex &index); + + /** + * Request starting an interactive move for the task at the given index. + * + * This is meant for tasks that have an associated window, and may be + * a no-op when there is no window. + * + * This base implementation does nothing. + * + * @param index An index in this tasks model. + **/ + void requestMove(const QModelIndex &index); + + /** + * Request starting an interactive resize for the task at the given index. + * + * This is meant for tasks that have an associated window, and may be a + * no-op when there is no window. + * + * @param index An index in this tasks model. + **/ + void requestResize(const QModelIndex &index); + + /** + * Request toggling the minimized state of the task at the given index. + * + * This is meant for tasks that have an associated window, and may be + * a no-op when there is no window. + * + * This base implementation does nothing. + * + * @param index An index in this tasks model. + **/ + void requestToggleMinimized(const QModelIndex &index); + + /** + * Request toggling the maximized state of the task at the given index. + * + * This is meant for tasks that have an associated window, and may be + * a no-op when there is no window. + * + * @param index An index in this tasks model. + **/ + void requestToggleMaximized(const QModelIndex &index); + + /** + * Request toggling the keep-above state of the task at the given index. + * + * This is meant for tasks that have an associated window, and may be + * a no-op when there is no window. + * + * @param index An index in this tasks model. + **/ + void requestToggleKeepAbove(const QModelIndex &index); + + /** + * Request toggling the keep-below state of the task at the given index. + * + * This is meant for tasks that have an associated window, and may be + * a no-op when there is no window. + * + * @param index An index in this tasks model. + **/ + void requestToggleKeepBelow(const QModelIndex &index); + + /** + * Request toggling the fullscreen state of the task at the given index. + * + * This is meant for tasks that have an associated window, and may be + * a no-op when there is no window. + * + * This base implementation does nothing. + * + * @param index An index in this tasks model. + **/ + void requestToggleFullScreen(const QModelIndex &index); + + /** + * Request toggling the shaded state of the task at the given index. + * + * This is meant for tasks that have an associated window, and may be + * a no-op when there is no window. + * + * @param index An index in this tasks model. + **/ + void requestToggleShaded(const QModelIndex &index); + + /** + * Request moving the task at the given index to the specified virtual + * desktop. + * + * This is meant for tasks that have an associated window, and may be + * a no-op when there is no window. + * + * @param index An index in this tasks model. + * @param desktop A virtual desktop number. + **/ + void requestVirtualDesktop(const QModelIndex &index, qint32 desktop); + + /** + * Request informing the window manager of new geometry for a visual + * delegate for the task at the given index. The geometry should be in + * screen coordinates. + * + * If the task at the given index is a group parent, the geometry is + * set for all of its children. If the task at the given index is a + * group member, the geometry is set for all of its siblings. + * + * @param index An index in this tasks model. + * @param geometry Visual delegate geometry in screen coordinates. + * @param delegate The delegate. Implementations are on their own with + * regard to extracting information from this, and should take care to + * reject invalid objects. + **/ + void requestPublishDelegateGeometry(const QModelIndex &index, const QRect &geometry, + QObject *delegate = nullptr); + + /** + * Request toggling whether the task at the given index, along with any + * tasks matching its kind, should be grouped or not. Task groups will be + * formed or broken apart as needed, along with affecting future grouping + * decisions as new tasks appear in the source model. + * + * As grouping is toggled for a task, updates are made to the blacklisted* + * properties of the model instance. + * + * @see blacklistedAppIds + * @see blacklistedLauncherUrls + * + * @param index An index in this tasks model. + **/ + void requestToggleGrouping(const QModelIndex &index); + +Q_SIGNALS: + void groupModeChanged() const; + void windowTasksThresholdChanged() const; + void blacklistedAppIdsChanged() const; + void blacklistedLauncherUrlsChanged() const; + +private: + class Private; + QScopedPointer d; + + Q_PRIVATE_SLOT(d, void sourceRowsAboutToBeInserted(const QModelIndex &parent, int first, int last)) + Q_PRIVATE_SLOT(d, void sourceRowsInserted(const QModelIndex &parent, int start, int end)) + Q_PRIVATE_SLOT(d, void sourceRowsAboutToBeRemoved(const QModelIndex &parent, int first, int last)) + Q_PRIVATE_SLOT(d, void sourceRowsRemoved(const QModelIndex &parent, int start, int end)) + Q_PRIVATE_SLOT(d, void sourceModelAboutToBeReset()) + Q_PRIVATE_SLOT(d, void sourceModelReset()) + Q_PRIVATE_SLOT(d, void sourceDataChanged(QModelIndex,QModelIndex,QVector)) +}; + +} + +#endif diff --git a/libtaskmanager/taskgroupingproxymodel.cpp b/libtaskmanager/taskgroupingproxymodel.cpp new file mode 100644 --- /dev/null +++ b/libtaskmanager/taskgroupingproxymodel.cpp @@ -0,0 +1,1113 @@ +/******************************************************************** +Copyright 2016 Eike Hein + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 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 6 of version 3 of the license. + +This library 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 +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library. If not, see . +*********************************************************************/ + +#include "taskgroupingproxymodel.h" +#include "abstracttasksmodel.h" +#include "tasktools.h" + +#include + +namespace TaskManager +{ + +class TaskGroupingProxyModel::Private +{ +public: + Private(TaskGroupingProxyModel *q); + + AbstractTasksModelIface *abstractTasksSourceModel = nullptr; + + TasksModel::GroupMode groupMode = TasksModel::GroupApplications; + int windowTasksThreshold = -1; + + QVector> rowMap; + + QSet blacklistedAppIds; + QSet blacklistedLauncherUrls; + + bool isGroup(int row); + bool any(const QModelIndex &parent, int role); + bool all(const QModelIndex &parent, int role); + + void sourceRowsAboutToBeInserted(const QModelIndex &parent, int first, int last); + void sourceRowsInserted(const QModelIndex &parent, int start, int end); + void sourceRowsAboutToBeRemoved(const QModelIndex &parent, int first, int last); + void sourceRowsRemoved(const QModelIndex &parent, int start, int end); + void sourceModelAboutToBeReset(); + void sourceModelReset(); + void sourceDataChanged(QModelIndex topLeft, QModelIndex bottomRight, + const QVector &roles = QVector()); + void adjustMap(int anchor, int delta); + + void rebuildMap(); + bool shouldGroupTasks(); + void checkGrouping(bool silent = false); + bool isBlacklisted(const QModelIndex &sourceIndex); + bool tryToGroup(const QModelIndex &sourceIndex, bool silent = false); + void formGroupFor(const QModelIndex &index); + void breakGroupFor(const QModelIndex &index, bool silent = false); + +private: + TaskGroupingProxyModel *q; +}; + +TaskGroupingProxyModel::Private::Private(TaskGroupingProxyModel *q) + : q(q) +{ +} + +bool TaskGroupingProxyModel::Private::isGroup(int row) +{ + if (row < 0 || row >= rowMap.count()) { + return false; + } + + return (rowMap.at(row).count() > 1); +} + +bool TaskGroupingProxyModel::Private::any(const QModelIndex &parent, int role) +{ + bool is = false; + + for (int i = 0; i < q->rowCount(parent); ++i) { + if (parent.child(i, 0).data(role).toBool()) { + return true; + } + } + + return is; +} + +bool TaskGroupingProxyModel::Private::all(const QModelIndex &parent, int role) +{ + bool is = true; + + for (int i = 0; i < q->rowCount(parent); ++i) { + if (!parent.child(i, 0).data(role).toBool()) { + return false; + } + } + + return is; +} + +void TaskGroupingProxyModel::Private::sourceRowsAboutToBeInserted(const QModelIndex &parent, int first, int last) +{ + Q_UNUSED(parent) + Q_UNUSED(first) + Q_UNUSED(last) +} + +void TaskGroupingProxyModel::Private::sourceRowsInserted(const QModelIndex &parent, int start, int end) +{ + // We only support flat source models. + if (parent.isValid()) { + return; + } + + adjustMap(start, (end - start) + 1); + + bool shouldGroup = shouldGroupTasks(); // Can be slightly expensive; cache return value. + + for (int i = start; i <= end; ++i) { + if (!shouldGroup || !tryToGroup(q->sourceModel()->index(i, 0))) { + q->beginInsertRows(QModelIndex(), rowMap.count(), rowMap.count()); + rowMap.append(QVector{i}); + q->endInsertRows(); + } + } + + checkGrouping(); +} + +void TaskGroupingProxyModel::Private::sourceRowsAboutToBeRemoved(const QModelIndex &parent, int first, int last) +{ + // We only support flat source models. + if (parent.isValid()) { + return; + } + + for (int i = first; i <= last; ++i) { + for (int j = 0; j < rowMap.count(); ++j) { + const QVector &sourceRows = rowMap.at(j); + const int mapIndex = sourceRows.indexOf(i); + + if (mapIndex != -1) { + // Remove top-level item. + if (sourceRows.count() == 1) { + q->beginRemoveRows(QModelIndex(), j, j); + rowMap.remove(j); + q->endRemoveRows(); + // Dissolve group. + } else if (sourceRows.count() == 2) { + const QModelIndex parent = q->index(j, 0); + q->beginRemoveRows(parent, 0, 1); + rowMap[j].remove(mapIndex); + q->endRemoveRows(); + q->dataChanged(parent, parent); + // Remove group member. + } else { + const QModelIndex parent = q->index(j, 0); + q->beginRemoveRows(parent, mapIndex, mapIndex); + rowMap[j].remove(mapIndex); + q->endRemoveRows(); + q->dataChanged(parent, parent); + } + + break; + } + } + } +} + +void TaskGroupingProxyModel::Private::sourceRowsRemoved(const QModelIndex &parent, int start, int end) +{ + // We only support flat source models. + if (parent.isValid()) { + return; + } + + adjustMap(start + 1, -((end - start) + 1)); + + checkGrouping(); +} + +void TaskGroupingProxyModel::Private::sourceModelAboutToBeReset() +{ + q->beginResetModel(); +} + +void TaskGroupingProxyModel::Private::sourceModelReset() +{ + rebuildMap(); + + q->endResetModel(); +} + +void TaskGroupingProxyModel::Private::sourceDataChanged(QModelIndex topLeft, QModelIndex bottomRight, const QVector &roles) +{ + for (int i = topLeft.row(); i <= bottomRight.row(); ++i) { + const QModelIndex &sourceIndex = q->sourceModel()->index(i, 0); + QModelIndex proxyIndex = q->mapFromSource(sourceIndex); + + if (!proxyIndex.isValid()) { + return; + } + + const QModelIndex parent = proxyIndex.parent(); + + // If a child item changes, its parent may need an update as well as many of + // the data roles evaluate child data. See data(). + // TODO: Some roles do not need to bubble up as they fall through to the first + // child in data(); it _might_ be worth adding constraints here later. + if (parent.isValid()) { + q->dataChanged(parent, parent, roles); + + return; + } + + // tryToGroup() exempts tasks which demand attention from being grouped. + // If this task is no longer demanding attention, we need to try grouping + // it now. + if (roles.contains(AbstractTasksModel::IsDemandingAttention) && + !sourceIndex.data(AbstractTasksModel::IsDemandingAttention).toBool()) { + + if (shouldGroupTasks() && tryToGroup(sourceIndex)) { + q->beginRemoveRows(QModelIndex(), proxyIndex.row(), proxyIndex.row()); + rowMap.remove(proxyIndex.row()); + q->endRemoveRows(); + } else { + q->dataChanged(proxyIndex, proxyIndex, roles); + } + } else { + q->dataChanged(proxyIndex, proxyIndex, roles); + } + } +} + +void TaskGroupingProxyModel::Private::adjustMap(int anchor, int delta) +{ + for (int i = 0; i < rowMap.count(); ++i) { + bool changed = false; + QVector sourceRows = rowMap.at(i); + QMutableVectorIterator it(sourceRows); + + while (it.hasNext()) { + it.next(); + + if (it.value() >= anchor) { + it.setValue(it.value() + delta); + changed = true; + } + } + + if (changed) { + rowMap.replace(i, sourceRows); + } + } +} + +void TaskGroupingProxyModel::Private::rebuildMap() +{ + rowMap.clear(); + + const int rows = q->sourceModel()->rowCount(); + + rowMap.reserve(rows); + + for (int i = 0; i < rows; ++i) { + rowMap.append(QVector{i}); + } + + checkGrouping(true /* silent */); +} + +bool TaskGroupingProxyModel::Private::shouldGroupTasks() +{ + if (groupMode == TasksModel::GroupDisabled) { + return false; + } + + if (windowTasksThreshold != -1) { + // We're going to check the number of window tasks in the source model + // against the grouping threshold. In practice that means we're ignoring + // launcher and startup tasks. Startup tasks because they're very short- + // lived (i.e. forming/breaking groups as they come and go would be very + // noisy) and launcher tasks because we expect consumers to budget for + // them in the threshold they set. + int windowTasksCount = 0; + + for (int i = 0; i < q->sourceModel()->rowCount(); ++i) { + const QModelIndex &idx = q->sourceModel()->index(i, 0); + + if (idx.data(AbstractTasksModel::IsWindow).toBool()) { + ++windowTasksCount; + } + } + + return (windowTasksCount > windowTasksThreshold); + } + + return true; +} + +void TaskGroupingProxyModel::Private::checkGrouping(bool silent) +{ + if (shouldGroupTasks()) { + for (int i = (rowMap.count()) - 1; i >= 0; --i) { + if (isGroup(i)) { + continue; + } + + if (tryToGroup(q->sourceModel()->index(rowMap.at(i).constFirst(), 0), silent)) { + q->beginRemoveRows(QModelIndex(), i, i); + rowMap.remove(i); // Safe since we're iterating backwards. + q->endRemoveRows(); + } + } + } else { + for (int i = (rowMap.count()) - 1; i >= 0; --i) { + breakGroupFor(q->index(i, 0), silent); + } + } +} + +bool TaskGroupingProxyModel::Private::isBlacklisted(const QModelIndex &sourceIndex) +{ + // Check app id against blacklist. + if (blacklistedAppIds.count() + && blacklistedAppIds.contains(sourceIndex.data(AbstractTasksModel::AppId).toString())) { + return true; + } + + // Check launcher URL (sans query items) against blacklist. + if (blacklistedLauncherUrls.count()) { + const QUrl &launcherUrl = sourceIndex.data(AbstractTasksModel::LauncherUrl).toUrl(); + const QString &launcherUrlString = launcherUrl.toString(QUrl::PrettyDecoded | QUrl::RemoveQuery); + + if (blacklistedLauncherUrls.contains(launcherUrlString)) { + return true; + } + } + + return false; +} + +bool TaskGroupingProxyModel::Private::tryToGroup(const QModelIndex &sourceIndex, bool silent) +{ + // NOTE: We only group window tasks at this time. If this ever changes, the + // implementation of data() will have to be adjusted significantly, as for + // many roles it currently falls through to the first child item when dealing + // with requests for the parent (e.g. IsWindow). + if (!sourceIndex.data(AbstractTasksModel::IsWindow).toBool()) { + return false; + } + + // If this task is demanding attention, don't group it at this time. We'll + // try to group it once it no longer demands attention. + if (sourceIndex.data(AbstractTasksModel::IsDemandingAttention).toBool()) { + return false; + } + + // Blacklist checks. + if (isBlacklisted(sourceIndex)) { + return false; + } + + // Meat of the matter: Try to add this source row to a sub-list with source rows + // associated with the same application. + for (int i = 0; i < rowMap.count(); ++i) { + const QModelIndex &groupRep = q->sourceModel()->index(rowMap.at(i).constFirst(), 0); + + // Don't match a row with itself. + if (sourceIndex == groupRep) { + continue; + } + + // Don't group windows with anything other than windows. + if (!groupRep.data(AbstractTasksModel::IsWindow).toBool()) { + continue; + } + + if (appsMatch(sourceIndex, groupRep)) { + const QModelIndex parent = q->index(i, 0); + bool groupFormed = false; + + if (!silent) { + const int newIndex = rowMap.at(i).count(); + + if (newIndex == 1) { + groupFormed = true; + q->beginInsertRows(parent, 0, 1); + } else { + q->beginInsertRows(parent, newIndex, newIndex); + } + } + + rowMap[i].append(sourceIndex.row()); + + if (!silent) { + q->endInsertRows(); + + // If we turned a top-level item into a group parent, we need + // to let consumers know. + // TODO: It _might_ be worth optimizing this at some point by + // adding a roles constaint (e.g. IsGroupParent, MimeType, ...); + // see data(). + if (groupFormed) { + q->dataChanged(parent, parent); + } + } + + return true; + } + } + + return false; +} + +void TaskGroupingProxyModel::Private::formGroupFor(const QModelIndex &index) +{ + // Already in group or a group. + if (index.parent().isValid() || isGroup(index.row())) { + return; + } + + // We need to grab a source index as we may invalidate the index passed + // in through grouping. + const QModelIndex &sourceTarget = q->mapToSource(index); + + for (int i = (rowMap.count() - 1); i >= 0; --i) { + const QModelIndex &sourceIndex = q->sourceModel()->index(rowMap.at(i).constFirst(), 0); + + if (!appsMatch(sourceTarget, sourceIndex)) { + continue; + } + + if (tryToGroup(sourceIndex)) { + q->beginRemoveRows(QModelIndex(), i, i); + rowMap.remove(i); // Safe since we're iterating backwards. + q->endRemoveRows(); + } + } +} + +void TaskGroupingProxyModel::Private::breakGroupFor(const QModelIndex &index, bool silent) +{ + const int row = index.row(); + + if (!isGroup(row)) { + return; + } + + // The first child will move up to the top level. + QVector extraChildren = rowMap.at(row).mid(1); + + // NOTE: We're going to do remove+insert transactions instead of a + // single reparenting move transaction to save on complexity in the + // proxies above us. + // TODO: This could technically be optimized, though it's very + // unlikely to be ever worth it. + if (!silent) { + q->beginRemoveRows(index, 0, (extraChildren.count() + 1)); + } + + rowMap[row].resize(1); + + if (!silent) { + q->endRemoveRows(); + + q->beginInsertRows(QModelIndex(), rowMap.count(), + rowMap.count() + (extraChildren.count() - 1)); + } + + for (int i = 0; i < extraChildren.count(); ++i) { + rowMap.append(QVector{extraChildren.at(i)}); + } + + if (!silent) { + q->endInsertRows(); + + // We're no longer a group parent. + q->dataChanged(index, index); + } +} + +TaskGroupingProxyModel::TaskGroupingProxyModel(QObject *parent) : QAbstractProxyModel(parent) + , d(new Private(this)) +{ +} + +TaskGroupingProxyModel::~TaskGroupingProxyModel() +{ +} + +QModelIndex TaskGroupingProxyModel::index(int row, int column, const QModelIndex &parent) const +{ + if (row < 0 || column < 0) { + return QModelIndex(); + } + + if (parent.isValid()) { + return createIndex(row, column, parent.row() + 1); + } + + return createIndex(row, column, (quintptr)0); +} + +QModelIndex TaskGroupingProxyModel::parent(const QModelIndex &child) const +{ + return (child.internalId() == 0) ? QModelIndex() : index(child.internalId() - 1, 0); +} + +QModelIndex TaskGroupingProxyModel::mapFromSource(const QModelIndex &sourceIndex) const +{ + if (!sourceIndex.isValid() || sourceIndex.model() != sourceModel()) { + return QModelIndex(); + } + + for (int i = 0; i < d->rowMap.count(); ++i) { + const QVector &sourceRows = d->rowMap.at(i); + const int childIndex = sourceRows.indexOf(sourceIndex.row()); + const QModelIndex parent = index(i, 0); + + if (childIndex == 0) { + // If the sub-list we found the source row in is larger than 1 (i.e. part + // of a group, map to the logical child item instead of the parent item + // the source row also stands in for. The parent is therefore unreachable + // from mapToSource(). + if (d->isGroup(i)) { + return index(0, 0, parent); + // Otherwise map to the top-level item. + } else { + return parent; + } + } else if (childIndex != -1) { + return index(childIndex, 0, parent); + } + } + + return QModelIndex(); +} + +QModelIndex TaskGroupingProxyModel::mapToSource(const QModelIndex &proxyIndex) const +{ + if (!proxyIndex.isValid() || proxyIndex.model() != this || !sourceModel()) { + return QModelIndex(); + } + + const QModelIndex &parent = proxyIndex.parent(); + + if (parent.isValid()) { + if (parent.row() < 0 || parent.row() >= d->rowMap.count()) { + return QModelIndex(); + } + + return sourceModel()->index(d->rowMap.at(parent.row()).at(proxyIndex.row()), 0); + } else { + // Group parents items therefore equate to the first child item; the source + // row logically appears twice in the proxy. + // mapFromSource() is not required to handle this well (consider proxies can + // filter out rows, too) and opts to map to the child item, as the group parent + // has its Qt::DisplayRole mangled by data(), and it's more useful for trans- + // lating dataChanged() from the source model. + return sourceModel()->index(d->rowMap.at(proxyIndex.row()).at(0), 0); + } + + return QModelIndex(); +} + +int TaskGroupingProxyModel::rowCount(const QModelIndex &parent) const +{ + if (!sourceModel()) { + return 0; + } + + if (parent.isValid() && parent.model() == this) { + if (parent.row() < 0 || parent.row() >= d->rowMap.count()) { + return 0; + } + + const uint rowCount = d->rowMap.at(parent.row()).count(); + + // If this sub-list in the map only has one entry, it's a plain item, not + // parent to a group. + if (rowCount == 1) { + return 0; + } else { + return rowCount; + } + } + + return d->rowMap.count(); +} + +int TaskGroupingProxyModel::columnCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent) + + return 1; +} + +QVariant TaskGroupingProxyModel::data(const QModelIndex &proxyIndex, int role) const +{ + if (!proxyIndex.isValid() || proxyIndex.model() != this || !sourceModel()) { + return QVariant(); + } + + const QModelIndex &parent = proxyIndex.parent(); + const bool isGroup = (!parent.isValid() && d->isGroup(proxyIndex.row())); + + // For group parent items, this will map to the first child task. + const QModelIndex &sourceIndex = mapToSource(proxyIndex); + + if (!sourceIndex.isValid()) { + return QVariant(); + } + + if (role == AbstractTasksModel::IsGroupable) { + return !d->isBlacklisted(sourceIndex); + } + + if (isGroup) { + // For group parent items, DisplayRole is mapped to AppName of the first child. + if (role == Qt::DisplayRole) { + return sourceIndex.data(AbstractTasksModel::AppName); + } else if (role == AbstractTasksModel::LegacyWinIdList) { + QVariantList winIds; + + for (int i = 0; i < rowCount(proxyIndex); ++i) { + winIds.append(proxyIndex.child(i, 0).data(AbstractTasksModel::LegacyWinIdList).toList()); + } + + return winIds; + } else if (role == AbstractTasksModel::MimeType) { + // FIXME: Legacy X11 stuff, but it's what we have for now. + return QStringLiteral("windowsystem/multiple-winids"); + } else if (role == AbstractTasksModel::MimeData) { + // FIXME TODO: Implement. + return QVariant(); + } else if (role == AbstractTasksModel::IsGroupParent) { + return true; + } else if (role == AbstractTasksModel::IsActive) { + return d->any(proxyIndex, AbstractTasksModel::IsActive); + } else if (role == AbstractTasksModel::IsClosable) { + return d->all(proxyIndex, AbstractTasksModel::IsClosable); + } else if (role == AbstractTasksModel::IsMovable) { + // Moving groups makes no sense. + return false; + } else if (role == AbstractTasksModel::IsResizable) { + // Resizing groups makes no sense. + return false; + } else if (role == AbstractTasksModel::IsMaximizable) { + return d->all(proxyIndex, AbstractTasksModel::IsMaximizable); + } else if (role == AbstractTasksModel::IsMaximized) { + return d->all(proxyIndex, AbstractTasksModel::IsMaximized); + } else if (role == AbstractTasksModel::IsMinimizable) { + return d->all(proxyIndex, AbstractTasksModel::IsMinimizable); + } else if (role == AbstractTasksModel::IsMinimized) { + return d->all(proxyIndex, AbstractTasksModel::IsMinimized); + } else if (role == AbstractTasksModel::IsKeepAbove) { + return d->all(proxyIndex, AbstractTasksModel::IsKeepAbove); + } else if (role == AbstractTasksModel::IsKeepBelow) { + return d->all(proxyIndex, AbstractTasksModel::IsKeepBelow); + } else if (role == AbstractTasksModel::IsFullScreenable) { + return d->all(proxyIndex, AbstractTasksModel::IsFullScreenable); + } else if (role == AbstractTasksModel::IsFullScreen) { + return d->all(proxyIndex, AbstractTasksModel::IsFullScreen); + } else if (role == AbstractTasksModel::IsShadeable) { + return d->all(proxyIndex, AbstractTasksModel::IsShadeable); + } else if (role == AbstractTasksModel::IsShaded) { + return d->all(proxyIndex, AbstractTasksModel::IsShaded); + } else if (role == AbstractTasksModel::IsVirtualDesktopChangeable) { + return d->all(proxyIndex, AbstractTasksModel::IsVirtualDesktopChangeable); + } else if (role == AbstractTasksModel::VirtualDesktop) { + // TODO: Nothing needs this for now and it would add complexity to + // make it a list; skip it until needed. + return QVariant(); + } else if (role == AbstractTasksModel::Screen) { + // TODO: Nothing needs this for now and it would add complexity to + // make it a list; skip it until needed. + return QVariant(); + } else if (role == AbstractTasksModel::Activities) { + QStringList activities; + + for (int i = 0; i < rowCount(proxyIndex); ++i) { + activities.append(proxyIndex.child(i, 0).data(AbstractTasksModel::Activities).toStringList()); + } + + activities.removeDuplicates(); + return activities; + } else if (role == AbstractTasksModel::IsDemandingAttention) { + return d->any(proxyIndex, AbstractTasksModel::IsDemandingAttention); + } else if (role == AbstractTasksModel::SkipTaskbar) { + return d->all(proxyIndex, AbstractTasksModel::SkipTaskbar); + } + } + + return sourceIndex.data(role); +} + +void TaskGroupingProxyModel::setSourceModel(QAbstractItemModel *sourceModel) +{ + if (sourceModel == QAbstractProxyModel::sourceModel()) { + return; + } + + beginResetModel(); + + if (QAbstractProxyModel::sourceModel()) { + QAbstractProxyModel::sourceModel()->disconnect(this); + } + + QAbstractProxyModel::setSourceModel(sourceModel); + d->abstractTasksSourceModel = dynamic_cast(sourceModel); + + if (sourceModel) { + d->rebuildMap(); + + connect(sourceModel, SIGNAL(rowsAboutToBeInserted(QModelIndex,int,int)), + this, SLOT(sourceRowsAboutToBeInserted(QModelIndex,int,int)), Qt::UniqueConnection); + connect(sourceModel, SIGNAL(rowsInserted(QModelIndex,int,int)), + this, SLOT(sourceRowsInserted(QModelIndex,int,int)), Qt::UniqueConnection); + connect(sourceModel, SIGNAL(rowsAboutToBeRemoved(QModelIndex,int,int)), + this, SLOT(sourceRowsAboutToBeRemoved(QModelIndex,int,int)), Qt::UniqueConnection); + connect(sourceModel, SIGNAL(rowsRemoved(QModelIndex,int,int)), + this, SLOT(sourceRowsRemoved(QModelIndex,int,int)), Qt::UniqueConnection); + connect(sourceModel, SIGNAL(modelAboutToBeReset()), + this, SLOT(sourceModelAboutToBeReset()), Qt::UniqueConnection); + connect(sourceModel, SIGNAL(modelReset()), + this, SLOT(sourceModelReset()), Qt::UniqueConnection); + connect(sourceModel, SIGNAL(dataChanged(QModelIndex,QModelIndex,QVector)), + this, SLOT(sourceDataChanged(QModelIndex,QModelIndex,QVector)), Qt::UniqueConnection); + } else { + d->rowMap.clear(); + } + + endResetModel(); +} + +TasksModel::GroupMode TaskGroupingProxyModel::groupMode() const +{ + return d->groupMode; +} + +void TaskGroupingProxyModel::setGroupMode(TasksModel::GroupMode mode) +{ + if (d->groupMode != mode) { + + d->groupMode = mode; + + d->checkGrouping(); + + emit groupModeChanged(); + } +} + +int TaskGroupingProxyModel::windowTasksThreshold() const +{ + return d->windowTasksThreshold; +} + +void TaskGroupingProxyModel::setWindowTasksThreshold(int threshold) +{ + if (d->windowTasksThreshold != threshold) { + d->windowTasksThreshold = threshold; + + d->checkGrouping(); + + emit windowTasksThresholdChanged(); + } +} + +QStringList TaskGroupingProxyModel::blacklistedAppIds() const +{ + return d->blacklistedAppIds.toList(); +} + +void TaskGroupingProxyModel::setBlacklistedAppIds(const QStringList &list) +{ + const QSet &set = QSet::fromList(list); + + if (d->blacklistedAppIds != set) { + d->blacklistedAppIds = set; + + // checkGrouping() will gather and group up what's newly-allowed under the changed + // blacklist. + d->checkGrouping(); + + // Now break apart what we need to. + for (int i = (d->rowMap.count() - 1); i >= 0; --i) { + if (d->isGroup(i)) { + const QModelIndex &groupRep = index(i, 0); + + if (set.contains(groupRep.data(AbstractTasksModel::AppId).toString())) { + d->breakGroupFor(groupRep); // Safe since we're iterating backwards. + } + } + } + + emit blacklistedAppIdsChanged(); + } +} + +QStringList TaskGroupingProxyModel::blacklistedLauncherUrls() const +{ + return d->blacklistedLauncherUrls.toList(); +} + +void TaskGroupingProxyModel::setBlacklistedLauncherUrls(const QStringList &list) +{ + const QSet &set = QSet::fromList(list); + + if (d->blacklistedLauncherUrls != set) { + d->blacklistedLauncherUrls = set; + + // checkGrouping() will gather and group up what's newly-allowed under the changed + // blacklist. + d->checkGrouping(); + + // Now break apart what we need to. + for (int i = (d->rowMap.count() - 1); i >= 0; --i) { + if (d->isGroup(i)) { + const QModelIndex &groupRep = index(i, 0); + const QUrl &launcherUrl = groupRep.data(AbstractTasksModel::LauncherUrl).toUrl(); + const QString &launcherUrlString = launcherUrl.toString(QUrl::RemoveQuery | QUrl::RemoveQuery); + + if (set.contains(launcherUrlString)) { + d->breakGroupFor(groupRep); // Safe since we're iterating backwards. + } + } + } + + emit blacklistedLauncherUrlsChanged(); + } +} + +void TaskGroupingProxyModel::requestActivate(const QModelIndex &index) +{ + if (!d->abstractTasksSourceModel || !index.isValid() || index.model() != this) { + return; + } + + if (index.parent().isValid() || !d->isGroup(index.row())) { + d->abstractTasksSourceModel->requestActivate(mapToSource(index)); + } +} + +void TaskGroupingProxyModel::requestNewInstance(const QModelIndex &index) +{ + if (!d->abstractTasksSourceModel || !index.isValid() || index.model() != this) { + return; + } + + d->abstractTasksSourceModel->requestNewInstance(mapToSource(index)); +} + +void TaskGroupingProxyModel::requestClose(const QModelIndex &index) +{ + if (!d->abstractTasksSourceModel || !index.isValid() || index.model() != this) { + return; + } + + if (index.parent().isValid() || !d->isGroup(index.row())) { + d->abstractTasksSourceModel->requestClose(mapToSource(index)); + } else { + const int row = index.row(); + + for (int i = (rowCount(index) - 1); i >= 1; --i) { + const QModelIndex &sourceChild = mapToSource(index.child(i, 0)); + d->abstractTasksSourceModel->requestClose(sourceChild); + } + + d->abstractTasksSourceModel->requestClose(mapToSource(TaskGroupingProxyModel::index(row, 0))); + } +} + +void TaskGroupingProxyModel::requestMove(const QModelIndex &index) +{ + if (!d->abstractTasksSourceModel || !index.isValid() || index.model() != this) { + return; + } + + if (index.parent().isValid() || !d->isGroup(index.row())) { + d->abstractTasksSourceModel->requestMove(mapToSource(index)); + } +} + +void TaskGroupingProxyModel::requestResize(const QModelIndex &index) +{ + if (!d->abstractTasksSourceModel || !index.isValid() || index.model() != this) { + return; + } + + if (index.parent().isValid() || !d->isGroup(index.row())) { + d->abstractTasksSourceModel->requestResize(mapToSource(index)); + } +} + +void TaskGroupingProxyModel::requestToggleMinimized(const QModelIndex &index) +{ + if (!d->abstractTasksSourceModel || !index.isValid() || index.model() != this) { + return; + } + + if (index.parent().isValid() || !d->isGroup(index.row())) { + d->abstractTasksSourceModel->requestToggleMinimized(mapToSource(index)); + } else { + const bool goalState = !index.data(AbstractTasksModel::IsMinimized).toBool(); + + for (int i = 0; i < rowCount(index); ++i) { + const QModelIndex &child = index.child(i, 0); + + if (child.data(AbstractTasksModel::IsMinimized).toBool() != goalState) { + d->abstractTasksSourceModel->requestToggleMinimized(mapToSource(child)); + } + } + } +} + +void TaskGroupingProxyModel::requestToggleMaximized(const QModelIndex &index) +{ + if (!d->abstractTasksSourceModel || !index.isValid() || index.model() != this) { + return; + } + + if (index.parent().isValid() || !d->isGroup(index.row())) { + d->abstractTasksSourceModel->requestToggleMaximized(mapToSource(index)); + } else { + const bool goalState = !index.data(AbstractTasksModel::IsMaximized).toBool(); + + for (int i = 0; i < rowCount(index); ++i) { + const QModelIndex &child = index.child(i, 0); + + if (child.data(AbstractTasksModel::IsMaximized).toBool() != goalState) { + d->abstractTasksSourceModel->requestToggleMaximized(mapToSource(child)); + } + } + } +} + +void TaskGroupingProxyModel::requestToggleKeepAbove(const QModelIndex &index) +{ + if (!d->abstractTasksSourceModel || !index.isValid() || index.model() != this) { + return; + } + + if (index.parent().isValid() || !d->isGroup(index.row())) { + d->abstractTasksSourceModel->requestToggleKeepAbove(mapToSource(index)); + } else { + const bool goalState = !index.data(AbstractTasksModel::IsKeepAbove).toBool(); + + for (int i = 0; i < rowCount(index); ++i) { + const QModelIndex &child = index.child(i, 0); + + if (child.data(AbstractTasksModel::IsKeepAbove).toBool() != goalState) { + d->abstractTasksSourceModel->requestToggleKeepAbove(mapToSource(child)); + } + } + } +} + +void TaskGroupingProxyModel::requestToggleKeepBelow(const QModelIndex &index) +{ + if (!d->abstractTasksSourceModel || !index.isValid() || index.model() != this) { + return; + } + + if (index.parent().isValid() || !d->isGroup(index.row())) { + d->abstractTasksSourceModel->requestToggleKeepBelow(mapToSource(index)); + } else { + const bool goalState = !index.data(AbstractTasksModel::IsKeepBelow).toBool(); + + for (int i = 0; i < rowCount(index); ++i) { + const QModelIndex &child = index.child(i, 0); + + if (child.data(AbstractTasksModel::IsKeepBelow).toBool() != goalState) { + d->abstractTasksSourceModel->requestToggleKeepBelow(mapToSource(child)); + } + } + } +} + +void TaskGroupingProxyModel::requestToggleFullScreen(const QModelIndex &index) +{ + if (!d->abstractTasksSourceModel || !index.isValid() || index.model() != this) { + return; + } + + if (index.parent().isValid() || !d->isGroup(index.row())) { + d->abstractTasksSourceModel->requestToggleFullScreen(mapToSource(index)); + } else { + const bool goalState = !index.data(AbstractTasksModel::IsFullScreen).toBool(); + + for (int i = 0; i < rowCount(index); ++i) { + const QModelIndex &child = index.child(i, 0); + + if (child.data(AbstractTasksModel::IsFullScreen).toBool() != goalState) { + d->abstractTasksSourceModel->requestToggleFullScreen(mapToSource(child)); + } + } + } +} + +void TaskGroupingProxyModel::requestToggleShaded(const QModelIndex &index) +{ + if (!d->abstractTasksSourceModel || !index.isValid() || index.model() != this) { + return; + } + + if (index.parent().isValid() || !d->isGroup(index.row())) { + d->abstractTasksSourceModel->requestToggleShaded(mapToSource(index)); + } else { + const bool goalState = !index.data(AbstractTasksModel::IsShaded).toBool(); + + for (int i = 0; i < rowCount(index); ++i) { + const QModelIndex &child = index.child(i, 0); + + if (child.data(AbstractTasksModel::IsShaded).toBool() != goalState) { + d->abstractTasksSourceModel->requestToggleShaded(mapToSource(child)); + } + } + } +} + +void TaskGroupingProxyModel::requestVirtualDesktop(const QModelIndex &index, qint32 desktop) +{ + if (!d->abstractTasksSourceModel || !index.isValid() || index.model() != this) { + return; + } + + if (index.parent().isValid() || !d->isGroup(index.row())) { + d->abstractTasksSourceModel->requestVirtualDesktop(mapToSource(index), desktop); + } else { + const int row = index.row(); + + for (int i = (rowCount(index) - 1); i >= 1; --i) { + const QModelIndex &sourceChild = mapToSource(index.child(i, 0)); + d->abstractTasksSourceModel->requestVirtualDesktop(sourceChild, desktop); + } + + d->abstractTasksSourceModel->requestVirtualDesktop(mapToSource(TaskGroupingProxyModel::index(row, 0)), + desktop); + } +} + +void TaskGroupingProxyModel::requestPublishDelegateGeometry(const QModelIndex &index, const QRect &geometry, QObject *delegate) +{ + if (!d->abstractTasksSourceModel || !index.isValid() || index.model() != this) { + return; + } + + if (index.parent().isValid() || !d->isGroup(index.row())) { + d->abstractTasksSourceModel->requestPublishDelegateGeometry(mapToSource(index), + geometry, delegate); + } else { + for (int i = 0; i < rowCount(index); ++i) { + d->abstractTasksSourceModel->requestPublishDelegateGeometry(mapToSource(index.child(i, 0)), + geometry, delegate); + } + } +} + +void TaskGroupingProxyModel::requestToggleGrouping(const QModelIndex &index) +{ + const QString &appId = index.data(AbstractTasksModel::AppId).toString(); + const QUrl &launcherUrl = index.data(AbstractTasksModel::LauncherUrl).toUrl(); + const QString &launcherUrlString = launcherUrl.toString(QUrl::RemoveQuery | QUrl::RemoveQuery); + + if (d->blacklistedAppIds.contains(appId) || d->blacklistedLauncherUrls.contains(launcherUrlString)) { + d->blacklistedAppIds.remove(appId); + d->blacklistedLauncherUrls.remove(launcherUrlString); + + if (d->groupMode != TasksModel::GroupDisabled) { + d->formGroupFor(index.parent().isValid() ? index.parent() : index); + } + } else { + d->blacklistedAppIds.insert(appId); + d->blacklistedLauncherUrls.insert(launcherUrlString); + + if (d->groupMode != TasksModel::GroupDisabled) { + d->breakGroupFor(index.parent().isValid() ? index.parent() : index); + } + } + + // Update IsGroupable data role for all relevant top-level items. We don't need to update + // for group members since they've just been inserted -- it's logically impossible to + // toggle grouping _on_ from a group member. + for (int i = 0; i < d->rowMap.count(); ++i) { + if (!d->isGroup(i)) { + const QModelIndex &idx = TaskGroupingProxyModel::index(i, 0); + + if (idx.data(AbstractTasksModel::AppId).toString() == appId + || launcherUrlsMatch(idx.data(AbstractTasksModel::LauncherUrl).toUrl(), launcherUrl, + IgnoreQueryItems)) { + dataChanged(idx, idx, QVector{AbstractTasksModel::IsGroupable}); + } + } + } + + emit blacklistedAppIdsChanged(); + emit blacklistedLauncherUrlsChanged(); +} + +} + +#include "moc_taskgroupingproxymodel.cpp" diff --git a/libtaskmanager/taskmanagerrulesrc b/libtaskmanager/taskmanagerrulesrc new file mode 100644 --- /dev/null +++ b/libtaskmanager/taskmanagerrulesrc @@ -0,0 +1,17 @@ +[Mapping] +Gimp-2.8=GIMP +Google-chrome=Google Chrome +Google-chrome-stable=Google Chrome +Systemsettings=System Settings +libreoffice-base=LibreOffice Base +libreoffice-calc=LibreOffice Calc +libreoffice-draw=LibreOffice Draw +libreoffice-writer=LibreOffice Writer +libreoffice-math=LibreOffice Math +libreoffice-impress=LibreOffice Impress +libreoffice-startcenter=LibreOffice +oracle-ide-boot-Launcher=Oracle SQL Developer +Dragon=dragonplayer +[Settings] +ManualOnly=Wine +MatchCommandLineFirst=VirtualBox diff --git a/libtaskmanager/tasksmodel.h b/libtaskmanager/tasksmodel.h new file mode 100644 --- /dev/null +++ b/libtaskmanager/tasksmodel.h @@ -0,0 +1,702 @@ +/******************************************************************** +Copyright 2016 Eike Hein + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 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 6 of version 3 of the license. + +This library 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 +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library. If not, see . +*********************************************************************/ + +#ifndef TASKSMODEL_H +#define TASKSMODEL_H + +#include + +#include "abstracttasksmodeliface.h" + +#include "taskmanager_export.h" + +namespace TaskManager +{ + +/** + * @short A unified tasks model. + * + * This model presents tasks sourced from supplied launcher URLs, startup + * notification data and window data retrieved from the windowing server + * the host process is connected to. The underlying windowing system is + * abstracted away. + * + * The source data is abstracted into a unified lifecycle for tasks + * suitable for presentation in a user interface. + * + * Matching startup and window tasks replace launcher tasks. Startup + * tasks are omitted when matching window tasks exist. Tasks that desire + * not to be shown in a user interface are omitted. + * + * Tasks may be filtered, sorted or grouped by setting properties on the + * model. + * + * Tasks may be interacted with by calling methods on the model. + * + * @author Eike Hein + **/ + +class TASKMANAGER_EXPORT TasksModel : public QSortFilterProxyModel, public AbstractTasksModelIface +{ + Q_OBJECT + + Q_ENUMS(SortMode) + Q_ENUMS(GroupMode) + + Q_PROPERTY(int count READ rowCount NOTIFY countChanged) + Q_PROPERTY(int launcherCount READ launcherCount NOTIFY launcherCountChanged) + + Q_PROPERTY(QStringList launcherList READ launcherList WRITE setLauncherList NOTIFY launcherListChanged) + + Q_PROPERTY(bool anyTaskDemandsAttention READ anyTaskDemandsAttention NOTIFY anyTaskDemandsAttentionChanged) + + Q_PROPERTY(int virtualDesktop READ virtualDesktop WRITE setVirtualDesktop NOTIFY virtualDesktopChanged) + Q_PROPERTY(int screen READ screen WRITE setScreen NOTIFY screenChanged) + Q_PROPERTY(QString activity READ activity WRITE setActivity NOTIFY activityChanged) + + Q_PROPERTY(bool filterByVirtualDesktop READ filterByVirtualDesktop WRITE setFilterByVirtualDesktop NOTIFY filterByVirtualDesktopChanged) + Q_PROPERTY(bool filterByScreen READ filterByScreen WRITE setFilterByScreen NOTIFY filterByScreenChanged) + Q_PROPERTY(bool filterByActivity READ filterByActivity WRITE setFilterByActivity NOTIFY filterByActivityChanged) + Q_PROPERTY(bool filterNotMinimized READ filterNotMinimized WRITE setFilterNotMinimized NOTIFY filterNotMinimizedChanged) + + Q_PROPERTY(SortMode sortMode READ sortMode WRITE setSortMode NOTIFY sortModeChanged) + Q_PROPERTY(bool separateLaunchers READ separateLaunchers WRITE setSeparateLaunchers NOTIFY separateLaunchersChanged) + Q_PROPERTY(bool launchInPlace READ launchInPlace WRITE setLaunchInPlace NOTIFY launchInPlaceChanged) + + Q_PROPERTY(GroupMode groupMode READ groupMode WRITE setGroupMode NOTIFY groupModeChanged) + Q_PROPERTY(int groupingWindowTasksThreshold READ groupingWindowTasksThreshold + WRITE setGroupingWindowTasksThreshold NOTIFY groupingWindowTasksThresholdChanged) + Q_PROPERTY(QStringList groupingAppIdBlacklist READ groupingAppIdBlacklist + WRITE setGroupingAppIdBlacklist NOTIFY groupingAppIdBlacklistChanged) + Q_PROPERTY(QStringList groupingLauncherUrlBlacklist READ groupingLauncherUrlBlacklist + WRITE setGroupingLauncherUrlBlacklist NOTIFY groupingLauncherUrlBlacklistChanged) + +public: + enum SortMode { + SortDisabled = 0, /**< No sorting is done. */ + SortManual, /**< Tasks can be moved with move() and syncLaunchers(). */ + SortAlpha, /**< Tasks are sorted alphabetically, by AbstractTasksModel::AppName and Qt::DisplayRole. */ + SortVirtualDesktop, /**< Tasks are sorted by the virtual desktop they are on. */ + SortActivity /**< Tasks are sorted by the number of tasks on the activities they're on. */ + }; + + enum GroupMode { + GroupDisabled = 0, /**< No grouping is done. */ + GroupApplications /**< Tasks are grouped by the application backing them. */ + }; + + explicit TasksModel(QObject *parent = 0); + virtual ~TasksModel(); + + QHash roleNames() const override; + + Q_INVOKABLE int rowCount(const QModelIndex &parent = QModelIndex()) const; // Invokable. + + /** + * The number of launcher tasks in the tast list. + * + * @returns the number of launcher tasks in the task list. + **/ + int launcherCount() const; + + /** + * The list of launcher URLs serialized to strings. + * + * @see setLauncherList + * @returns the list of launcher URLs serialized to strings. + **/ + QStringList launcherList() const; + + /** + * Replace the list of launcher URL strings. + * + * Invalid or empty URLs will be rejected. Duplicate URLs will be + * collapsed. + * + * @see launcherList + * @param launchers A list of launcher URL strings. + **/ + void setLauncherList(const QStringList &launchers); + + /** + * Returns whether any task in the model currently demands attention + * (AbstractTasksModel::IsDemandingAttention). + * + * @returns whether any task in the model currently demands attention. + **/ + bool anyTaskDemandsAttention() const; + + /** + * The number of the virtual desktop used in filtering by virtual + * desktop. Usually set to the number of the current virtual desktop. + * Defaults to @c -1. + * + * @see setVirtualDesktop + * @returns the number of the virtual desktop used in filtering. + **/ + int virtualDesktop() const; + + /** + * Set the number of the virtual desktop to use in filtering by virtual + * desktop. + * + * If set to @c -1, filtering by virtual desktop is disabled. + * + * @see virtualDesktop + * @param virtualDesktop A virtual desktop number. + **/ + void setVirtualDesktop(int virtualDesktop); + + /** + * The number of the screen used in filtering by screen. Usually + * set to the number of the current screen. Defaults to @c -1. + * + * @see setScreen + * @returns the number of the screen used in filtering. + **/ + int screen() const; + + /** + * Set the number of the screen to use in filtering by screen. + * + * If set to @c -1, filtering by screen is disabled. + * + * @see screen + * @param screen A screen number. + **/ + void setScreen(int screen); + + /** + * The id of the activity used in filtering by activity. Usually + * set to the id of the current activity. Defaults to an empty id. + * + * FIXME: Implement. + * + * @see setActivity + * @returns the id of the activity used in filtering. + **/ + QString activity() const; + + /** + * Set the id of the activity to use in filtering by activity. + * + * FIXME: Implement. + * + * @see activity + * @param activity An activity id. + **/ + void setActivity(const QString &activity); + + /** + * Whether tasks should be filtered by virtual desktop. Defaults to + * @c false. + * + * Filtering by virtual desktop only happens if a virtual desktop + * number is set, even if this returns @c true. + * + * @see setFilterByVirtualDesktop + * @see setVirtualDesktop + * @returns @c true if tasks should be filtered by virtual desktop. + **/ + bool filterByVirtualDesktop() const; + + /** + * Set whether tasks should be filtered by virtual desktop. + * + * Filtering by virtual desktop only happens if a virtual desktop + * number is set, even if this is set to @c true. + * + * @see filterByVirtualDesktop + * @see setVirtualDesktop + * @param filter Whether tasks should be filtered by virtual desktop. + **/ + void setFilterByVirtualDesktop(bool filter); + + /** + * Whether tasks should be filtered by screen. Defaults to @c false. + * + * Filtering by screen only happens if a screen number is set, even + * if this returns @c true. + * + * @see setFilterByScreen + * @see setScreen + * @returns @c true if tasks should be filtered by screen. + **/ + bool filterByScreen() const; + + /** + * Set whether tasks should be filtered by screen. + * + * Filtering by screen only happens if a screen number is set, even + * if this is set to @c true. + * + * @see filterByScreen + * @see setScreen + * @param filter Whether tasks should be filtered by screen. + **/ + void setFilterByScreen(bool filter); + + /** + * Whether tasks should be filtered by activity. Defaults to @c false. + * + * Filtering by activity only happens if an activity id is set, even + * if this returns @c true. + * + * @see setFilterByActivity + * @see setActivity + * @returns @ctrue if tasks should be filtered by activity. + **/ + bool filterByActivity() const; + + /** + * Set whether tasks should be filtered by activity. Defaults to + * @c false. + * + * Filtering by virtual desktop only happens if an activity id is set, + * even if this is set to @c true. + * + * @see filterByActivity + * @see setActivity + * @param filter Whether tasks should be filtered by activity. + **/ + void setFilterByActivity(bool filter); + + /** + * Whether non-minimized tasks should be filtered. Defaults to + * @c false. + * + * @see setFilterNotMinimized + * @returns @c true if non-minimized tasks should be filtered. + **/ + bool filterNotMinimized() const; + + /** + * Set whether non-minimized tasks should be filtered. + * + * @see filterNotMinimized + * @param filter Whether non-minimized tasks should be filtered. + **/ + void setFilterNotMinimized(bool filter); + + /** + * The sort mode used in sorting tasks. Defaults to SortAlpha. + * + * @see setSortMode + * @returns the curent sort mode. + **/ + SortMode sortMode() const; + + /** + * Sets the sort mode used in sorting tasks. + * + * @see sortMode + * @param mode A sort mode. + **/ + void setSortMode(SortMode mode); + + // FIXME TODO: Add docs once fully implemented. + bool separateLaunchers() const; + void setSeparateLaunchers(bool separate); + + /** + * Whether window tasks should be sorted as their associated launcher + * tasks or separately. Defaults to @c false. + * + * @see setLaunchInPlace + * @returns whether window tasks should be sorted as their associated + * launcher tasks. + **/ + bool launchInPlace() const; + + /** + * Sets whether window tasks should be sorted as their associated launcher + * tasks or separately. + * + * @see launchInPlace + * @param launchInPlace Whether window tasks should be sorted as their + * associated launcher tasks. + **/ + void setLaunchInPlace(bool launchInPlace); + + /** + * Returns the current group mode, i.e. the criteria by which tasks should + * be grouped. + * + * Defaults to TasksModel::GroupApplication, which groups tasks backed by + * the same application. + * + * If the group mode is TasksModel::GroupDisabled, no grouping is done. + * + * @see setGroupMode + * @returns the current group mode. + **/ + TasksModel::GroupMode groupMode() const; + + /** + * Sets the group mode, i.e. the criteria by which tasks should be grouped. + * + * The group mode can be set to TasksModel::GroupDisabled to disable grouping + * entirely, breaking apart any existing groups. + * + * @see groupMode + * @param mode A group mode. + **/ + void setGroupMode(TasksModel::GroupMode mode); + + /** + * As window tasks (AbstractTasksModel::IsWindow) come and go, groups will + * be formed when this threshold value is exceeded, and broken apart when + * it matches or falls below. + * + * Defaults to @c -1, which means grouping is done regardless of the number + * of window tasks. + * + * @see setGroupingWindowTasksThreshold + * @return the threshold number of window tasks used in grouping decisions. + **/ + int groupingWindowTasksThreshold() const; + + /** + * Sets the number of window tasks (AbstractTasksModel::IsWindow) above which + * groups will be formed, and at or below which groups will be broken apart. + * + * If set to -1, grouping will be done regardless of the number of window tasks + * in the source model. + * + * @see groupingWindowTasksThreshold + * @param threshold A threshold number of window tasks used in grouping + * decisions. + **/ + void setGroupingWindowTasksThreshold(int threshold); + + /** + * A blacklist of app ids (AbstractTasksModel::AppId) that is consulted before + * grouping a task. If a task's app id is found on the blacklist, it is not + * grouped. + * + * The default app id blacklist is empty. + * + * @see setGroupingAppIdBlacklist + * @returns the blacklist of app ids consulted before grouping a task. + **/ + QStringList groupingAppIdBlacklist() const; + + /** + * Sets the blacklist of app ids (AbstractTasksModel::AppId) that is consulted + * before grouping a task. If a task's app id is found on the blacklist, it is + * not grouped. + * + * When set, groups will be formed and broken apart as necessary. + * + * @see groupingAppIdBlacklist + * @param list a blacklist of app ids to be consulted before grouping a task. + **/ + void setGroupingAppIdBlacklist(const QStringList &list); + + /** + * A blacklist of launcher URLs (AbstractTasksModel::LauncherUrl) that is + * consulted before grouping a task. If a task's launcher URL is found on the + * blacklist, it is not grouped. + * + * The default launcher URL blacklist is empty. + * + * @see setGroupingLauncherUrlBlacklist + * @returns the blacklist of launcher URLs consulted before grouping a task. + **/ + QStringList groupingLauncherUrlBlacklist() const; + + /** + * Sets the blacklist of launcher URLs (AbstractTasksModel::LauncherUrl) that + * is consulted before grouping a task. If a task's launcher URL is found on + * the blacklist, it is not grouped. + * + * When set, groups will be formed and broken apart as necessary. + * + * @see groupingLauncherUrlBlacklist + * @param list a blacklist of launcher URLs to be consulted before grouping a task. + **/ + void setGroupingLauncherUrlBlacklist(const QStringList &list); + + /** + * Request adding a launcher with the given URL. + * + * If this URL is already in the list, the request will fail. URLs are + * compared for equality after removing the query string used to hold + * metadata. + * + * @see launcherUrlsMatch + * @param url A launcher URL. + * @returns @c true if a launcher was added. + */ + Q_INVOKABLE bool requestAddLauncher(const QUrl &url); + + /** + * Request removing the launcher with the given URL. + * + * If this URL is already in the list, the request will fail. URLs are + * compared for equality after removing the query string used to hold + * metadata. + * + * @see launcherUrlsMatch + * @param url A launcher URL. + * @returns @c true if the launcher was removed. + */ + Q_INVOKABLE bool requestRemoveLauncher(const QUrl &url); + + /** + * Return the position of the launcher with the given URL. + * + * URLs are compared for equality after removing the query string used + * to hold metadata. + * + * @see launcherUrlsMatch + * @param url A launcher URL. + * @returns @c -1 if no launcher exists for the given URL. + */ + Q_INVOKABLE int launcherPosition(const QUrl &url) const; + + /** + * Request activation of the task at the given index. Derived classes are + * free to interpret the meaning of "activate" themselves depending on + * the nature and state of the task, e.g. launch or raise a window task. + * + * @param index An index in this tasks model. + **/ + Q_INVOKABLE void requestActivate(const QModelIndex &index); + + /** + * Request an additional instance of the application backing the task + * at the given index. + * + * @param index An index in this tasks model. + **/ + Q_INVOKABLE void requestNewInstance(const QModelIndex &index); + + /** + * Request the task at the given index be closed. + * + * @param index An index in this tasks model. + **/ + Q_INVOKABLE void requestClose(const QModelIndex &index); + + /** + * Request starting an interactive move for the task at the given index. + * + * This is meant for tasks that have an associated window, and may be + * a no-op when there is no window. + * + * @param index An index in this tasks model. + **/ + Q_INVOKABLE void requestMove(const QModelIndex &index); + + /** + * Request starting an interactive resize for the task at the given index. + * + * This is meant for tasks that have an associated window, and may be a + * no-op when there is no window. + * + * @param index An index in this tasks model. + **/ + Q_INVOKABLE void requestResize(const QModelIndex &index); + + /** + * Request toggling the minimized state of the task at the given index. + * + * This is meant for tasks that have an associated window, and may be + * a no-op when there is no window. + * + * @param index An index in this tasks model. + **/ + Q_INVOKABLE void requestToggleMinimized(const QModelIndex &index); + + /** + * Request toggling the maximized state of the task at the given index. + * + * This is meant for tasks that have an associated window, and may be + * a no-op when there is no window. + * + * @param index An index in this tasks model. + **/ + Q_INVOKABLE void requestToggleMaximized(const QModelIndex &index); + + /** + * Request toggling the keep-above state of the task at the given index. + * + * This is meant for tasks that have an associated window, and may be + * a no-op when there is no window. + * + * @param index An index in this tasks model. + **/ + Q_INVOKABLE void requestToggleKeepAbove(const QModelIndex &index); + + /** + * Request toggling the keep-below state of the task at the given index. + * + * This is meant for tasks that have an associated window, and may be + * a no-op when there is no window. + * + * @param index An index in this tasks model. + **/ + Q_INVOKABLE void requestToggleKeepBelow(const QModelIndex &index); + + /** + * Request toggling the fullscreen state of the task at the given index. + * + * This is meant for tasks that have an associated window, and may be + * a no-op when there is no window. + * + * @param index An index in this tasks model. + **/ + Q_INVOKABLE void requestToggleFullScreen(const QModelIndex &index); + + /** + * Request toggling the shaded state of the task at the given index. + * + * This is meant for tasks that have an associated window, and may be + * a no-op when there is no window. + * + * @param index An index in this tasks model. + **/ + Q_INVOKABLE void requestToggleShaded(const QModelIndex &index); + + /** + * Request moving the task at the given index to the specified virtual + * desktop. + * + * This is meant for tasks that have an associated window, and may be + * a no-op when there is no window. + * + * @param index An index in this tasks model. + * @param desktop A virtual desktop number. + **/ + Q_INVOKABLE void requestVirtualDesktop(const QModelIndex &index, qint32 desktop); + + /** + * Request informing the window manager of new geometry for a visual + * delegate for the task at the given index. The geometry should be in + * screen coordinates. + * + * If the task at the given index is a group parent, the geometry is + * set for all of its children. If the task at the given index is a + * group member, the geometry is set for all of its siblings. + * + * @param index An index in this tasks model. + * @param geometry Visual delegate geometry in screen coordinates. + * @param delegate The delegate. Implementations are on their own with + * regard to extracting information from this, and should take care to + * reject invalid objects. + **/ + Q_INVOKABLE void requestPublishDelegateGeometry(const QModelIndex &index, const QRect &geometry, + QObject *delegate = nullptr); + + /** + * Request toggling whether the task at the given index, along with any + * tasks matching its kind, should be grouped or not. Task groups will be + * formed or broken apart as needed, along with affecting future grouping + * decisions as new tasks appear. + * + * As grouping is toggled for a task, updates are made to the + * grouping*Blacklist properties of the model instance. + * + * @see groupingAppIdBlacklist + * @see groupingLauncherUrlBlacklist + * + * @param index An index in this tasks model. + **/ + Q_INVOKABLE void requestToggleGrouping(const QModelIndex &index); + + /** + * Moves a (top-level) task to a new position in the list. The insert + * position is bounded to the list start and end. + * syncLaunchers() should be called after a set of move operations to + * update the launcher list to reflect the new order. + * + * @see syncLaunchers + * @see launcherList + * @param index An index in this tasks model. + * @param newPos The new list position to move the task to. + */ + Q_INVOKABLE bool move(int row, int newPos); + + /** + * Updates the launcher list to reflect the new order after calls to + * move(), if needed. + * + * @see move + * @see launcherList + */ + Q_INVOKABLE void syncLaunchers(); + + /** + * Finds the first active (AbstractTasksModel::IsActive) task in the model + * and returns its QModelIndex, or a null QModelIndex if no active task is + * found. + * + * @returns the model index for the first active task, if any. + */ + Q_INVOKABLE QModelIndex activeTask() const; + + /** + * Given a row in the model, returns a QModelIndex for it. To get an index + * for a child in a task group, an optional child row may be passed as well. + * + * This easier to use from Qt Quick views than QAbstractItemModel::index is. + * + * @param row A row index in the model. + * @param childRow A row index for a child of the task group at the given row. + * @returns a model index for the task at the given row, or for one of its + * child tasks. + */ + Q_INVOKABLE QModelIndex makeModelIndex(int row, int childRow = -1) const; + +Q_SIGNALS: + void countChanged() const; + void launcherCountChanged() const; + void launcherListChanged() const; + void anyTaskDemandsAttentionChanged() const; + void virtualDesktopChanged() const; + void screenChanged() const; + void activityChanged() const; + void filterByVirtualDesktopChanged() const; + void filterByScreenChanged() const; + void filterByActivityChanged() const; + void filterNotMinimizedChanged() const; + void sortModeChanged() const; + void separateLaunchersChanged() const; + void launchInPlaceChanged() const; + void groupModeChanged() const; + void groupingWindowTasksThresholdChanged() const; + void groupingAppIdBlacklistChanged() const; + void groupingLauncherUrlBlacklistChanged() const; + +protected: + bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const; + bool lessThan(const QModelIndex &left, const QModelIndex &right) const; + +private: + class Private; + class TasksModelLessThan; + friend class TasksModelLessThan; + QScopedPointer d; +}; + +} + +#endif diff --git a/libtaskmanager/tasksmodel.cpp b/libtaskmanager/tasksmodel.cpp new file mode 100644 --- /dev/null +++ b/libtaskmanager/tasksmodel.cpp @@ -0,0 +1,1339 @@ +/******************************************************************** +Copyright 2016 Eike Hein + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 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 6 of version 3 of the license. + +This library 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 +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library. If not, see . +*********************************************************************/ + +#include "tasksmodel.h" +#include "activityinfo.h" +#include "concatenatetasksproxymodel.h" +#include "taskfilterproxymodel.h" +#include "taskgroupingproxymodel.h" +#include "tasktools.h" + +#include + +#include "launchertasksmodel.h" +#include "waylandtasksmodel.h" +#include "startuptasksmodel.h" +#if HAVE_X11 +#include "xwindowtasksmodel.h" +#endif + +#include + +#include +#include +#include + +#if HAVE_X11 +#include +#endif + +#include + +namespace TaskManager +{ + +class TasksModel::Private +{ +public: + Private(TasksModel *q); + ~Private(); + + static int instanceCount; + + static AbstractTasksModel* windowTasksModel; + static StartupTasksModel* startupTasksModel; + LauncherTasksModel* launcherTasksModel = nullptr; + ConcatenateTasksProxyModel* concatProxyModel = nullptr; + TaskFilterProxyModel* filterProxyModel = nullptr; + TaskGroupingProxyModel* groupingProxyModel = nullptr; + + bool anyTaskDemandsAttention = false; + + int virtualDesktop = -1; + int screen = -1; + QString activity; + + SortMode sortMode = SortAlpha; + bool separateLaunchers = true; + bool launchInPlace = false; + bool launcherSortingDirty = false; + QList sortedPreFilterRows; + QVector sortRowInsertQueue; + QHash activityTaskCounts; + static ActivityInfo* activityInfo; + static int activityInfoUsers; + + void initModels(); + void updateAnyTaskDemandsAttention(); + void updateManualSortMap(); + void syncManualSortMapForGroup(const QModelIndex &parent); + QModelIndex preFilterIndex(const QModelIndex &sourceIndex) const; + void updateActivityTaskCounts(); + void forceResort(); + bool lessThan(const QModelIndex &left, const QModelIndex &right, + bool sortOnlyLaunchers = false) const; + +private: + TasksModel *q; +}; + +class TasksModel::TasksModelLessThan +{ +public: + inline TasksModelLessThan(const QAbstractItemModel *s, TasksModel *p, bool sortOnlyLaunchers) + : sourceModel(s), tasksModel(p), sortOnlyLaunchers(sortOnlyLaunchers) {} + + inline bool operator()(int r1, int r2) const + { + QModelIndex i1 = sourceModel->index(r1, 0); + QModelIndex i2 = sourceModel->index(r2, 0); + return tasksModel->d->lessThan(i1, i2, sortOnlyLaunchers); + } + +private: + const QAbstractItemModel *sourceModel; + const TasksModel *tasksModel; + bool sortOnlyLaunchers; +}; + +int TasksModel::Private::instanceCount = 0; +AbstractTasksModel* TasksModel::Private::windowTasksModel = nullptr; +StartupTasksModel* TasksModel::Private::startupTasksModel = nullptr; +ActivityInfo* TasksModel::Private::activityInfo = nullptr; +int TasksModel::Private::activityInfoUsers = 0; + +TasksModel::Private::Private(TasksModel *q) + : q(q) +{ + ++instanceCount; +} + +TasksModel::Private::~Private() +{ + --instanceCount; + + if (sortMode == SortActivity) { + --activityInfoUsers; + } + + if (!instanceCount) { + delete windowTasksModel; + windowTasksModel = nullptr; + delete startupTasksModel; + startupTasksModel = nullptr; + delete activityInfo; + activityInfo = nullptr; + } +} + +void TasksModel::Private::initModels() +{ + // NOTE: Overview over the entire model chain assembled here: + // {X11,Wayland}WindowTasksModel, StartupTasksModel, LauncherTasksModel + // -> ConcatenateTasksProxyModel concatenates them into a single list. + // -> TaskFilterProxyModel filters by state (e.g. virtual desktop). + // -> TaskGroupingProxyModel groups by application (we go from flat list to tree). + // -> TasksModel collapses top-level items into task lifecycle abstraction, sorts. + + if (!windowTasksModel && QGuiApplication::platformName().startsWith(QLatin1String("wayland"))) { + windowTasksModel = new WaylandTasksModel(); + } + +#if HAVE_X11 + if (!windowTasksModel && QX11Info::isPlatformX11()) { + windowTasksModel = new XWindowTasksModel(); + } +#endif + + QObject::connect(windowTasksModel, &QAbstractItemModel::rowsInserted, q, + [this]() { + if (sortMode == SortActivity) { + updateActivityTaskCounts(); + } + } + ); + + QObject::connect(windowTasksModel, &QAbstractItemModel::rowsRemoved, q, + [this]() { + if (sortMode == SortActivity) { + updateActivityTaskCounts(); + forceResort(); + } + } + ); + + QObject::connect(windowTasksModel, &QAbstractItemModel::dataChanged, q, + [this](const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector &roles) { + Q_UNUSED(topLeft) + Q_UNUSED(bottomRight) + + if (sortMode == SortActivity && roles.contains(AbstractTasksModel::Activities)) { + updateActivityTaskCounts(); + } + } + ); + + if (!startupTasksModel) { + startupTasksModel = new StartupTasksModel(); + } + + launcherTasksModel = new LauncherTasksModel(q); + QObject::connect(launcherTasksModel, &LauncherTasksModel::launcherListChanged, + q, &TasksModel::launcherListChanged); + QObject::connect(launcherTasksModel, &QAbstractItemModel::rowsInserted, + q, &TasksModel::launcherCountChanged); + QObject::connect(launcherTasksModel, &QAbstractItemModel::rowsRemoved, + q, &TasksModel::launcherCountChanged); + QObject::connect(launcherTasksModel, &QAbstractItemModel::modelReset, + q, &TasksModel::launcherCountChanged); + + concatProxyModel = new ConcatenateTasksProxyModel(q); + + concatProxyModel->addSourceModel(windowTasksModel); + concatProxyModel->addSourceModel(startupTasksModel); + concatProxyModel->addSourceModel(launcherTasksModel); + + // If we're in manual sort mode, we need to seed the sort map on pending row + // insertions. + QObject::connect(concatProxyModel, &QAbstractItemModel::rowsAboutToBeInserted, q, + [this](const QModelIndex &parent, int start, int end) { + Q_UNUSED(parent) + + if (sortMode != SortManual) { + return; + } + + const int delta = (end - start) + 1; + QMutableListIterator it(sortedPreFilterRows); + + while (it.hasNext()) { + it.next(); + + if (it.value() >= start) { + it.setValue(it.value() + delta); + } + } + + for (int i = start; i <= end; ++i) { + sortedPreFilterRows.append(i); + + if (0 && !separateLaunchers) { // FIXME TODO: Disable until done. + sortRowInsertQueue.append(sortedPreFilterRows.count() - 1); + } + } + } + ); + + // If we're in manual sort mode, we need to update the sort map on row insertions. + QObject::connect(concatProxyModel, &QAbstractItemModel::rowsInserted, q, + [this](const QModelIndex &parent, int start, int end) { + Q_UNUSED(parent) + Q_UNUSED(start) + Q_UNUSED(end) + + if (sortMode == SortManual) { + updateManualSortMap(); + } + } + ); + + // If we're in manual sort mode, we need to update the sort map on row removals. + QObject::connect(concatProxyModel, &QAbstractItemModel::rowsAboutToBeRemoved, q, + [this](const QModelIndex &parent, int first, int last) { + Q_UNUSED(parent) + + if (sortMode != SortManual) { + return; + } + + for (int i = first; i <= last; ++i) { + sortedPreFilterRows.removeOne(i); + } + + const int delta = (last - first) + 1; + QMutableListIterator it(sortedPreFilterRows); + + while (it.hasNext()) { + it.next(); + + if (it.value() > last) { + it.setValue(it.value() - delta); + } + } + } + ); + + filterProxyModel = new TaskFilterProxyModel(q); + filterProxyModel->setSourceModel(concatProxyModel); + QObject::connect(filterProxyModel, &TaskFilterProxyModel::virtualDesktopChanged, + q, &TasksModel::virtualDesktopChanged); + QObject::connect(filterProxyModel, &TaskFilterProxyModel::screenChanged, + q, &TasksModel::screenChanged); + QObject::connect(filterProxyModel, &TaskFilterProxyModel::activityChanged, + q, &TasksModel::activityChanged); + QObject::connect(filterProxyModel, &TaskFilterProxyModel::filterByVirtualDesktopChanged, + q, &TasksModel::filterByVirtualDesktopChanged); + QObject::connect(filterProxyModel, &TaskFilterProxyModel::filterByScreenChanged, + q, &TasksModel::filterByScreenChanged); + QObject::connect(filterProxyModel, &TaskFilterProxyModel::filterByActivityChanged, + q, &TasksModel::filterByActivityChanged); + QObject::connect(filterProxyModel, &TaskFilterProxyModel::filterNotMinimizedChanged, + q, &TasksModel::filterNotMinimizedChanged); + + groupingProxyModel = new TaskGroupingProxyModel(q); + groupingProxyModel->setSourceModel(filterProxyModel); + QObject::connect(groupingProxyModel, &TaskGroupingProxyModel::groupModeChanged, + q, &TasksModel::groupModeChanged); + QObject::connect(groupingProxyModel, &TaskGroupingProxyModel::windowTasksThresholdChanged, + q, &TasksModel::groupingWindowTasksThresholdChanged); + QObject::connect(groupingProxyModel, &TaskGroupingProxyModel::blacklistedAppIdsChanged, + q, &TasksModel::groupingAppIdBlacklistChanged); + QObject::connect(groupingProxyModel, &TaskGroupingProxyModel::blacklistedLauncherUrlsChanged, + q, &TasksModel::groupingLauncherUrlBlacklistChanged); + + QObject::connect(groupingProxyModel, &QAbstractItemModel::rowsInserted, q, + [this](const QModelIndex &parent, int first, int last) { + // We can ignore group members. + if (parent.isValid()) { + return; + } + + for (int i = first; i <= last; ++i) { + const QModelIndex &sourceIndex = groupingProxyModel->index(i, 0); + const QString &appId = sourceIndex.data(AbstractTasksModel::AppId).toString(); + + if (sourceIndex.data(AbstractTasksModel::IsDemandingAttention).toBool()) { + updateAnyTaskDemandsAttention(); + } + + // When we get a window we have a startup for, cause the startup to be re-filtered. + if (sourceIndex.data(AbstractTasksModel::IsWindow).toBool()) { + const QString &appName = sourceIndex.data(AbstractTasksModel::AppName).toString(); + + for (int i = 0; i < startupTasksModel->rowCount(); ++i) { + QModelIndex startupIndex = startupTasksModel->index(i, 0); + + if (appId == startupIndex.data(AbstractTasksModel::AppId).toString() + || appName == startupIndex.data(AbstractTasksModel::AppName).toString()) { + startupTasksModel->dataChanged(startupIndex, startupIndex); + } + } + } + + // When we get a window or startup we have a launcher for, cause the launcher to be re-filtered. + if (sourceIndex.data(AbstractTasksModel::IsWindow).toBool() + || sourceIndex.data(AbstractTasksModel::IsStartup).toBool()) { + const QUrl &launcherUrl = sourceIndex.data(AbstractTasksModel::LauncherUrl).toUrl(); + + for (int i = 0; i < launcherTasksModel->rowCount(); ++i) { + QModelIndex launcherIndex = launcherTasksModel->index(i, 0); + const QString &launcherAppId = launcherIndex.data(AbstractTasksModel::AppId).toString(); + + if ((!appId.isEmpty() && appId == launcherAppId) + || launcherUrlsMatch(launcherUrl, launcherIndex.data(AbstractTasksModel::LauncherUrl).toUrl(), + IgnoreQueryItems)) { + launcherTasksModel->dataChanged(launcherIndex, launcherIndex); + } + } + } + } + } + ); + + // When a window is removed, we have to trigger a re-filter of matching launchers. + QObject::connect(groupingProxyModel, &QAbstractItemModel::rowsAboutToBeRemoved, q, + [this](const QModelIndex &parent, int first, int last) { + // We can ignore group members. + if (parent.isValid()) { + return; + } + + for (int i = first; i <= last; ++i) { + const QModelIndex &sourceIndex = groupingProxyModel->index(i, 0); + + if (sourceIndex.data(AbstractTasksModel::IsDemandingAttention).toBool()) { + updateAnyTaskDemandsAttention(); + } + + if (!sourceIndex.data(AbstractTasksModel::IsWindow).toBool()) { + continue; + } + + const QUrl &launcherUrl = sourceIndex.data(AbstractTasksModel::LauncherUrl).toUrl(); + + if (!launcherUrl.isEmpty() && launcherUrl.isValid()) { + const int pos = launcherTasksModel->launcherPosition(launcherUrl); + + if (pos != -1) { + QModelIndex launcherIndex = launcherTasksModel->index(pos, 0); + + QMetaObject::invokeMethod(launcherTasksModel, "dataChanged", Qt::QueuedConnection, + Q_ARG(QModelIndex, launcherIndex), Q_ARG(QModelIndex, launcherIndex)); + QMetaObject::invokeMethod(q, "launcherCountChanged", Qt::QueuedConnection); + } + } + } + } + ); + + // Update anyTaskDemandsAttention on source data changes. + QObject::connect(groupingProxyModel, &QAbstractItemModel::dataChanged, q, + [this](const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector &roles) { + Q_UNUSED(bottomRight) + + // We can ignore group members. + if (topLeft.isValid()) { + return; + } + + if (roles.isEmpty() || roles.contains(AbstractTasksModel::IsDemandingAttention)) { + updateAnyTaskDemandsAttention(); + } + } + ); + + // Update anyTaskDemandsAttention on source model resets. + QObject::connect(groupingProxyModel, &QAbstractItemModel::modelReset, q, + [this]() { updateAnyTaskDemandsAttention(); } + ); + + q->setSourceModel(groupingProxyModel); + + QObject::connect(q, &QAbstractItemModel::rowsInserted, q, + [this](const QModelIndex &parent, int first, int last) { + Q_UNUSED(first) + Q_UNUSED(last) + + q->countChanged(); + + // If we're in manual sort mode, we need to consolidate new children + // of a group in the manual sort map to prepare for when a group + // gets dissolved. + // This is done after we've already had a chance to sort the new child + // in alphabetically in this proxy. + if (sortMode == SortManual && parent.isValid()) { + syncManualSortMapForGroup(parent); + } + } + ); + + + QObject::connect(q, &QAbstractItemModel::rowsRemoved, q, &TasksModel::countChanged); + QObject::connect(q, &QAbstractItemModel::modelReset, q, &TasksModel::countChanged); +} + +void TasksModel::Private::updateAnyTaskDemandsAttention() +{ + bool taskFound = false; + + for (int i = 0; i < groupingProxyModel->rowCount(); ++i) { + if (groupingProxyModel->index(i, 0).data(AbstractTasksModel::IsDemandingAttention).toBool()) { + taskFound = true; + break; + } + } + + if (taskFound != anyTaskDemandsAttention) { + anyTaskDemandsAttention = taskFound; + q->anyTaskDemandsAttentionChanged(); + } +} + +void TasksModel::Private::updateManualSortMap() +{ + // Empty map; full sort. + if (sortedPreFilterRows.isEmpty()) { + sortedPreFilterRows.reserve(concatProxyModel->rowCount()); + + for (int i = 0; i < concatProxyModel->rowCount(); ++i) { + sortedPreFilterRows.append(i); + } + + // Full sort. + TasksModelLessThan lt(concatProxyModel, q, false); + std::stable_sort(sortedPreFilterRows.begin(), sortedPreFilterRows.end(), lt); + + return; + } + + // Existing map; check whether launchers need sorting by launcher list position. + if (separateLaunchers) { + // Sort only launchers. + TasksModelLessThan lt(concatProxyModel, q, true); + std::stable_sort(sortedPreFilterRows.begin(), sortedPreFilterRows.end(), lt); + // Otherwise process any entries in the insert queue and move them intelligently + // in the sort map. + } else if (0) { // FIXME TODO: Disable until done. + while (sortRowInsertQueue.count()) { + const int row = sortRowInsertQueue.takeFirst(); + const QModelIndex &idx = concatProxyModel->index(sortedPreFilterRows.at(row), 0); + + // New launcher tasks go after the last launcher in the proxy, or to the start of + // the map if there are none. + if (idx.data(AbstractTasksModel::IsLauncher).toBool()) { + int insertPos = 0; + + for (int i = 0; i < row; ++i) { + const QModelIndex &proxyIdx = q->index(i, 0); + + if (proxyIdx.data(AbstractTasksModel::IsLauncher).toBool()) { + insertPos = i + 1; + } else { + break; + } + } + + sortedPreFilterRows.move(row, insertPos); + // Anything else goes after its right-most app sibling, if any. If there are + // none it just stays put. + } else { + for (int i = (row - 1); i >= 0; --i) { + const QModelIndex &concatProxyIndex = concatProxyModel->index(sortedPreFilterRows.at(i), 0); + + if (appsMatch(concatProxyIndex, idx)) { + sortedPreFilterRows.move(row, i + 1); + + break; + } + } + } + } + } +} + +void TasksModel::Private::syncManualSortMapForGroup(const QModelIndex &parent) +{ + const int childCount = q->rowCount(parent); + + if (childCount != -1) { + const QModelIndex &preFilterParent = preFilterIndex(q->mapToSource(parent)); + + // We're moving the trailing children to the sort map position of + // the first child, so we're skipping the first child. + for (int i = 1; i < childCount; ++i) { + const QModelIndex &preFilterChildIndex = preFilterIndex(q->mapToSource(parent.child(i, 0))); + const int childSortIndex = sortedPreFilterRows.indexOf(preFilterChildIndex.row()); + const int parentSortIndex = sortedPreFilterRows.indexOf(preFilterParent.row()); + const int insertPos = (parentSortIndex + i) + ((parentSortIndex + i) > childSortIndex ? -1 : 0); + sortedPreFilterRows.move(childSortIndex, insertPos); + } + } +} + +QModelIndex TasksModel::Private::preFilterIndex(const QModelIndex &sourceIndex) const { + return filterProxyModel->mapToSource(groupingProxyModel->mapToSource(sourceIndex)); +} + +void TasksModel::Private::updateActivityTaskCounts() +{ + // Collects the number of window tasks on each activity. + + activityTaskCounts.clear(); + + if (!windowTasksModel || !activityInfo) { + return; + } + + foreach(const QString &activity, activityInfo->runningActivities()) { + activityTaskCounts.insert(activity, 0); + } + + for (int i = 0; i < windowTasksModel->rowCount(); ++i) { + const QModelIndex &windowIndex = windowTasksModel->index(i, 0); + const QStringList &activities = windowIndex.data(AbstractTasksModel::Activities).toStringList(); + + if (activities.isEmpty()) { + QMutableHashIterator i(activityTaskCounts); + + while (i.hasNext()) { + i.next(); + i.setValue(i.value() + 1); + } + } else { + foreach(const QString &activity, activities) { + ++activityTaskCounts[activity]; + } + } + } +} + +void TasksModel::Private::forceResort() +{ + // HACK: This causes QSortFilterProxyModel to run all rows through + // our lessThan() implementation again. + q->setDynamicSortFilter(false); + q->setDynamicSortFilter(true); +} + +bool TasksModel::Private::lessThan(const QModelIndex &left, const QModelIndex &right, bool sortOnlyLaunchers) const +{ + // Launcher tasks go first. + // When launchInPlace is enabled, startup and window tasks are sorted + // as the launchers they replace (see also move()). + if (left.data(AbstractTasksModel::IsLauncher).toBool() && right.data(AbstractTasksModel::IsLauncher).toBool()) { + return (left.row() < right.row()); + } else if (left.data(AbstractTasksModel::IsLauncher).toBool() && !right.data(AbstractTasksModel::IsLauncher).toBool()) { + if (launchInPlace) { + const int rightPos = q->launcherPosition(right.data(AbstractTasksModel::LauncherUrl).toUrl()); + + if (rightPos != -1) { + return (left.row() < rightPos); + } + } + + return true; + } else if (!left.data(AbstractTasksModel::IsLauncher).toBool() && right.data(AbstractTasksModel::IsLauncher).toBool()) { + if (launchInPlace) { + const int leftPos = q->launcherPosition(left.data(AbstractTasksModel::LauncherUrl).toUrl()); + + if (leftPos != -1) { + return (leftPos < right.row()); + } + } + + return false; + } else if (launchInPlace) { + const int leftPos = q->launcherPosition(left.data(AbstractTasksModel::LauncherUrl).toUrl()); + const int rightPos = q->launcherPosition(right.data(AbstractTasksModel::LauncherUrl).toUrl()); + + if (leftPos != -1 && rightPos != -1) { + return (leftPos < rightPos); + } else if (leftPos != -1 && rightPos == -1) { + return true; + } else if (leftPos == -1 && rightPos != -1) { + return false; + } + } + + // If told to stop after launchers we fall through to the existing map if it exists. + if (sortOnlyLaunchers && !sortedPreFilterRows.isEmpty()) { + return (sortedPreFilterRows.indexOf(left.row()) < sortedPreFilterRows.indexOf(right.row())); + } + + // Sort other cases by sort mode. + switch (sortMode) { + case SortVirtualDesktop: { + const QVariant &leftDesktopVariant = left.data(AbstractTasksModel::VirtualDesktop); + bool leftOk = false; + const int leftDesktop = leftDesktopVariant.toInt(&leftOk); + + const QVariant &rightDesktopVariant = right.data(AbstractTasksModel::VirtualDesktop); + bool rightOk = false; + const int rightDesktop = rightDesktopVariant.toInt(&rightOk); + + if (leftOk && rightOk && (leftDesktop != rightDesktop)) { + return (leftDesktop < rightDesktop); + } else if (leftOk && !rightOk) { + return false; + } else if (!leftOk && rightOk) { + return true; + } + } + case SortActivity: { + // updateActivityTaskCounts() counts the number of window tasks on each + // activity. This will sort tasks by comparing a cumulative score made + // up of the task counts for each acvtivity a task is assigned to, and + // otherwise fall through to alphabetical sorting. + int leftScore = -1; + int rightScore = -1; + + const QStringList &leftActivities = left.data(AbstractTasksModel::Activities).toStringList(); + + if (!leftActivities.isEmpty()) { + foreach(const QString& activity, leftActivities) { + leftScore += activityTaskCounts[activity]; + } + } + + const QStringList &rightActivities = right.data(AbstractTasksModel::Activities).toStringList(); + + if (!rightActivities.isEmpty()) { + foreach(const QString& activity, rightActivities) { + rightScore += activityTaskCounts[activity]; + } + } + + if (leftScore == -1 || rightScore == -1) { + const QList &counts = activityTaskCounts.values(); + const int sumScore = std::accumulate(counts.begin(), counts.end(), 0); + + if (leftScore == -1) { + leftScore = sumScore; + } + + if (rightScore == -1) { + rightScore = sumScore; + } + } + + if (leftScore != rightScore) { + return (leftScore > rightScore); + } + } + // Fall through to source order if sorting is disabled or manual, or alphabetical by app name otherwise. + default: { + if (sortMode == SortDisabled) { + return (left.row() < right.row()); + } else { + const QString &leftSortString = left.data(AbstractTasksModel::AppName).toString() + + left.data(Qt::DisplayRole).toString(); + + const QString &rightSortString = right.data(AbstractTasksModel::AppName).toString() + + right.data(Qt::DisplayRole).toString(); + + return (leftSortString.localeAwareCompare(rightSortString) < 0); + } + } + } +} + +TasksModel::TasksModel(QObject *parent) + : QSortFilterProxyModel(parent) + , d(new Private(this)) +{ + d->initModels(); + + // Start sorting. + sort(0); +} + +TasksModel::~TasksModel() +{ +} + +QHash TasksModel::roleNames() const +{ + if (d->windowTasksModel) { + return d->windowTasksModel->roleNames(); + } + + return QHash(); +} + +int TasksModel::rowCount(const QModelIndex &parent) const +{ + return QSortFilterProxyModel::rowCount(parent); +} + +int TasksModel::launcherCount() const +{ + // TODO: Optimize algorithm or cache the output. + + QList launchers = QUrl::fromStringList(d->launcherTasksModel->launcherList()); + + for(int i = 0; i < d->filterProxyModel->rowCount(); ++i) { + const QModelIndex &filterIndex = d->filterProxyModel->index(i, 0); + + if (!filterIndex.data(AbstractTasksModel::IsLauncher).toBool()) { + const QUrl &launcherUrl = filterIndex.data(AbstractTasksModel::LauncherUrl).toUrl(); + + QMutableListIterator it(launchers); + + while(it.hasNext()) { + it.next(); + + if (launcherUrlsMatch(launcherUrl, it.value(), IgnoreQueryItems)) { + it.remove(); + } + } + } + } + + return launchers.count(); +} + +bool TasksModel::anyTaskDemandsAttention() const +{ + return d->anyTaskDemandsAttention; +} + +int TasksModel::virtualDesktop() const +{ + return d->filterProxyModel->virtualDesktop(); +} + +void TasksModel::setVirtualDesktop(int virtualDesktop) +{ + d->filterProxyModel->setVirtualDesktop(virtualDesktop); +} + +int TasksModel::screen() const +{ + return d->filterProxyModel->screen(); +} + +void TasksModel::setScreen(int screen) +{ + d->filterProxyModel->setScreen(screen); +} + +QString TasksModel::activity() const +{ + return d->filterProxyModel->activity(); +} + +void TasksModel::setActivity(const QString &activity) +{ + d->filterProxyModel->setActivity(activity); +} + +bool TasksModel::filterByVirtualDesktop() const +{ + return d->filterProxyModel->filterByVirtualDesktop(); +} + +void TasksModel::setFilterByVirtualDesktop(bool filter) +{ + d->filterProxyModel->setFilterByVirtualDesktop(filter); +} + +bool TasksModel::filterByScreen() const +{ + return d->filterProxyModel->filterByScreen(); +} + +void TasksModel::setFilterByScreen(bool filter) +{ + d->filterProxyModel->setFilterByScreen(filter); +} + +bool TasksModel::filterByActivity() const +{ + return d->filterProxyModel->filterByActivity(); +} + +void TasksModel::setFilterByActivity(bool filter) +{ + d->filterProxyModel->setFilterByActivity(filter); +} + +bool TasksModel::filterNotMinimized() const +{ + return d->filterProxyModel->filterNotMinimized(); +} + +void TasksModel::setFilterNotMinimized(bool filter) +{ + d->filterProxyModel->setFilterNotMinimized(filter); +} + +TasksModel::SortMode TasksModel::sortMode() const +{ + return d->sortMode; +} + +void TasksModel::setSortMode(SortMode mode) +{ + if (d->sortMode != mode) { + if (mode == SortManual) { + d->updateManualSortMap(); + } else if (d->sortMode == SortManual) { + d->sortedPreFilterRows.clear(); + } + + if (mode == SortActivity) { + if (!d->activityInfo) { + d->activityInfo = new ActivityInfo(); + } + + ++d->activityInfoUsers; + + d->updateActivityTaskCounts(); + setSortRole(AbstractTasksModel::Activities); + } else if (d->sortMode == SortActivity) { + --d->activityInfoUsers; + + if (!d->activityInfoUsers) { + delete d->activityInfo; + d->activityInfo = nullptr; + } + + d->activityTaskCounts.clear(); + setSortRole(Qt::DisplayRole); + } + + d->sortMode = mode; + + d->forceResort(); + + emit sortModeChanged(); + } +} + +bool TasksModel::separateLaunchers() const +{ + return d->separateLaunchers; +} + +void TasksModel::setSeparateLaunchers(bool separate) +{ + return; // FIXME TODO: Disable until done. + + if (d->separateLaunchers != separate) { + d->separateLaunchers = separate; + + d->forceResort(); + + emit separateLaunchersChanged(); + } +} + +bool TasksModel::launchInPlace() const +{ + return d->launchInPlace; +} + +void TasksModel::setLaunchInPlace(bool launchInPlace) +{ + if (d->launchInPlace != launchInPlace) { + d->launchInPlace = launchInPlace; + + d->forceResort(); + + emit launchInPlaceChanged(); + } +} + +TasksModel::GroupMode TasksModel::groupMode() const +{ + if (!d->groupingProxyModel) { + return GroupDisabled; + } + + return d->groupingProxyModel->groupMode(); +} + +void TasksModel::setGroupMode(GroupMode mode) +{ + if (d->groupingProxyModel) { + d->groupingProxyModel->setGroupMode(mode); + } +} + +int TasksModel::groupingWindowTasksThreshold() const +{ + if (!d->groupingProxyModel) { + return -1; + } + + return d->groupingProxyModel->windowTasksThreshold(); +} + +void TasksModel::setGroupingWindowTasksThreshold(int threshold) +{ + if (d->groupingProxyModel) { + d->groupingProxyModel->setWindowTasksThreshold(threshold); + } +} + +QStringList TasksModel::groupingAppIdBlacklist() const +{ + if (!d->groupingProxyModel) { + return QStringList(); + } + + return d->groupingProxyModel->blacklistedAppIds(); +} + +void TasksModel::setGroupingAppIdBlacklist(const QStringList &list) +{ + if (d->groupingProxyModel) { + d->groupingProxyModel->setBlacklistedAppIds(list); + } +} + +QStringList TasksModel::groupingLauncherUrlBlacklist() const +{ + if (!d->groupingProxyModel) { + return QStringList(); + } + + return d->groupingProxyModel->blacklistedLauncherUrls(); +} + +void TasksModel::setGroupingLauncherUrlBlacklist(const QStringList &list) +{ + if (d->groupingProxyModel) { + d->groupingProxyModel->setBlacklistedLauncherUrls(list); + } +} + +QStringList TasksModel::launcherList() const +{ + if (d->launcherTasksModel) { + return d->launcherTasksModel->launcherList(); + } + + return QStringList(); +} + +void TasksModel::setLauncherList(const QStringList &launchers) +{ + if (d->launcherTasksModel) { + d->launcherTasksModel->setLauncherList(launchers); + } +} + +bool TasksModel::requestAddLauncher(const QUrl &url) +{ + if (d->launcherTasksModel) { + bool added = d->launcherTasksModel->requestAddLauncher(url); + + // If using manual sorting and launch-in-place sorting, we need + // to trigger a sort map update to move any window tasks to their + // launcher position now. + if (added && d->sortMode == SortManual && (d->launchInPlace || !d->separateLaunchers)) { + d->updateManualSortMap(); + d->forceResort(); + } + + return added; + } + + return false; +} + +bool TasksModel::requestRemoveLauncher(const QUrl &url) +{ + if (d->launcherTasksModel) { + return d->launcherTasksModel->requestRemoveLauncher(url); + } + + return false; +} + +int TasksModel::launcherPosition(const QUrl &url) const +{ + if (d->launcherTasksModel) { + return d->launcherTasksModel->launcherPosition(url); + } + + return -1; +} + +void TasksModel::requestActivate(const QModelIndex &index) +{ + if (index.isValid() && index.model() == this) { + d->groupingProxyModel->requestActivate(mapToSource(index)); + } +} + +void TasksModel::requestNewInstance(const QModelIndex &index) +{ + if (index.isValid() && index.model() == this) { + d->groupingProxyModel->requestNewInstance(mapToSource(index)); + } +} + +void TasksModel::requestClose(const QModelIndex &index) +{ + if (index.isValid() && index.model() == this) { + d->groupingProxyModel->requestClose(mapToSource(index)); + } +} + +void TasksModel::requestMove(const QModelIndex &index) +{ + if (index.isValid() && index.model() == this) { + d->groupingProxyModel->requestMove(mapToSource(index)); + } +} + +void TasksModel::requestResize(const QModelIndex &index) +{ + if (index.isValid() && index.model() == this) { + d->groupingProxyModel->requestResize(mapToSource(index)); + } +} + +void TasksModel::requestToggleMinimized(const QModelIndex &index) +{ + if (index.isValid() && index.model() == this) { + d->groupingProxyModel->requestToggleMinimized(mapToSource(index)); + } +} + +void TasksModel::requestToggleMaximized(const QModelIndex &index) +{ + if (index.isValid() && index.model() == this) { + d->groupingProxyModel->requestToggleMaximized(mapToSource(index)); + } +} + +void TasksModel::requestToggleKeepAbove(const QModelIndex &index) +{ + if (index.isValid() && index.model() == this) { + d->groupingProxyModel->requestToggleKeepAbove(mapToSource(index)); + } +} + +void TasksModel::requestToggleKeepBelow(const QModelIndex &index) +{ + if (index.isValid() && index.model() == this) { + d->groupingProxyModel->requestToggleKeepBelow(mapToSource(index)); + } +} + +void TasksModel::requestToggleFullScreen(const QModelIndex &index) +{ + if (index.isValid() && index.model() == this) { + d->groupingProxyModel->requestToggleFullScreen(mapToSource(index)); + } +} + +void TasksModel::requestToggleShaded(const QModelIndex &index) +{ + if (index.isValid() && index.model() == this) { + d->groupingProxyModel->requestToggleShaded(mapToSource(index)); + } +} + +void TasksModel::requestVirtualDesktop(const QModelIndex &index, qint32 desktop) +{ + if (index.isValid() && index.model() == this) { + d->groupingProxyModel->requestVirtualDesktop(mapToSource(index), desktop); + } +} + +void TasksModel::requestPublishDelegateGeometry(const QModelIndex &index, const QRect &geometry, QObject *delegate) +{ + if (index.isValid() && index.model() == this) { + d->groupingProxyModel->requestPublishDelegateGeometry(mapToSource(index), geometry, delegate); + } +} + +void TasksModel::requestToggleGrouping(const QModelIndex &index) +{ + if (index.isValid() && index.model() == this) { + d->groupingProxyModel->requestToggleGrouping(mapToSource(index)); + } +} + +bool TasksModel::move(int row, int newPos) +{ + if (d->sortMode != SortManual || row == newPos || newPos < 0 || newPos >= rowCount()) { + return false; + } + + const QModelIndex &idx = index(row, 0); + bool isLauncherMove = false; + + // Figure out if we're moving a launcher so we can run barrier checks. + if (idx.isValid()) { + if (idx.data(AbstractTasksModel::IsLauncher).toBool()) { + isLauncherMove = true; + // When using launch-in-place sorting, launcher-backed window tasks act as launchers. + } else if ((d->launchInPlace || !d->separateLaunchers) + && idx.data(AbstractTasksModel::IsWindow).toBool()) { + const QUrl &launcherUrl = idx.data(AbstractTasksModel::LauncherUrl).toUrl(); + const int launcherPos = launcherPosition(launcherUrl); + + if (launcherPos != -1) { + isLauncherMove = true; + } + } + } else { + return false; + } + + if (d->separateLaunchers) { + const int firstTask = (d->launchInPlace ? d->launcherTasksModel->rowCount() : launcherCount()); + + // Don't allow launchers to be moved past the last launcher. + if (isLauncherMove && newPos >= firstTask) { + return false; + } + + // Don't allow tasks to be moved into the launchers. + if (!isLauncherMove && newPos < firstTask) { + return false; + } + } + + beginMoveRows(QModelIndex(), row, row, QModelIndex(), (newPos >row) ? newPos + 1 : newPos); + + // Translate to sort map indices. + const QModelIndex &rowIndex = index(row, 0); + const QModelIndex &preFilterRowIndex = d->preFilterIndex(mapToSource(rowIndex)); + row = d->sortedPreFilterRows.indexOf(preFilterRowIndex.row()); + newPos = d->sortedPreFilterRows.indexOf(d->preFilterIndex(mapToSource(index(newPos, 0))).row()); + + // Update sort mapping. + d->sortedPreFilterRows.move(row, newPos); + + endMoveRows(); + + // Move children along with the group. + // This can be safely done after the row move transaction as the sort + // map isn't consulted for rows below the top level. + if (groupMode() != GroupDisabled && rowCount(rowIndex)) { + d->syncManualSortMapForGroup(rowIndex); + } + + // Resort. + d->forceResort(); + + // Setup for syncLaunchers(). + d->launcherSortingDirty = isLauncherMove; + + return true; +} + +void TasksModel::syncLaunchers() +{ + // Writes the launcher order exposed through the model back to the launcher + // tasks model, committing any move() operations to persistent state. + + if (!d->launcherTasksModel || !d->launcherSortingDirty) { + return; + } + + QMap sortedLaunchers; + + foreach(const QUrl &launcherUrl, launcherList()) { + int row = -1; + + for (int i = 0; i < rowCount(); ++i) { + const QUrl &rowLauncherUrl = index(i, 0).data(AbstractTasksModel::LauncherUrl).toUrl(); + + if (launcherUrlsMatch(launcherUrl, rowLauncherUrl, IgnoreQueryItems)) { + row = i; + break; + } + } + + if (row != -1) { + sortedLaunchers.insert(row, launcherUrl); + } + } + + setLauncherList(QUrl::toStringList(sortedLaunchers.values())); + d->launcherSortingDirty = false; +} + +QModelIndex TasksModel::activeTask() const +{ + for (int i = 0; i < rowCount(); ++i) { + const QModelIndex &idx = index(i, 0); + + if (idx.data(AbstractTasksModel::IsActive).toBool()) { + if (groupMode() != GroupDisabled && rowCount(idx)) { + for (int j = 0; j < rowCount(idx); ++j) { + const QModelIndex &child = idx.child(j, 0); + + if (child.data(AbstractTasksModel::IsActive).toBool()) { + return child; + } + } + } else { + return idx; + } + } + } + + return QModelIndex(); +} + +QModelIndex TasksModel::makeModelIndex(int row, int childRow) const +{ + if (row < 0 || row >= rowCount()) { + return QModelIndex(); + } + + const QModelIndex &parent = index(row, 0); + + if (childRow == -1) { + return index(row, 0); + } else { + const QModelIndex &parent = index(row, 0); + + if (childRow < rowCount(parent)) { + return parent.child(childRow, 0); + } + } + + return QModelIndex(); +} + +bool TasksModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const +{ + // All our filtering occurs at the top-level; group children always go through. + if (sourceParent.isValid()) { + return true; + } + + const QModelIndex &sourceIndex = sourceModel()->index(sourceRow, 0); + const QString &appId = sourceIndex.data(AbstractTasksModel::AppId).toString(); + const QString &appName = sourceIndex.data(AbstractTasksModel::AppName).toString(); + + // Filter startup tasks we already have a window task for. + if (sourceIndex.data(AbstractTasksModel::IsStartup).toBool()) { + for (int i = 0; i < d->windowTasksModel->rowCount(); ++i) { + const QModelIndex &windowIndex = d->windowTasksModel->index(i, 0); + + if (appId == windowIndex.data(AbstractTasksModel::AppId).toString() + || appName == windowIndex.data(AbstractTasksModel::AppName).toString()) { + return false; + } + } + } + + // Filter launcher tasks we already have a startup or window task for (that + // got through filtering). + if (sourceIndex.data(AbstractTasksModel::IsLauncher).toBool()) { + const QUrl &launcherUrl = sourceIndex.data(AbstractTasksModel::LauncherUrl).toUrl(); + + for (int i = 0; i < d->filterProxyModel->rowCount(); ++i) { + const QModelIndex &filteredIndex = d->filterProxyModel->index(i, 0); + + if (!filteredIndex.data(AbstractTasksModel::IsWindow).toBool() && + !filteredIndex.data(AbstractTasksModel::IsStartup).toBool()) { + continue; + } + + const QString &filteredAppId = filteredIndex.data(AbstractTasksModel::AppId).toString(); + + if ((!appId.isEmpty() && appId == filteredAppId) + || launcherUrlsMatch(launcherUrl, filteredIndex.data(AbstractTasksModel::LauncherUrl).toUrl(), + IgnoreQueryItems)) { + emit launcherCountChanged(); + + return false; + } + } + } + + return true; +} + +bool TasksModel::lessThan(const QModelIndex &left, const QModelIndex &right) const +{ + // In manual sort mode we sort top-level items by referring to a map we keep. + // Insertions into the map are placed using a combination of Private::lessThan + // and simple append behavior. Child items are sorted alphabetically. + if (d->sortMode == SortManual && !left.parent().isValid() && !right.parent().isValid()) { + return (d->sortedPreFilterRows.indexOf(d->preFilterIndex(left).row()) + < d->sortedPreFilterRows.indexOf(d->preFilterIndex(right).row())); + } + + return d->lessThan(left, right); +} + +} diff --git a/libtaskmanager/tasktools.h b/libtaskmanager/tasktools.h new file mode 100644 --- /dev/null +++ b/libtaskmanager/tasktools.h @@ -0,0 +1,102 @@ +/******************************************************************** +Copyright 2016 Eike Hein + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 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 6 of version 3 of the license. + +This library 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 +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library. If not, see . +*********************************************************************/ + +#ifndef TASKTOOLS_H +#define TASKTOOLS_H + +#include "taskmanager_export.h" + +#include +#include +#include + +namespace TaskManager +{ + +struct AppData +{ + QString id; // Application id (*.desktop sans extension). + QString name; // Application name. + QString genericName; // Generic application name. + QIcon icon; + QUrl url; +}; + +enum UrlComparisonMode { + Strict = 0, + IgnoreQueryItems +}; + +/** + * Fills in and returns an AppData struct based on the given URL. + * + * If the URL contains iconData in its query string, it is decoded and + * set as AppData.icon, taking precedence over normal icon discovery. + * + * If the URL is using the preferred:// scheme, the URL it resolves to + * is set as AppData.url. + * + * The supplied fallback icon is set as AppData.icon if no other icon + * could be found. + * + * @see defaultApplication + * @param url A URL to a .desktop file or executable, or a preferred:// URL. + * @param fallbackIcon An icon to use when none could be read from the URL or + * otherwise found. + * @returns @c AppData filled in based on the given URL. + */ +TASKMANAGER_EXPORT AppData appDataFromUrl(const QUrl &url, const QIcon &fallbackIcon = QIcon()); + +/** + * Returns an application id for an URL using the preferred:// scheme. + * + * Recognized values for the host component of the URL are: + * - "browser" + * - "mailer" + * - "terminal" + * - "windowmanager" + * + * If the host component matches none of the above, an attempt is made + * to match to application links stored in kcm_componentchooser/. + * + * @param url A URL using the preferred:// scheme. + * @returns an application id for the given URL. + **/ +TASKMANAGER_EXPORT QString defaultApplication(const QUrl &url); + +/** + * Compares two launcher URLs either strictly or ignoring the query string. + * + * In launcher URLs, the query string is used to hold metadata such as an + * icon. When comparing tasks by launcher URL, this metadata should usually + * be ignored. This function serves this need. + * + * @see LauncherTasksModel + * @param a The first launcher URL. + * @param b The second launcher URL. + * @param c The comparison mode. Either Strict or IgnoreQueryItems. + * @returns @c true if the URLs match. + **/ +TASKMANAGER_EXPORT bool launcherUrlsMatch(const QUrl &a, const QUrl &b, UrlComparisonMode mode = Strict); + +TASKMANAGER_EXPORT bool appsMatch(const QModelIndex &a, const QModelIndex &b); +} + +#endif diff --git a/libtaskmanager/tasktools.cpp b/libtaskmanager/tasktools.cpp new file mode 100644 --- /dev/null +++ b/libtaskmanager/tasktools.cpp @@ -0,0 +1,253 @@ +/******************************************************************** +Copyright 2016 Eike Hein + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 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 6 of version 3 of the license. + +This library 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 +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library. If not, see . +*********************************************************************/ + +#include "tasktools.h" +#include "abstracttasksmodel.h" + +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace TaskManager +{ + +AppData appDataFromUrl(const QUrl &url, const QIcon &fallbackIcon) +{ + AppData data; + data.url = url; + + if (url.hasQuery()) { + QUrlQuery uQuery(url); + + if (uQuery.hasQueryItem(QLatin1String("iconData"))) { + QString iconData(uQuery.queryItemValue(QLatin1String("iconData"))); + QPixmap pixmap; + QByteArray bytes = QByteArray::fromBase64(iconData.toLocal8Bit(), QByteArray::Base64UrlEncoding); + pixmap.loadFromData(bytes); + data.icon.addPixmap(pixmap); + } + } + + if (url.isLocalFile() && KDesktopFile::isDesktopFile(url.toLocalFile())) { + KDesktopFile f(url.toLocalFile()); + + const KService::Ptr service = KService::serviceByStorageId(f.fileName()); + + if (service && QUrl::fromLocalFile(service->entryPath()) == url) { + data.name = service->name(); + data.genericName = service->genericName(); + data.id = service->storageId(); + + if (data.icon.isNull()) { + data.icon = QIcon::fromTheme(service->icon()); + } + } else if (f.tryExec()) { + data.name = f.readName(); + data.genericName = f.readGenericName(); + data.id = QUrl::fromLocalFile(f.fileName()).fileName(); + + if (data.icon.isNull()) { + data.icon = QIcon::fromTheme(f.readIcon()); + } + } + + if (data.id.endsWith(".desktop")) { + data.id = data.id.left(data.id.length() - 8); + } + } else if (url.scheme() == QLatin1String("preferred")) { + data.id = defaultApplication(url); + + const KService::Ptr service = KService::serviceByStorageId(data.id); + + if (service) { + QString desktopFile = service->entryPath(); + + // Update with resolved URL. + data.url = QUrl::fromLocalFile(desktopFile); + + KDesktopFile f(desktopFile); + KConfigGroup cg(&f, "Desktop Entry"); + + data.icon = QIcon::fromTheme(f.readIcon()); + const QString exec = cg.readEntry("Exec", QString()); + data.name = cg.readEntry("Name", QString()); + + if (data.name.isEmpty() && !exec.isEmpty()) { + data.name = exec.split(' ').at(0); + } + + data.genericName = f.readGenericName(); + } + } + + if (data.name.isEmpty()) { + data.name = url.fileName(); + } + + if (data.icon.isNull()) { + data.icon = fallbackIcon; + } + + return data; +} + +QString defaultApplication(const QUrl &url) +{ + if (url.scheme() != QLatin1String("preferred")) { + return QString(); + } + + const QString &application = url.host(); + + if (application.isEmpty()) { + return QString(); + } + + if (application.compare(QLatin1String("mailer"), Qt::CaseInsensitive) == 0) { + KEMailSettings settings; + + // In KToolInvocation, the default is kmail; but let's be friendlier. + QString command = settings.getSetting(KEMailSettings::ClientProgram); + + if (command.isEmpty()) { + if (KService::Ptr kontact = KService::serviceByStorageId(QStringLiteral("kontact"))) { + return kontact->storageId(); + } else if (KService::Ptr kmail = KService::serviceByStorageId(QStringLiteral("kmail"))) { + return kmail->storageId(); + } + } + + if (!command.isEmpty()) { + if (settings.getSetting(KEMailSettings::ClientTerminal) == QLatin1String("true")) { + KConfigGroup confGroup(KSharedConfig::openConfig(), "General"); + const QString preferredTerminal = confGroup.readPathEntry("TerminalApplication", + QStringLiteral("konsole")); + command = preferredTerminal + QLatin1String(" -e ") + command; + } + + return command; + } + } else if (application.compare(QLatin1String("browser"), Qt::CaseInsensitive) == 0) { + KConfigGroup config(KSharedConfig::openConfig(), "General"); + QString browserApp = config.readPathEntry("BrowserApplication", QString()); + + if (browserApp.isEmpty()) { + const KService::Ptr htmlApp = KMimeTypeTrader::self()->preferredService(QStringLiteral("text/html")); + + if (htmlApp) { + browserApp = htmlApp->storageId(); + } + } else if (browserApp.startsWith('!')) { + browserApp = browserApp.mid(1); + } + + return browserApp; + } else if (application.compare(QLatin1String("terminal"), Qt::CaseInsensitive) == 0) { + KConfigGroup confGroup(KSharedConfig::openConfig(), "General"); + + return confGroup.readPathEntry("TerminalApplication", QStringLiteral("konsole")); + } else if (application.compare(QLatin1String("filemanager"), Qt::CaseInsensitive) == 0) { + KService::Ptr service = KMimeTypeTrader::self()->preferredService(QStringLiteral("inode/directory")); + + if (service) { + return service->storageId(); + } + } else if (application.compare(QLatin1String("windowmanager"), Qt::CaseInsensitive) == 0) { + KConfig cfg(QStringLiteral("ksmserverrc"), KConfig::NoGlobals); + KConfigGroup confGroup(&cfg, "General"); + + return confGroup.readEntry("windowManager", QStringLiteral("kwin")); + } else if (KService::Ptr service = KMimeTypeTrader::self()->preferredService(application)) { + return service->storageId(); + } else { + // Try the files in share/apps/kcm_componentchooser/*.desktop. + QStringList directories = QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, QStringLiteral("kcm_componentchooser"), QStandardPaths::LocateDirectory); + QStringList services; + + foreach(const QString& directory, directories) { + QDir dir(directory); + foreach(const QString& f, dir.entryList(QStringList("*.desktop"))) + services += dir.absoluteFilePath(f); + } + + foreach (const QString & service, services) { + KConfig config(service, KConfig::SimpleConfig); + KConfigGroup cg = config.group(QByteArray()); + const QString type = cg.readEntry("valueName", QString()); + + if (type.compare(application, Qt::CaseInsensitive) == 0) { + KConfig store(cg.readPathEntry("storeInFile", QStringLiteral("null"))); + KConfigGroup storeCg(&store, cg.readEntry("valueSection", QString())); + const QString exec = storeCg.readPathEntry(cg.readEntry("valueName", "kcm_componenchooser_null"), + cg.readEntry("defaultImplementation", QString())); + + if (!exec.isEmpty()) { + return exec; + } + + break; + } + } + } + + return QString(""); +} + +bool launcherUrlsMatch(const QUrl &_a, const QUrl &_b, UrlComparisonMode mode) +{ + if (mode == IgnoreQueryItems) { + QUrl a(_a); + a.setQuery(QUrlQuery()); + + QUrl b(_b); + b.setQuery(QUrlQuery()); + + return (a == b); + } + + return (_a == _b); +} + +bool appsMatch(const QModelIndex &a, const QModelIndex &b) +{ + const QString &aAppId = a.data(AbstractTasksModel::AppId).toString(); + const QString &bAppId = b.data(AbstractTasksModel::AppId).toString(); + + if (!aAppId.isEmpty() && bAppId.isEmpty() && (aAppId == bAppId)) { + return true; + } + + const QUrl &aLauncherUrl = a.data(AbstractTasksModel::LauncherUrl).toUrl(); + const QUrl &bLauncherUrl = b.data(AbstractTasksModel::LauncherUrl).toUrl(); + + if (aLauncherUrl.isValid() && bLauncherUrl.isValid() && (aLauncherUrl == bLauncherUrl)) { + return true; + } + + return false; +} + +} diff --git a/libtaskmanager/virtualdesktopinfo.h b/libtaskmanager/virtualdesktopinfo.h new file mode 100644 --- /dev/null +++ b/libtaskmanager/virtualdesktopinfo.h @@ -0,0 +1,90 @@ +/******************************************************************** +Copyright 2016 Eike Hein + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 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 6 of version 3 of the license. + +This library 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 +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library. If not, see . +*********************************************************************/ + +#ifndef VIRTUALDESKTOPINFO_H +#define VIRTUALDESKTOPINFO_H + +#include + +#include "taskmanager_export.h" + +namespace TaskManager +{ + +/** + * @short Provides basic virtual desktop information. + * + * This class provides basic information about the virtual desktops present + * in the session as a set of notifiable properties. + * + * @NOTE: This is a placeholder, to be moved into KWindowSystem (which it + * wraps) or the Task Manager applet backend (which used to fill this role + * in the past). + * + * @see KWindowSystem + * + * @author Eike Hein + **/ + +class TASKMANAGER_EXPORT VirtualDesktopInfo : public QObject +{ + Q_OBJECT + + Q_PROPERTY(int currentDesktop READ currentDesktop NOTIFY currentDesktopChanged) + Q_PROPERTY(int numberOfDesktops READ numberOfDesktops NOTIFY numberOfDesktopsChanged) + Q_PROPERTY(QStringList desktopNames READ desktopNames NOTIFY desktopNamesChanged) + +public: + explicit VirtualDesktopInfo(QObject *parent = 0); + virtual ~VirtualDesktopInfo(); + + /** + * The currently active virtual desktop. + * + * @returns the number of the currently active virtual desktop. + **/ + int currentDesktop() const; + + /** + * The number of virtual desktops present in the session. + * + * @returns the number of virtual desktops present in the session. + **/ + int numberOfDesktops() const; + + /** + * The names of all virtual desktops present in the session. Note that + * virtual desktops are indexed starting at 1, so the name for virtual + * desktop 1 is at index 0 in this list. + * + * @returns a the list of names for the virtual desktops present in the + * session. + **/ + QStringList desktopNames() const; + +Q_SIGNALS: + void currentDesktopChanged() const; + void numberOfDesktopsChanged() const; + void desktopNamesChanged() const; +}; + +} + +#endif diff --git a/libtaskmanager/virtualdesktopinfo.cpp b/libtaskmanager/virtualdesktopinfo.cpp new file mode 100644 --- /dev/null +++ b/libtaskmanager/virtualdesktopinfo.cpp @@ -0,0 +1,66 @@ +/******************************************************************** +Copyright 2016 Eike Hein + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 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 6 of version 3 of the license. + +This library 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 +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library. If not, see . +*********************************************************************/ + +#include "virtualdesktopinfo.h" + +#include + +namespace TaskManager +{ + +VirtualDesktopInfo::VirtualDesktopInfo(QObject *parent) : QObject(parent) +{ + connect(KWindowSystem::self(), &KWindowSystem::currentDesktopChanged, + this, &VirtualDesktopInfo::currentDesktopChanged); + + connect(KWindowSystem::self(), &KWindowSystem::numberOfDesktopsChanged, + this, &VirtualDesktopInfo::numberOfDesktopsChanged); + + connect(KWindowSystem::self(), &KWindowSystem::desktopNamesChanged, + this, &VirtualDesktopInfo::desktopNamesChanged); +} + +VirtualDesktopInfo::~VirtualDesktopInfo() +{ +} + +int VirtualDesktopInfo::currentDesktop() const +{ + return KWindowSystem::currentDesktop(); +} + +int VirtualDesktopInfo::numberOfDesktops() const +{ + return KWindowSystem::numberOfDesktops(); +} + +QStringList VirtualDesktopInfo::desktopNames() const +{ + QStringList names; + + // Virtual desktop numbers start at 1. + for (int i = 1; i <= KWindowSystem::numberOfDesktops(); ++i) { + names << KWindowSystem::desktopName(i); + } + + return names; +} + +} diff --git a/libtaskmanager/waylandtasksmodel.h b/libtaskmanager/waylandtasksmodel.h new file mode 100644 --- /dev/null +++ b/libtaskmanager/waylandtasksmodel.h @@ -0,0 +1,194 @@ +/******************************************************************** +Copyright 2016 Eike Hein + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 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 6 of version 3 of the license. + +This library 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 +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library. If not, see . +*********************************************************************/ + +#ifndef WAYLANDTASKSMODEL_H +#define WAYLANDTASKSMODEL_H + +#include "abstracttasksmodel.h" + +#include "taskmanager_export.h" + +namespace KWayland { + +namespace Client { + class PlasmaWindowManagement; + class Surface; +} + +} + +namespace TaskManager +{ + +/** + * @short A tasks model for Wayland windows. + * + * This model presents tasks sourced from window data on the Wayland + * server the host process is connected to. + * + * FIXME: Filtering by window type still needed. + * FIXME: Support for taskmanagerrulesrc (maybe) still needed. + * + * @author Eike Hein + */ + +class TASKMANAGER_EXPORT WaylandTasksModel : public AbstractTasksModel +{ + Q_OBJECT + +public: + explicit WaylandTasksModel(QObject *parent = 0); + virtual ~WaylandTasksModel(); + + virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + virtual int rowCount(const QModelIndex &parent = QModelIndex()) const override; + + virtual QModelIndex index(int row, int column = 0, const QModelIndex &parent = QModelIndex()) const override; + + /** + * Request activation of the window at the given index. + * + * FIXME: Lacks transient handling of X Windows version. + * + * @param index An index in this window tasks model. + **/ + void requestActivate(const QModelIndex &index) override; + + /** + * Request an additional instance of the application owning the window + * at the given index. Success depends on whether a + * AbstractTasksModel::LauncherUrl could be derived from window metadata. + * + * @param index An index in this window tasks model. + **/ + void requestNewInstance(const QModelIndex &index) override; + + /** + * Request the window at the given index be closed. + * + * @param index An index in this window tasks model. + **/ + void requestClose(const QModelIndex &index) override; + + /** + * Request starting an interactive move for the window at the given index. + * + * FIXME: X Windows version has extra virtual desktop logic. + * + * @param index An index in this window tasks model. + **/ + void requestMove(const QModelIndex &index) override; + + /** + * Request starting an interactive move for the window at the given index. + * + * FIXME: X Windows version has extra virtual desktop logic. + * + * @param index An index in this window tasks model. + **/ + void requestResize(const QModelIndex &index) override; + + /** + * Request toggling the minimized state of the window at the given index. + * + * FIXME: X Windows version has extra virtual desktop logic. + * + * @param index An index in this window tasks model. + **/ + void requestToggleMinimized(const QModelIndex &index) override; + + /** + * Request toggling the maximized state of the task at the given index. + * + * FIXME: X Windows version has extra virtual desktop logic. + * + * @param index An index in this window tasks model. + **/ + void requestToggleMaximized(const QModelIndex &index) override; + + /** + * Request toggling the keep-above state of the task at the given index. + * + * @param index An index in this window tasks model. + **/ + void requestToggleKeepAbove(const QModelIndex &index) override; + + /** + * Request toggling the keep-below state of the task at the given index. + * + * @param index An index in this window tasks model. + **/ + void requestToggleKeepBelow(const QModelIndex &index) override; + + /** + * Request toggling the fullscreen state of the task at the given index. + * + * @param index An index in this window tasks model. + **/ + void requestToggleFullScreen(const QModelIndex &index) override; + + /** + * Request toggling the shaded state of the task at the given index. + * + * @param index An index in this window tasks model. + **/ + void requestToggleShaded(const QModelIndex &index) override; + + /** + * Request moving the window at the given index to the specified virtual + * desktop. + * + * FIXME: X Windows version has extra virtual desktop logic. + * + * @param index An index in this window tasks model. + * @param desktop A virtual desktop number. + **/ + void requestVirtualDesktop(const QModelIndex &index, qint32 desktop) override; + + /** + * Request informing the window manager of new geometry for a visual + * delegate for the window at the given index. The geometry is retrieved + * from the delegate object passed. Right now, QQuickItem is the only + * supported delegate object type. + * + * FIXME: This introduces the dependency on Qt5::Quick. I might prefer + * reversing this and publishing the window pointer through the model, + * then calling PlasmaWindow::setMinimizeGeometry in the applet backend, + * rather than hand delegate items into the lib, keeping the lib more UI- + * agnostic. + * + * @param index An index in this window tasks model. + * @param geometry Visual delegate geometry in screen coordinates. Unused + * in this implementation. + * @param delegate The delegate. This implementation will attempt to cast + * it to QQuickItem, map its coordinates to its window and find the Wayland + * Surface for the window. + **/ + void requestPublishDelegateGeometry(const QModelIndex &index, const QRect &geometry, + QObject *delegate = nullptr) override; + +private: + class Private; + QScopedPointer d; +}; + +} + +#endif diff --git a/libtaskmanager/waylandtasksmodel.cpp b/libtaskmanager/waylandtasksmodel.cpp new file mode 100644 --- /dev/null +++ b/libtaskmanager/waylandtasksmodel.cpp @@ -0,0 +1,503 @@ +/******************************************************************** +Copyright 2016 Eike Hein + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 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 6 of version 3 of the license. + +This library 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 +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library. If not, see . +*********************************************************************/ + +#include "waylandtasksmodel.h" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace TaskManager +{ + +class WaylandTasksModel::Private +{ +public: + Private(WaylandTasksModel *q); + QList windows; + QHash serviceCache; + KWayland::Client::PlasmaWindowManagement *windowManagement = nullptr; + + void initWayland(); + void addWindow(KWayland::Client::PlasmaWindow *window); + void dataChanged(KWayland::Client::PlasmaWindow *window, int role); + +private: + WaylandTasksModel *q; +}; + +WaylandTasksModel::Private::Private(WaylandTasksModel *q) + : q(q) +{ +} + +void WaylandTasksModel::Private::initWayland() +{ + if (!QGuiApplication::platformName().startsWith(QLatin1String("wayland"), Qt::CaseInsensitive)) { + return; + } + + KWayland::Client::ConnectionThread *connection = KWayland::Client::ConnectionThread::fromApplication(q); + + if (!connection) { + return; + } + + KWayland::Client::Registry *registry = new KWayland::Client::Registry(q); + registry->create(connection); + + QObject::connect(registry, &KWayland::Client::Registry::plasmaWindowManagementAnnounced, [this, registry] (quint32 name, quint32 version) { + windowManagement = registry->createPlasmaWindowManagement(name, version, q); + + QObject::connect(windowManagement, &KWayland::Client::PlasmaWindowManagement::interfaceAboutToBeReleased, q, + [this] { + q->beginResetModel(); + windows.clear(); + q->endResetModel(); + } + ); + + QObject::connect(windowManagement, &KWayland::Client::PlasmaWindowManagement::windowCreated, q, + [this](KWayland::Client::PlasmaWindow *window) { + addWindow(window); + } + ); + + for (auto it = windowManagement->windows().constBegin(); it != windowManagement->windows().constEnd(); ++it) { + addWindow(*it); + } + } + ); + + registry->setup(); +} + +void WaylandTasksModel::Private::addWindow(KWayland::Client::PlasmaWindow *window) +{ + if (windows.indexOf(window) != -1) { + return; + } + + const int count = windows.count(); + + q->beginInsertRows(QModelIndex(), count, count); + + windows.append(window); + + // FIXME TODO: appid apparently not available yet. + KService::Ptr service = KService::serviceByStorageId(window->appId()); + + if (service) { + serviceCache.insert(window, service); + } + + q->endInsertRows(); + + auto removeWindow = [window, this] { + const int row = windows.indexOf(window); + if (row != -1) { + q->beginRemoveRows(QModelIndex(), row, row); + windows.removeAt(row); + serviceCache.remove(window); + q->endRemoveRows(); + } + }; + + QObject::connect(window, &KWayland::Client::PlasmaWindow::unmapped, q, removeWindow); + QObject::connect(window, &QObject::destroyed, q, removeWindow); + + QObject::connect(window, &KWayland::Client::PlasmaWindow::titleChanged, q, + [window, this] { dataChanged(window, Qt::DisplayRole); } + ); + + QObject::connect(window, &KWayland::Client::PlasmaWindow::iconChanged, q, + [window, this] { dataChanged(window, Qt::DecorationRole); } + ); + + QObject::connect(window, &KWayland::Client::PlasmaWindow::appIdChanged, q, + [window, this] { + KService::Ptr service = KService::serviceByStorageId(window->appId()); + + if (service) { + serviceCache.insert(window, service); + } else { + serviceCache.remove(window); + } + + dataChanged(window, AppId); + } + ); + + QObject::connect(window, &KWayland::Client::PlasmaWindow::activeChanged, q, + [window, this] { this->dataChanged(window, IsActive); } + ); + + QObject::connect(window, &KWayland::Client::PlasmaWindow::closeableChanged, q, + [window, this] { this->dataChanged(window, IsClosable); } + ); + + QObject::connect(window, &KWayland::Client::PlasmaWindow::movableChanged, q, + [window, this] { this->dataChanged(window, IsMovable); } + ); + + QObject::connect(window, &KWayland::Client::PlasmaWindow::resizableChanged, q, + [window, this] { this->dataChanged(window, IsResizable); } + ); + + QObject::connect(window, &KWayland::Client::PlasmaWindow::fullscreenableChanged, q, + [window, this] { this->dataChanged(window, IsFullScreenable); } + ); + + QObject::connect(window, &KWayland::Client::PlasmaWindow::fullscreenChanged, q, + [window, this] { this->dataChanged(window, IsFullScreen); } + ); + + QObject::connect(window, &KWayland::Client::PlasmaWindow::maximizeableChanged, q, + [window, this] { this->dataChanged(window, IsMaximizable); } + ); + + QObject::connect(window, &KWayland::Client::PlasmaWindow::maximizedChanged, q, + [window, this] { this->dataChanged(window, IsMaximized); } + ); + + QObject::connect(window, &KWayland::Client::PlasmaWindow::minimizeableChanged, q, + [window, this] { this->dataChanged(window, IsMinimizable); } + ); + + QObject::connect(window, &KWayland::Client::PlasmaWindow::minimizedChanged, q, + [window, this] { this->dataChanged(window, IsMinimized); } + ); + + QObject::connect(window, &KWayland::Client::PlasmaWindow::keepAboveChanged, q, + [window, this] { this->dataChanged(window, IsKeepAbove); } + ); + + QObject::connect(window, &KWayland::Client::PlasmaWindow::keepBelowChanged, q, + [window, this] { this->dataChanged(window, IsKeepBelow); } + ); + + QObject::connect(window, &KWayland::Client::PlasmaWindow::shadeableChanged, q, + [window, this] { this->dataChanged(window, IsShadeable); } + ); + + QObject::connect(window, &KWayland::Client::PlasmaWindow::virtualDesktopChangeableChanged, q, + [window, this] { this->dataChanged(window, IsVirtualDesktopChangeable); } + ); + + QObject::connect(window, &KWayland::Client::PlasmaWindow::virtualDesktopChanged, q, + [window, this] { this->dataChanged(window, VirtualDesktop); } + ); + + QObject::connect(window, &KWayland::Client::PlasmaWindow::onAllDesktopsChanged, q, + [window, this] { this->dataChanged(window, IsOnAllVirtualDesktops); } + ); + + QObject::connect(window, &KWayland::Client::PlasmaWindow::demandsAttentionChanged, q, + [window, this] { this->dataChanged(window, IsDemandingAttention); } + ); + + QObject::connect(window, &KWayland::Client::PlasmaWindow::skipTaskbarChanged, q, + [window, this] { this->dataChanged(window, SkipTaskbar); } + ); +} + +void WaylandTasksModel::Private::dataChanged(KWayland::Client::PlasmaWindow *window, int role) +{ + QModelIndex idx = q->index(windows.indexOf(window)); + emit q->dataChanged(idx, idx, QVector{role}); +} + +WaylandTasksModel::WaylandTasksModel(QObject *parent) + : AbstractTasksModel(parent) + , d(new Private(this)) +{ + d->initWayland(); +} + +WaylandTasksModel::~WaylandTasksModel() = default; + +QVariant WaylandTasksModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid() || index.row() >= d->windows.count()) { + return QVariant(); + } + + KWayland::Client::PlasmaWindow *window = d->windows.at(index.row()); + + if (role == Qt::DisplayRole) { + return window->title(); + } else if (role == Qt::DecorationRole) { + return window->icon(); + } else if (role == AppId) { + return window->appId(); + } else if (role == AppName) { + if (d->serviceCache.contains(window)) { + return d->serviceCache.value(window)->name(); + } else { + return window->title(); + } + } else if (role == GenericName) { + if (d->serviceCache.contains(window)) { + return d->serviceCache.value(window)->genericName(); + } + } else if (role == LauncherUrl) { + if (d->serviceCache.contains(window)) { + return QUrl::fromLocalFile(d->serviceCache.value(window)->entryPath()); + } else { + return window->title(); + } + } else if (role == IsWindow) { + return true; + } else if (role == IsActive) { + return window->isActive(); + } else if (role == IsClosable) { + return window->isCloseable(); + } else if (role == IsMovable) { + return window->isMovable(); + } else if (role == IsResizable) { + return window->isResizable(); + } else if (role == IsMaximizable) { + return window->isMaximizeable(); + } else if (role == IsMaximized) { + return window->isMaximized(); + } else if (role == IsMinimizable) { + return window->isMinimizeable(); + } else if (role == IsMinimized) { + return window->isMinimized(); + } else if (role == IsKeepAbove) { + return window->isKeepAbove(); + } else if (role == IsKeepBelow) { + return window->isKeepBelow(); + } else if (role == IsFullScreenable) { + return window->isFullscreenable(); + } else if (role == IsFullScreen) { + return window->isFullscreen(); + } else if (role == IsShadeable) { + return window->isShadeable(); + } else if (role == IsShaded) { + return window->isShaded(); + } else if (role == IsVirtualDesktopChangeable) { + return window->isVirtualDesktopChangeable(); + } else if (role == VirtualDesktop) { + return window->virtualDesktop(); + } else if (role == IsOnAllVirtualDesktops) { + return window->isOnAllDesktops(); + } else if (role == Screen) { + // FIXME Implement. + } else if (role == Activities) { + // FIXME Implement. + } else if (role == IsDemandingAttention) { + return window->isDemandingAttention(); + } else if (role == SkipTaskbar) { + return window->skipTaskbar(); + } + + return QVariant(); +} + +int WaylandTasksModel::rowCount(const QModelIndex &parent) const +{ + return parent.isValid() ? 0 : d->windows.count(); +} + +QModelIndex WaylandTasksModel::index(int row, int column, const QModelIndex &parent) const +{ + return hasIndex(row, column, parent) ? createIndex(row, column, d->windows.at(row)) : QModelIndex(); +} + +void WaylandTasksModel::requestActivate(const QModelIndex &index) +{ + // FIXME Lacks transient handling of the XWindows version. + + if (!index.isValid() || index.model() != this || index.row() < 0 || index.row() >= d->windows.count()) { + return; + } + + d->windows.at(index.row())->requestActivate(); +} + +void WaylandTasksModel::requestNewInstance(const QModelIndex &index) +{ + if (!index.isValid() || index.model() != this || index.row() < 0 || index.row() >= d->windows.count()) { + return; + } + + KWayland::Client::PlasmaWindow* window = d->windows.at(index.row()); + + if (d->serviceCache.contains(window)) { + const KService::Ptr service = d->serviceCache.value(window); + + new KRun(QUrl::fromLocalFile(service->entryPath()), 0, false); + + KActivities::ResourceInstance::notifyAccessed(QUrl("applications:" + service->storageId()), + "org.kde.libtaskmanager"); + } +} + +void WaylandTasksModel::requestClose(const QModelIndex &index) +{ + if (!index.isValid() || index.model() != this || index.row() < 0 || index.row() >= d->windows.count()) { + return; + } + + d->windows.at(index.row())->requestClose(); +} + +void WaylandTasksModel::requestMove(const QModelIndex &index) +{ + // FIXME Move-to-desktop logic from XWindows version. (See also others.) + + if (!index.isValid() || index.model() != this || index.row() < 0 || index.row() >= d->windows.count()) { + return; + } + + d->windows.at(index.row())->requestMove(); +} + +void WaylandTasksModel::requestResize(const QModelIndex &index) +{ + // FIXME Move-to-desktop logic from XWindows version. (See also others.) + + if (!index.isValid() || index.model() != this || index.row() < 0 || index.row() >= d->windows.count()) { + return; + } + + d->windows.at(index.row())->requestResize(); +} + +void WaylandTasksModel::requestToggleMinimized(const QModelIndex &index) +{ + // FIXME Move-to-desktop logic from XWindows version. (See also others.) + + if (!index.isValid() || index.model() != this || index.row() < 0 || index.row() >= d->windows.count()) { + return; + } + + d->windows.at(index.row())->requestToggleMinimized(); +} + +void WaylandTasksModel::requestToggleMaximized(const QModelIndex &index) +{ + // FIXME Move-to-desktop logic from XWindows version. (See also others.) + + if (!index.isValid() || index.model() != this || index.row() < 0 || index.row() >= d->windows.count()) { + return; + } + + d->windows.at(index.row())->requestToggleMaximized(); +} + +void WaylandTasksModel::requestToggleKeepAbove(const QModelIndex &index) +{ + Q_UNUSED(index) + + // FIXME Implement. +} + +void WaylandTasksModel::requestToggleKeepBelow(const QModelIndex &index) +{ + Q_UNUSED(index) + + // FIXME Implement. +} + +void WaylandTasksModel::requestToggleFullScreen(const QModelIndex &index) +{ + Q_UNUSED(index) + + // FIXME Implement. +} + +void WaylandTasksModel::requestToggleShaded(const QModelIndex &index) +{ + if (!index.isValid() || index.model() != this || index.row() < 0 || index.row() >= d->windows.count()) { + return; + } + + d->windows.at(index.row())->requestToggleShaded(); +} + +void WaylandTasksModel::requestVirtualDesktop(const QModelIndex &index, qint32 desktop) +{ + // FIXME Lacks add-new-desktop code from XWindows version. + // FIXME Does this do the set-on-all-desktops stuff from the XWindows version? + + if (!index.isValid() || index.model() != this || index.row() < 0 || index.row() >= d->windows.count()) { + return; + } + + d->windows.at(index.row())->requestVirtualDesktop(desktop); +} + +void WaylandTasksModel::requestPublishDelegateGeometry(const QModelIndex &index, const QRect &geometry, QObject *delegate) +{ + /* + FIXME: This introduces the dependency on Qt5::Quick. I might prefer + reversing this and publishing the window pointer through the model, + then calling PlasmaWindow::setMinimizeGeometry in the applet backend, + rather than hand delegate items into the lib, keeping the lib more UI- + agnostic. + */ + + Q_UNUSED(geometry) + + if (!index.isValid() || index.model() != this || index.row() < 0 || index.row() >= d->windows.count()) { + return; + } + + const QQuickItem *item = qobject_cast(delegate); + + if (!item || !item->parentItem() || !item->window()) { + return; + } + + QWindow *itemWindow = item->window(); + + if (!itemWindow) { + return; + } + + using namespace KWayland::Client; + Surface *surface = Surface::fromWindow(itemWindow); + + if (!surface) { + return; + } + + QRect rect(item->x(), item->y(), item->width(), item->height()); + rect.moveTopLeft(item->parentItem()->mapToScene(rect.topLeft()).toPoint()); + + KWayland::Client::PlasmaWindow *window = d->windows.at(index.row()); + + window->setMinimizedGeometry(surface, rect); +} + +} diff --git a/libtaskmanager/xwindowtasksmodel.h b/libtaskmanager/xwindowtasksmodel.h new file mode 100644 --- /dev/null +++ b/libtaskmanager/xwindowtasksmodel.h @@ -0,0 +1,197 @@ +/******************************************************************** +Copyright 2016 Eike Hein + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 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 6 of version 3 of the license. + +This library 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 +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library. If not, see . +*********************************************************************/ + +#ifndef XWINDOWTASKSMODEL_H +#define XWINDOWTASKSMODEL_H + +#include "abstracttasksmodel.h" + +#include "taskmanager_export.h" + +namespace TaskManager +{ + +/** + * @short A tasks model for X Windows windows. + * + * This model presents tasks sourced from window data on the X Windows + * server the host process is connected to. + * + * For the purposes of presentation in a user interface and efficiency, + * certain types of windows (e.g. utility windows, or windows that are + * transients for an otherwise-included window) are omitted from the + * model. + * + * @author Eike Hein + */ + +class TASKMANAGER_EXPORT XWindowTasksModel : public AbstractTasksModel +{ + Q_OBJECT + +public: + explicit XWindowTasksModel(QObject *parent = 0); + virtual ~XWindowTasksModel(); + + virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + virtual int rowCount(const QModelIndex &parent = QModelIndex()) const override; + + /** + * Request activation of the window at the given index. + * + * If the window has a transient demanding attention, it will be + * activated instead. + * + * If the window has a transient in shaded state, it will be + * activated instead. + * + * @param index An index in this window tasks model. + **/ + void requestActivate(const QModelIndex &index) override; + + /** + * Request an additional instance of the application owning the window + * at the given index. Success depends on whether a + * AbstractTasksModel::LauncherUrl could be derived from window metadata. + * + * @param index An index in this window tasks model. + **/ + void requestNewInstance(const QModelIndex &index) override; + + /** + * Request the window at the given index be closed. + * + * @param index An index in this window tasks model. + **/ + void requestClose(const QModelIndex &index) override; + + /** + * Request starting an interactive move for the window at the given index. + * + * If the window is not currently the active window, it will be activated. + * + * If the window is not on the current desktop, the current desktop will + * be set to the window's desktop. + * FIXME: Desktop logic should maybe move into proxy. + * + * @param index An index in this window tasks model. + **/ + void requestMove(const QModelIndex &index) override; + + /** + * Request starting an interactive resize for the window at the given index. + * + * If the window is not currently the active window, it will be activated. + * + * If the window is not on the current desktop, the current desktop will + * be set to the window's desktop. + * FIXME: Desktop logic should maybe move into proxy. + * + * @param index An index in this window tasks model. + **/ + void requestResize(const QModelIndex &index) override; + + /** + * Request toggling the minimized state of the window at the given index. + * + * If the window is not on the current desktop, the current desktop will + * be set to the window's desktop. + * FIXME: Desktop logic should maybe move into proxy. + * + * @param index An index in this window tasks model. + **/ + void requestToggleMinimized(const QModelIndex &index) override; + + /** + * Request toggling the maximized state of the task at the given index. + * + * If the window is not on the current desktop, the current desktop will + * be set to the window's desktop. + * FIXME: Desktop logic should maybe move into proxy. + * + * @param index An index in this window tasks model. + **/ + void requestToggleMaximized(const QModelIndex &index) override; + + /** + * Request toggling the keep-above state of the task at the given index. + * + * @param index An index in this window tasks model. + **/ + void requestToggleKeepAbove(const QModelIndex &index) override; + + /** + * Request toggling the keep-below state of the task at the given index. + * + * @param index An index in this window tasks model. + **/ + void requestToggleKeepBelow(const QModelIndex &index) override; + + /** + * Request toggling the fullscreen state of the task at the given index. + * + * @param index An index in this window tasks model. + **/ + void requestToggleFullScreen(const QModelIndex &index) override; + + /** + * Request toggling the shaded state of the task at the given index. + * + * @param index An index in this window tasks model. + **/ + void requestToggleShaded(const QModelIndex &index) override; + + /** + * Request moving the window at the given index to the specified virtual + * desktop. + * + * If the specified virtual desktop is 0, IsOnAllVirtualDesktops is + * toggled instead. + * + * If the specified desktop number exceeds the number of virtual + * desktops in the session, a new desktop is created before moving + * the window. + * + * FIXME: Desktop logic should maybe move into proxy. + * + * @param index An index in this window tasks model. + * @param desktop A virtual desktop number. + **/ + void requestVirtualDesktop(const QModelIndex &index, qint32 desktop) override; + + /** + * Request informing the window manager of new geometry for a visual + * delegate for the window at the given index. + * + * @param index An index in this window tasks model. + * @param geometry Visual delegate geometry in screen coordinates. + * @param delegate The delegate. Unused in this implementation. + **/ + void requestPublishDelegateGeometry(const QModelIndex &index, const QRect &geometry, + QObject *delegate = nullptr) override; + +private: + class Private; + QScopedPointer d; +}; + +} + +#endif diff --git a/libtaskmanager/xwindowtasksmodel.cpp b/libtaskmanager/xwindowtasksmodel.cpp new file mode 100644 --- /dev/null +++ b/libtaskmanager/xwindowtasksmodel.cpp @@ -0,0 +1,1173 @@ +/******************************************************************** +Copyright 2016 Eike Hein + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 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 6 of version 3 of the license. + +This library 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 +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library. If not, see . +*********************************************************************/ + +#include "xwindowtasksmodel.h" +#include "tasktools.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace TaskManager +{ + +static const NET::Properties windowInfoFlags = NET::WMState | NET::XAWMState | NET::WMDesktop | + NET::WMVisibleName | NET::WMGeometry | + NET::WMWindowType; +static const NET::Properties2 windowInfoFlags2 = NET::WM2WindowClass | NET::WM2AllowedActions; + +class XWindowTasksModel::Private +{ +public: + Private(XWindowTasksModel *q); + ~Private(); + + QVector windows; + QSet transients; + QHash transientsDemandingAttention; + QHash windowInfoCache; + QHash appDataCache; + QHash delegateGeometries; + WId activeWindow = -1; + KSharedConfig::Ptr rulesConfig; + KDirWatch *configWatcher = nullptr; + QTimer sycocaChangeTimer; + + void init(); + void addWindow(WId window); + void removeWindow(WId window); + void windowChanged(WId window, NET::Properties properties, NET::Properties2 properties2); + void transientChanged(WId window, NET::Properties properties, NET::Properties2 properties2); + void dataChanged(WId window, const QVector &roles); + + KWindowInfo* windowInfo(WId window); + AppData appData(WId window); + + QIcon icon(WId window); + QString mimeType() const; + QUrl windowUrl(WId window); + QUrl launcherUrl(WId window); + QUrl serviceUrl(int pid, const QString &type, const QStringList &cmdRemovals); + KService::List servicesFromPid(int pid); + int screen(WId window); + QStringList activities(WId window); + bool demandsAttention(WId window); + +private: + XWindowTasksModel *q; +}; + +XWindowTasksModel::Private::Private(XWindowTasksModel *q) + : q(q) +{ +} + +XWindowTasksModel::Private::~Private() +{ + qDeleteAll(windowInfoCache); + windowInfoCache.clear(); +} + +void XWindowTasksModel::Private::init() +{ + rulesConfig = KSharedConfig::openConfig(QStringLiteral("taskmanagerrulesrc")); + configWatcher = new KDirWatch(q); + + foreach (const QString &location, QStandardPaths::standardLocations(QStandardPaths::ConfigLocation)) { + configWatcher->addFile(location + QLatin1String("/taskmanagerrulesrc")); + } + + QObject::connect(configWatcher, &KDirWatch::dirty, [this] { rulesConfig->reparseConfiguration(); }); + QObject::connect(configWatcher, &KDirWatch::created, [this] { rulesConfig->reparseConfiguration(); }); + QObject::connect(configWatcher, &KDirWatch::deleted, [this] { rulesConfig->reparseConfiguration(); }); + + sycocaChangeTimer.setSingleShot(true); + sycocaChangeTimer.setInterval(100); + + QObject::connect(&sycocaChangeTimer, &QTimer::timeout, q, + [this]() { + if (!windows.count()) { + return; + } + + appDataCache.clear(); + + // Emit changes of all roles satisfied from app data cache. + q->dataChanged(q->index(0, 0), q->index(windows.count() - 1, 0), + QVector{Qt::DecorationRole, AbstractTasksModel::AppId, + AbstractTasksModel::AppName, AbstractTasksModel::GenericName, + AbstractTasksModel::LauncherUrl}); + } + ); + + void (KSycoca::*myDatabaseChangeSignal)(const QStringList &) = &KSycoca::databaseChanged; + QObject::connect(KSycoca::self(), myDatabaseChangeSignal, q, + [this](const QStringList &changedResources) { + if (changedResources.contains(QLatin1String("services")) + || changedResources.contains(QLatin1String("apps")) + || changedResources.contains(QLatin1String("xdgdata-apps"))) { + sycocaChangeTimer.start(); + } + } + ); + + QObject::connect(KWindowSystem::self(), &KWindowSystem::windowAdded, q, + [this](WId window) { + addWindow(window); + } + ); + + QObject::connect(KWindowSystem::self(), &KWindowSystem::windowRemoved, q, + [this](WId window) { + removeWindow(window); + } + ); + + void (KWindowSystem::*myWindowChangeSignal)(WId window, + NET::Properties properties, NET::Properties2 properties2) = &KWindowSystem::windowChanged; + QObject::connect(KWindowSystem::self(), myWindowChangeSignal, q, + [this](WId window, NET::Properties properties, NET::Properties2 properties2) { + windowChanged(window, properties, properties2); + } + ); + + // Update IsActive for previously- and newly-active windows. + QObject::connect(KWindowSystem::self(), &KWindowSystem::activeWindowChanged, q, + [this](WId window) { + const WId oldActiveWindow = activeWindow; + activeWindow = window; + + int row = windows.indexOf(oldActiveWindow); + + if (row != -1) { + dataChanged(oldActiveWindow, QVector{IsActive}); + } + + row = windows.indexOf(window); + + if (row != -1) { + dataChanged(window, QVector{IsActive}); + } + } + ); + + activeWindow = KWindowSystem::activeWindow(); + + // Add existing windows. + foreach(const WId window, KWindowSystem::windows()) { + addWindow(window); + } +} + +void XWindowTasksModel::Private::addWindow(WId window) +{ + // Don't add window twice. + if (windows.contains(window)) { + return; + } + + KWindowInfo info(window, + NET::WMWindowType | NET::WMState | NET::WMName | NET::WMVisibleName, + NET::WM2TransientFor); + + NET::WindowType wType = info.windowType(NET::NormalMask | NET::DesktopMask | NET::DockMask | + NET::ToolbarMask | NET::MenuMask | NET::DialogMask | + NET::OverrideMask | NET::TopMenuMask | + NET::UtilityMask | NET::SplashMask); + + const WId leader = info.transientFor(); + + // Handle transient. + if (leader > 0 && leader != QX11Info::appRootWindow() + && !transients.contains(window) && windows.contains(leader)) { + transients.insert(window); + + // Update demands attention state for leader. + if (info.state() & NET::DemandsAttention && windows.contains(leader)) { + transientsDemandingAttention.insertMulti(leader, window); + dataChanged(leader, QVector{IsDemandingAttention}); + } + + return; + } + + // Ignore NET::Tool and other special window types; they are not considered tasks. + if (wType != NET::Normal && wType != NET::Override && wType != NET::Unknown && + wType != NET::Dialog && wType != NET::Utility) { + + return; + } + + const int count = windows.count(); + q->beginInsertRows(QModelIndex(), count, count); + windows.append(window); + q->endInsertRows(); +} + +void XWindowTasksModel::Private::removeWindow(WId window) +{ + const int row = windows.indexOf(window); + + if (row != -1) { + q->beginRemoveRows(QModelIndex(), row, row); + windows.removeAt(row); + transientsDemandingAttention.remove(window); + windowInfoCache.remove(window); + appDataCache.remove(window); + delegateGeometries.remove(window); + q->endRemoveRows(); + } else { // Could be a transient. + // Removing a transient might change the demands attention state of the leader. + if (transients.remove(window)) { + QMutableHashIterator i(transientsDemandingAttention); + WId leader = 0; + + while (i.hasNext()) { + i.next(); + + if (i.value() == window) { + if (leader == 0) { + leader = i.key(); + } + + i.remove(); + } + } + + if (leader != 0) { + dataChanged(leader, QVector{IsDemandingAttention}); + } + } + } + + if (activeWindow == window) { + activeWindow = -1; + } +} + +void XWindowTasksModel::Private::transientChanged(WId window, NET::Properties properties, NET::Properties2 properties2) +{ + // Changes to a transient's state might change demands attention state for leader. + if (properties & (NET::WMState | NET::XAWMState)) { + const KWindowInfo info(window, NET::WMState | NET::XAWMState, NET::WM2TransientFor); + const WId leader = info.transientFor(); + + if (!windows.contains(leader)) { + return; + } + + if (info.state() & NET::DemandsAttention) { + if (!transientsDemandingAttention.values(leader).contains(window)) { + transientsDemandingAttention.insertMulti(leader, window); + dataChanged(leader, QVector{IsDemandingAttention}); + } + } else if (transientsDemandingAttention.remove(window)) { + dataChanged(leader, QVector{IsDemandingAttention}); + } + // Leader might have changed. + } else if (properties2 & NET::WM2TransientFor) { + const KWindowInfo info(window, NET::WMState | NET::XAWMState, NET::WM2TransientFor); + + if (info.state() & NET::DemandsAttention) { + WId oldLeader = info.transientFor(); + QMutableHashIterator i(transientsDemandingAttention); + + while (i.hasNext()) { + i.next(); + + if (i.value() == window) { + i.remove(); + oldLeader = i.key(); + } + } + + if (oldLeader != 0) { + const WId leader = info.transientFor(); + transientsDemandingAttention.insertMulti(leader, window); + dataChanged(oldLeader, QVector{IsDemandingAttention}); + dataChanged(leader, QVector{IsDemandingAttention}); + } + } + } +} + +void XWindowTasksModel::Private::windowChanged(WId window, NET::Properties properties, NET::Properties2 properties2) +{ + if (transients.contains(window)) { + transientChanged(window, properties, properties2); + + return; + } + + bool wipeInfoCache = false; + bool wipeAppDataCache = false; + QVector changedRoles; + + if (properties & (NET::WMName | NET::WMVisibleName | NET::WM2WindowClass | NET::WMPid) + || properties2 & NET::WM2WindowClass) { + wipeInfoCache = true; + wipeAppDataCache = true; + changedRoles << Qt::DisplayRole << Qt::DecorationRole << AppId << AppName << GenericName << LauncherUrl; + } + + if ((properties & NET::WMIcon) && !changedRoles.contains(Qt::DecorationRole)) { + changedRoles << Qt::DecorationRole; + } + + // FIXME TODO: It might be worth keeping track of which windows were demanding + // attention (or not) to avoid emitting this role on every state change, as + // TaskGroupingProxyModel needs to check for group-ability when a change to it + // is announced and the queried state is false. + if (properties & (NET::WMState | NET::XAWMState)) { + wipeInfoCache = true; + changedRoles << IsFullScreen << IsMaximized << IsMinimized << IsKeepAbove << IsKeepBelow; + changedRoles << IsShaded << IsDemandingAttention << SkipTaskbar; + } + + if (properties2 & NET::WM2AllowedActions) { + wipeInfoCache = true; + changedRoles << IsClosable << IsMovable << IsResizable << IsMaximizable << IsMinimizable; + changedRoles << IsFullScreenable << IsShadeable << IsVirtualDesktopChangeable; + } + + if (properties & NET::WMDesktop) { + wipeInfoCache = true; + changedRoles << VirtualDesktop << IsOnAllVirtualDesktops; + } + + if (properties & NET::WMGeometry) { + wipeInfoCache = true; + changedRoles << Screen; + } + + if (properties2 & NET::WM2Activities) { + changedRoles << Activities; + } + + if (wipeInfoCache) { + delete windowInfoCache.take(window); + } + + if (wipeAppDataCache) { + appDataCache.remove(window); + } + + if (!changedRoles.isEmpty()) { + dataChanged(window, changedRoles); + } +} + +void XWindowTasksModel::Private::dataChanged(WId window, const QVector &roles) +{ + const int i = windows.indexOf(window); + + if (i == -1) { + return; + } + + QModelIndex idx = q->index(i); + emit q->dataChanged(idx, idx, roles); +} + +KWindowInfo* XWindowTasksModel::Private::windowInfo(WId window) +{ + if (!windowInfoCache.contains(window)) { + KWindowInfo *info = new KWindowInfo(window, windowInfoFlags, windowInfoFlags2); + windowInfoCache.insert(window, info); + + return info; + } + + return windowInfoCache.value(window); +} + +AppData XWindowTasksModel::Private::appData(WId window) +{ + if (!appDataCache.contains(window)) { + const AppData &data = appDataFromUrl(windowUrl(window)); + appDataCache.insert(window, data); + + return data; + } + + return appDataCache.value(window); +} + +QIcon XWindowTasksModel::Private::icon(WId window) +{ + const AppData &app = appData(window); + + if (!app.icon.isNull()) { + return app.icon; + } + + QIcon icon; + + icon.addPixmap(KWindowSystem::icon(window, KIconLoader::SizeSmall, KIconLoader::SizeSmall, false)); + icon.addPixmap(KWindowSystem::icon(window, KIconLoader::SizeSmallMedium, KIconLoader::SizeSmallMedium, false)); + icon.addPixmap(KWindowSystem::icon(window, KIconLoader::SizeMedium, KIconLoader::SizeMedium, false)); + icon.addPixmap(KWindowSystem::icon(window, KIconLoader::SizeLarge, KIconLoader::SizeLarge, false)); + + return icon; +} + +QString XWindowTasksModel::Private::mimeType() const +{ + return QStringLiteral("windowsystem/winid"); +} + +QUrl XWindowTasksModel::Private::windowUrl(WId window) +{ + QUrl url; + + const KWindowInfo *info = windowInfo(window); + const QString &classClass = info->windowClassClass(); + const QString &className = info->windowClassName(); + + KService::List services; + bool triedPid = false; + + if (!(classClass.isEmpty() && className.isEmpty())) { + int pid = NETWinInfo(QX11Info::connection(), window, QX11Info::appRootWindow(), NET::WMPid, 0).pid(); + + // For KCModules, if we matched on window class, etc, we would end up matching to kcmshell4 - but we are more than likely + // interested in the actual control module. Therefore we obtain this via the commandline. This commandline may contain + // "kdeinit4:" or "[kdeinit]", so we remove these first. + // FIXME This looks like ancient old crap we can do better now. + if ("Kcmshell5" == classClass) { + url = serviceUrl(pid, QStringLiteral("KCModule"), QStringList() << QStringLiteral("kdeinit5:") << QStringLiteral("[kdeinit]")); + + if (!url.isEmpty()) { + return url; + } + } + + // Check to see if this wmClass matched a saved one ... + KConfigGroup grp(rulesConfig, "Mapping"); + KConfigGroup set(rulesConfig, "Settings"); + + // Some apps have different launchers depending upon command line ... + QStringList matchCommandLineFirst = set.readEntry("MatchCommandLineFirst", QStringList()); + + if (!classClass.isEmpty() && matchCommandLineFirst.contains(classClass)) { + triedPid = true; + services = servicesFromPid(pid); + } + + // Try to match using className also. + if (!className.isEmpty() && matchCommandLineFirst.contains("::"+className)) { + triedPid = true; + services = servicesFromPid(pid); + } + + // If the user has manualy set a mapping, respect this first... + QString mapped(grp.readEntry(classClass + "::" + className, QString())); + + if (mapped.endsWith(QLatin1String(".desktop"))) { + url = mapped; + return url; + } + + if (!classClass.isEmpty()) { + if (mapped.isEmpty()) { + mapped = grp.readEntry(classClass, QString()); + + if (mapped.endsWith(QLatin1String(".desktop"))) { + url = mapped; + return url; + } + } + + // Some apps, such as Wine, cannot use className to map to launcher name - as Wine itself is not a GUI app + // So, Settings/ManualOnly lists window classes where the user will always have to manualy set the launcher ... + QStringList manualOnly = set.readEntry("ManualOnly", QStringList()); + + if (!classClass.isEmpty() && manualOnly.contains(classClass)) { + return url; + } + + if (!mapped.isEmpty()) { + services = KServiceTypeTrader::self()->query(QStringLiteral("Application"), QStringLiteral("exist Exec and ('%1' =~ DesktopEntryName)").arg(mapped)); + } + + if (!mapped.isEmpty() && services.empty()) { + services = KServiceTypeTrader::self()->query(QStringLiteral("Application"), QStringLiteral("exist Exec and ('%1' =~ Name)").arg(mapped)); + } + + // To match other docks (docky, unity, etc.) attempt to match on DesktopEntryName first ... + if (services.empty()) { + services = KServiceTypeTrader::self()->query(QStringLiteral("Application"), QStringLiteral("exist Exec and ('%1' =~ DesktopEntryName)").arg(classClass)); + } + + // Try StartupWMClass. + if (services.empty()) { + services = KServiceTypeTrader::self()->query(QStringLiteral("Application"), QStringLiteral("exist Exec and ('%1' =~ StartupWMClass)").arg(classClass)); + } + + // Try 'Name' - unfortunately this can be translated, so has a good chance of failing! (As it does for KDE's own "System Settings" (even in English!!)) + if (services.empty()) { + services = KServiceTypeTrader::self()->query(QStringLiteral("Application"), QStringLiteral("exist Exec and ('%1' =~ Name)").arg(classClass)); + } + } + + // Ok, absolute *last* chance, try matching via pid (but only if we have not already tried this!) ... + if (services.empty() && !triedPid) { + services = servicesFromPid(pid); + } + } + + // Try to improve on a possible from-binary fallback. + // If no services were found or we got a fake-service back from getServicesViaPid() + // we attempt to improve on this by adding a loosely matched reverse-domain-name + // DesktopEntryName. Namely anything that is '*.classClass.desktop' would qualify here. + // + // Illustrative example of a case where the above heuristics would fail to produce + // a reasonable result: + // - org.kde.dragonplayer.desktop + // - binary is 'dragon' + // - qapp appname and thus classClass is 'dragonplayer' + // - classClass cannot directly match the desktop file because of RDN + // - classClass also cannot match the binary because of name mismatch + // - in the following code *.classClass can match org.kde.dragonplayer though + if (services.empty() || services.at(0)->desktopEntryName().isEmpty()) { + auto matchingServices = KServiceTypeTrader::self()->query(QStringLiteral("Application"), + QStringLiteral("exist Exec and ('%1' ~~ DesktopEntryName)").arg(classClass)); + QMutableListIterator it(matchingServices); + while (it.hasNext()) { + auto service = it.next(); + if (!service->desktopEntryName().endsWith("." + classClass)) { + it.remove(); + } + } + // Exactly one match is expected, otherwise we discard the results as to reduce + // the likelihood of false-positive mappings. Since we essentially eliminate the + // uniqueness that RDN is meant to bring to the table we could potentially end + // up with more than one match here. + if (matchingServices.length() == 1) { + services = matchingServices; + } + } + + if (!services.empty()) { + QString path = services[0]->entryPath(); + if (path.isEmpty()) { + path = services[0]->exec(); + } + + if (!path.isEmpty()) { + url = QUrl::fromLocalFile(path); + } + } + + return url; +} + +QUrl XWindowTasksModel::Private::launcherUrl(WId window) +{ + const AppData &data = appData(window); + + if (!data.icon.name().isEmpty()) { + return data.url; + } + + const QIcon &i = icon(window); + + if (i.isNull()) { + return data.url; + } + + QUrl url = data.url; + QUrlQuery uQuery(url); + + // FIXME Hard-coding 64px is not scaling-aware. + const QPixmap pixmap = i.pixmap(QSize(64, 64)); + QByteArray bytes; + QBuffer buffer(&bytes); + buffer.open(QIODevice::WriteOnly); + pixmap.save(&buffer, "PNG"); + uQuery.addQueryItem(QStringLiteral("iconData"), bytes.toBase64(QByteArray::Base64UrlEncoding)); + + url.setQuery(uQuery); + + return url; +} + +QUrl XWindowTasksModel::Private::serviceUrl(int pid, const QString &type, const QStringList &cmdRemovals = QStringList()) +{ + if (pid == 0) { + return QUrl(); + } + + KSysGuard::Processes procs; + procs.updateOrAddProcess(pid); + + KSysGuard::Process *proc = procs.getProcess(pid); + QString cmdline = proc ? proc->command().simplified() : QString(); // proc->command has a trailing space??? + + if (cmdline.isEmpty()) { + return QUrl(); + } + + foreach (const QString & r, cmdRemovals) { + cmdline.replace(r, QLatin1String("")); + } + + KService::List services = KServiceTypeTrader::self()->query(type, QStringLiteral("exist Exec and ('%1' =~ Exec)").arg(cmdline)); + + if (services.empty()) { + // Could not find with complete command line, so strip out path part ... + int slash = cmdline.lastIndexOf('/', cmdline.indexOf(' ')); + if (slash > 0) { + services = KServiceTypeTrader::self()->query(type, QStringLiteral("exist Exec and ('%1' =~ Exec)").arg(cmdline.mid(slash + 1))); + } + + if (services.empty()) { + return QUrl(); + } + } + + if (!services.isEmpty()) { + QString path = services[0]->entryPath(); + + if (!QDir::isAbsolutePath(path)) { + QString absolutePath = QStandardPaths::locate(QStandardPaths::GenericDataLocation, "kservices5/"+path); + if (!absolutePath.isEmpty()) + path = absolutePath; + } + + if (QFile::exists(path)) { + return QUrl::fromLocalFile(path); + } + } + + return QUrl(); +} + +KService::List XWindowTasksModel::Private::servicesFromPid(int pid) +{ + // Attempt to find using commandline... + KService::List services; + + if (pid == 0) { + return services; + } + + KSysGuard::Processes procs; + procs.updateOrAddProcess(pid); + + KSysGuard::Process *proc = procs.getProcess(pid); + QString cmdline = proc ? proc->command().simplified() : QString(); // proc->command has a trailing space??? + + if (cmdline.isEmpty()) { + return services; + } + + const int firstSpace = cmdline.indexOf(' '); + + services = KServiceTypeTrader::self()->query(QStringLiteral("Application"), QStringLiteral("exist Exec and ('%1' =~ Exec)").arg(cmdline)); + if (services.empty()) { + // Could not find with complete commandline, so strip out path part... + int slash = cmdline.lastIndexOf('/', firstSpace); + if (slash > 0) { + services = KServiceTypeTrader::self()->query(QStringLiteral("Application"), QStringLiteral("exist Exec and ('%1' =~ Exec)").arg(cmdline.mid(slash + 1))); + } + } + + if (services.empty() && firstSpace > 0) { + // Could not find with arguments, so try without... + cmdline = cmdline.left(firstSpace); + services = KServiceTypeTrader::self()->query(QStringLiteral("Application"), QStringLiteral("exist Exec and ('%1' =~ Exec)").arg(cmdline)); + + int slash = cmdline.lastIndexOf('/'); + if (slash > 0) { + services = KServiceTypeTrader::self()->query(QStringLiteral("Application"), QStringLiteral("exist Exec and ('%1' =~ Exec)").arg(cmdline.mid(slash + 1))); + } + } + + if (services.empty() && proc && !QStandardPaths::findExecutable(cmdline).isEmpty()) { + // cmdline now exists without arguments if there were any + services << QExplicitlySharedDataPointer(new KService(proc->name(), cmdline, QString())); + } + return services; +} + +int XWindowTasksModel::Private::screen(WId window) +{ + const KWindowInfo *info = windowInfo(window); + const QList &screens = QGuiApplication::screens(); + + for (int i = 0; i < screens.count(); ++i) { + if (screens.at(i)->geometry().intersects(info->geometry())) { + return i; + } + } + + return -1; +} + +QStringList XWindowTasksModel::Private::activities(WId window) +{ + NETWinInfo ni(QX11Info::connection(), window, QX11Info::appRootWindow(), 0, NET::WM2Activities); + + const QString result(ni.activities()); + + if (!result.isEmpty() && result != QLatin1String("00000000-0000-0000-0000-000000000000")) { + return result.split(','); + } + + return QStringList(); +} + +bool XWindowTasksModel::Private::demandsAttention(WId window) +{ + if (windows.contains(window)) { + return ((windowInfo(window)->state() & NET::DemandsAttention) + || transientsDemandingAttention.contains(window)); + } + + return false; +} + +XWindowTasksModel::XWindowTasksModel(QObject *parent) + : AbstractTasksModel(parent) + , d(new Private(this)) +{ + d->init(); +} + +XWindowTasksModel::~XWindowTasksModel() +{ +} + +QVariant XWindowTasksModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid() || index.row() >= d->windows.count()) { + return QVariant(); + } + + const WId window = d->windows.at(index.row()); + + if (role == Qt::DisplayRole) { + return d->windowInfo(window)->visibleName(); + } else if (role == Qt::DecorationRole) { + return d->icon(window); + } else if (role == AppId) { + return d->appData(window).id; + } else if (role == AppName) { + return d->appData(window).name; + } else if (role == GenericName) { + return d->appData(window).genericName; + } else if (role == LauncherUrl) { + return d->launcherUrl(window); + } else if (role == LegacyWinIdList) { + return QVariantList() << window; + } else if (role == MimeType) { + return d->mimeType(); + } else if (role == MimeData) { + return QByteArray((char*)&window, sizeof(window));; + } else if (role == IsWindow) { + return true; + } else if (role == IsActive) { + return (window == d->activeWindow); + } else if (role == IsClosable) { + return d->windowInfo(window)->actionSupported(NET::ActionClose); + } else if (role == IsMovable) { + return d->windowInfo(window)->actionSupported(NET::ActionMove); + } else if (role == IsResizable) { + return d->windowInfo(window)->actionSupported(NET::ActionResize); + } else if (role == IsMaximizable) { + return d->windowInfo(window)->actionSupported(NET::ActionMax); + } else if (role == IsMaximized) { + const KWindowInfo *info = d->windowInfo(window); + return (bool)(info->state() & NET::MaxHoriz) && (bool)(info->state() & NET::MaxVert); + } else if (role == IsMinimizable) { + return d->windowInfo(window)->actionSupported(NET::ActionMinimize); + } else if (role == IsMinimized) { + return d->windowInfo(window)->isMinimized(); + } else if (role == IsKeepAbove) { + return (bool)(d->windowInfo(window)->state() & NET::StaysOnTop); + } else if (role == IsKeepBelow) { + return (bool)(d->windowInfo(window)->state() & NET::KeepBelow); + } else if (role == IsFullScreenable) { + return d->windowInfo(window)->actionSupported(NET::ActionFullScreen); + } else if (role == IsFullScreen) { + return (bool)(d->windowInfo(window)->state() & NET::FullScreen); + } else if (role == IsShadeable) { + return d->windowInfo(window)->actionSupported(NET::ActionShade); + } else if (role == IsShaded) { + return (bool)(d->windowInfo(window)->state() & NET::Shaded); + } else if (role == IsVirtualDesktopChangeable) { + return d->windowInfo(window)->actionSupported(NET::ActionChangeDesktop); + } else if (role == VirtualDesktop) { + return d->windowInfo(window)->desktop(); + } else if (role == IsOnAllVirtualDesktops) { + return d->windowInfo(window)->onAllDesktops(); + } else if (role == Screen) { + return d->screen(window); + } else if (role == Activities) { + return d->activities(window); + } else if (role == IsDemandingAttention) { + return d->demandsAttention(window); + } else if (role == SkipTaskbar) { + return (bool)(d->windowInfo(window)->state() & NET::SkipTaskbar); + } + + return QVariant(); +} + +int XWindowTasksModel::rowCount(const QModelIndex &parent) const +{ + return parent.isValid() ? 0 : d->windows.count(); +} + +void XWindowTasksModel::requestActivate(const QModelIndex &index) +{ + if (!index.isValid() || index.model() != this || index.row() < 0 || index.row() >= d->windows.count()) { + return; + } + + if (index.row() >= 0 && index.row() < d->windows.count()) { + WId window = d->windows.at(index.row()); + + // Pull forward any transient demanding attention. + if (d->transientsDemandingAttention.contains(window)) { + window = d->transientsDemandingAttention.value(window); + // Quote from legacy libtaskmanager: + // "this is a work around for (at least?) kwin where a shaded transient will prevent the main + // window from being brought forward unless the transient is actually pulled forward, most + // easily reproduced by opening a modal file open/save dialog on an app then shading the file + // dialog and trying to bring the window forward by clicking on it in a tasks widget + // TODO: do we need to check all the transients for shaded?" + } else if (!d->transients.isEmpty()) { + foreach (const WId transient, d->transients) { + KWindowInfo info(transient, NET::WMState, NET::WM2TransientFor); + + if (info.valid(true) && (info.state() & NET::Shaded) && info.transientFor() == window) { + window = transient; + break; + } + } + } + + KWindowSystem::forceActiveWindow(window); + } +} + +void XWindowTasksModel::requestNewInstance(const QModelIndex &index) +{ + if (!index.isValid() || index.model() != this || index.row() < 0 || index.row() >= d->windows.count()) { + return; + } + + const QUrl &url = d->appData(d->windows.at(index.row())).url; + + if (url.isValid()) { + new KRun(url, 0, false, KStartupInfo::createNewStartupIdForTimestamp(QX11Info::appUserTime())); + } +} + +void XWindowTasksModel::requestClose(const QModelIndex &index) +{ + if (!index.isValid() || index.model() != this || index.row() < 0 || index.row() >= d->windows.count()) { + return; + } + + NETRootInfo ri(QX11Info::connection(), NET::CloseWindow); + ri.closeWindowRequest(d->windows.at(index.row())); +} + +void XWindowTasksModel::requestMove(const QModelIndex &index) +{ + if (!index.isValid() || index.model() != this || index.row() < 0 || index.row() >= d->windows.count()) { + return; + } + + const WId window = d->windows.at(index.row()); + const KWindowInfo *info = d->windowInfo(window); + + bool onCurrent = info->isOnCurrentDesktop(); + + if (!onCurrent) { + KWindowSystem::setCurrentDesktop(info->desktop()); + KWindowSystem::forceActiveWindow(window); + } + + if (info->isMinimized()) { + KWindowSystem::unminimizeWindow(window); + } + + const QRect &geom = info->geometry(); + + NETRootInfo ri(QX11Info::connection(), NET::WMMoveResize); + ri.moveResizeRequest(window, geom.center().x(), geom.center().y(), NET::Move); +} + +void XWindowTasksModel::requestResize(const QModelIndex &index) +{ + if (!index.isValid() || index.model() != this || index.row() < 0 || index.row() >= d->windows.count()) { + return; + } + + const WId window = d->windows.at(index.row()); + const KWindowInfo *info = d->windowInfo(window); + + bool onCurrent = info->isOnCurrentDesktop(); + + if (!onCurrent) { + KWindowSystem::setCurrentDesktop(info->desktop()); + KWindowSystem::forceActiveWindow(window); + } + + if (info->isMinimized()) { + KWindowSystem::unminimizeWindow(window); + } + + const QRect &geom = info->geometry(); + + NETRootInfo ri(QX11Info::connection(), NET::WMMoveResize); + ri.moveResizeRequest(window, geom.bottomRight().x(), geom.bottomRight().y(), NET::BottomRight); +} + +void XWindowTasksModel::requestToggleMinimized(const QModelIndex &index) +{ + if (!index.isValid() || index.model() != this || index.row() < 0 || index.row() >= d->windows.count()) { + return; + } + + const WId window = d->windows.at(index.row()); + const KWindowInfo *info = d->windowInfo(window); + + if (info->isMinimized()) { + bool onCurrent = info->isOnCurrentDesktop(); + + // FIXME: Move logic up into proxy? (See also others.) + if (!onCurrent) { + KWindowSystem::setCurrentDesktop(info->desktop()); + } + + KWindowSystem::unminimizeWindow(window); + + if (onCurrent) { + KWindowSystem::forceActiveWindow(window); + } + } else { + KWindowSystem::minimizeWindow(window); + } +} + +void XWindowTasksModel::requestToggleMaximized(const QModelIndex &index) +{ + if (!index.isValid() || index.model() != this || index.row() < 0 || index.row() >= d->windows.count()) { + return; + } + + const WId window = d->windows.at(index.row()); + const KWindowInfo *info = d->windowInfo(window); + bool onCurrent = info->isOnCurrentDesktop(); + bool restore = (info->state() & NET::MaxHoriz) && (bool)(info->state() & NET::MaxVert); + + // FIXME: Move logic up into proxy? (See also others.) + if (!onCurrent) { + KWindowSystem::setCurrentDesktop(info->desktop()); + } + + if (info->isMinimized()) { + KWindowSystem::unminimizeWindow(window); + } + + NETWinInfo ni(QX11Info::connection(), window, QX11Info::appRootWindow(), NET::WMState, 0); + + if (restore) { + ni.setState(0, NET::Max); + } else { + ni.setState(NET::Max, NET::Max); + } + + if (!onCurrent) { + KWindowSystem::forceActiveWindow(window); + } +} + +void XWindowTasksModel::requestToggleKeepAbove(const QModelIndex &index) +{ + if (!index.isValid() || index.model() != this || index.row() < 0 || index.row() >= d->windows.count()) { + return; + } + + const WId window = d->windows.at(index.row()); + const KWindowInfo *info = d->windowInfo(window); + + NETWinInfo ni(QX11Info::connection(), window, QX11Info::appRootWindow(), NET::WMState, 0); + + if (info->state() & NET::StaysOnTop) { + ni.setState(0, NET::StaysOnTop); + } else { + ni.setState(NET::StaysOnTop, NET::StaysOnTop); + } +} + +void XWindowTasksModel::requestToggleKeepBelow(const QModelIndex &index) +{ + if (!index.isValid() || index.model() != this || index.row() < 0 || index.row() >= d->windows.count()) { + return; + } + + const WId window = d->windows.at(index.row()); + const KWindowInfo *info = d->windowInfo(window); + + NETWinInfo ni(QX11Info::connection(), window, QX11Info::appRootWindow(), NET::WMState, 0); + + if (info->state() & NET::KeepBelow) { + ni.setState(0, NET::KeepBelow); + } else { + ni.setState(NET::KeepBelow, NET::KeepBelow); + } +} + +void XWindowTasksModel::requestToggleFullScreen(const QModelIndex &index) +{ + if (!index.isValid() || index.model() != this || index.row() < 0 || index.row() >= d->windows.count()) { + return; + } + + const WId window = d->windows.at(index.row()); + const KWindowInfo *info = d->windowInfo(window); + + NETWinInfo ni(QX11Info::connection(), window, QX11Info::appRootWindow(), NET::WMState, 0); + + if (info->state() & NET::FullScreen) { + ni.setState(0, NET::FullScreen); + } else { + ni.setState(NET::FullScreen, NET::FullScreen); + } +} + +void XWindowTasksModel::requestToggleShaded(const QModelIndex &index) +{ + if (!index.isValid() || index.model() != this || index.row() < 0 || index.row() >= d->windows.count()) { + return; + } + + const WId window = d->windows.at(index.row()); + const KWindowInfo *info = d->windowInfo(window); + + NETWinInfo ni(QX11Info::connection(), window, QX11Info::appRootWindow(), NET::WMState, 0); + + if (info->state() & NET::Shaded) { + ni.setState(0, NET::Shaded); + } else { + ni.setState(NET::Shaded, NET::Shaded); + } +} + +void XWindowTasksModel::requestVirtualDesktop(const QModelIndex &index, qint32 desktop) +{ + if (!index.isValid() || index.model() != this || index.row() < 0 || index.row() >= d->windows.count()) { + return; + } + + const WId window = d->windows.at(index.row()); + const KWindowInfo *info = d->windowInfo(window); + + if (desktop == 0) { + if (info->onAllDesktops()) { + KWindowSystem::setOnDesktop(window, KWindowSystem::currentDesktop()); + KWindowSystem::forceActiveWindow(window); + } else { + KWindowSystem::setOnAllDesktops(window, true); + } + + return; + // FIXME Move add-new-desktop logic up into proxy. + } else if (desktop > KWindowSystem::numberOfDesktops()) { + desktop = KWindowSystem::numberOfDesktops() + 1; + + // FIXME Arbitrary limit of 20 copied from old code. + if (desktop > 20) { + return; + } + + NETRootInfo ri(QX11Info::connection(), NET::NumberOfDesktops); + ri.setNumberOfDesktops(desktop); + } + + KWindowSystem::setOnDesktop(window, desktop); + + if (desktop == KWindowSystem::currentDesktop()) { + KWindowSystem::forceActiveWindow(window); + } +} + +void XWindowTasksModel::requestPublishDelegateGeometry(const QModelIndex &index, const QRect &geometry, QObject *delegate) +{ + Q_UNUSED(delegate) + + if (!index.isValid() || index.model() != this || index.row() < 0 || index.row() >= d->windows.count()) { + return; + } + + const WId window = d->windows.at(index.row()); + + if (d->delegateGeometries.contains(window) + && d->delegateGeometries.value(window) == geometry) { + return; + } + + NETWinInfo ni(QX11Info::connection(), window, QX11Info::appRootWindow(), 0, 0); + NETRect rect; + + if (geometry.isValid()) { + rect.pos.x = geometry.x(); + rect.pos.y = geometry.y(); + rect.size.width = geometry.width(); + rect.size.height = geometry.height(); + + d->delegateGeometries.insert(window, geometry); + } else { + d->delegateGeometries.remove(window); + } + + ni.setIconGeometry(rect); +} + +}