mirror of
https://invent.kde.org/marcoa/shift-shell.git
synced 2026-04-26 14:23:09 +00:00
671 lines
18 KiB
QML
671 lines
18 KiB
QML
// SPDX-FileCopyrightText: 2025 Luis Büchi <luis.buechi@kdemail.net>
|
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
|
|
pragma ComponentBehavior: Bound
|
|
|
|
import QtQuick
|
|
import QtQuick.Layouts
|
|
|
|
import org.kde.plasma.components 3.0 as PlasmaComponents
|
|
import org.kde.kirigami 2.19 as Kirigami
|
|
|
|
Item {
|
|
id: root
|
|
|
|
required property int phoneWidth
|
|
required property int phoneHeight
|
|
|
|
Layout.preferredWidth: phoneWidth
|
|
Layout.preferredHeight: phoneHeight
|
|
|
|
property bool showBackground: true
|
|
property int fingerSize: 20
|
|
property int _endTimeout: 2000
|
|
|
|
Rectangle {
|
|
id: phone
|
|
width: root.phoneWidth
|
|
height: root.phoneHeight
|
|
|
|
|
|
border.color: {
|
|
let color = Kirigami.Theme.textColor
|
|
// note: luminance calculation from https://en.wikipedia.org/wiki/Relative_luminance
|
|
let luminance = (0.2126*color.r + 0.7152*color.g + 0.0722*color.b);
|
|
if (luminance > 0.5) {
|
|
return Qt.darker(color);
|
|
}
|
|
return Qt.lighter(color);
|
|
}
|
|
border.width: 2
|
|
color: Qt.darker(Kirigami.Theme.backgroundColor)
|
|
radius: Kirigami.Units.largeSpacing
|
|
|
|
Kirigami.Icon {
|
|
visible: root.showBackground
|
|
|
|
source: "start-here-kde"
|
|
smooth: true
|
|
|
|
anchors.verticalCenter: phone.verticalCenter
|
|
anchors.horizontalCenter: phone.horizontalCenter
|
|
}
|
|
|
|
Item {
|
|
id: phoneContent
|
|
|
|
clip: true
|
|
|
|
anchors.horizontalCenter: phone.horizontalCenter
|
|
anchors.verticalCenter: phone.verticalCenter
|
|
|
|
width: phone.width - phone.border.width * 2
|
|
height: phone.height - phone.border.width * 2
|
|
|
|
Rectangle {
|
|
id: dummyWindow
|
|
|
|
Kirigami.Theme.inherit: false
|
|
Kirigami.Theme.colorSet: Kirigami.Theme.Window
|
|
|
|
property real scale: 1
|
|
property real offset: 0
|
|
|
|
color: Kirigami.Theme.backgroundColor
|
|
width: Math.round(phoneContent.width * scale)
|
|
height: Math.round(phoneContent.height * scale)
|
|
radius: Math.max(0, phone.radius - phone.border.width)
|
|
|
|
anchors.verticalCenter: phoneContent.verticalCenter
|
|
anchors.horizontalCenter: phoneContent.horizontalCenter
|
|
anchors.horizontalCenterOffset: Math.round(phoneContent.width * offset)
|
|
|
|
Image {
|
|
source: "konqi_kde.png"
|
|
|
|
anchors.horizontalCenter: dummyWindow.horizontalCenter
|
|
anchors.verticalCenter: dummyWindow.verticalCenter
|
|
|
|
width: {
|
|
if (dummyWindow.width > dummyWindow.height * 0.8) {
|
|
return Math.round(dummyWindow.height * 0.6)
|
|
}
|
|
return Math.round(dummyWindow.width * 0.75)
|
|
}
|
|
fillMode: Image.PreserveAspectFit
|
|
}
|
|
}
|
|
|
|
Rectangle {
|
|
id: dummyWindow2
|
|
|
|
Kirigami.Theme.inherit: false
|
|
Kirigami.Theme.colorSet: Kirigami.Theme.Window
|
|
|
|
property real scale: dummyWindow.scale
|
|
property real offset: 0.5
|
|
|
|
color: Kirigami.Theme.backgroundColor
|
|
width: phoneContent.width * scale
|
|
height: phoneContent.height * scale
|
|
radius: Math.max(0, phone.radius - phone.border.width)
|
|
|
|
anchors.verticalCenter: phoneContent.verticalCenter
|
|
anchors.horizontalCenter: phoneContent.horizontalCenter
|
|
anchors.horizontalCenterOffset: phoneContent.width * (-offset - 0.6 + dummyWindow.offset)
|
|
|
|
Image {
|
|
source: "katie.png"
|
|
|
|
anchors.horizontalCenter: dummyWindow2.horizontalCenter
|
|
anchors.verticalCenter: dummyWindow2.verticalCenter
|
|
|
|
width: {
|
|
if (dummyWindow.width > dummyWindow.height * 0.8) {
|
|
return Math.round(dummyWindow.height * 0.6)
|
|
}
|
|
return Math.round(dummyWindow.width * 0.75)
|
|
}
|
|
fillMode: Image.PreserveAspectFit
|
|
}
|
|
}
|
|
|
|
Rectangle {
|
|
id: dummyWindow3
|
|
|
|
Kirigami.Theme.inherit: false
|
|
Kirigami.Theme.colorSet: Kirigami.Theme.Window
|
|
|
|
property real scale: dummyWindow.scale
|
|
property real offset: 1.2
|
|
|
|
color: Kirigami.Theme.backgroundColor
|
|
width: phoneContent.width * scale
|
|
height: phoneContent.height * scale
|
|
radius: Math.max(0, phone.radius - phone.border.width)
|
|
|
|
anchors.verticalCenter: phoneContent.verticalCenter
|
|
anchors.horizontalCenter: phoneContent.horizontalCenter
|
|
anchors.horizontalCenterOffset: phoneContent.width * (-offset + dummyWindow.offset - dummyWindow2.offset)
|
|
|
|
Image {
|
|
source: "Mascot_konqi-base-plasma.png"
|
|
|
|
anchors.horizontalCenter: dummyWindow3.horizontalCenter
|
|
anchors.verticalCenter: dummyWindow3.verticalCenter
|
|
|
|
width: {
|
|
if (dummyWindow.width > dummyWindow.height * 0.8) {
|
|
return Math.round(dummyWindow.height * 0.6)
|
|
}
|
|
return Math.round(dummyWindow.width * 0.75)
|
|
}
|
|
fillMode: Image.PreserveAspectFit
|
|
}
|
|
}
|
|
}
|
|
|
|
Rectangle {
|
|
id: touchPoint
|
|
|
|
property int size: root.fingerSize
|
|
|
|
width: size
|
|
height: size
|
|
radius: size / 2
|
|
|
|
property real yPosition: 0
|
|
property real xPosition: 0
|
|
|
|
anchors.verticalCenter: phone.bottom
|
|
anchors.verticalCenterOffset: Math.round(-yPosition * root.phoneHeight / 6)
|
|
|
|
anchors.horizontalCenter: phone.horizontalCenter
|
|
anchors.horizontalCenterOffset: Math.round(xPosition * root.phoneWidth * 0.3)
|
|
|
|
color: Qt.lighter(Kirigami.Theme.focusColor)
|
|
border.width: 1
|
|
border.color: Qt.darker(Kirigami.Theme.backgroundColor)
|
|
}
|
|
}
|
|
|
|
// into task switcher animation
|
|
AnimationHandler {
|
|
id: switcherAnimation
|
|
|
|
endTimeout: root._endTimeout
|
|
|
|
function reset(): void {
|
|
touchPoint.yPosition = 0;
|
|
touchPoint.xPosition = 0;
|
|
}
|
|
|
|
animations: [
|
|
NumberAnimation {
|
|
target: touchPoint
|
|
property: "yPosition"
|
|
|
|
from: 0
|
|
to: 1
|
|
|
|
onStarted: {
|
|
switcherAnimation.reset();
|
|
root.touchOnAnim.start()
|
|
}
|
|
|
|
duration: 1500
|
|
easing.type: Easing.InOutQuad
|
|
},
|
|
NumberAnimation {
|
|
target: touchPoint
|
|
property: "size"
|
|
|
|
to: 0
|
|
|
|
duration: Kirigami.Units.longDuration
|
|
easing.type: Easing.InQuad
|
|
},
|
|
NumberAnimation {
|
|
target: touchPoint
|
|
property: "xPosition"
|
|
|
|
from: 0
|
|
to: 1.5
|
|
|
|
onStarted: {
|
|
touchPoint.yPosition = 2;
|
|
root.touchOnAnim.start()
|
|
}
|
|
duration: 500
|
|
easing.type: Easing.InOutQuad
|
|
},
|
|
NumberAnimation {
|
|
target: touchPoint
|
|
property: "size"
|
|
|
|
to: 0
|
|
|
|
duration: Kirigami.Units.longDuration
|
|
easing.type: Easing.InQuad
|
|
}
|
|
]
|
|
delays: [
|
|
500,
|
|
500,
|
|
-Kirigami.Units.longDuration * 2,
|
|
]
|
|
}
|
|
|
|
AnimationHandler {
|
|
id: windowSwitcherAnimation
|
|
|
|
endTimeout: root._endTimeout
|
|
|
|
function reset(): void {
|
|
dummyWindow.offset = 0;
|
|
dummyWindow.scale = 1;
|
|
dummyWindow2.offset = 0.5;
|
|
}
|
|
|
|
animations: [
|
|
NumberAnimation {
|
|
target: dummyWindow
|
|
property: "scale"
|
|
|
|
from: 1
|
|
to: 0.5
|
|
|
|
onStarted: {
|
|
windowSwitcherAnimation.reset();
|
|
}
|
|
|
|
duration: switcherAnimation.animations[0].duration
|
|
easing.type: switcherAnimation.animations[0].easing.type
|
|
},
|
|
NumberAnimation {
|
|
target: dummyWindow
|
|
property: "scale"
|
|
|
|
to: 0.55
|
|
|
|
duration: Kirigami.Units.longDuration
|
|
easing.type: Easing.InOutQuad
|
|
},
|
|
NumberAnimation {
|
|
target: dummyWindow2
|
|
property: "offset"
|
|
|
|
to: 0
|
|
|
|
duration: Kirigami.Units.longDuration
|
|
easing.type: Easing.InOutQuad
|
|
},
|
|
NumberAnimation {
|
|
target: dummyWindow
|
|
property: "offset"
|
|
|
|
to: 0.6
|
|
|
|
duration: switcherAnimation.animations[2].duration
|
|
easing.type: switcherAnimation.animations[2].easing.type
|
|
}
|
|
]
|
|
delays: [
|
|
switcherAnimation.delays[0],
|
|
immediate,
|
|
switcherAnimation.delays[1],
|
|
]
|
|
}
|
|
|
|
// flick to home animation
|
|
AnimationHandler {
|
|
id: flickAnimation
|
|
|
|
endTimeout: root._endTimeout
|
|
|
|
function reset(): void {
|
|
touchPoint.yPosition = 0;
|
|
}
|
|
|
|
animations: [
|
|
NumberAnimation {
|
|
target: touchPoint
|
|
property: "yPosition"
|
|
|
|
from: 0
|
|
to: 1
|
|
|
|
onStarted: {
|
|
root.touchOnAnim.start()
|
|
}
|
|
|
|
duration: 900
|
|
easing.type: Easing.InQuart
|
|
},
|
|
NumberAnimation {
|
|
target: touchPoint
|
|
property: "size"
|
|
|
|
to: 0
|
|
|
|
duration: Kirigami.Units.longDuration
|
|
easing.type: Easing.InQuad
|
|
}
|
|
]
|
|
delays: [
|
|
-Kirigami.Units.longDuration,
|
|
]
|
|
}
|
|
|
|
AnimationHandler {
|
|
id: windowFlickAnimation
|
|
|
|
endTimeout: root._endTimeout
|
|
|
|
function reset(): void {
|
|
dummyWindow.offset = 0;
|
|
dummyWindow.opacity = 1;
|
|
dummyWindow.scale = 1;
|
|
}
|
|
|
|
animations: [
|
|
NumberAnimation {
|
|
target: dummyWindow
|
|
property: "scale"
|
|
|
|
from: 1
|
|
to: 0.5
|
|
|
|
onStarted: {
|
|
windowFlickAnimation.reset();
|
|
dummyWindow2.offset = 0.5;
|
|
}
|
|
|
|
duration: flickAnimation.animations[0].duration
|
|
easing.type: flickAnimation.animations[0].easing.type
|
|
},
|
|
NumberAnimation {
|
|
target: dummyWindow
|
|
property: "scale"
|
|
|
|
to: 0.1
|
|
|
|
duration: 300
|
|
easing.type: Easing.InQuad
|
|
},
|
|
NumberAnimation {
|
|
target: dummyWindow
|
|
property: "opacity"
|
|
|
|
to: 0
|
|
|
|
duration: 300
|
|
easing.type: Easing.Linear
|
|
}
|
|
]
|
|
delays: [
|
|
1, // for some reason setting this to 0 creates a runtime error
|
|
immediate,
|
|
]
|
|
}
|
|
|
|
// scrub animation
|
|
AnimationHandler {
|
|
id: scrubAnimation
|
|
|
|
endTimeout: root._endTimeout
|
|
|
|
function reset(): void {
|
|
touchPoint.yPosition = 0;
|
|
touchPoint.xPosition = 0;
|
|
}
|
|
|
|
animations: [
|
|
NumberAnimation {
|
|
target: touchPoint
|
|
property: "yPosition"
|
|
|
|
from: 0
|
|
to: 0.2
|
|
|
|
onStarted: {
|
|
scrubAnimation.reset();
|
|
root.touchOnAnim.start()
|
|
}
|
|
|
|
duration: 900
|
|
easing.type: Easing.InOutQuart
|
|
},
|
|
NumberAnimation {
|
|
target: touchPoint
|
|
property: "xPosition"
|
|
|
|
from: 0
|
|
to: 1
|
|
|
|
duration: 1500
|
|
easing.type: Easing.InOutQuart
|
|
},
|
|
NumberAnimation {
|
|
target: touchPoint
|
|
property: "xPosition"
|
|
|
|
to: 0.5
|
|
|
|
duration: 700
|
|
easing.type: Easing.InOutCubic
|
|
},
|
|
NumberAnimation {
|
|
target: touchPoint
|
|
property: "size"
|
|
|
|
to: 0
|
|
|
|
duration: Kirigami.Units.longDuration
|
|
easing.type: Easing.InQuad
|
|
}
|
|
]
|
|
delays: [
|
|
0,
|
|
0,
|
|
500
|
|
]
|
|
}
|
|
|
|
AnimationHandler {
|
|
id: windowScrubAnimation
|
|
|
|
endTimeout: root._endTimeout
|
|
|
|
function reset(): void {
|
|
dummyWindow.scale = 1;
|
|
dummyWindow.offset = 0;
|
|
dummyWindow2.offset = 0.5
|
|
dummyWindow3.offset = 1.2;
|
|
}
|
|
|
|
animations: [
|
|
NumberAnimation {
|
|
// this is just to add some delay at the start when starting at the same time as the touch point animation
|
|
target: dummyWindow
|
|
property: "opacity"
|
|
|
|
to: 1
|
|
|
|
onStarted: {
|
|
windowScrubAnimation.reset();
|
|
}
|
|
|
|
duration: 0
|
|
},
|
|
NumberAnimation {
|
|
target: dummyWindow
|
|
property: "scale"
|
|
|
|
to: 0.55
|
|
|
|
|
|
duration: 300
|
|
easing.type: Easing.InOutQuad
|
|
},
|
|
NumberAnimation {
|
|
target: dummyWindow2
|
|
property: "offset"
|
|
|
|
to: 0
|
|
|
|
duration: 300
|
|
easing.type: Easing.InOutQuad
|
|
},
|
|
NumberAnimation {
|
|
target: dummyWindow
|
|
property: "offset"
|
|
|
|
to: 1.2
|
|
|
|
duration: scrubAnimation.animations[1].duration
|
|
easing.type: Easing.InOutQuint
|
|
},
|
|
NumberAnimation {
|
|
target: dummyWindow
|
|
property: "offset"
|
|
|
|
to: 0.6
|
|
|
|
duration: scrubAnimation.animations[2].duration
|
|
easing.type: Easing.InOutQuart
|
|
},
|
|
NumberAnimation {
|
|
target: dummyWindow
|
|
property: "scale"
|
|
|
|
to: 1
|
|
|
|
duration: Kirigami.Units.longDuration
|
|
easing.type: Easing.InOutQuad
|
|
},
|
|
NumberAnimation { // move leftmost window out of the way, otherwise it overlaps
|
|
target: dummyWindow3
|
|
property: "offset"
|
|
|
|
to: 1.8
|
|
|
|
duration: Kirigami.Units.longDuration
|
|
easing.type: Easing.InOutQuad
|
|
},
|
|
NumberAnimation { // move middle window (that's to be focused) a bit to the side to counteract moving of the first window
|
|
target: dummyWindow2
|
|
property: "offset"
|
|
|
|
to: 0.4
|
|
|
|
duration: Kirigami.Units.longDuration
|
|
easing.type: Easing.Linear
|
|
},
|
|
NumberAnimation { // move first (rightmost) window to get a bit more space between it and the middle one during the animation
|
|
target: dummyWindow
|
|
property: "offset"
|
|
|
|
to: 1
|
|
|
|
duration: Kirigami.Units.longDuration
|
|
easing.type: Easing.Linear
|
|
}
|
|
]
|
|
delays: [
|
|
400,
|
|
immediate,
|
|
scrubAnimation.animations[0].duration - 300 - 400,
|
|
scrubAnimation.delays[1],
|
|
scrubAnimation.delays[2],
|
|
immediate,
|
|
immediate,
|
|
immediate
|
|
]
|
|
}
|
|
|
|
function stopAnimation(): void {
|
|
switcherAnimation.finished.disconnect(startSwitcherAnimation);
|
|
flickAnimation.finished.disconnect(startFlickAnimation);
|
|
scrubAnimation.finished.disconnect(startScrubAnimation);
|
|
|
|
switcherAnimation.stop();
|
|
flickAnimation.stop();
|
|
scrubAnimation.stop();
|
|
}
|
|
|
|
function startSwitcherAnimation(): void {
|
|
switcherAnimation.start();
|
|
windowSwitcherAnimation.start();
|
|
}
|
|
|
|
function loopSwitcherAnimation(): void {
|
|
switcherAnimation.finished.connect(startSwitcherAnimation);
|
|
startSwitcherAnimation();
|
|
}
|
|
|
|
function startFlickAnimation(): void {
|
|
flickAnimation.start();
|
|
windowFlickAnimation.start();
|
|
}
|
|
|
|
function loopFlickAnimation(): void {
|
|
flickAnimation.finished.connect(startFlickAnimation);
|
|
startFlickAnimation();
|
|
|
|
}
|
|
|
|
function startScrubAnimation(): void {
|
|
scrubAnimation.start();
|
|
windowScrubAnimation.start();
|
|
}
|
|
|
|
function loopScrubAnimation(): void {
|
|
scrubAnimation.finished.connect(startScrubAnimation);
|
|
startScrubAnimation();
|
|
}
|
|
|
|
function startCyclingAnimations(): void {
|
|
switcherAnimation.finished.connect(() => {
|
|
switcherAnimation.reset();
|
|
windowSwitcherAnimation.reset();
|
|
|
|
startFlickAnimation();
|
|
});
|
|
flickAnimation.finished.connect(() => {
|
|
flickAnimation.reset();
|
|
windowFlickAnimation.reset();
|
|
|
|
startScrubAnimation();
|
|
});
|
|
scrubAnimation.finished.connect(() => {
|
|
scrubAnimation.reset();
|
|
windowScrubAnimation.reset();
|
|
|
|
startSwitcherAnimation();
|
|
});
|
|
|
|
startSwitcherAnimation();
|
|
}
|
|
|
|
NumberAnimation {
|
|
id: touchOffAnim
|
|
target: touchPoint
|
|
property: "size"
|
|
|
|
to: 0
|
|
|
|
duration: Kirigami.Units.longDuration
|
|
easing.type: Easing.InQuad
|
|
}
|
|
|
|
property var touchOnAnim: NumberAnimation {
|
|
target: touchPoint
|
|
property: "size"
|
|
|
|
to: root.fingerSize
|
|
|
|
duration: Kirigami.Units.longDuration
|
|
easing.type: Easing.OutQuad
|
|
}
|
|
}
|