diff --git a/CMakeLists.txt b/CMakeLists.txt index 25cf8612..6186dd47 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -72,6 +72,7 @@ find_package(KF6 ${KF6_MIN_VERSION} REQUIRED COMPONENTS find_package(Plasma CONFIG REQUIRED) find_package(PlasmaQuick CONFIG REQUIRED) +find_package(PlasmaActivities CONFIG REQUIRED) find_package(KF6Screen CONFIG REQUIRED) find_package(KWayland CONFIG REQUIRED) diff --git a/kwin/mobiletaskswitcher/CMakeLists.txt b/kwin/mobiletaskswitcher/CMakeLists.txt index 97462847..a7c3a532 100644 --- a/kwin/mobiletaskswitcher/CMakeLists.txt +++ b/kwin/mobiletaskswitcher/CMakeLists.txt @@ -6,6 +6,8 @@ target_sources(mobiletaskswitcher PRIVATE main.cpp mobiletaskswitchereffect.cpp effecttouchborder.cpp + taskfiltermodel.cpp + taskmodel.cpp ) target_link_libraries(mobiletaskswitcher @@ -19,6 +21,7 @@ target_link_libraries(mobiletaskswitcher Qt::Core KWin::kwin + Plasma::Activities ) # install(TARGETS kwin4_effect_taskswitcher DESTINATION ${PLUGIN_INSTALL_DIR}/kwin/effects/plugins) diff --git a/kwin/mobiletaskswitcher/mobiletaskswitchereffect.cpp b/kwin/mobiletaskswitcher/mobiletaskswitchereffect.cpp index ae66400d..a6b05ece 100644 --- a/kwin/mobiletaskswitcher/mobiletaskswitchereffect.cpp +++ b/kwin/mobiletaskswitcher/mobiletaskswitchereffect.cpp @@ -193,13 +193,18 @@ void MobileTaskSwitcherState::processTouchPositionChanged(qreal primaryDelta, qr MobileTaskSwitcherEffect::MobileTaskSwitcherEffect() : m_effectState{new EffectTouchBorderState(this)} , m_taskSwitcherState{new MobileTaskSwitcherState(m_effectState)} + , m_taskModel{new TaskModel{this}} , m_border{new EffectTouchBorder{m_effectState}} , m_toggleAction{std::make_unique()} , m_shutdownTimer{new QTimer{this}} { const char *uri = "org.kde.private.mobileshell.taskswitcher"; + qmlRegisterType(uri, 1, 0, "TaskFilterModel"); + qmlRegisterSingletonType(uri, 1, 0, "TaskModel", [this](QQmlEngine *, QJSEngine *) -> QObject * { + return m_taskModel; + }); qmlRegisterSingletonType(uri, 1, 0, "TaskSwitcherState", [this](QQmlEngine *, QJSEngine *) -> QObject * { - return this->m_taskSwitcherState; + return m_taskSwitcherState; }); connect(m_border, &EffectTouchBorder::touchPositionChanged, m_taskSwitcherState, &MobileTaskSwitcherState::processTouchPositionChanged); diff --git a/kwin/mobiletaskswitcher/mobiletaskswitchereffect.h b/kwin/mobiletaskswitcher/mobiletaskswitchereffect.h index 1f33420d..ef87bc77 100644 --- a/kwin/mobiletaskswitcher/mobiletaskswitchereffect.h +++ b/kwin/mobiletaskswitcher/mobiletaskswitchereffect.h @@ -21,6 +21,8 @@ #include #include "effecttouchborder.h" +#include "taskfiltermodel.h" +#include "taskmodel.h" namespace KWin { @@ -184,6 +186,7 @@ private: EffectTouchBorderState *const m_effectState; MobileTaskSwitcherState *const m_taskSwitcherState; + TaskModel *const m_taskModel; EffectTouchBorder *const m_border; QList m_borderActivate = {ElectricBorder::ElectricBottom}; diff --git a/kwin/mobiletaskswitcher/qml/TaskSwitcher.qml b/kwin/mobiletaskswitcher/qml/TaskSwitcher.qml index 67ebcddd..59d8b03c 100644 --- a/kwin/mobiletaskswitcher/qml/TaskSwitcher.qml +++ b/kwin/mobiletaskswitcher/qml/TaskSwitcher.qml @@ -39,33 +39,9 @@ FocusScope { stateClass: TaskSwitcherData.TaskSwitcherState } - KWinComponents.WindowModel { - id: stackModel - } - - KWinComponents.VirtualDesktopModel { - id: desktopModel - } - - property var baseTasksModel: KWinComponents.WindowFilterModel { - activity: KWinComponents.Workspace.currentActivity - desktop: KWinComponents.Workspace.currentDesktop + property var tasksModel: TaskSwitcherData.TaskFilterModel { screenName: root.targetScreen.name - windowModel: stackModel - minimizedWindows: true - windowType: ~KWinComponents.WindowFilterModel.Dock & - ~KWinComponents.WindowFilterModel.Desktop & - ~KWinComponents.WindowFilterModel.Notification & - ~KWinComponents.WindowFilterModel.CriticalNotification - } - - property var tasksModel: KSortFilterProxyModel { - sourceModel: baseTasksModel - filterRoleName: 'window' - filterRowCallback: function(source_row, source_parent) { - const window = sourceModel.data(sourceModel.index(source_row, 0, source_parent), Qt.DisplayRole); - return !window.skipSwitcher; - } + windowModel: TaskSwitcherData.TaskModel } readonly property int tasksCount: taskList.count diff --git a/kwin/mobiletaskswitcher/taskfiltermodel.cpp b/kwin/mobiletaskswitcher/taskfiltermodel.cpp new file mode 100644 index 00000000..aaccd306 --- /dev/null +++ b/kwin/mobiletaskswitcher/taskfiltermodel.cpp @@ -0,0 +1,139 @@ +// SPDX-FileCopyrightText: 2021 Vlad Zahorodnii +// SPDX-FileCopyrightText: 2024 Devin Lin +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "taskfiltermodel.h" + +// KWin +#include +#include +#include +#include +#include + +namespace KWin +{ + +TaskFilterModel::TaskFilterModel(QObject *parent) + : QSortFilterProxyModel(parent) +{ + setSortRole(TaskModel::LastActivatedRole); + + // Don't auto-sort, because this model is loaded at runtime during the task switcher + // -> We don't want to re-sort while the task switcher is open + setDynamicSortFilter(false); +} + +TaskModel *TaskFilterModel::windowModel() const +{ + return m_taskModel; +} + +void TaskFilterModel::setWindowModel(TaskModel *taskModel) +{ + if (taskModel == m_taskModel) { + return; + } + m_taskModel = taskModel; + setSourceModel(m_taskModel); + Q_EMIT windowModelChanged(); + + // Sort after source model is set + sort(0); +} + +QString TaskFilterModel::screenName() const +{ + return m_output ? m_output->name() : QString(); +} + +void TaskFilterModel::setScreenName(const QString &screen) +{ + Output *output = kwinApp()->outputBackend()->findOutput(screen); + if (m_output != output) { + m_output = output; + Q_EMIT screenNameChanged(); + invalidateFilter(); + } +} + +bool TaskFilterModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const +{ + if (!m_taskModel) { + return false; + } + const QModelIndex index = m_taskModel->index(sourceRow, 0, sourceParent); + if (!index.isValid()) { + return false; + } + const QVariant data = index.data(); + if (!data.isValid()) { + // an invalid QVariant is valid data + return true; + } + + Window *window = qvariant_cast(data); + if (!window || !window->isClient()) { + return false; + } + + // Filter by same activity + auto activity = Workspace::self()->activities()->current(); + if (!window->isOnActivity(activity)) { + return false; + } + + // Filter by same desktop + auto desktop = VirtualDesktopManager::self()->currentDesktop(); + if (!window->isOnDesktop(desktop)) { + return false; + } + + // Filter by same screen + if (window->output() != m_output) { + return false; + } + + if (window->isDock()) { + return false; + } + if (window->isDesktop()) { + return false; + } + if (window->isNotification()) { + return false; + } + if (window->isCriticalNotification()) { + return false; + } + if (window->skipSwitcher()) { + return false; + } + + return true; +} + +bool TaskFilterModel::lessThan(const QModelIndex &left, const QModelIndex &right) const +{ + qint64 leftLastActivated = qvariant_cast(left.data(TaskModel::LastActivatedRole)); + qint64 rightLastActivated = qvariant_cast(right.data(TaskModel::LastActivatedRole)); + + // Sort order: oldest -> newest + // - For ties: alphabetically + + if (leftLastActivated != rightLastActivated) { + return leftLastActivated > rightLastActivated; + } else { + // If leftLastActivated == rightLastActivated, sort alphabetically by window title + Window *leftWindow = qvariant_cast(left.data(TaskModel::WindowRole)); + Window *rightWindow = qvariant_cast(right.data(TaskModel::WindowRole)); + + if (!leftWindow || !rightWindow) { + return true; + } + + return leftWindow->caption() < rightWindow->caption(); + } +} + +} // namespace KWin diff --git a/kwin/mobiletaskswitcher/taskfiltermodel.h b/kwin/mobiletaskswitcher/taskfiltermodel.h new file mode 100644 index 00000000..10336e9d --- /dev/null +++ b/kwin/mobiletaskswitcher/taskfiltermodel.h @@ -0,0 +1,47 @@ +// SPDX-FileCopyrightText: 2021 Vlad Zahorodnii +// SPDX-FileCopyrightText: 2024 Devin Lin +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "taskmodel.h" + +#include + +#include +#include +#include +#include + +namespace KWin +{ + +class TaskFilterModel : public QSortFilterProxyModel +{ + Q_OBJECT + Q_PROPERTY(TaskModel *windowModel READ windowModel WRITE setWindowModel NOTIFY windowModelChanged) + Q_PROPERTY(QString screenName READ screenName WRITE setScreenName NOTIFY screenNameChanged) + +public: + explicit TaskFilterModel(QObject *parent = nullptr); + + TaskModel *windowModel() const; + void setWindowModel(TaskModel *taskModel); + + QString screenName() const; + void setScreenName(const QString &screenName); + +protected: + bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override; + bool lessThan(const QModelIndex &left, const QModelIndex &right) const override; + +Q_SIGNALS: + void screenNameChanged(); + void windowModelChanged(); + +private: + TaskModel *m_taskModel = nullptr; + QPointer m_output; +}; + +} // namespace KWin diff --git a/kwin/mobiletaskswitcher/taskmodel.cpp b/kwin/mobiletaskswitcher/taskmodel.cpp new file mode 100644 index 00000000..c1718c38 --- /dev/null +++ b/kwin/mobiletaskswitcher/taskmodel.cpp @@ -0,0 +1,140 @@ +// SPDX-FileCopyrightText: 2021 Vlad Zahorodnii +// SPDX-FileCopyrightText: 2024 Devin Lin +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "taskmodel.h" + +// KWin +#include +#include +#include + +namespace KWin +{ + +TaskModel::TaskModel(QObject *parent) + : QAbstractListModel(parent) +{ + connect(workspace(), &Workspace::windowAdded, this, &TaskModel::handleWindowAdded); + connect(workspace(), &Workspace::windowRemoved, this, &TaskModel::handleWindowRemoved); + connect(workspace(), &Workspace::windowActivated, this, &TaskModel::handleActiveWindowChanged); + + auto windows = workspace()->windows(); + const qint64 currentTime = QDateTime::currentMSecsSinceEpoch(); + + for (Window *window : std::as_const(windows)) { + m_windows.push_back({window, currentTime}); + setupWindowConnections(window); + } +} + +void TaskModel::markRoleChanged(Window *window, int role) +{ + int windowIndex = -1; + for (int i = 0; i < m_windows.size(); ++i) { + if (m_windows[i].first == window) { + windowIndex = i; + break; + } + } + const QModelIndex row = index(windowIndex, 0); + Q_EMIT dataChanged(row, row, {role}); +} + +void TaskModel::setupWindowConnections(Window *window) +{ + connect(window, &Window::desktopsChanged, this, [this, window]() { + markRoleChanged(window, DesktopRole); + }); + connect(window, &Window::outputChanged, this, [this, window]() { + markRoleChanged(window, OutputRole); + }); + connect(window, &Window::activitiesChanged, this, [this, window]() { + markRoleChanged(window, ActivityRole); + }); +} + +void TaskModel::handleWindowAdded(Window *window) +{ + beginInsertRows(QModelIndex(), m_windows.count(), m_windows.count()); + const qint64 currentTime = QDateTime::currentMSecsSinceEpoch(); + m_windows.append({window, currentTime}); + endInsertRows(); + + setupWindowConnections(window); +} + +void TaskModel::handleWindowRemoved(Window *window) +{ + int index = -1; + for (int i = 0; i < m_windows.size(); ++i) { + if (m_windows[i].first == window) { + index = i; + break; + } + } + Q_ASSERT(index != -1); + + beginRemoveRows(QModelIndex(), index, index); + m_windows.removeAt(index); + endRemoveRows(); +} + +QHash TaskModel::roleNames() const +{ + return { + {Qt::DisplayRole, QByteArrayLiteral("display")}, + {WindowRole, QByteArrayLiteral("window")}, + {OutputRole, QByteArrayLiteral("output")}, + {DesktopRole, QByteArrayLiteral("desktop")}, + {ActivityRole, QByteArrayLiteral("activity")}, + }; +} + +QVariant TaskModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid() || index.row() < 0 || index.row() >= m_windows.count()) { + return QVariant(); + } + + Window *window = m_windows[index.row()].first; + qint64 lastActivated = m_windows[index.row()].second; + switch (role) { + case Qt::DisplayRole: + case WindowRole: + return QVariant::fromValue(window); + case OutputRole: + return QVariant::fromValue(window->output()); + case DesktopRole: + return QVariant::fromValue(window->desktops()); + case ActivityRole: + return window->activities(); + case LastActivatedRole: + return lastActivated; + default: + return QVariant(); + } +} + +int TaskModel::rowCount(const QModelIndex &parent) const +{ + return parent.isValid() ? 0 : m_windows.count(); +} + +void TaskModel::handleActiveWindowChanged() +{ + Window *window = workspace()->activeWindow(); + if (!window) { + return; + } + + const qint64 currentTime = QDateTime::currentMSecsSinceEpoch(); + for (int i = 0; i < m_windows.size(); ++i) { + if (m_windows[i].first == window) { + m_windows[i] = {window, currentTime}; + Q_EMIT dataChanged(index(i, 0), index(i, 0), {TaskModel::LastActivatedRole}); + } + } +} + +} // namespace KWin diff --git a/kwin/mobiletaskswitcher/taskmodel.h b/kwin/mobiletaskswitcher/taskmodel.h new file mode 100644 index 00000000..a92238c1 --- /dev/null +++ b/kwin/mobiletaskswitcher/taskmodel.h @@ -0,0 +1,43 @@ +// SPDX-FileCopyrightText: 2021 Vlad Zahorodnii +// SPDX-FileCopyrightText: 2024 Devin Lin +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include +#include +#include +#include + +namespace KWin +{ + +class TaskModel : public QAbstractListModel +{ + Q_OBJECT + +public: + enum Roles { WindowRole = Qt::UserRole + 1, OutputRole, DesktopRole, ActivityRole, LastActivatedRole }; + + explicit TaskModel(QObject *parent = nullptr); + + QHash roleNames() const override; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + +private: + void markRoleChanged(Window *window, int role); + + void handleWindowAdded(Window *window); + void handleWindowRemoved(Window *window); + void setupWindowConnections(Window *window); + + void handleActiveWindowChanged(); + + // qint64 - Last activated timestamp + QList> m_windows; +}; + +}; // namespace KWin \ No newline at end of file