shift-shell/components/mobileshellstate/startupfeedbackmodel.cpp
Devin Lin 2d2b7407a6 startupfeedback: Change to be window based and controlled by a model
This makes the startup feedback more robust, by having instances be controlled by a model which can listen to window changes. Being window based also allows for the close button and gestures to work properly with it, since it will show up in the task switcher as well.

Fixes:
* https://invent.kde.org/plasma/plasma-mobile/-/issues/357
* https://invent.kde.org/plasma/plasma-mobile/-/issues/338
* https://invent.kde.org/plasma/plasma-mobile/-/issues/335 (dark themes now tint the background color)
* https://invent.kde.org/plasma/plasma-mobile/-/issues/330
* https://invent.kde.org/plasma/plasma-mobile/-/issues/30
2024-07-13 16:30:07 +00:00

300 lines
8.4 KiB
C++

// SPDX-FileCopyrightText: 2024 Devin Lin <devin@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
#include "startupfeedbackmodel.h"
#include "windowlistener.h"
constexpr int STARTUP_FEEDBACK_TIMEOUT_MS = 8000;
StartupFeedback::StartupFeedback(QObject *parent,
QString iconName,
QString title,
QString storageId,
qreal iconStartX,
qreal iconStartY,
qreal iconSize,
int screen)
: QObject{parent}
, m_iconName{iconName}
, m_title{title}
, m_storageId{storageId}
, m_iconStartX{iconStartX}
, m_iconStartY{iconStartY}
, m_iconSize{iconSize}
, m_screen{screen}
, m_timeoutTimer{new QTimer{this}}
{
connect(m_timeoutTimer, &QTimer::timeout, this, &StartupFeedback::timeout);
}
QString StartupFeedback::iconName() const
{
return m_iconName;
}
QString StartupFeedback::title() const
{
return m_title;
}
QString StartupFeedback::storageId() const
{
return m_storageId;
}
qreal StartupFeedback::iconStartX() const
{
return m_iconStartX;
}
qreal StartupFeedback::iconStartY() const
{
return m_iconStartY;
}
qreal StartupFeedback::iconSize() const
{
return m_iconSize;
}
int StartupFeedback::screen() const
{
return m_screen;
}
QString StartupFeedback::windowUuid() const
{
return m_windowUuid;
}
void StartupFeedback::setWindowUuid(QString uuid)
{
m_windowUuid = uuid;
}
void StartupFeedback::startTimeoutTimer()
{
// Timeout of 5 seconds before closing
m_timeoutTimer->start(STARTUP_FEEDBACK_TIMEOUT_MS);
}
StartupFeedbackModel::StartupFeedbackModel(QObject *parent)
: QAbstractListModel{parent}
{
connect(WindowListener::instance(), &WindowListener::windowCreated, this, &StartupFeedbackModel::onWindowOpened);
connect(WindowListener::instance(), &WindowListener::plasmaWindowCreated, this, &StartupFeedbackModel::onPlasmaWindowOpened);
connect(WindowListener::instance(), &WindowListener::activeWindowChanged, this, &StartupFeedbackModel::onActiveWindowChanged);
}
void StartupFeedbackModel::addApp(StartupFeedback *startupFeedback)
{
beginInsertRows(QModelIndex{}, m_list.size(), m_list.size());
m_list.append(startupFeedback);
updateActiveWindowIsStartupFeedback();
startupFeedback->startTimeoutTimer();
connect(startupFeedback, &StartupFeedback::timeout, this, [this, startupFeedback]() {
int index = m_list.indexOf(startupFeedback);
if (index == -1) {
return;
}
beginRemoveRows(QModelIndex{}, index, index);
m_list.removeAt(index);
updateActiveWindowIsStartupFeedback();
endRemoveRows();
});
// Prepare state for active window being startupfeedback early, otherwise we have a race condition between
// the Plasma window opening and the visual (causes panels to flash background color)
m_activeWindowIsStartupFeedback = true;
Q_EMIT activeWindowIsStartupFeedbackChanged();
endInsertRows();
}
int StartupFeedbackModel::rowCount(const QModelIndex &parent) const
{
Q_UNUSED(parent)
return m_list.count();
}
QVariant StartupFeedbackModel::data(const QModelIndex &index, int role) const
{
if (!index.isValid()) {
return QVariant();
}
auto delegate = m_list[index.row()];
switch (role) {
case DelegateRole:
return QVariant::fromValue(delegate);
case ScreenRole:
return delegate->screen();
default:
return QVariant();
}
}
QHash<int, QByteArray> StartupFeedbackModel::roleNames() const
{
return {{DelegateRole, QByteArrayLiteral("delegate")}, {ScreenRole, QByteArrayLiteral("screen")}};
}
bool StartupFeedbackModel::activeWindowIsStartupFeedback() const
{
return m_activeWindowIsStartupFeedback;
}
void StartupFeedbackModel::onWindowOpened(KWayland::Client::PlasmaWindow *window)
{
if (!window) {
return;
}
QString appId = window->appId();
int indexToRemove = 0;
// storageId may get suffixed with ".desktop", check for that
const QString suffix = QStringLiteral(".desktop");
// Remove StartupFeedback when the respective window is created
// NOTE: often, the window "appId" does not match the actual app storageId in third-party apps, so we can't rely on this.
for (int i = 0; i < m_list.size(); ++i) {
auto *startupFeedback = m_list[i];
if (startupFeedback->storageId() == appId || startupFeedback->storageId() == appId + suffix) {
indexToRemove = i;
break;
}
}
// If no windows were matched, the oldest StartupFeedback (since indexToRemove = 0)
// NOTE: This is our fallback if the window "appId" doesn't match anything.
if (m_list.size() > indexToRemove) {
StartupFeedback *feedbackToRemove = m_list[indexToRemove];
// Only delete StartupFeedback once the window becomes active
// -> There is a gap of time between when a window is created and when it is actually visible/active
connect(window, &KWayland::Client::PlasmaWindow::activeChanged, this, [this, window, feedbackToRemove]() {
if (!window->isActive()) {
return;
}
int indexToRemove = m_list.indexOf(feedbackToRemove);
if (indexToRemove != -1) {
beginRemoveRows(QModelIndex{}, indexToRemove, indexToRemove);
m_list[indexToRemove]->deleteLater();
m_list.removeAt(indexToRemove);
updateActiveWindowIsStartupFeedback();
endRemoveRows();
}
window->disconnect(this);
});
}
}
void StartupFeedbackModel::onPlasmaWindowOpened(KWayland::Client::PlasmaWindow *window)
{
// Fill in the respective StartupFeedback with the window uuid
// Heuristic: window title should match
for (auto *startupFeedback : m_list) {
if (startupFeedback->title() == window->title() && startupFeedback->windowUuid().isEmpty()) {
startupFeedback->setWindowUuid(window->uuid());
}
}
// Update variable that depends on window uuid
updateActiveWindowIsStartupFeedback();
}
void StartupFeedbackModel::onActiveWindowChanged(KWayland::Client::PlasmaWindow *activeWindow)
{
m_activeWindow = activeWindow;
updateActiveWindowIsStartupFeedback();
}
void StartupFeedbackModel::updateActiveWindowIsStartupFeedback()
{
bool isStartupFeedback = false;
if (m_activeWindow) {
// Check if there exists a StartupFeedback window with the same id as the active window
for (const auto *startupFeedback : m_list) {
if (startupFeedback->windowUuid() == m_activeWindow->uuid()) {
isStartupFeedback = true;
break;
}
}
}
if (isStartupFeedback != m_activeWindowIsStartupFeedback) {
m_activeWindowIsStartupFeedback = isStartupFeedback;
Q_EMIT activeWindowIsStartupFeedbackChanged();
}
}
StartupFeedbackFilterModel::StartupFeedbackFilterModel(QObject *parent)
: QSortFilterProxyModel(parent)
{
setSortRole(StartupFeedbackModel::ScreenRole);
}
StartupFeedbackModel *StartupFeedbackFilterModel::startupFeedbackModel() const
{
return m_startupFeedbackModel;
}
void StartupFeedbackFilterModel::setStartupFeedbackModel(StartupFeedbackModel *startupFeedbackModel)
{
if (startupFeedbackModel == m_startupFeedbackModel) {
return;
}
m_startupFeedbackModel = startupFeedbackModel;
setSourceModel(m_startupFeedbackModel);
Q_EMIT startupFeedbackModelChanged();
}
int StartupFeedbackFilterModel::screen() const
{
return m_screen;
}
void StartupFeedbackFilterModel::setScreen(int screen)
{
if (m_screen == screen) {
return;
}
m_screen = screen;
Q_EMIT screenChanged();
}
bool StartupFeedbackFilterModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const
{
if (!m_startupFeedbackModel) {
return false;
}
const QModelIndex index = m_startupFeedbackModel->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;
}
StartupFeedback *startupFeedback = qvariant_cast<StartupFeedback *>(data);
return startupFeedback->screen() == m_screen;
}