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
This commit is contained in:
Devin Lin 2024-07-13 16:30:07 +00:00
parent 41de61ef4c
commit 2d2b7407a6
26 changed files with 994 additions and 444 deletions

View file

@ -65,9 +65,14 @@ MobileShell.BaseItem {
} else if (root.settingsCommand && !root.restrictedPermissions) {
closeRequested();
MobileShellState.ShellDBusClient.openAppLaunchAnimation(
MobileShellState.ShellDBusClient.openAppLaunchAnimationWithPosition(
__getCurrentScreenNumber(),
root.icon);
root.icon,
root.text,
'org.kde.mobile.plasmasettings', // settings window id
-1,
-1,
Math.min(root.iconItem.width, root.iconItem.height));
MobileShell.ShellUtil.executeCommand(root.settingsCommand);
}
}
@ -75,9 +80,14 @@ MobileShell.BaseItem {
function delegatePressAndHold() {
if (root.settingsCommand && !root.restrictedPermissions) {
closeRequested();
MobileShellState.ShellDBusClient.openAppLaunchAnimation(
MobileShellState.ShellDBusClient.openAppLaunchAnimationWithPosition(
__getCurrentScreenNumber(),
root.icon);
root.icon,
root.text,
'org.kde.mobile.plasmasettings', // settings window id
-1,
-1,
Math.min(root.iconItem.width, root.iconItem.height));
MobileShell.ShellUtil.executeCommand(root.settingsCommand);
} else if (root.toggleFunction) {
root.toggleFunction();

View file

@ -1,261 +0,0 @@
// SPDX-FileCopyrightText: 2015 Marco Martin <notmart@gmail.com>
// SPDX-FileCopyrightText: 2021-2023 Devin Lin <devin@kde.org>
// SPDX-License-Identifier: LGPL-2.0-or-later
import QtQuick
import QtQuick.Effects
import QtQuick.Layouts
import QtQuick.Window
import org.kde.kirigami as Kirigami
import org.kde.plasma.private.mobileshell.shellsettingsplugin as ShellSettings
import org.kde.plasma.private.mobileshell.state as MobileShellState
import org.kde.plasma.private.mobileshell.windowplugin as WindowPlugin
import org.kde.plasma.plasmoid
/**
* Component that animates an app opening from a location.
*/
MouseArea { // use mousearea to ensure clicks don't go behind
id: root
visible: false
property alias backgroundColor: background.color
property alias icon: icon.source
property bool __openRequested: false
function open(splashIcon) {
iconParent.scale = 0.5;
background.scale = 0.5;
backgroundParent.x = 0;
backgroundParent.y = 0;
__openRequested = true;
updateIconSource(splashIcon);
}
function openWithPosition(splashIcon, x, y, sourceIconSize) {
iconParent.scale = sourceIconSize/iconParent.width;
background.scale = 0;
backgroundParent.x = -root.width/2 + x
backgroundParent.y = -root.height/2 + y
__openRequested = true;
updateIconSource(splashIcon);
}
function close() {
visible = false;
colorGenerator.resetColor();
}
// call this after everything has loaded
function actuallyOpen() {
__openRequested = false;
if (ShellSettings.Settings.animationsEnabled) {
openAnimComplex.restart();
} else {
openAnimSimple.restart();
}
}
// close when an app opens
property bool windowActive: Window.active
onWindowActiveChanged: root.close();
// close when homescreen requested
Connections {
target: MobileShellState.ShellDBusClient
function onOpenHomeScreenRequested() {
root.close();
}
}
Connections {
target: WindowPlugin.WindowUtil
// Open StartupFeedback when the notifier gives an app (ex. from Milou search)
// TODO: This is problematic with multiple screens, because we don't have any info given
// on which screen the app is opening on. Thus StartupFeedback would just open on
// every single screen...
// -> We have it disabled for now until some solution is found. We manually open StartupFeedback
// from launches in the homescreen (call open()).
//
// function onAppActivationStarted(appId, iconName) {
// if (!openAnimComplex.running && !root.__openRequested) {
// // TODO: this doesn't work because it gets triggered on screen 0 even if the app is opening on screen 1
// // HACK: We have no way of knowing which screen this app is going to open on
// // -> Assume the first screen for now
// if (Plasmoid.screen === 0) {
// root.open(iconName);
// }
// }
// }
function onAppActivationFinished(appId, iconName) {
if (iconName === root.icon.name) {
root.close();
}
}
}
function updateIconSource(source) {
if (icon.source !== source) {
// the colors are generated async from the icon, so we need to ensure we don't display an old color
// for a moment when an app opens
colorGenerator.resetColor();
} else {
// case where we set the same icon, ensure the color is set
colorGenerator.updateColor();
}
icon.source = source;
}
Kirigami.ImageColors {
id: colorGenerator
source: icon.source
// the colors are generated async from the icon, so we need to ensure we don't display an old color
// for a moment when an app opens
property color colorToUse: 'transparent'
function resetColor() {
colorToUse = 'transparent';
}
function updateColor() {
colorToUse = colorGenerator.dominant;
// once color is finished updating, start the animation
if (root.__openRequested) {
root.actuallyOpen();
}
}
onPaletteChanged: {
// update color once palette has loaded
updateColor();
}
}
// animation that moves the icon
SequentialAnimation {
id: openAnimComplex
ScriptAction {
script: {
root.opacity = 1;
root.visible = true;
}
}
// slight pause to give slower devices time to catch up when the item becomes visible
PauseAnimation { duration: 20 }
ParallelAnimation {
id: parallelAnim
property real animationDuration: Kirigami.Units.longDuration + Kirigami.Units.shortDuration
ScaleAnimator {
target: background
from: background.scale
to: 1
duration: parallelAnim.animationDuration
easing.type: Easing.OutCubic
}
ScaleAnimator {
target: iconParent
from: iconParent.scale
to: 1
duration: parallelAnim.animationDuration
easing.type: Easing.OutCubic
}
XAnimator {
target: backgroundParent
from: backgroundParent.x
to: 0
duration: parallelAnim.animationDuration
easing.type: Easing.OutCubic
}
YAnimator {
target: backgroundParent
from: backgroundParent.y
to: 0
duration: parallelAnim.animationDuration
easing.type: Easing.OutCubic
}
}
ScriptAction {
script: {
// close the app drawer after it isn't visible
MobileShellState.ShellDBusClient.resetHomeScreenPosition();
}
}
}
// animation that just fades in
SequentialAnimation {
id: openAnimSimple
ScriptAction {
script: {
root.opacity = 0;
root.visible = true;
background.scale = 1;
iconParent.scale = 1;
backgroundParent.x = 0;
backgroundParent.y = 0;
}
}
NumberAnimation {
target: root
properties: "opacity"
from: 0
to: 1
duration: Kirigami.Units.longDuration
easing.type: Easing.OutCubic
}
ScriptAction {
script: {
// close the app drawer after it isn't visible
MobileShellState.ShellDBusClient.resetHomeScreenPosition();
}
}
}
Item {
id: backgroundParent
width: root.width
height: root.height
Rectangle {
id: background
anchors.fill: parent
color: colorGenerator.colorToUse
}
Item {
id: iconParent
anchors.centerIn: background
width: Kirigami.Units.iconSizes.enormous
height: width
Kirigami.Icon {
id: icon
anchors.fill: parent
}
MultiEffect {
anchors.fill: icon
source: icon
shadowEnabled: true
blurMax: 16
shadowColor: "#80000000"
}
}
}
}

View file

@ -0,0 +1,48 @@
// SPDX-FileCopyrightText: 2024 Devin Lin <devin@kde.org>
// SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
import QtQuick
import org.kde.plasma.private.mobileshell.state as MobileShellState
// Component to supplement the StartupFeedback window maximization animation for panel backgrounds.
Rectangle {
id: root
property real fullHeight
property int screen
property var maximizedTracker
// Smooth animation for colored rectangle
NumberAnimation on height {
id: heightAnim
from: 0
to: root.fullHeight
duration: 200
easing.type: Easing.OutExpo
}
// Reset when maximized window state changes
Connections {
target: maximizedTracker
function onShowingWindowChanged() {
root.color = 'transparent';
}
}
// Listen to event from shell dbus
Connections {
target: MobileShellState.ShellDBusClient
function onAppLaunchMaximizePanelAnimationTriggered(screen, color) {
if (root.screen !== screen) {
return;
}
root.color = color;
heightAnim.restart();
}
}
}

View file

@ -0,0 +1,221 @@
// SPDX-FileCopyrightText: 2024 Devin Lin <devin@kde.org>
// SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
import QtQuick
import QtQuick.Effects
import org.kde.kirigami as Kirigami
import org.kde.plasma.components 3.0 as PC3
import org.kde.plasma.private.mobileshell.shellsettingsplugin as ShellSettings
import org.kde.plasma.private.mobileshell.state as MobileShellState
import org.kde.plasma.private.nanoshell 2.0 as NanoShell
Item {
id: root
property int screen
property real topMargin
property real bottomMargin
property real leftMargin
property real rightMargin
Repeater {
id: repeater
model: MobileShellState.StartupFeedbackFilterModel {
startupFeedbackModel: MobileShellState.ShellDBusObject.startupFeedbackModel
screen: root.screen
}
delegate: Item {
Window {
id: window
property var startupFeedback: model.delegate
visibility: Window.Maximized
flags: Qt.FramelessWindowHint
color: 'transparent'
title: startupFeedback.title
Component.onCompleted: {
// root is anchored to the homescreen which fills up the whole screen,
// but the startup feedback window will have margins (ex. status bar)
const realHeight = root.height - root.topMargin - root.bottomMargin;
const realWidth = root.width - root.leftMargin - root.rightMargin;
iconParent.scale = startupFeedback.iconSize / iconParent.width;
background.scale = 0;
if (startupFeedback.iconStartX === -1 && startupFeedback.iconStartY === -1) {
backgroundParent.x = 0;
backgroundParent.y = 0;
} else {
backgroundParent.x = -realWidth/2 + startupFeedback.iconStartX - root.leftMargin;
backgroundParent.y = -realHeight/2 + startupFeedback.iconStartY - root.topMargin;
}
if (ShellSettings.Settings.animationsEnabled) {
openAnimComplex.restart();
} else {
openAnimSimple.restart();
}
}
// animation that moves the icon
SequentialAnimation {
id: openAnimComplex
// slight pause to give slower devices time to catch up when the item becomes visible
PauseAnimation { duration: 20 }
ParallelAnimation {
id: parallelAnim
property real animationDuration: Kirigami.Units.longDuration + Kirigami.Units.shortDuration
ScaleAnimator {
target: background
from: background.scale
to: 1
duration: parallelAnim.animationDuration
easing.type: Easing.OutCubic
}
ScaleAnimator {
target: iconParent
from: iconParent.scale
to: 1
duration: parallelAnim.animationDuration
easing.type: Easing.OutCubic
}
XAnimator {
target: backgroundParent
from: backgroundParent.x
to: 0
duration: parallelAnim.animationDuration
easing.type: Easing.OutCubic
}
YAnimator {
target: backgroundParent
from: backgroundParent.y
to: 0
duration: parallelAnim.animationDuration
easing.type: Easing.OutCubic
}
}
ScriptAction {
script: {
// Animation has finished, trigger event for panels to update color
MobileShellState.ShellDBusClient.triggerAppLaunchMaximizePanelAnimation(root.screen, background.color);
// close the app drawer after it isn't visible
MobileShellState.ShellDBusClient.resetHomeScreenPosition();
}
}
}
// animation that just fades in
SequentialAnimation {
id: openAnimSimple
ScriptAction {
script: {
background.scale = 1;
iconParent.scale = 1;
backgroundParent.x = 0;
backgroundParent.y = 0;
}
}
NumberAnimation {
target: windowRoot
properties: "opacity"
from: 0
to: 1
duration: Kirigami.Units.longDuration
easing.type: Easing.OutCubic
}
ScriptAction {
script: {
// Animation has finished, trigger event for panels to update color
MobileShellState.ShellDBusClient.triggerAppLaunchMaximizePanelAnimation(root.screen, background.color);
// close the app drawer after it isn't visible
MobileShellState.ShellDBusClient.resetHomeScreenPosition();
}
}
}
Item {
id: windowRoot
anchors.fill: parent
Item {
id: backgroundParent
width: windowRoot.width
height: windowRoot.height
Rectangle {
id: background
anchors.fill: parent
// Tint the background color if a dark theme is being used
color: Kirigami.ColorUtils.brightnessForColor(Kirigami.Theme.backgroundColor) === Kirigami.ColorUtils.Dark ?
Kirigami.ColorUtils.tintWithAlpha(colorGenerator.dominant, Kirigami.Theme.backgroundColor, 0.7) :
colorGenerator.dominant
Kirigami.ImageColors {
id: colorGenerator
source: icon.source
}
}
Item {
id: iconParent
anchors.centerIn: background
width: Kirigami.Units.iconSizes.enormous
height: Kirigami.Units.iconSizes.enormous
Kirigami.Icon {
id: icon
anchors.fill: parent
source: window.startupFeedback.iconName
}
MultiEffect {
anchors.fill: icon
source: icon
shadowEnabled: true
blurMax: 16
shadowColor: "#80000000"
}
Timer {
running: true
interval: 2000
onTriggered: loadingIndicator.opacity = 1
}
// Show loading indicator after two seconds have passed
PC3.BusyIndicator {
id: loadingIndicator
anchors.top: icon.bottom
anchors.horizontalCenter: icon.horizontalCenter
anchors.topMargin: Kirigami.Units.gridUnit
opacity: 0
Behavior on opacity {
NumberAnimation {}
}
implicitHeight: Kirigami.Units.iconSizes.smallMedium
implicitWidth: Kirigami.Units.iconSizes.smallMedium
}
}
}
}
}
}
}
}

View file

@ -40,12 +40,6 @@ Item {
*/
property alias contentItem: itemContainer.contentItem
/**
* Whether a component is being shown on top of the homescreen within the same
* window.
*/
readonly property bool overlayShown: startupFeedback.visible
/**
* The root PlasmoidItem of the containment this is used into
*/
@ -102,26 +96,6 @@ Item {
root.resetHomeScreenPosition();
}
function onOpenAppLaunchAnimationRequested(screen, splashIcon) {
if (screen !== Plasmoid.screen) {
return;
}
startupFeedback.open(splashIcon);
}
function onOpenAppLaunchAnimationWithPositionRequested(screen, splashIcon, title, x, y, sourceIconSize) {
if (screen !== Plasmoid.screen) {
return;
}
startupFeedback.openWithPosition(splashIcon, x, y, sourceIconSize);
}
function onCloseAppLaunchAnimationRequested() {
startupFeedback.close();
}
function onIsTaskSwitcherVisibleChanged() {
if (MobileShellState.ShellDBusClient.isTaskSwitcherVisible) {
itemContainer.zoomOutImmediately();
@ -210,10 +184,17 @@ Item {
}
}
// start app animation component
MobileShell.StartupFeedback {
id: startupFeedback
z: 999999
// App start animation component
MobileShell.StartupFeedbackWindows {
id: startupFeedbackWindows
screen: Plasmoid.screen
topMargin: root.topMargin
bottomMargin: root.bottomMargin
leftMargin: root.leftMargin
rightMargin: root.rightMargin
anchors.fill: parent
visible: false
}
}

View file

@ -5,6 +5,8 @@ set(mobileshellstateplugin_SRCS
shelldbusobject.cpp
shelldbusclient.cpp
lockscreendbusclient.cpp
startupfeedbackmodel.cpp
windowlistener.cpp
)
qt_generate_dbus_interface(
@ -34,6 +36,7 @@ target_link_libraries(mobileshellstateplugin
Qt::Quick
Qt::DBus
Plasma::Plasma
Plasma::KWaylandClient
KF6::I18n
KF6::Notifications
Plasma::PlasmaQuick

View file

@ -34,12 +34,10 @@ void ShellDBusClient::connectSignals()
connect(m_interface, &OrgKdePlasmashellInterface::isTaskSwitcherVisibleChanged, this, &ShellDBusClient::updateIsTaskSwitcherVisible);
connect(m_interface, &OrgKdePlasmashellInterface::openActionDrawerRequested, this, &ShellDBusClient::openActionDrawerRequested);
connect(m_interface, &OrgKdePlasmashellInterface::closeActionDrawerRequested, this, &ShellDBusClient::closeActionDrawerRequested);
connect(m_interface, &OrgKdePlasmashellInterface::openAppLaunchAnimationRequested, this, &ShellDBusClient::openAppLaunchAnimationRequested);
connect(m_interface,
&OrgKdePlasmashellInterface::openAppLaunchAnimationWithPositionRequested,
&OrgKdePlasmashellInterface::appLaunchMaximizePanelAnimationTriggered,
this,
&ShellDBusClient::openAppLaunchAnimationWithPositionRequested);
connect(m_interface, &OrgKdePlasmashellInterface::closeAppLaunchAnimationRequested, this, &ShellDBusClient::closeAppLaunchAnimationRequested);
&ShellDBusClient::appLaunchMaximizePanelAnimationTriggered);
connect(m_interface, &OrgKdePlasmashellInterface::openHomeScreenRequested, this, &ShellDBusClient::openHomeScreenRequested);
connect(m_interface, &OrgKdePlasmashellInterface::resetHomeScreenPositionRequested, this, &ShellDBusClient::resetHomeScreenPositionRequested);
connect(m_interface, &OrgKdePlasmashellInterface::showVolumeOSDRequested, this, &ShellDBusClient::showVolumeOSDRequested);
@ -84,19 +82,20 @@ bool ShellDBusClient::isTaskSwitcherVisible() const
return m_isTaskSwitcherVisible;
}
void ShellDBusClient::openAppLaunchAnimation(int screen, QString splashIcon)
void ShellDBusClient::openAppLaunchAnimationWithPosition(int screen,
QString splashIcon,
QString title,
QString storageId,
qreal x,
qreal y,
qreal sourceIconSize)
{
m_interface->openAppLaunchAnimation(screen, splashIcon);
m_interface->openAppLaunchAnimationWithPosition(screen, splashIcon, title, storageId, x, y, sourceIconSize);
}
void ShellDBusClient::openAppLaunchAnimationWithPosition(int screen, QString splashIcon, QString title, qreal x, qreal y, qreal sourceIconSize)
void ShellDBusClient::triggerAppLaunchMaximizePanelAnimation(int screen, QString color)
{
m_interface->openAppLaunchAnimationWithPosition(screen, splashIcon, title, x, y, sourceIconSize);
}
void ShellDBusClient::closeAppLaunchAnimation()
{
m_interface->closeAppLaunchAnimation();
m_interface->triggerAppLaunchMaximizePanelAnimation(screen, color);
}
void ShellDBusClient::openHomeScreen()

View file

@ -34,9 +34,9 @@ public:
Q_INVOKABLE void openActionDrawer();
Q_INVOKABLE void closeActionDrawer();
Q_INVOKABLE void openAppLaunchAnimation(int screen, QString splashIcon);
Q_INVOKABLE void openAppLaunchAnimationWithPosition(int screen, QString splashIcon, QString title, qreal x, qreal y, qreal sourceIconSize);
Q_INVOKABLE void closeAppLaunchAnimation();
Q_INVOKABLE void
openAppLaunchAnimationWithPosition(int screen, QString splashIcon, QString title, QString storageId, qreal x, qreal y, qreal sourceIconSize);
Q_INVOKABLE void triggerAppLaunchMaximizePanelAnimation(int screen, QString color);
Q_INVOKABLE void openHomeScreen();
Q_INVOKABLE void resetHomeScreenPosition();
@ -48,9 +48,7 @@ Q_SIGNALS:
void isTaskSwitcherVisibleChanged();
void openActionDrawerRequested();
void closeActionDrawerRequested();
void openAppLaunchAnimationRequested(int screen, QString splashIcon);
void openAppLaunchAnimationWithPositionRequested(int screen, QString splashIcon, QString title, qreal x, qreal y, qreal sourceIconSize);
void closeAppLaunchAnimationRequested();
void appLaunchMaximizePanelAnimationTriggered(int screen, QString color);
void openHomeScreenRequested();
void resetHomeScreenPositionRequested();
void showVolumeOSDRequested();

View file

@ -8,6 +8,7 @@
ShellDBusObject::ShellDBusObject(QObject *parent)
: QObject{parent}
, m_startupFeedbackModel{new StartupFeedbackModel{this}}
{
}
@ -20,6 +21,11 @@ void ShellDBusObject::registerObject()
}
}
StartupFeedbackModel *ShellDBusObject::startupFeedbackModel()
{
return m_startupFeedbackModel;
}
bool ShellDBusObject::doNotDisturb()
{
return m_doNotDisturb;
@ -69,19 +75,25 @@ void ShellDBusObject::closeActionDrawer()
Q_EMIT closeActionDrawerRequested();
}
void ShellDBusObject::openAppLaunchAnimation(int screen, QString splashIcon)
void ShellDBusObject::openAppLaunchAnimationWithPosition(int screen,
QString splashIcon,
QString title,
QString storageId,
qreal x,
qreal y,
qreal sourceIconSize)
{
Q_EMIT openAppLaunchAnimationRequested(screen, splashIcon);
if (!m_startupFeedbackModel) {
return;
}
void ShellDBusObject::openAppLaunchAnimationWithPosition(int screen, QString splashIcon, QString title, qreal x, qreal y, qreal sourceIconSize)
{
Q_EMIT openAppLaunchAnimationWithPositionRequested(screen, splashIcon, title, x, y, sourceIconSize);
StartupFeedback *feedback = new StartupFeedback{m_startupFeedbackModel, splashIcon, title, storageId, x, y, sourceIconSize, screen};
m_startupFeedbackModel->addApp(feedback);
}
void ShellDBusObject::closeAppLaunchAnimation()
void ShellDBusObject::triggerAppLaunchMaximizePanelAnimation(int screen, QString color)
{
Q_EMIT closeAppLaunchAnimationRequested();
Q_EMIT appLaunchMaximizePanelAnimationTriggered(screen, color);
}
void ShellDBusObject::openHomeScreen()

View file

@ -7,12 +7,15 @@
#include <QString>
#include <qqmlregistration.h>
#include "startupfeedbackmodel.h"
class ShellDBusObject : public QObject
{
Q_OBJECT
QML_ELEMENT
QML_SINGLETON
Q_CLASSINFO("D-Bus Interface", "org.kde.plasmashell")
Q_PROPERTY(StartupFeedbackModel *startupFeedbackModel READ startupFeedbackModel CONSTANT)
public:
ShellDBusObject(QObject *parent = nullptr);
@ -20,15 +23,15 @@ public:
// called by QML
Q_INVOKABLE void registerObject();
StartupFeedbackModel *startupFeedbackModel();
Q_SIGNALS:
Q_SCRIPTABLE void doNotDisturbChanged();
Q_SCRIPTABLE void isActionDrawerOpenChanged();
Q_SCRIPTABLE void isTaskSwitcherVisibleChanged();
Q_SCRIPTABLE void openActionDrawerRequested();
Q_SCRIPTABLE void closeActionDrawerRequested();
Q_SCRIPTABLE void openAppLaunchAnimationRequested(int screen, QString splashIcon);
Q_SCRIPTABLE void openAppLaunchAnimationWithPositionRequested(int screen, QString splashIcon, QString title, qreal x, qreal y, qreal sourceIconSize);
Q_SCRIPTABLE void closeAppLaunchAnimationRequested();
Q_SCRIPTABLE void appLaunchMaximizePanelAnimationTriggered(int screen, QString color);
Q_SCRIPTABLE void openHomeScreenRequested();
Q_SCRIPTABLE void resetHomeScreenPositionRequested();
Q_SCRIPTABLE void showVolumeOSDRequested();
@ -47,18 +50,20 @@ public Q_SLOTS:
Q_SCRIPTABLE void openActionDrawer();
Q_SCRIPTABLE void closeActionDrawer();
Q_SCRIPTABLE void openAppLaunchAnimation(int screen, QString splashIcon);
Q_SCRIPTABLE void openAppLaunchAnimationWithPosition(int screen, QString splashIcon, QString title, qreal x, qreal y, qreal sourceIconSize);
Q_SCRIPTABLE void closeAppLaunchAnimation();
Q_SCRIPTABLE void
openAppLaunchAnimationWithPosition(int screen, QString splashIcon, QString title, QString storageId, qreal x, qreal y, qreal sourceIconSize);
Q_SCRIPTABLE void triggerAppLaunchMaximizePanelAnimation(int screen, QString color);
Q_SCRIPTABLE void openHomeScreen();
Q_SCRIPTABLE void resetHomeScreenPosition();
Q_SCRIPTABLE void showVolumeOSD();
private:
bool m_initialized = false;
bool m_initialized{false};
bool m_doNotDisturb = false;
bool m_isActionDrawerOpen = false;
bool m_isTaskSwitcherVisible = false;
bool m_doNotDisturb{false};
bool m_isActionDrawerOpen{false};
bool m_isTaskSwitcherVisible{false};
StartupFeedbackModel *m_startupFeedbackModel{nullptr};
};

View file

@ -0,0 +1,300 @@
// 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;
}

View file

@ -0,0 +1,132 @@
// SPDX-FileCopyrightText: 2024 Devin Lin <devin@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <QAbstractListModel>
#include <QObject>
#include <QSortFilterProxyModel>
#include <QTimer>
#include <qqmlregistration.h>
#include <KWayland/Client/plasmawindowmanagement.h>
class StartupFeedback : public QObject
{
Q_OBJECT
Q_PROPERTY(QString iconName READ iconName CONSTANT)
Q_PROPERTY(QString title READ title CONSTANT)
Q_PROPERTY(QString storageId READ storageId CONSTANT)
Q_PROPERTY(qreal iconStartX READ iconStartX CONSTANT)
Q_PROPERTY(qreal iconStartY READ iconStartY CONSTANT)
Q_PROPERTY(qreal iconSize READ iconSize CONSTANT)
Q_PROPERTY(int screen READ screen CONSTANT)
public:
explicit StartupFeedback(QObject *parent = nullptr,
QString iconName = "",
QString title = "",
QString storageId = "",
qreal iconStartX = 0.0,
qreal iconStartY = 0.0,
qreal iconSize = 0.0,
int screen = 0);
explicit StartupFeedback();
QString iconName() const;
QString title() const;
QString storageId() const;
qreal iconStartX() const;
qreal iconStartY() const;
qreal iconSize() const;
int screen() const;
// Set by StartupFeedbackModel
QString windowUuid() const;
void setWindowUuid(QString uuid);
void startTimeoutTimer();
Q_SIGNALS:
void timeout();
private:
const QString m_iconName;
const QString m_title;
const QString m_storageId;
const qreal m_iconStartX;
const qreal m_iconStartY;
const qreal m_iconSize;
const int m_screen;
QString m_windowUuid;
QTimer *m_timeoutTimer{nullptr};
};
class StartupFeedbackModel : public QAbstractListModel
{
Q_OBJECT
Q_PROPERTY(bool activeWindowIsStartupFeedback READ activeWindowIsStartupFeedback NOTIFY activeWindowIsStartupFeedbackChanged)
public:
enum Roles {
DelegateRole = Qt::UserRole,
ScreenRole,
};
explicit StartupFeedbackModel(QObject *parent = nullptr);
void addApp(StartupFeedback *startupFeedback);
bool activeWindowIsStartupFeedback() const;
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
QHash<int, QByteArray> roleNames() const override;
Q_SIGNALS:
void activeWindowIsStartupFeedbackChanged();
private Q_SLOTS:
void onWindowOpened(KWayland::Client::PlasmaWindow *window);
void onPlasmaWindowOpened(KWayland::Client::PlasmaWindow *window);
void onActiveWindowChanged(KWayland::Client::PlasmaWindow *activeWindow);
private:
void updateActiveWindowIsStartupFeedback();
bool m_activeWindowIsStartupFeedback{false};
QList<StartupFeedback *> m_list;
KWayland::Client::PlasmaWindow *m_activeWindow{nullptr};
};
class StartupFeedbackFilterModel : public QSortFilterProxyModel
{
Q_OBJECT
QML_ELEMENT
Q_PROPERTY(StartupFeedbackModel *startupFeedbackModel READ startupFeedbackModel WRITE setStartupFeedbackModel NOTIFY startupFeedbackModelChanged)
Q_PROPERTY(int screen READ screen WRITE setScreen NOTIFY screenChanged)
public:
explicit StartupFeedbackFilterModel(QObject *parent = nullptr);
StartupFeedbackModel *startupFeedbackModel() const;
void setStartupFeedbackModel(StartupFeedbackModel *taskModel);
int screen() const;
void setScreen(int screen);
protected:
bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override;
Q_SIGNALS:
void screenChanged();
void startupFeedbackModelChanged();
private:
StartupFeedbackModel *m_startupFeedbackModel{nullptr};
int m_screen{0};
};

View file

@ -0,0 +1,57 @@
// SPDX-FileCopyrightText: 2022 Devin Lin <devin@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
#include "windowlistener.h"
WindowListener::WindowListener(QObject *parent)
: QObject{parent}
{
// initialize wayland window checking
KWayland::Client::ConnectionThread *connection = KWayland::Client::ConnectionThread::fromApplication(this);
if (!connection) {
return;
}
auto *registry = new KWayland::Client::Registry(this);
registry->create(connection);
connect(registry, &KWayland::Client::Registry::plasmaWindowManagementAnnounced, this, [this, registry](quint32 name, quint32 version) {
m_windowManagement = registry->createPlasmaWindowManagement(name, version, this);
connect(m_windowManagement, &KWayland::Client::PlasmaWindowManagement::windowCreated, this, &WindowListener::onWindowCreated);
connect(m_windowManagement, &KWayland::Client::PlasmaWindowManagement::activeWindowChanged, this, [this]() {
Q_EMIT activeWindowChanged(m_windowManagement->activeWindow());
});
});
registry->setup();
connection->roundtrip();
}
WindowListener *WindowListener::instance()
{
static WindowListener *listener = new WindowListener();
return listener;
}
void WindowListener::onWindowCreated(KWayland::Client::PlasmaWindow *window)
{
QString storageId = window->appId();
// Ignore empty windows
if (storageId == "") {
return;
}
// Special handling for plasmashell windows, don't track them
if (storageId == "org.kde.plasmashell") {
Q_EMIT plasmaWindowCreated(window);
return;
}
// listen for window close
connect(window, &KWayland::Client::PlasmaWindow::unmapped, this, [this, storageId]() {
Q_EMIT windowRemoved(storageId);
});
Q_EMIT windowCreated(window);
}

View file

@ -0,0 +1,36 @@
// SPDX-FileCopyrightText: 2022 Devin Lin <devin@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <QList>
#include <QObject>
#include <KWayland/Client/connection_thread.h>
#include <KWayland/Client/plasmawindowmanagement.h>
#include <KWayland/Client/registry.h>
#include <KWayland/Client/surface.h>
class WindowListener : public QObject
{
Q_OBJECT
public:
WindowListener(QObject *parent = nullptr);
static WindowListener *instance();
QList<KWayland::Client::PlasmaWindow *> windowsFromStorageId(QString &storageId) const;
public Q_SLOTS:
void onWindowCreated(KWayland::Client::PlasmaWindow *window);
Q_SIGNALS:
void windowCreated(KWayland::Client::PlasmaWindow *window);
void plasmaWindowCreated(KWayland::Client::PlasmaWindow *window);
void windowRemoved(QString storageId);
void activeWindowChanged(KWayland::Client::PlasmaWindow *activeWindow);
private:
KWayland::Client::PlasmaWindowManagement *m_windowManagement{nullptr};
};

View file

@ -15,6 +15,7 @@ QtObject {
property alias screenGeometry: tasksModel.screenGeometry
readonly property bool showingWindow: __internal.count > 0 && !WindowPlugin.WindowUtil.isShowingDesktop
readonly property int windowCount: __internal.count
property var __internal: KItemModels.KSortFilterProxyModel {
id: visibleMaximizedWindowsModel

View file

@ -41,9 +41,6 @@ public:
public Q_SLOTS:
void sycocaDbChanged();
Q_SIGNALS:
void launchError(const QString &msg);
protected:
HomeScreen *m_homeScreen{nullptr};
QList<FolioDelegate *> m_delegates;

View file

@ -27,11 +27,12 @@ AbstractDelegate {
property bool turnToFolderAnimEnabled: false
function launchApp() {
if (application.icon !== "") {
if (application.icon !== "" && !root.application.running) {
MobileShellState.ShellDBusClient.openAppLaunchAnimationWithPosition(
Plasmoid.screen,
application.icon,
application.name,
application.storageId,
root.iconItem.Kirigami.ScenePosition.x + root.iconItem.width/2,
root.iconItem.Kirigami.ScenePosition.y + root.iconItem.height/2,
Math.min(root.iconItem.width, root.iconItem.height));

View file

@ -166,17 +166,6 @@ ContainmentItem {
bottomMargin: homeScreen.bottomMargin
leftMargin: homeScreen.leftMargin
rightMargin: homeScreen.rightMargin
// make the homescreen not interactable when task switcher or startup feedback is on
interactive: !homeScreen.overlayShown
}
}
// listen to app launch errors
Connections {
target: folio.ApplicationListModel
function onLaunchError(msg) {
MobileShellState.ShellDBusClient.closeAppLaunchAnimation()
}
}
}

View file

@ -82,6 +82,7 @@ Item {
Plasmoid.screen,
source,
title,
storageId,
iconLoader.Kirigami.ScenePosition.x + iconLoader.width/2,
iconLoader.Kirigami.ScenePosition.y + iconLoader.height/2,
Math.min(iconLoader.width, iconLoader.height));

View file

@ -93,6 +93,7 @@ MobileShell.GridView {
Plasmoid.screen,
icon,
title,
storageId,
delegate.iconItem.Kirigami.ScenePosition.x + delegate.iconItem.width/2,
delegate.iconItem.Kirigami.ScenePosition.y + delegate.iconItem.height/2,
Math.min(delegate.iconItem.width, delegate.iconItem.height));

View file

@ -58,7 +58,7 @@ ContainmentItem {
Rectangle {
id: darkenBackground
color: homeScreen.overlayShown ? 'transparent' : (halcyonHomeScreen.page == 1 ? Qt.rgba(0, 0, 0, 0.7) : Qt.rgba(0, 0, 0, 0.2))
color: (halcyonHomeScreen.page == 1 ? Qt.rgba(0, 0, 0, 0.7) : Qt.rgba(0, 0, 0, 0.2))
anchors.fill: parent
z: -1
Behavior on color {
@ -101,8 +101,6 @@ ContainmentItem {
leftMargin: homeScreen.leftMargin
rightMargin: homeScreen.rightMargin
// make the homescreen not interactable when task switcher or startup feedback is on
interactive: !homeScreen.overlayShown
searchWidget: search
}

View file

@ -35,9 +35,6 @@ public:
public Q_SLOTS:
void sycocaDbChanged();
Q_SIGNALS:
void launchError(const QString &msg);
protected:
QList<Application *> m_applicationList;
};

View file

@ -44,7 +44,8 @@ ContainmentItem {
}
// only opaque if there are no maximized windows on this screen
readonly property bool showingApp: windowMaximizedTracker.showingWindow
readonly property bool showingStartupFeedback: MobileShellState.ShellDBusObject.startupFeedbackModel.activeWindowIsStartupFeedback && windowMaximizedTracker.windowCount === 1
readonly property bool showingApp: windowMaximizedTracker.showingWindow && !showingStartupFeedback
readonly property color backgroundColor: topPanel.colorScopeColor
WindowPlugin.WindowMaximizedTracker {
@ -96,6 +97,17 @@ ContainmentItem {
MobileShell.VolumeOSDProviderLoader.load();
}
MobileShell.StartupFeedbackPanelFill {
id: startupFeedbackColorAnimation
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
fullHeight: root.height
screen: Plasmoid.screen
maximizedTracker: windowMaximizedTracker
}
// top panel component
MobileShell.StatusBar {
id: topPanel

View file

@ -98,7 +98,6 @@ MobileShell.NavigationPanel {
if (tasksModel.activeTask !== 0) {
tasksModel.requestClose(tasksModel.activeTask);
}
MobileShellState.ShellDBusClient.closeAppLaunchAnimation();
}
}
}

View file

@ -16,6 +16,7 @@ import org.kde.kquickcontrolsaddons 2.0
import org.kde.plasma.private.mobileshell as MobileShell
import org.kde.plasma.private.mobileshell.shellsettingsplugin as ShellSettings
import org.kde.plasma.private.mobileshell.windowplugin as WindowPlugin
import org.kde.plasma.private.mobileshell.state as MobileShellState
ContainmentItem {
id: root
@ -110,13 +111,25 @@ ContainmentItem {
Component.onCompleted: setWindowProperties();
// only opaque if there are no maximized windows on this screen
readonly property bool opaqueBar: windowMaximizedTracker.showingWindow
readonly property bool showingStartupFeedback: MobileShellState.ShellDBusObject.startupFeedbackModel.activeWindowIsStartupFeedback && windowMaximizedTracker.windowCount === 1
readonly property bool opaqueBar: windowMaximizedTracker.showingWindow && !showingStartupFeedback
WindowPlugin.WindowMaximizedTracker {
id: windowMaximizedTracker
screenGeometry: Plasmoid.containment.screenGeometry
}
MobileShell.StartupFeedbackPanelFill {
id: startupFeedbackColorAnimation
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
fullHeight: root.height
screen: Plasmoid.screen
maximizedTracker: windowMaximizedTracker
}
Item {
anchors.fill: parent