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