startupfeedback: Change to be window based and controlled by a model

This makes the startup feedback more robust, by having instances be controlled by a model which can listen to window changes. Being window based also allows for the close button and gestures to work properly with it, since it will show up in the task switcher as well.

Fixes:
* https://invent.kde.org/plasma/plasma-mobile/-/issues/357
* https://invent.kde.org/plasma/plasma-mobile/-/issues/338
* https://invent.kde.org/plasma/plasma-mobile/-/issues/335 (dark themes now tint the background color)
* https://invent.kde.org/plasma/plasma-mobile/-/issues/330
* https://invent.kde.org/plasma/plasma-mobile/-/issues/30
This commit is contained in:
Devin Lin 2024-07-13 16:30:07 +00:00
parent 41de61ef4c
commit 2d2b7407a6
26 changed files with 994 additions and 444 deletions

View file

@ -32,7 +32,7 @@ file(GLOB_RECURSE _qml_sources
ecm_target_qml_sources(mobileshellplugin SOURCES ${_qml_sources}) ecm_target_qml_sources(mobileshellplugin SOURCES ${_qml_sources})
target_link_libraries(mobileshellplugin target_link_libraries(mobileshellplugin
PUBLIC PUBLIC
Qt::Core Qt::Core
PRIVATE PRIVATE

View file

@ -18,9 +18,9 @@ import org.kde.plasma.components 3.0 as PlasmaComponents
MobileShell.BaseItem { MobileShell.BaseItem {
id: root id: root
required property bool restrictedPermissions required property bool restrictedPermissions
// Model interface // Model interface
required property string text required property string text
required property string status required property string status
@ -28,19 +28,19 @@ MobileShell.BaseItem {
required property bool enabled required property bool enabled
required property string settingsCommand required property string settingsCommand
required property var toggleFunction required property var toggleFunction
signal closeRequested() signal closeRequested()
// set by children // set by children
property var iconItem property var iconItem
readonly property color enabledButtonBorderColor: Qt.darker(Kirigami.Theme.highlightColor, 1.25) 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 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 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 enabledButtonPressedColor: Kirigami.ColorUtils.tintWithAlpha(Kirigami.Theme.highlightColor, Kirigami.Theme.backgroundColor, 0.4);
readonly property color disabledButtonColor: Kirigami.Theme.backgroundColor readonly property color disabledButtonColor: Kirigami.Theme.backgroundColor
readonly property color disabledButtonPressedColor: Qt.darker(disabledButtonColor, 1.1) readonly property color disabledButtonPressedColor: Qt.darker(disabledButtonColor, 1.1)
// scale animation on press // scale animation on press
property real zoomScale: 1 property real zoomScale: 1
Behavior on zoomScale { Behavior on zoomScale {
@ -49,14 +49,14 @@ MobileShell.BaseItem {
easing.type: Easing.OutExpo easing.type: Easing.OutExpo
} }
} }
transform: Scale { transform: Scale {
origin.x: root.width / 2; origin.x: root.width / 2;
origin.y: root.height / 2; origin.y: root.height / 2;
xScale: root.zoomScale xScale: root.zoomScale
yScale: root.zoomScale yScale: root.zoomScale
} }
function delegateClick() { function delegateClick() {
if (root.toggle) { if (root.toggle) {
root.toggle(); root.toggle();
@ -65,19 +65,29 @@ MobileShell.BaseItem {
} else if (root.settingsCommand && !root.restrictedPermissions) { } else if (root.settingsCommand && !root.restrictedPermissions) {
closeRequested(); closeRequested();
MobileShellState.ShellDBusClient.openAppLaunchAnimation( MobileShellState.ShellDBusClient.openAppLaunchAnimationWithPosition(
__getCurrentScreenNumber(), __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); MobileShell.ShellUtil.executeCommand(root.settingsCommand);
} }
} }
function delegatePressAndHold() { function delegatePressAndHold() {
if (root.settingsCommand && !root.restrictedPermissions) { if (root.settingsCommand && !root.restrictedPermissions) {
closeRequested(); closeRequested();
MobileShellState.ShellDBusClient.openAppLaunchAnimation( MobileShellState.ShellDBusClient.openAppLaunchAnimationWithPosition(
__getCurrentScreenNumber(), __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); MobileShell.ShellUtil.executeCommand(root.settingsCommand);
} else if (root.toggleFunction) { } else if (root.toggleFunction) {
root.toggleFunction(); root.toggleFunction();

View file

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

View file

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

View file

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

View file

@ -40,12 +40,6 @@ Item {
*/ */
property alias contentItem: itemContainer.contentItem 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 * The root PlasmoidItem of the containment this is used into
*/ */
@ -102,26 +96,6 @@ Item {
root.resetHomeScreenPosition(); 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() { function onIsTaskSwitcherVisibleChanged() {
if (MobileShellState.ShellDBusClient.isTaskSwitcherVisible) { if (MobileShellState.ShellDBusClient.isTaskSwitcherVisible) {
itemContainer.zoomOutImmediately(); itemContainer.zoomOutImmediately();
@ -210,10 +184,17 @@ Item {
} }
} }
// start app animation component // App start animation component
MobileShell.StartupFeedback { MobileShell.StartupFeedbackWindows {
id: startupFeedback id: startupFeedbackWindows
z: 999999 screen: Plasmoid.screen
topMargin: root.topMargin
bottomMargin: root.bottomMargin
leftMargin: root.leftMargin
rightMargin: root.rightMargin
anchors.fill: parent anchors.fill: parent
visible: false
} }
} }

View file

@ -5,6 +5,8 @@ set(mobileshellstateplugin_SRCS
shelldbusobject.cpp shelldbusobject.cpp
shelldbusclient.cpp shelldbusclient.cpp
lockscreendbusclient.cpp lockscreendbusclient.cpp
startupfeedbackmodel.cpp
windowlistener.cpp
) )
qt_generate_dbus_interface( 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_sources(mobileshellstateplugin PRIVATE ${mobileshellstateplugin_SRCS} ${RESOURCES})
target_link_libraries(mobileshellstateplugin target_link_libraries(mobileshellstateplugin
PUBLIC PUBLIC
Qt::Core Qt::Core
PRIVATE PRIVATE
@ -34,6 +36,7 @@ target_link_libraries(mobileshellstateplugin
Qt::Quick Qt::Quick
Qt::DBus Qt::DBus
Plasma::Plasma Plasma::Plasma
Plasma::KWaylandClient
KF6::I18n KF6::I18n
KF6::Notifications KF6::Notifications
Plasma::PlasmaQuick Plasma::PlasmaQuick

View file

@ -34,12 +34,10 @@ void ShellDBusClient::connectSignals()
connect(m_interface, &OrgKdePlasmashellInterface::isTaskSwitcherVisibleChanged, this, &ShellDBusClient::updateIsTaskSwitcherVisible); connect(m_interface, &OrgKdePlasmashellInterface::isTaskSwitcherVisibleChanged, this, &ShellDBusClient::updateIsTaskSwitcherVisible);
connect(m_interface, &OrgKdePlasmashellInterface::openActionDrawerRequested, this, &ShellDBusClient::openActionDrawerRequested); connect(m_interface, &OrgKdePlasmashellInterface::openActionDrawerRequested, this, &ShellDBusClient::openActionDrawerRequested);
connect(m_interface, &OrgKdePlasmashellInterface::closeActionDrawerRequested, this, &ShellDBusClient::closeActionDrawerRequested); connect(m_interface, &OrgKdePlasmashellInterface::closeActionDrawerRequested, this, &ShellDBusClient::closeActionDrawerRequested);
connect(m_interface, &OrgKdePlasmashellInterface::openAppLaunchAnimationRequested, this, &ShellDBusClient::openAppLaunchAnimationRequested);
connect(m_interface, connect(m_interface,
&OrgKdePlasmashellInterface::openAppLaunchAnimationWithPositionRequested, &OrgKdePlasmashellInterface::appLaunchMaximizePanelAnimationTriggered,
this, this,
&ShellDBusClient::openAppLaunchAnimationWithPositionRequested); &ShellDBusClient::appLaunchMaximizePanelAnimationTriggered);
connect(m_interface, &OrgKdePlasmashellInterface::closeAppLaunchAnimationRequested, this, &ShellDBusClient::closeAppLaunchAnimationRequested);
connect(m_interface, &OrgKdePlasmashellInterface::openHomeScreenRequested, this, &ShellDBusClient::openHomeScreenRequested); connect(m_interface, &OrgKdePlasmashellInterface::openHomeScreenRequested, this, &ShellDBusClient::openHomeScreenRequested);
connect(m_interface, &OrgKdePlasmashellInterface::resetHomeScreenPositionRequested, this, &ShellDBusClient::resetHomeScreenPositionRequested); connect(m_interface, &OrgKdePlasmashellInterface::resetHomeScreenPositionRequested, this, &ShellDBusClient::resetHomeScreenPositionRequested);
connect(m_interface, &OrgKdePlasmashellInterface::showVolumeOSDRequested, this, &ShellDBusClient::showVolumeOSDRequested); connect(m_interface, &OrgKdePlasmashellInterface::showVolumeOSDRequested, this, &ShellDBusClient::showVolumeOSDRequested);
@ -84,19 +82,20 @@ bool ShellDBusClient::isTaskSwitcherVisible() const
return m_isTaskSwitcherVisible; 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); m_interface->triggerAppLaunchMaximizePanelAnimation(screen, color);
}
void ShellDBusClient::closeAppLaunchAnimation()
{
m_interface->closeAppLaunchAnimation();
} }
void ShellDBusClient::openHomeScreen() void ShellDBusClient::openHomeScreen()

View file

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

View file

@ -8,6 +8,7 @@
ShellDBusObject::ShellDBusObject(QObject *parent) ShellDBusObject::ShellDBusObject(QObject *parent)
: 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() bool ShellDBusObject::doNotDisturb()
{ {
return m_doNotDisturb; return m_doNotDisturb;
@ -69,19 +75,25 @@ void ShellDBusObject::closeActionDrawer()
Q_EMIT closeActionDrawerRequested(); 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); Q_EMIT appLaunchMaximizePanelAnimationTriggered(screen, color);
}
void ShellDBusObject::closeAppLaunchAnimation()
{
Q_EMIT closeAppLaunchAnimationRequested();
} }
void ShellDBusObject::openHomeScreen() void ShellDBusObject::openHomeScreen()

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -10,7 +10,7 @@ import org.kde.plasma.core as PlasmaCore
import org.kde.plasma.components 3.0 as PlasmaComponents import org.kde.plasma.components 3.0 as PlasmaComponents
import org.kde.kquickcontrolsaddons 2.0 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 as MobileShell
import org.kde.plasma.private.mobileshell.shellsettingsplugin as ShellSettings import org.kde.plasma.private.mobileshell.shellsettingsplugin as ShellSettings
import org.kde.plasma.private.mobileshell.state as MobileShellState import org.kde.plasma.private.mobileshell.state as MobileShellState
@ -23,30 +23,30 @@ Item {
property int visualIndex: 0 property int visualIndex: 0
property real dragFolderAnimationProgress: 0 property real dragFolderAnimationProgress: 0
property list<Kirigami.Action> menuActions property list<Kirigami.Action> menuActions
// whether this delegate is a folder // whether this delegate is a folder
property bool isFolder property bool isFolder
// folder object // folder object
property var folder property var folder
readonly property string folderName: folder ? folder.name : "" readonly property string folderName: folder ? folder.name : ""
// app object // app object
property var application property var application
readonly property string applicationName: application ? application.name : "" readonly property string applicationName: application ? application.name : ""
readonly property string applicationStorageId: application ? application.storageId : "" readonly property string applicationStorageId: application ? application.storageId : ""
readonly property string applicationIcon: application ? application.icon : "" readonly property string applicationIcon: application ? application.icon : ""
signal folderOpenRequested() signal folderOpenRequested()
property alias drag: mouseArea.drag property alias drag: mouseArea.drag
Drag.active: delegate.drag.active Drag.active: delegate.drag.active
Drag.source: delegate Drag.source: delegate
Drag.hotSpot.x: delegate.width / 2 Drag.hotSpot.x: delegate.width / 2
Drag.hotSpot.y: delegate.height / 2 Drag.hotSpot.y: delegate.height / 2
// close context menu if drag move // close context menu if drag move
onXChanged: { onXChanged: {
if (dialogLoader.item) { if (dialogLoader.item) {
@ -58,12 +58,12 @@ Item {
dialogLoader.item.close() dialogLoader.item.close()
} }
} }
function openContextMenu() { function openContextMenu() {
dialogLoader.active = true; dialogLoader.active = true;
dialogLoader.item.open(); dialogLoader.item.open();
} }
function launch() { function launch() {
if (isFolder) { if (isFolder) {
folderOpenRequested(); folderOpenRequested();
@ -75,13 +75,14 @@ Item {
} }
} }
} }
function launchAppWithAnim(x: int, y: int, source, title: string, storageId: string) { function launchAppWithAnim(x: int, y: int, source, title: string, storageId: string) {
if (source !== "") { if (source !== "") {
MobileShellState.ShellDBusClient.openAppLaunchAnimationWithPosition( MobileShellState.ShellDBusClient.openAppLaunchAnimationWithPosition(
Plasmoid.screen, Plasmoid.screen,
source, source,
title, title,
storageId,
iconLoader.Kirigami.ScenePosition.x + iconLoader.width/2, iconLoader.Kirigami.ScenePosition.x + iconLoader.width/2,
iconLoader.Kirigami.ScenePosition.y + iconLoader.height/2, iconLoader.Kirigami.ScenePosition.y + iconLoader.height/2,
Math.min(iconLoader.width, iconLoader.height)); Math.min(iconLoader.width, iconLoader.height));
@ -90,16 +91,16 @@ Item {
application.setMinimizedDelegate(delegate); application.setMinimizedDelegate(delegate);
MobileShell.AppLaunch.launchOrActivateApp(application.storageId); MobileShell.AppLaunch.launchOrActivateApp(application.storageId);
} }
Loader { Loader {
id: dialogLoader id: dialogLoader
active: false active: false
sourceComponent: PlasmaComponents.Menu { sourceComponent: PlasmaComponents.Menu {
id: menu id: menu
title: label.text title: label.text
closePolicy: PlasmaComponents.Menu.CloseOnReleaseOutside | PlasmaComponents.Menu.CloseOnEscape closePolicy: PlasmaComponents.Menu.CloseOnReleaseOutside | PlasmaComponents.Menu.CloseOnEscape
Repeater { Repeater {
model: menuActions model: menuActions
delegate: PlasmaComponents.MenuItem { delegate: PlasmaComponents.MenuItem {
@ -108,18 +109,18 @@ Item {
onClicked: modelData.triggered() onClicked: modelData.triggered()
} }
} }
onClosed: dialogLoader.active = false onClosed: dialogLoader.active = false
} }
} }
MouseArea { MouseArea {
id: mouseArea id: mouseArea
anchors.fill: parent anchors.fill: parent
property bool inDrag: false property bool inDrag: false
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton | Qt.RightButton acceptedButtons: Qt.LeftButton | Qt.RightButton
onReleased: { onReleased: {
@ -128,18 +129,18 @@ Item {
} }
onPressAndHold: { inDrag = true; openContextMenu() } onPressAndHold: { inDrag = true; openContextMenu() }
drag.target: inDrag ? delegate : undefined drag.target: inDrag ? delegate : undefined
// grow/shrink animation // grow/shrink animation
property real zoomScale: 1 property real zoomScale: 1
transform: Scale { transform: Scale {
origin.x: mouseArea.width / 2; origin.x: mouseArea.width / 2;
origin.y: mouseArea.height / 2; origin.y: mouseArea.height / 2;
xScale: mouseArea.zoomScale xScale: mouseArea.zoomScale
yScale: mouseArea.zoomScale yScale: mouseArea.zoomScale
} }
property bool launchAppRequested: false property bool launchAppRequested: false
NumberAnimation on zoomScale { NumberAnimation on zoomScale {
id: shrinkAnim id: shrinkAnim
running: false running: false
@ -151,7 +152,7 @@ Item {
} }
} }
} }
NumberAnimation on zoomScale { NumberAnimation on zoomScale {
id: growAnim id: growAnim
running: false running: false
@ -164,7 +165,7 @@ Item {
} }
} }
} }
onPressedChanged: { onPressedChanged: {
if (pressed) { if (pressed) {
growAnim.stop(); growAnim.stop();
@ -173,7 +174,7 @@ Item {
growAnim.restart(); growAnim.restart();
} }
} }
// launch app handled by press animation // launch app handled by press animation
onClicked: mouse => { onClicked: mouse => {
if (mouse.button === Qt.RightButton) { if (mouse.button === Qt.RightButton) {
@ -182,19 +183,19 @@ Item {
launchAppRequested = true; launchAppRequested = true;
} }
} }
HoverHandler { HoverHandler {
id: hoverHandler id: hoverHandler
acceptedDevices: PointerDevice.Mouse acceptedDevices: PointerDevice.Mouse
acceptedPointerTypes: PointerDevice.Generic acceptedPointerTypes: PointerDevice.Generic
} }
Rectangle { Rectangle {
anchors.fill: parent anchors.fill: parent
radius: height / 2 radius: height / 2
color: mouseArea.pressed ? Qt.rgba(255, 255, 255, 0.2) : "transparent" color: mouseArea.pressed ? Qt.rgba(255, 255, 255, 0.2) : "transparent"
} }
RowLayout { RowLayout {
id: rowLayout id: rowLayout
anchors { anchors {
@ -234,11 +235,11 @@ Item {
font.pointSize: Kirigami.Theme.defaultFont.pointSize font.pointSize: Kirigami.Theme.defaultFont.pointSize
font.weight: Font.Bold font.weight: Font.Bold
color: "white" color: "white"
layer.enabled: true layer.enabled: true
layer.effect: MobileShell.TextDropShadow {} layer.effect: MobileShell.TextDropShadow {}
} }
Kirigami.Icon { Kirigami.Icon {
Layout.alignment: Qt.AlignRight Layout.alignment: Qt.AlignRight
Layout.preferredWidth: Kirigami.Units.iconSizes.small Layout.preferredWidth: Kirigami.Units.iconSizes.small
@ -259,10 +260,10 @@ Item {
} }
} }
} }
Component { Component {
id: appIconComponent id: appIconComponent
Item { Item {
Rectangle { Rectangle {
anchors.fill: parent anchors.fill: parent
@ -271,14 +272,14 @@ Item {
radius: Kirigami.Units.smallSpacing radius: Kirigami.Units.smallSpacing
opacity: delegate.dragFolderAnimationProgress opacity: delegate.dragFolderAnimationProgress
} }
Kirigami.Icon { Kirigami.Icon {
id: icon id: icon
anchors.fill: parent anchors.fill: parent
source: delegate.isFolder ? 'document-open-folder' : delegate.applicationIcon source: delegate.isFolder ? 'document-open-folder' : delegate.applicationIcon
transform: Scale { transform: Scale {
origin.x: icon.width / 2 origin.x: icon.width / 2
origin.y: icon.height / 2 origin.y: icon.height / 2
xScale: 1 - delegate.dragFolderAnimationProgress * 0.5 xScale: 1 - delegate.dragFolderAnimationProgress * 0.5
yScale: 1 - delegate.dragFolderAnimationProgress * 0.5 yScale: 1 - delegate.dragFolderAnimationProgress * 0.5
@ -295,7 +296,7 @@ Item {
height: width height: width
color: Kirigami.Theme.highlightColor color: Kirigami.Theme.highlightColor
} }
layer.enabled: true layer.enabled: true
layer.effect: MultiEffect { layer.effect: MultiEffect {
shadowEnabled: true shadowEnabled: true
@ -306,10 +307,10 @@ Item {
} }
} }
} }
Component { Component {
id: folderIconComponent id: folderIconComponent
Item { Item {
Rectangle { Rectangle {
id: rect id: rect
@ -317,31 +318,31 @@ Item {
anchors.margins: Kirigami.Units.smallSpacing anchors.margins: Kirigami.Units.smallSpacing
color: Qt.rgba(255, 255, 255, 0.2) color: Qt.rgba(255, 255, 255, 0.2)
radius: Kirigami.Units.smallSpacing radius: Kirigami.Units.smallSpacing
transform: Scale { transform: Scale {
origin.x: rect.width / 2 origin.x: rect.width / 2
origin.y: rect.height / 2 origin.y: rect.height / 2
xScale: 1 + delegate.dragFolderAnimationProgress * 0.5 xScale: 1 + delegate.dragFolderAnimationProgress * 0.5
yScale: 1 + delegate.dragFolderAnimationProgress * 0.5 yScale: 1 + delegate.dragFolderAnimationProgress * 0.5
} }
} }
Grid { Grid {
id: grid id: grid
anchors.fill: parent anchors.fill: parent
anchors.margins: Kirigami.Units.smallSpacing * 2 anchors.margins: Kirigami.Units.smallSpacing * 2
columns: 2 columns: 2
spacing: Kirigami.Units.smallSpacing spacing: Kirigami.Units.smallSpacing
property var previews: model.folder.appPreviews property var previews: model.folder.appPreviews
Repeater { Repeater {
model: grid.previews model: grid.previews
delegate: Kirigami.Icon { delegate: Kirigami.Icon {
implicitWidth: (grid.width - Kirigami.Units.smallSpacing) / 2 implicitWidth: (grid.width - Kirigami.Units.smallSpacing) / 2
implicitHeight: (grid.width - Kirigami.Units.smallSpacing) / 2 implicitHeight: (grid.width - Kirigami.Units.smallSpacing) / 2
source: modelData.icon source: modelData.icon
layer.enabled: true layer.enabled: true
layer.effect: MultiEffect { layer.effect: MultiEffect {
shadowEnabled: true shadowEnabled: true

View file

@ -19,7 +19,7 @@ MobileShell.GridView {
id: gridView id: gridView
cacheBuffer: cellHeight * 20 // 10 rows above and below cacheBuffer: cellHeight * 20 // 10 rows above and below
reuseItems: true reuseItems: true
Controls.ScrollBar.vertical: Controls.ScrollBar {} Controls.ScrollBar.vertical: Controls.ScrollBar {}
Connections { Connections {
@ -32,10 +32,10 @@ MobileShell.GridView {
// ensure items aren't visible out of bounds // ensure items aren't visible out of bounds
layer.enabled: true layer.enabled: true
readonly property int reservedSpaceForLabel: metrics.height readonly property int reservedSpaceForLabel: metrics.height
readonly property real effectiveContentWidth: width - leftMargin - rightMargin readonly property real effectiveContentWidth: width - leftMargin - rightMargin
cellWidth: gridView.effectiveContentWidth / Math.min(Math.floor(effectiveContentWidth / (Kirigami.Units.iconSizes.huge + Kirigami.Units.largeSpacing * 2)), 8) cellWidth: gridView.effectiveContentWidth / Math.min(Math.floor(effectiveContentWidth / (Kirigami.Units.iconSizes.huge + Kirigami.Units.largeSpacing * 2)), 8)
cellHeight: cellWidth + reservedSpaceForLabel cellHeight: cellWidth + reservedSpaceForLabel
@ -45,14 +45,14 @@ MobileShell.GridView {
function goToBeginning() { function goToBeginning() {
goToBeginningAnim.restart(); goToBeginningAnim.restart();
} }
NumberAnimation on contentY { NumberAnimation on contentY {
id: goToBeginningAnim id: goToBeginningAnim
to: gridView.originY to: gridView.originY
duration: 200 duration: 200
easing.type: Easing.InOutQuad easing.type: Easing.InOutQuad
} }
model: Halcyon.ApplicationListModel model: Halcyon.ApplicationListModel
header: MobileShell.BaseItem { header: MobileShell.BaseItem {
@ -60,7 +60,7 @@ MobileShell.GridView {
topPadding: Kirigami.Units.gridUnit + Math.round(gridView.height * 0.1) topPadding: Kirigami.Units.gridUnit + Math.round(gridView.height * 0.1)
bottomPadding: Kirigami.Units.gridUnit bottomPadding: Kirigami.Units.gridUnit
leftPadding: Kirigami.Units.smallSpacing leftPadding: Kirigami.Units.smallSpacing
contentItem: PC3.Label { contentItem: PC3.Label {
color: "white" color: "white"
font.pointSize: 16 font.pointSize: 16
@ -68,7 +68,7 @@ MobileShell.GridView {
text: i18n("Applications") text: i18n("Applications")
} }
} }
PC3.Label { PC3.Label {
id: metrics id: metrics
text: "M\nM" text: "M\nM"
@ -76,13 +76,13 @@ MobileShell.GridView {
font.pointSize: Kirigami.Theme.defaultFont.pointSize * 0.85 font.pointSize: Kirigami.Theme.defaultFont.pointSize * 0.85
font.weight: Font.Bold font.weight: Font.Bold
} }
Keys.onReturnPressed: currentItem.launchApp() Keys.onReturnPressed: currentItem.launchApp()
delegate: GridAppDelegate { delegate: GridAppDelegate {
id: delegate id: delegate
property Halcyon.Application application: model.application property Halcyon.Application application: model.application
width: gridView.cellWidth width: gridView.cellWidth
height: gridView.cellHeight height: gridView.cellHeight
reservedSpaceForLabel: gridView.reservedSpaceForLabel reservedSpaceForLabel: gridView.reservedSpaceForLabel
@ -93,6 +93,7 @@ MobileShell.GridView {
Plasmoid.screen, Plasmoid.screen,
icon, icon,
title, title,
storageId,
delegate.iconItem.Kirigami.ScenePosition.x + delegate.iconItem.width/2, delegate.iconItem.Kirigami.ScenePosition.x + delegate.iconItem.width/2,
delegate.iconItem.Kirigami.ScenePosition.y + delegate.iconItem.height/2, delegate.iconItem.Kirigami.ScenePosition.y + delegate.iconItem.height/2,
Math.min(delegate.iconItem.width, delegate.iconItem.height)); Math.min(delegate.iconItem.width, delegate.iconItem.height));

View file

@ -58,7 +58,7 @@ ContainmentItem {
Rectangle { Rectangle {
id: darkenBackground 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 anchors.fill: parent
z: -1 z: -1
Behavior on color { Behavior on color {
@ -101,8 +101,6 @@ ContainmentItem {
leftMargin: homeScreen.leftMargin leftMargin: homeScreen.leftMargin
rightMargin: homeScreen.rightMargin rightMargin: homeScreen.rightMargin
// make the homescreen not interactable when task switcher or startup feedback is on
interactive: !homeScreen.overlayShown
searchWidget: search searchWidget: search
} }

View file

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

View file

@ -44,7 +44,8 @@ ContainmentItem {
} }
// only opaque if there are no maximized windows on this screen // 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 readonly property color backgroundColor: topPanel.colorScopeColor
WindowPlugin.WindowMaximizedTracker { WindowPlugin.WindowMaximizedTracker {
@ -96,6 +97,17 @@ ContainmentItem {
MobileShell.VolumeOSDProviderLoader.load(); 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 // top panel component
MobileShell.StatusBar { MobileShell.StatusBar {
id: topPanel id: topPanel

View file

@ -20,7 +20,7 @@ import org.kde.kirigami as Kirigami
MobileShell.NavigationPanel { MobileShell.NavigationPanel {
id: root id: root
required property bool opaqueBar required property bool opaqueBar
// background is: // background is:
// - opaque if an app is shown or vkbd is shown // - opaque if an app is shown or vkbd is shown
// - translucent if the task switcher is open // - translucent if the task switcher is open
@ -28,7 +28,7 @@ MobileShell.NavigationPanel {
backgroundColor: (Keyboards.KWinVirtualKeyboard.active || opaqueBar) ? Kirigami.Theme.backgroundColor : "transparent"; backgroundColor: (Keyboards.KWinVirtualKeyboard.active || opaqueBar) ? Kirigami.Theme.backgroundColor : "transparent";
foregroundColorGroup: opaqueBar ? Kirigami.Theme.Window : Kirigami.Theme.Complementary foregroundColorGroup: opaqueBar ? Kirigami.Theme.Window : Kirigami.Theme.Complementary
shadow: !opaqueBar shadow: !opaqueBar
TaskManager.VirtualDesktopInfo { TaskManager.VirtualDesktopInfo {
id: virtualDesktopInfo id: virtualDesktopInfo
} }
@ -53,42 +53,42 @@ MobileShell.NavigationPanel {
// ~~~~ // ~~~~
// navigation panel actions // navigation panel actions
// toggle task switcher button // toggle task switcher button
leftAction: MobileShell.NavigationPanelAction { leftAction: MobileShell.NavigationPanelAction {
id: taskSwitcherAction id: taskSwitcherAction
enabled: true enabled: true
iconSource: "mobile-task-switcher" iconSource: "mobile-task-switcher"
iconSizeFactor: 0.75 iconSizeFactor: 0.75
onTriggered: { onTriggered: {
Plasmoid.triggerTaskSwitcher(); Plasmoid.triggerTaskSwitcher();
} }
} }
// home button // home button
middleAction: MobileShell.NavigationPanelAction { middleAction: MobileShell.NavigationPanelAction {
id: homeAction id: homeAction
enabled: true enabled: true
iconSource: "start-here-kde" iconSource: "start-here-kde"
iconSizeFactor: 1 iconSizeFactor: 1
onTriggered: { onTriggered: {
MobileShellState.ShellDBusClient.openHomeScreen(); MobileShellState.ShellDBusClient.openHomeScreen();
} }
} }
// close app/keyboard button // close app/keyboard button
rightAction: MobileShell.NavigationPanelAction { rightAction: MobileShell.NavigationPanelAction {
id: closeAppAction id: closeAppAction
enabled: Keyboards.KWinVirtualKeyboard.active || WindowPlugin.WindowUtil.hasCloseableActiveWindow enabled: Keyboards.KWinVirtualKeyboard.active || WindowPlugin.WindowUtil.hasCloseableActiveWindow
iconSource: Keyboards.KWinVirtualKeyboard.active ? "go-down-symbolic" : "mobile-close-app" 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 // mobile-close-app (from plasma-frameworks) seems to have fewer margins than icons from breeze-icons
iconSizeFactor: Keyboards.KWinVirtualKeyboard.active ? 1 : 0.75 iconSizeFactor: Keyboards.KWinVirtualKeyboard.active ? 1 : 0.75
onTriggered: { onTriggered: {
if (Keyboards.KWinVirtualKeyboard.active) { if (Keyboards.KWinVirtualKeyboard.active) {
// close keyboard if it is open // close keyboard if it is open
@ -98,19 +98,18 @@ MobileShell.NavigationPanel {
if (tasksModel.activeTask !== 0) { if (tasksModel.activeTask !== 0) {
tasksModel.requestClose(tasksModel.activeTask); tasksModel.requestClose(tasksModel.activeTask);
} }
MobileShellState.ShellDBusClient.closeAppLaunchAnimation();
} }
} }
} }
rightCornerAction: MobileShell.NavigationPanelAction { rightCornerAction: MobileShell.NavigationPanelAction {
id: keyboardToggleAction id: keyboardToggleAction
visible: ShellSettings.Settings.alwaysShowKeyboardToggleOnNavigationPanel || visible: ShellSettings.Settings.alwaysShowKeyboardToggleOnNavigationPanel ||
(Keyboards.KWinVirtualKeyboard.available && !Keyboards.KWinVirtualKeyboard.activeClientSupportsTextInput) (Keyboards.KWinVirtualKeyboard.available && !Keyboards.KWinVirtualKeyboard.activeClientSupportsTextInput)
enabled: true enabled: true
iconSource: "input-keyboard-virtual-symbolic" iconSource: "input-keyboard-virtual-symbolic"
iconSizeFactor: 0.75 iconSizeFactor: 0.75
onTriggered: { onTriggered: {
if (Keyboards.KWinVirtualKeyboard.active) { if (Keyboards.KWinVirtualKeyboard.active) {
Keyboards.KWinVirtualKeyboard.active = false; Keyboards.KWinVirtualKeyboard.active = false;

View file

@ -16,6 +16,7 @@ import org.kde.kquickcontrolsaddons 2.0
import org.kde.plasma.private.mobileshell as MobileShell import org.kde.plasma.private.mobileshell as MobileShell
import org.kde.plasma.private.mobileshell.shellsettingsplugin as ShellSettings import org.kde.plasma.private.mobileshell.shellsettingsplugin as ShellSettings
import org.kde.plasma.private.mobileshell.windowplugin as WindowPlugin import org.kde.plasma.private.mobileshell.windowplugin as WindowPlugin
import org.kde.plasma.private.mobileshell.state as MobileShellState
ContainmentItem { ContainmentItem {
id: root id: root
@ -110,13 +111,25 @@ ContainmentItem {
Component.onCompleted: setWindowProperties(); Component.onCompleted: setWindowProperties();
// only opaque if there are no maximized windows on this screen // 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 { WindowPlugin.WindowMaximizedTracker {
id: windowMaximizedTracker id: windowMaximizedTracker
screenGeometry: Plasmoid.containment.screenGeometry 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 { Item {
anchors.fill: parent anchors.fill: parent