From 2d2b7407a663ad26d69dd482d7d7f82cf96c56da Mon Sep 17 00:00:00 2001 From: Devin Lin Date: Sat, 13 Jul 2024 16:30:07 +0000 Subject: [PATCH] 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 --- components/mobileshell/CMakeLists.txt | 2 +- .../quicksettings/QuickSettingsDelegate.qml | 42 ++- .../qml/components/StartupFeedback.qml | 261 --------------- .../components/StartupFeedbackPanelFill.qml | 48 +++ .../qml/components/StartupFeedbackWindows.qml | 221 +++++++++++++ .../mobileshell/qml/homescreen/HomeScreen.qml | 41 +-- components/mobileshellstate/CMakeLists.txt | 5 +- .../mobileshellstate/shelldbusclient.cpp | 25 +- components/mobileshellstate/shelldbusclient.h | 10 +- .../mobileshellstate/shelldbusobject.cpp | 30 +- components/mobileshellstate/shelldbusobject.h | 25 +- .../mobileshellstate/startupfeedbackmodel.cpp | 300 ++++++++++++++++++ .../mobileshellstate/startupfeedbackmodel.h | 132 ++++++++ .../mobileshellstate/windowlistener.cpp | 57 ++++ components/mobileshellstate/windowlistener.h | 36 +++ .../qml/WindowMaximizedTracker.qml | 1 + .../homescreens/folio/applicationlistmodel.h | 3 - .../contents/ui/delegate/AppDelegate.qml | 3 +- .../folio/package/contents/ui/main.qml | 11 - .../contents/ui/FavoritesAppDelegate.qml | 99 +++--- .../package/contents/ui/GridAppList.qml | 21 +- .../halcyon/package/contents/ui/main.qml | 4 +- .../halcyon/plugin/applicationlistmodel.h | 3 - .../panel/package/contents/ui/main.qml | 14 +- .../contents/ui/NavigationPanelComponent.qml | 29 +- .../taskpanel/package/contents/ui/main.qml | 15 +- 26 files changed, 994 insertions(+), 444 deletions(-) delete mode 100644 components/mobileshell/qml/components/StartupFeedback.qml create mode 100644 components/mobileshell/qml/components/StartupFeedbackPanelFill.qml create mode 100644 components/mobileshell/qml/components/StartupFeedbackWindows.qml create mode 100644 components/mobileshellstate/startupfeedbackmodel.cpp create mode 100644 components/mobileshellstate/startupfeedbackmodel.h create mode 100644 components/mobileshellstate/windowlistener.cpp create mode 100644 components/mobileshellstate/windowlistener.h diff --git a/components/mobileshell/CMakeLists.txt b/components/mobileshell/CMakeLists.txt index 61901330..9e3e6de0 100644 --- a/components/mobileshell/CMakeLists.txt +++ b/components/mobileshell/CMakeLists.txt @@ -32,7 +32,7 @@ file(GLOB_RECURSE _qml_sources ecm_target_qml_sources(mobileshellplugin SOURCES ${_qml_sources}) -target_link_libraries(mobileshellplugin +target_link_libraries(mobileshellplugin PUBLIC Qt::Core PRIVATE diff --git a/components/mobileshell/qml/actiondrawer/quicksettings/QuickSettingsDelegate.qml b/components/mobileshell/qml/actiondrawer/quicksettings/QuickSettingsDelegate.qml index 4b932023..84afa96b 100644 --- a/components/mobileshell/qml/actiondrawer/quicksettings/QuickSettingsDelegate.qml +++ b/components/mobileshell/qml/actiondrawer/quicksettings/QuickSettingsDelegate.qml @@ -18,9 +18,9 @@ import org.kde.plasma.components 3.0 as PlasmaComponents MobileShell.BaseItem { id: root - + required property bool restrictedPermissions - + // Model interface required property string text required property string status @@ -28,19 +28,19 @@ MobileShell.BaseItem { required property bool enabled required property string settingsCommand required property var toggleFunction - + signal closeRequested() - + // set by children property var iconItem - + readonly property color enabledButtonBorderColor: Qt.darker(Kirigami.Theme.highlightColor, 1.25) readonly property color disabledButtonBorderColor: Kirigami.ColorUtils.tintWithAlpha(Kirigami.Theme.textColor, Kirigami.Theme.backgroundColor, 0.75) readonly property color enabledButtonColor: Kirigami.ColorUtils.tintWithAlpha(Kirigami.Theme.highlightColor, Kirigami.Theme.backgroundColor, 0.6) readonly property color enabledButtonPressedColor: Kirigami.ColorUtils.tintWithAlpha(Kirigami.Theme.highlightColor, Kirigami.Theme.backgroundColor, 0.4); readonly property color disabledButtonColor: Kirigami.Theme.backgroundColor readonly property color disabledButtonPressedColor: Qt.darker(disabledButtonColor, 1.1) - + // scale animation on press property real zoomScale: 1 Behavior on zoomScale { @@ -49,14 +49,14 @@ MobileShell.BaseItem { easing.type: Easing.OutExpo } } - - transform: Scale { - origin.x: root.width / 2; - origin.y: root.height / 2; + + transform: Scale { + origin.x: root.width / 2; + origin.y: root.height / 2; xScale: root.zoomScale yScale: root.zoomScale } - + function delegateClick() { if (root.toggle) { root.toggle(); @@ -65,19 +65,29 @@ 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); } } - + 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(); diff --git a/components/mobileshell/qml/components/StartupFeedback.qml b/components/mobileshell/qml/components/StartupFeedback.qml deleted file mode 100644 index a10cdadd..00000000 --- a/components/mobileshell/qml/components/StartupFeedback.qml +++ /dev/null @@ -1,261 +0,0 @@ -// SPDX-FileCopyrightText: 2015 Marco Martin -// SPDX-FileCopyrightText: 2021-2023 Devin Lin -// 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" - } - } - } -} - diff --git a/components/mobileshell/qml/components/StartupFeedbackPanelFill.qml b/components/mobileshell/qml/components/StartupFeedbackPanelFill.qml new file mode 100644 index 00000000..ae39ad8f --- /dev/null +++ b/components/mobileshell/qml/components/StartupFeedbackPanelFill.qml @@ -0,0 +1,48 @@ +// SPDX-FileCopyrightText: 2024 Devin Lin +// 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(); + } + } +} diff --git a/components/mobileshell/qml/components/StartupFeedbackWindows.qml b/components/mobileshell/qml/components/StartupFeedbackWindows.qml new file mode 100644 index 00000000..c308e0e6 --- /dev/null +++ b/components/mobileshell/qml/components/StartupFeedbackWindows.qml @@ -0,0 +1,221 @@ +// SPDX-FileCopyrightText: 2024 Devin Lin +// 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 + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/components/mobileshell/qml/homescreen/HomeScreen.qml b/components/mobileshell/qml/homescreen/HomeScreen.qml index 1a9af191..24d0eb82 100644 --- a/components/mobileshell/qml/homescreen/HomeScreen.qml +++ b/components/mobileshell/qml/homescreen/HomeScreen.qml @@ -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 } } diff --git a/components/mobileshellstate/CMakeLists.txt b/components/mobileshellstate/CMakeLists.txt index e07fdd0d..bd113006 100644 --- a/components/mobileshellstate/CMakeLists.txt +++ b/components/mobileshellstate/CMakeLists.txt @@ -5,6 +5,8 @@ set(mobileshellstateplugin_SRCS shelldbusobject.cpp shelldbusclient.cpp lockscreendbusclient.cpp + startupfeedbackmodel.cpp + windowlistener.cpp ) qt_generate_dbus_interface( @@ -24,7 +26,7 @@ ecm_add_qml_module(mobileshellstateplugin URI org.kde.plasma.private.mobileshell target_sources(mobileshellstateplugin PRIVATE ${mobileshellstateplugin_SRCS} ${RESOURCES}) -target_link_libraries(mobileshellstateplugin +target_link_libraries(mobileshellstateplugin PUBLIC Qt::Core PRIVATE @@ -34,6 +36,7 @@ target_link_libraries(mobileshellstateplugin Qt::Quick Qt::DBus Plasma::Plasma + Plasma::KWaylandClient KF6::I18n KF6::Notifications Plasma::PlasmaQuick diff --git a/components/mobileshellstate/shelldbusclient.cpp b/components/mobileshellstate/shelldbusclient.cpp index 701ceab5..d672e39c 100644 --- a/components/mobileshellstate/shelldbusclient.cpp +++ b/components/mobileshellstate/shelldbusclient.cpp @@ -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() diff --git a/components/mobileshellstate/shelldbusclient.h b/components/mobileshellstate/shelldbusclient.h index f2f620d7..8daa668d 100644 --- a/components/mobileshellstate/shelldbusclient.h +++ b/components/mobileshellstate/shelldbusclient.h @@ -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(); diff --git a/components/mobileshellstate/shelldbusobject.cpp b/components/mobileshellstate/shelldbusobject.cpp index 2af26c70..ec1be630 100644 --- a/components/mobileshellstate/shelldbusobject.cpp +++ b/components/mobileshellstate/shelldbusobject.cpp @@ -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; + } + + StartupFeedback *feedback = new StartupFeedback{m_startupFeedbackModel, splashIcon, title, storageId, x, y, sourceIconSize, screen}; + m_startupFeedbackModel->addApp(feedback); } -void ShellDBusObject::openAppLaunchAnimationWithPosition(int screen, QString splashIcon, QString title, qreal x, qreal y, qreal sourceIconSize) +void ShellDBusObject::triggerAppLaunchMaximizePanelAnimation(int screen, QString color) { - Q_EMIT openAppLaunchAnimationWithPositionRequested(screen, splashIcon, title, x, y, sourceIconSize); -} - -void ShellDBusObject::closeAppLaunchAnimation() -{ - Q_EMIT closeAppLaunchAnimationRequested(); + Q_EMIT appLaunchMaximizePanelAnimationTriggered(screen, color); } void ShellDBusObject::openHomeScreen() diff --git a/components/mobileshellstate/shelldbusobject.h b/components/mobileshellstate/shelldbusobject.h index 7121428c..f51185f7 100644 --- a/components/mobileshellstate/shelldbusobject.h +++ b/components/mobileshellstate/shelldbusobject.h @@ -7,12 +7,15 @@ #include #include +#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}; }; diff --git a/components/mobileshellstate/startupfeedbackmodel.cpp b/components/mobileshellstate/startupfeedbackmodel.cpp new file mode 100644 index 00000000..3c811a3f --- /dev/null +++ b/components/mobileshellstate/startupfeedbackmodel.cpp @@ -0,0 +1,300 @@ +// SPDX-FileCopyrightText: 2024 Devin Lin +// 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 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(data); + return startupFeedback->screen() == m_screen; +} diff --git a/components/mobileshellstate/startupfeedbackmodel.h b/components/mobileshellstate/startupfeedbackmodel.h new file mode 100644 index 00000000..4bdfd45d --- /dev/null +++ b/components/mobileshellstate/startupfeedbackmodel.h @@ -0,0 +1,132 @@ +// SPDX-FileCopyrightText: 2024 Devin Lin +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#include +#include +#include + +#include + +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 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 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}; +}; diff --git a/components/mobileshellstate/windowlistener.cpp b/components/mobileshellstate/windowlistener.cpp new file mode 100644 index 00000000..04a53b82 --- /dev/null +++ b/components/mobileshellstate/windowlistener.cpp @@ -0,0 +1,57 @@ +// SPDX-FileCopyrightText: 2022 Devin Lin +// 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); +} diff --git a/components/mobileshellstate/windowlistener.h b/components/mobileshellstate/windowlistener.h new file mode 100644 index 00000000..de874400 --- /dev/null +++ b/components/mobileshellstate/windowlistener.h @@ -0,0 +1,36 @@ +// SPDX-FileCopyrightText: 2022 Devin Lin +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include + +#include +#include +#include +#include + +class WindowListener : public QObject +{ + Q_OBJECT + +public: + WindowListener(QObject *parent = nullptr); + + static WindowListener *instance(); + + QList 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}; +}; diff --git a/components/windowplugin/qml/WindowMaximizedTracker.qml b/components/windowplugin/qml/WindowMaximizedTracker.qml index 87611ea9..8737e646 100644 --- a/components/windowplugin/qml/WindowMaximizedTracker.qml +++ b/components/windowplugin/qml/WindowMaximizedTracker.qml @@ -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 diff --git a/containments/homescreens/folio/applicationlistmodel.h b/containments/homescreens/folio/applicationlistmodel.h index 4838cfb8..15a2ae9b 100644 --- a/containments/homescreens/folio/applicationlistmodel.h +++ b/containments/homescreens/folio/applicationlistmodel.h @@ -41,9 +41,6 @@ public: public Q_SLOTS: void sycocaDbChanged(); -Q_SIGNALS: - void launchError(const QString &msg); - protected: HomeScreen *m_homeScreen{nullptr}; QList m_delegates; diff --git a/containments/homescreens/folio/package/contents/ui/delegate/AppDelegate.qml b/containments/homescreens/folio/package/contents/ui/delegate/AppDelegate.qml index 9af8b7fb..9e7529c0 100644 --- a/containments/homescreens/folio/package/contents/ui/delegate/AppDelegate.qml +++ b/containments/homescreens/folio/package/contents/ui/delegate/AppDelegate.qml @@ -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)); diff --git a/containments/homescreens/folio/package/contents/ui/main.qml b/containments/homescreens/folio/package/contents/ui/main.qml index 59a9e987..10d4102d 100644 --- a/containments/homescreens/folio/package/contents/ui/main.qml +++ b/containments/homescreens/folio/package/contents/ui/main.qml @@ -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() } } } diff --git a/containments/homescreens/halcyon/package/contents/ui/FavoritesAppDelegate.qml b/containments/homescreens/halcyon/package/contents/ui/FavoritesAppDelegate.qml index 93afa924..c7eeff18 100644 --- a/containments/homescreens/halcyon/package/contents/ui/FavoritesAppDelegate.qml +++ b/containments/homescreens/halcyon/package/contents/ui/FavoritesAppDelegate.qml @@ -10,7 +10,7 @@ import org.kde.plasma.core as PlasmaCore import org.kde.plasma.components 3.0 as PlasmaComponents import org.kde.kquickcontrolsaddons 2.0 -import org.kde.plasma.private.containmentlayoutmanager 1.0 as ContainmentLayoutManager +import org.kde.plasma.private.containmentlayoutmanager 1.0 as ContainmentLayoutManager import org.kde.plasma.private.mobileshell as MobileShell import org.kde.plasma.private.mobileshell.shellsettingsplugin as ShellSettings import org.kde.plasma.private.mobileshell.state as MobileShellState @@ -23,30 +23,30 @@ Item { property int visualIndex: 0 property real dragFolderAnimationProgress: 0 - + property list menuActions - + // whether this delegate is a folder property bool isFolder - + // folder object property var folder readonly property string folderName: folder ? folder.name : "" - + // app object property var application readonly property string applicationName: application ? application.name : "" readonly property string applicationStorageId: application ? application.storageId : "" readonly property string applicationIcon: application ? application.icon : "" - + signal folderOpenRequested() - + property alias drag: mouseArea.drag Drag.active: delegate.drag.active Drag.source: delegate Drag.hotSpot.x: delegate.width / 2 Drag.hotSpot.y: delegate.height / 2 - + // close context menu if drag move onXChanged: { if (dialogLoader.item) { @@ -58,12 +58,12 @@ Item { dialogLoader.item.close() } } - + function openContextMenu() { dialogLoader.active = true; dialogLoader.item.open(); } - + function launch() { if (isFolder) { folderOpenRequested(); @@ -75,13 +75,14 @@ Item { } } } - + function launchAppWithAnim(x: int, y: int, source, title: string, storageId: string) { if (source !== "") { MobileShellState.ShellDBusClient.openAppLaunchAnimationWithPosition( 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)); @@ -90,16 +91,16 @@ Item { application.setMinimizedDelegate(delegate); MobileShell.AppLaunch.launchOrActivateApp(application.storageId); } - + Loader { id: dialogLoader active: false - + sourceComponent: PlasmaComponents.Menu { id: menu title: label.text closePolicy: PlasmaComponents.Menu.CloseOnReleaseOutside | PlasmaComponents.Menu.CloseOnEscape - + Repeater { model: menuActions delegate: PlasmaComponents.MenuItem { @@ -108,18 +109,18 @@ Item { onClicked: modelData.triggered() } } - + onClosed: dialogLoader.active = false } } - + MouseArea { id: mouseArea - + anchors.fill: parent - + property bool inDrag: false - + cursorShape: Qt.PointingHandCursor acceptedButtons: Qt.LeftButton | Qt.RightButton onReleased: { @@ -128,18 +129,18 @@ Item { } onPressAndHold: { inDrag = true; openContextMenu() } drag.target: inDrag ? delegate : undefined - + // grow/shrink animation property real zoomScale: 1 transform: Scale { - origin.x: mouseArea.width / 2; - origin.y: mouseArea.height / 2; + origin.x: mouseArea.width / 2; + origin.y: mouseArea.height / 2; xScale: mouseArea.zoomScale yScale: mouseArea.zoomScale } - + property bool launchAppRequested: false - + NumberAnimation on zoomScale { id: shrinkAnim running: false @@ -151,7 +152,7 @@ Item { } } } - + NumberAnimation on zoomScale { id: growAnim running: false @@ -164,7 +165,7 @@ Item { } } } - + onPressedChanged: { if (pressed) { growAnim.stop(); @@ -173,7 +174,7 @@ Item { growAnim.restart(); } } - + // launch app handled by press animation onClicked: mouse => { if (mouse.button === Qt.RightButton) { @@ -182,19 +183,19 @@ Item { launchAppRequested = true; } } - + HoverHandler { id: hoverHandler acceptedDevices: PointerDevice.Mouse acceptedPointerTypes: PointerDevice.Generic } - + Rectangle { anchors.fill: parent - radius: height / 2 + radius: height / 2 color: mouseArea.pressed ? Qt.rgba(255, 255, 255, 0.2) : "transparent" } - + RowLayout { id: rowLayout anchors { @@ -234,11 +235,11 @@ Item { font.pointSize: Kirigami.Theme.defaultFont.pointSize font.weight: Font.Bold color: "white" - + layer.enabled: true layer.effect: MobileShell.TextDropShadow {} } - + Kirigami.Icon { Layout.alignment: Qt.AlignRight Layout.preferredWidth: Kirigami.Units.iconSizes.small @@ -259,10 +260,10 @@ Item { } } } - + Component { id: appIconComponent - + Item { Rectangle { anchors.fill: parent @@ -271,14 +272,14 @@ Item { radius: Kirigami.Units.smallSpacing opacity: delegate.dragFolderAnimationProgress } - + Kirigami.Icon { id: icon anchors.fill: parent source: delegate.isFolder ? 'document-open-folder' : delegate.applicationIcon - - transform: Scale { - origin.x: icon.width / 2 + + transform: Scale { + origin.x: icon.width / 2 origin.y: icon.height / 2 xScale: 1 - delegate.dragFolderAnimationProgress * 0.5 yScale: 1 - delegate.dragFolderAnimationProgress * 0.5 @@ -295,7 +296,7 @@ Item { height: width color: Kirigami.Theme.highlightColor } - + layer.enabled: true layer.effect: MultiEffect { shadowEnabled: true @@ -306,10 +307,10 @@ Item { } } } - + Component { id: folderIconComponent - + Item { Rectangle { id: rect @@ -317,31 +318,31 @@ Item { anchors.margins: Kirigami.Units.smallSpacing color: Qt.rgba(255, 255, 255, 0.2) radius: Kirigami.Units.smallSpacing - - transform: Scale { - origin.x: rect.width / 2 + + transform: Scale { + origin.x: rect.width / 2 origin.y: rect.height / 2 xScale: 1 + delegate.dragFolderAnimationProgress * 0.5 yScale: 1 + delegate.dragFolderAnimationProgress * 0.5 } } - + Grid { id: grid anchors.fill: parent anchors.margins: Kirigami.Units.smallSpacing * 2 columns: 2 spacing: Kirigami.Units.smallSpacing - + property var previews: model.folder.appPreviews - + Repeater { model: grid.previews delegate: Kirigami.Icon { implicitWidth: (grid.width - Kirigami.Units.smallSpacing) / 2 implicitHeight: (grid.width - Kirigami.Units.smallSpacing) / 2 source: modelData.icon - + layer.enabled: true layer.effect: MultiEffect { shadowEnabled: true diff --git a/containments/homescreens/halcyon/package/contents/ui/GridAppList.qml b/containments/homescreens/halcyon/package/contents/ui/GridAppList.qml index 795ba26f..14498c3a 100644 --- a/containments/homescreens/halcyon/package/contents/ui/GridAppList.qml +++ b/containments/homescreens/halcyon/package/contents/ui/GridAppList.qml @@ -19,7 +19,7 @@ MobileShell.GridView { id: gridView cacheBuffer: cellHeight * 20 // 10 rows above and below reuseItems: true - + Controls.ScrollBar.vertical: Controls.ScrollBar {} Connections { @@ -32,10 +32,10 @@ MobileShell.GridView { // ensure items aren't visible out of bounds layer.enabled: true - + readonly property int reservedSpaceForLabel: metrics.height readonly property real effectiveContentWidth: width - leftMargin - rightMargin - + cellWidth: gridView.effectiveContentWidth / Math.min(Math.floor(effectiveContentWidth / (Kirigami.Units.iconSizes.huge + Kirigami.Units.largeSpacing * 2)), 8) cellHeight: cellWidth + reservedSpaceForLabel @@ -45,14 +45,14 @@ MobileShell.GridView { function goToBeginning() { goToBeginningAnim.restart(); } - + NumberAnimation on contentY { id: goToBeginningAnim to: gridView.originY duration: 200 easing.type: Easing.InOutQuad } - + model: Halcyon.ApplicationListModel header: MobileShell.BaseItem { @@ -60,7 +60,7 @@ MobileShell.GridView { topPadding: Kirigami.Units.gridUnit + Math.round(gridView.height * 0.1) bottomPadding: Kirigami.Units.gridUnit leftPadding: Kirigami.Units.smallSpacing - + contentItem: PC3.Label { color: "white" font.pointSize: 16 @@ -68,7 +68,7 @@ MobileShell.GridView { text: i18n("Applications") } } - + PC3.Label { id: metrics text: "M\nM" @@ -76,13 +76,13 @@ MobileShell.GridView { font.pointSize: Kirigami.Theme.defaultFont.pointSize * 0.85 font.weight: Font.Bold } - + Keys.onReturnPressed: currentItem.launchApp() delegate: GridAppDelegate { id: delegate - + property Halcyon.Application application: model.application - + width: gridView.cellWidth height: gridView.cellHeight reservedSpaceForLabel: gridView.reservedSpaceForLabel @@ -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)); diff --git a/containments/homescreens/halcyon/package/contents/ui/main.qml b/containments/homescreens/halcyon/package/contents/ui/main.qml index 7a9496a5..53f120f3 100644 --- a/containments/homescreens/halcyon/package/contents/ui/main.qml +++ b/containments/homescreens/halcyon/package/contents/ui/main.qml @@ -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 } diff --git a/containments/homescreens/halcyon/plugin/applicationlistmodel.h b/containments/homescreens/halcyon/plugin/applicationlistmodel.h index ea27988f..167ed6f6 100644 --- a/containments/homescreens/halcyon/plugin/applicationlistmodel.h +++ b/containments/homescreens/halcyon/plugin/applicationlistmodel.h @@ -35,9 +35,6 @@ public: public Q_SLOTS: void sycocaDbChanged(); -Q_SIGNALS: - void launchError(const QString &msg); - protected: QList m_applicationList; }; diff --git a/containments/panel/package/contents/ui/main.qml b/containments/panel/package/contents/ui/main.qml index ae45bce5..c9c55933 100644 --- a/containments/panel/package/contents/ui/main.qml +++ b/containments/panel/package/contents/ui/main.qml @@ -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 diff --git a/containments/taskpanel/package/contents/ui/NavigationPanelComponent.qml b/containments/taskpanel/package/contents/ui/NavigationPanelComponent.qml index ff01396b..2280d343 100644 --- a/containments/taskpanel/package/contents/ui/NavigationPanelComponent.qml +++ b/containments/taskpanel/package/contents/ui/NavigationPanelComponent.qml @@ -20,7 +20,7 @@ import org.kde.kirigami as Kirigami MobileShell.NavigationPanel { id: root required property bool opaqueBar - + // background is: // - opaque if an app is shown or vkbd is shown // - translucent if the task switcher is open @@ -28,7 +28,7 @@ MobileShell.NavigationPanel { backgroundColor: (Keyboards.KWinVirtualKeyboard.active || opaqueBar) ? Kirigami.Theme.backgroundColor : "transparent"; foregroundColorGroup: opaqueBar ? Kirigami.Theme.Window : Kirigami.Theme.Complementary shadow: !opaqueBar - + TaskManager.VirtualDesktopInfo { id: virtualDesktopInfo } @@ -53,42 +53,42 @@ MobileShell.NavigationPanel { // ~~~~ // navigation panel actions - + // toggle task switcher button leftAction: MobileShell.NavigationPanelAction { id: taskSwitcherAction - + enabled: true iconSource: "mobile-task-switcher" iconSizeFactor: 0.75 - + onTriggered: { Plasmoid.triggerTaskSwitcher(); } } - + // home button middleAction: MobileShell.NavigationPanelAction { id: homeAction - + enabled: true iconSource: "start-here-kde" iconSizeFactor: 1 - + onTriggered: { MobileShellState.ShellDBusClient.openHomeScreen(); } } - + // close app/keyboard button rightAction: MobileShell.NavigationPanelAction { id: closeAppAction - + enabled: Keyboards.KWinVirtualKeyboard.active || WindowPlugin.WindowUtil.hasCloseableActiveWindow iconSource: Keyboards.KWinVirtualKeyboard.active ? "go-down-symbolic" : "mobile-close-app" // mobile-close-app (from plasma-frameworks) seems to have fewer margins than icons from breeze-icons iconSizeFactor: Keyboards.KWinVirtualKeyboard.active ? 1 : 0.75 - + onTriggered: { if (Keyboards.KWinVirtualKeyboard.active) { // close keyboard if it is open @@ -98,19 +98,18 @@ MobileShell.NavigationPanel { if (tasksModel.activeTask !== 0) { tasksModel.requestClose(tasksModel.activeTask); } - MobileShellState.ShellDBusClient.closeAppLaunchAnimation(); } } } - + rightCornerAction: MobileShell.NavigationPanelAction { id: keyboardToggleAction - visible: ShellSettings.Settings.alwaysShowKeyboardToggleOnNavigationPanel || + visible: ShellSettings.Settings.alwaysShowKeyboardToggleOnNavigationPanel || (Keyboards.KWinVirtualKeyboard.available && !Keyboards.KWinVirtualKeyboard.activeClientSupportsTextInput) enabled: true iconSource: "input-keyboard-virtual-symbolic" iconSizeFactor: 0.75 - + onTriggered: { if (Keyboards.KWinVirtualKeyboard.active) { Keyboards.KWinVirtualKeyboard.active = false; diff --git a/containments/taskpanel/package/contents/ui/main.qml b/containments/taskpanel/package/contents/ui/main.qml index ca76f979..3d092a72 100644 --- a/containments/taskpanel/package/contents/ui/main.qml +++ b/containments/taskpanel/package/contents/ui/main.qml @@ -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