lockscreen: Rework and simplify keypad appearance, and fix lockscreen state

The current lockscreen mockups were originally from this mockup: https://phabricator.kde.org/T12717

There are some issues with it however:
- The 0 key is on the right, which is a bit strange and differs from the layouts of all other platforms
- One-handed usability is worse due to 4 columns instead of 3
- Most mobile lockscreen have the keypad toward the center for one-handed usability, our keypad is stuck to the bottom
- It makes use of a lot of shadows which makes it slower to load
- It's supposed to emulate the design of the keyboard, but it doesn't look like it so it's out of place

The new design is much simpler, with a centered 3 column approach making keys much easier to reach with one hand. It also avoids the use of shadows and layers.

![image](/uploads/52bd7accf2aaffc2b4054e13ac742bce/image.png)

![image](/uploads/4fe8b43abb1f5e2c2b8278f98b28bc71/image.png)

![image](/uploads/a306cfbe628e8b3e49175cd8f8e77ec1/image.png)

TODO:

- Investigate some improvements for keyboard input
This commit is contained in:
Devin Lin 2024-06-26 02:57:42 +00:00
parent b2bb4b4d6f
commit 5c41092c0f
11 changed files with 456 additions and 618 deletions

View file

@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: 2023 Devin Lin <espidev@gmail.com>
// SPDX-FileCopyrightText: 2023-2024 Devin Lin <devin@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
import QtQuick
@ -27,7 +27,6 @@ Loader {
Kirigami.Icon {
implicitWidth: Kirigami.Units.iconSizes.small
implicitHeight: Kirigami.Units.iconSizes.small
opacity: 1 - flickable.openFactor
Kirigami.Theme.colorSet: Kirigami.Theme.Complementary
source: "arrow-up"
@ -39,7 +38,6 @@ Loader {
Kirigami.Icon {
source: 'fingerprint-symbolic'
opacity: 1 - flickable.openFactor
implicitWidth: Kirigami.Units.iconSizes.medium
implicitHeight: Kirigami.Units.iconSizes.medium

View file

@ -1,6 +1,6 @@
/*
* SPDX-FileCopyrightText: 2019 Nicolas Fella <nicolas.fella@gmx.de>
* SPDX-FileCopyrightText: 2020-2022 Devin Lin <devin@kde.org>
* SPDX-FileCopyrightText: 2020-2024 Devin Lin <devin@kde.org>
* SPDX-License-Identifier: GPL-2.0-or-later
*/
@ -17,9 +17,9 @@ Item {
id: root
implicitHeight: clockColumn.implicitHeight
implicitWidth: clockColumn.implicitWidth
property int layoutAlignment
P5Support.DataSource {
id: timeSource
engine: "time"
@ -27,19 +27,19 @@ Item {
interval: 60000
intervalAlignment: P5Support.Types.AlignToMinute
}
ColumnLayout {
id: clockColumn
spacing: Kirigami.Units.gridUnit
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
PC3.Label {
text: Qt.formatTime(timeSource.data["Local"]["DateTime"], MobileShell.ShellUtil.isSystem24HourFormat ? "h:mm" : "h:mm ap")
color: "white"
Layout.alignment: root.layoutAlignment
font.weight: Font.Bold
font.pointSize: 36
@ -49,11 +49,11 @@ Item {
blurMax: 16
}
}
PC3.Label {
text: Qt.formatDate(timeSource.data["Local"]["DateTime"], "ddd, MMM d")
color: "white"
Layout.alignment: root.layoutAlignment
font.weight: Font.Bold
font.pointSize: 10

View file

@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: 2021-2022 Devin Lin <devin@kde.org>
// SPDX-FileCopyrightText: 2021-2024 Devin Lin <devin@kde.org>
// SPDX-License-Identifier: LGPL-2.0-or-later
import QtQuick 2.15
@ -9,59 +9,66 @@ import org.kde.plasma.private.mobileshell as MobileShell
MobileShell.SwipeArea {
id: root
mode: MobileShell.SwipeArea.VerticalOnly
property int position: 0
required property real keypadHeight
readonly property real openFactor: position / keypadHeight
property real position: 0
property bool movingUp: false
property real __oldPosition: position
signal opened()
mode: MobileShell.SwipeArea.VerticalOnly
function cancelAnimations() {
positionAnim.stop();
}
function goToOpenPosition() {
positionAnim.to = keypadHeight;
positionAnim.restart();
}
function goToClosePosition() {
positionAnim.to = 0;
positionAnim.restart();
}
function updateState() {
// don't update state if at end
if (position <= 0 || position >= keypadHeight) return;
if (movingUp) {
goToOpenPosition();
} else {
goToClosePosition();
}
}
NumberAnimation on position {
id: positionAnim
duration: Kirigami.Units.veryLongDuration
duration: 800
easing.type: Easing.OutExpo
onFinished: {
if (root.position === keypadHeight) {
root.opened();
}
}
}
property int oldPosition: position
property bool movingUp: false
onPositionChanged: {
movingUp = oldPosition <= position;
oldPosition = position;
movingUp = __oldPosition <= position;
__oldPosition = position;
// Limit position to between 0 and keypadHeight
if (position > keypadHeight) {
position = keypadHeight;
} else if (position < 0) {
position = 0;
}
}
onSwipeStarted: cancelAnimations();
onSwipeEnded: {
if (!positionAnim.running) {

View file

@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: 2020-2023 Devin Lin <espidev@gmail.com>
// SPDX-FileCopyrightText: 2020-2024 Devin Lin <devin@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
import QtQuick
@ -13,182 +13,153 @@ import org.kde.plasma.private.mobileshell as MobileShell
import org.kde.kirigami 2.12 as Kirigami
Rectangle {
id: keypadRoot
Item {
id: root
required property real openProgress
required property var lockScreenState
property alias passwordBar: passwordBar
// 0 - keypad is not shown, 1 - keypad is shown
property double swipeProgress
// slightly translucent background, for key contrast
color: Kirigami.ColorUtils.adjustColor(Kirigami.Theme.backgroundColor, {"alpha": 0.9*255})
// colour calculations
readonly property color buttonColor: Qt.lighter(Kirigami.Theme.backgroundColor, 1.3)
readonly property color buttonPressedColor: Qt.darker(Kirigami.Theme.backgroundColor, 1.08)
readonly property color buttonTextColor: Kirigami.Theme.textColor
readonly property color dropShadowColor: Qt.darker(Kirigami.Theme.backgroundColor, 1.2)
readonly property color headerBackgroundColor: Qt.lighter(Kirigami.Theme.backgroundColor, 1.3)
opacity: Math.sin((Math.PI / 2) * swipeProgress + 1.5 * Math.PI) + 1
implicitHeight: {
if (passwordBar.isPinMode && !Qt.inputMethod.visible) {
return Kirigami.Units.gridUnit * 17;
} else {
return Math.min(root.height - passwordBar.implicitHeight, // don't make the password bar go off the screen
Kirigami.Units.smallSpacing * 2 + Qt.inputMethod.keyboardRectangle.height + passwordBar.implicitHeight);
}
}
Behavior on implicitHeight {
NumberAnimation {
duration: Kirigami.Units.longDuration
easing.type: Easing.InOutQuad
}
}
MobileShell.HapticsEffect {
id: haptics
}
// pin display and bar
PasswordBar {
id: passwordBar
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
color: keypadRoot.headerBackgroundColor
opacity: (Math.sin(2*((Math.PI / 2) * keypadRoot.swipeProgress + 1.5 * Math.PI)) + 1)
lockScreenState: keypadRoot.lockScreenState
keypadOpen: swipeProgress === 1
previewCharIndex: -2
layer.enabled: true
layer.effect: MultiEffect {
blurMax: 16
shadowEnabled: true
shadowVerticalOffset: 1
shadowOpacity: 0.3
shadowColor: keypadRoot.dropShadowColor
}
}
// actual number keys
// Column layout - most cases
ColumnLayout {
visible: opacity > 0
opacity: passwordBar.isPinMode ? 1 : 0
Behavior on opacity {
NumberAnimation {
duration: Kirigami.Units.longDuration
easing.type: Easing.InOutQuad
}
}
anchors {
left: parent.left
right: parent.right
top: passwordBar.bottom
bottom: parent.bottom
topMargin: Kirigami.Units.gridUnit
bottomMargin: Kirigami.Units.gridUnit
}
id: keypadVerticalContainer
visible: root.height > Kirigami.Units.gridUnit * 25
anchors.centerIn: parent
spacing: Kirigami.Units.gridUnit
GridLayout {
id: grid
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
Layout.leftMargin: Kirigami.Units.gridUnit * 0.5
Layout.rightMargin: Kirigami.Units.gridUnit * 0.5
Layout.maximumWidth: Kirigami.Units.gridUnit * 22
Layout.maximumHeight: Kirigami.Units.gridUnit * 12.5
opacity: (Math.sin(2*((Math.PI / 2) * keypadRoot.swipeProgress + 1.5 * Math.PI)) + 1)
columns: 4
readonly property real keyRadius: 5
// numpad keys
Repeater {
model: ["1", "2", "3", "R", "4", "5", "6", "0", "7", "8", "9", "E"]
LayoutItemProxy { target: header }
LayoutItemProxy { target: keypadGrid }
}
delegate: AbstractButton {
id: button
Layout.fillWidth: true
Layout.fillHeight: true
visible: modelData.length > 0
opacity: (Math.sin(2*((Math.PI / 2) * keypadRoot.swipeProgress + 1.5 * Math.PI)) + 1)
// Row layout - used when there is restricted height
RowLayout {
id: keypadHorizontalContainer
visible: !keypadVerticalContainer.visible
background: Rectangle {
id: keyRect
radius: grid.keyRadius
color: button.pressed ? keypadRoot.buttonPressedColor : keypadRoot.buttonColor
anchors.centerIn: parent
spacing: Kirigami.Units.gridUnit * 2
LayoutItemProxy { target: header }
LayoutItemProxy { target: keypadGrid }
}
ColumnLayout {
id: header
spacing: Kirigami.Units.gridUnit
// label ("wrong pin", "enter pin")
Label {
id: descriptionLabel
Layout.alignment: Qt.AlignHCenter
opacity: root.lockScreenState.password.length === 0 ? 1 : 0
text: root.lockScreenState.pinLabel
font.pointSize: 12
font.bold: true
color: 'white'
// Enforce extra margin at top of vertical container
Layout.topMargin: keypadVerticalContainer.visible ? Kirigami.Units.gridUnit * 3 : 0
Behavior on opacity {
NumberAnimation { duration: 200 }
}
}
// pin display and bar
PasswordBar {
id: passwordBar
Layout.preferredWidth: Kirigami.Units.gridUnit * 14
Layout.preferredHeight: Kirigami.Units.gridUnit * 2.5
lockScreenState: root.lockScreenState
isKeypadOpen: root.openProgress >= 0.9
}
}
GridLayout {
id: keypadGrid
columnSpacing: Kirigami.Units.gridUnit
rowSpacing: Kirigami.Units.gridUnit
uniformCellHeights: true
uniformCellWidths: true
readonly property real intendedWidth: Kirigami.Units.gridUnit * 14
Layout.preferredWidth: Kirigami.Units.gridUnit * 14
Layout.preferredHeight: Kirigami.Units.gridUnit * 22
readonly property real cellLength: (intendedWidth - columnSpacing * 2) / 3
columns: 3
// numpad keys
Repeater {
model: ["1", "2", "3", "4", "5", "6", "7", "8", "9", "R", "0", "E"]
delegate: AbstractButton {
id: button
implicitWidth: keypadGrid.cellLength
implicitHeight: keypadGrid.cellLength
visible: modelData.length > 0
enabled: root.openProgress >= 0.8 // Only enable after a certain point in animation
opacity: enabled
Behavior on opacity {
NumberAnimation { duration: 100 * index }
}
background: Rectangle {
readonly property real restingOpacity: (modelData !== "R" && modelData !== "E") ? 0.2 : 0.0
radius: width
color: Qt.rgba(255, 255, 255,
button.pressed ? 0.5 : restingOpacity)
}
onPressedChanged: {
if (pressed) {
haptics.buttonVibrate();
}
}
onClicked: {
if (modelData === "R") {
passwordBar.backspace();
} else if (modelData === "E") {
passwordBar.enter();
} else {
passwordBar.keyPress(modelData);
}
}
onPressAndHold: {
if (modelData === "R") {
haptics.buttonVibrate();
passwordBar.clear();
}
}
contentItem: Item {
PlasmaComponents.Label {
visible: modelData !== "R" && modelData !== "E"
text: modelData
anchors.centerIn: parent
font.pointSize: 18
color: 'white'
}
layer.enabled: true
layer.effect: MultiEffect {
blurMax: 16
shadowEnabled: true
shadowVerticalOffset: 1
shadowOpacity: 0.3
shadowColor: keypadRoot.dropShadowColor
}
onPressedChanged: {
if (pressed) {
haptics.buttonVibrate();
}
}
onClicked: {
if (modelData === "R") {
passwordBar.backspace();
} else if (modelData === "E") {
passwordBar.enter();
} else {
passwordBar.keyPress(modelData);
}
}
onPressAndHold: {
if (modelData === "R") {
haptics.buttonVibrate();
passwordBar.clear();
}
}
contentItem: Item {
PlasmaComponents.Label {
visible: modelData !== "R" && modelData !== "E"
text: modelData
anchors.centerIn: parent
font.pointSize: 18
font.weight: Font.Light
color: keypadRoot.buttonTextColor
}
Kirigami.Icon {
visible: modelData === "R"
anchors.centerIn: parent
width: Kirigami.Units.iconSizes.small
height: Kirigami.Units.iconSizes.small
source: "edit-clear"
}
Kirigami.Icon {
visible: modelData === "E"
anchors.centerIn: parent
width: Kirigami.Units.iconSizes.small
height: Kirigami.Units.iconSizes.small
source: "go-next"
}
Kirigami.Icon {
visible: modelData === "R" || modelData === "E"
anchors.centerIn: parent
width: Kirigami.Units.iconSizes.small
height: Kirigami.Units.iconSizes.small
source: modelData === "R" ? "edit-clear" : "go-next"
Kirigami.Theme.inherit: false
Kirigami.Theme.colorSet: Kirigami.Theme.Complementary
}
}
}

View file

@ -1,5 +1,5 @@
// SPDX-FileCopyrightText: 2019 Nicolas Fella <nicolas.fella@gmx.de>
// SPDX-FileCopyrightText: 2021-2022 Devin Lin <espidev@gmail.com>
// SPDX-FileCopyrightText: 2021-2024 Devin Lin <devin@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
import QtQuick
@ -19,47 +19,56 @@ import org.kde.kirigami 2.12 as Kirigami
Item {
id: root
property var lockScreenState: LockScreenState {}
property var notifModel: Notifications.WatchedNotificationsModel {}
readonly property var lockScreenState: LockScreenState {}
readonly property var notifModel: Notifications.WatchedNotificationsModel {}
// only show widescreen mode for short height devices (ex. phone landscape)
property bool isWidescreen: root.height < 720 && (root.height < root.width * 0.75)
// Only show widescreen mode for short height devices (ex. phone landscape)
readonly property bool isWidescreen: root.height < 720 && (root.height < root.width * 0.75)
property bool notificationsShown: false
readonly property bool drawerOpen: flickable.openFactor >= 1
property var passwordBar: keypadLoader.item.passwordBar
property var passwordBar: keypad.passwordBar
// listen for keyboard events, and focus on input area
Component.onCompleted: forceActiveFocus();
Component.onCompleted: {
forceActiveFocus();
// Go to closed position when loaded
flickable.position = 0;
flickable.goToClosePosition();
}
// Listen for keyboard events, and focus on input area
Keys.onPressed: {
passwordBar.isPinMode = false;
root.lockScreenState.isKeyboardMode = true;
flickable.goToOpenPosition();
passwordBar.textField.forceActiveFocus();
}
// wallpaper blur
// Wallpaper blur
Loader {
anchors.fill: parent
asynchronous: true
sourceComponent: WallpaperBlur {
source: wallpaper
shouldBlur: root.notificationsShown || root.drawerOpen // only blur once animation finished for performance
opacity: flickable.openFactor
}
}
Connections {
target: root.lockScreenState
// ensure keypad is opened when password is updated (ex. keyboard)
// Ensure keypad is opened when password is updated (ex. keyboard)
function onPasswordChanged() {
flickable.goToOpenPosition()
if (root.lockScreenState.password !== "") {
flickable.goToOpenPosition();
}
}
}
Item {
id: lockscreenContainer
anchors.fill: parent
// header bar and action drawer
// Header bar and action drawer
Loader {
id: headerBarLoader
z: 1 // on top of flick area
@ -80,67 +89,35 @@ Item {
id: flickable
anchors.fill: parent
property real openFactor: position / keypadHeight
onOpened: {
if (root.lockScreenState.passwordless) {
// try unlocking if flicked to the top, and we have passwordless login
root.lockScreenState.tryPassword();
}
}
// Distance to swipe to fully open keypad
keypadHeight: Kirigami.Units.gridUnit * 20
// go to closed position when loaded
Component.onCompleted: {
flickable.position = 0;
flickable.goToClosePosition();
}
// update position, and cap it at the keypad height
onPositionChanged: {
if (position > keypadHeight) {
position = keypadHeight;
} else if (position < 0) {
position = 0;
// Clear entered password after closing keypad
onOpenFactorChanged: {
if (flickable.openFactor < 0.1) {
root.passwordBar.clear();
}
}
LockScreenNarrowContent {
id: phoneComponent
LockScreenContent {
id: lockScreenContent
visible: !isWidescreen
active: visible
opacity: 1 - flickable.openFactor
isVertical: !root.isWidescreen
opacity: Math.max(0, 1 - flickable.openFactor * 2)
transform: [
Scale {
origin.x: lockScreenContent.width / 2
origin.y: lockScreenContent.height / 2
yScale: 1 - (flickable.openFactor * 2) * 0.1
xScale: 1 - (flickable.openFactor * 2) * 0.1
}
]
fullHeight: root.height
lockScreenState: root.lockScreenState
notificationsModel: root.notifModel
onNotificationsShownChanged: root.notificationsShown = notificationsShown
onPasswordRequested: flickable.goToOpenPosition()
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
// move while swiping up
transform: Translate { y: Math.round((1 - phoneComponent.opacity) * (-root.height / 6)) }
}
LockScreenWideScreenContent {
id: tabletComponent
visible: isWidescreen
active: visible
opacity: 1 - flickable.openFactor
lockScreenState: root.lockScreenState
notificationsModel: root.notifModel
onNotificationsShownChanged: root.notificationsShown = notificationsShown
onPasswordRequested: flickable.goToOpenPosition()
anchors.topMargin: headerBarLoader.statusBarHeight
@ -148,75 +125,36 @@ Item {
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
// move while swiping up
transform: Translate { y: Math.round((1 - phoneComponent.opacity) * (-root.height / 6)) }
}
// scroll up icon
BottomIconIndicator {
id: scrollUpIconLoader
lockScreenState: root.lockScreenState
opacity: Math.max(0, 1 - flickable.openFactor * 2)
anchors.bottom: parent.bottom
anchors.bottomMargin: Kirigami.Units.gridUnit + flickable.position * 0.5
anchors.bottomMargin: Kirigami.Units.gridUnit + flickable.position * 0.1
anchors.horizontalCenter: parent.horizontalCenter
}
// password keypad
Loader {
id: keypadLoader
width: parent.width
asynchronous: true
active: !root.lockScreenState.passwordless // only load keypad if not passwordless
Rectangle {
id: keypadScrim
anchors.fill: parent
visible: opacity > 0
opacity: flickable.openFactor
color: Qt.rgba(0, 0, 0, 0.5)
}
anchors.bottom: parent.bottom
Keypad {
id: keypad
anchors.fill: parent
openProgress: flickable.openFactor
lockScreenState: root.lockScreenState
sourceComponent: ColumnLayout {
property alias passwordBar: keypad.passwordBar
transform: Translate { y: flickable.keypadHeight - flickable.position }
spacing: 0
// info notification text
Label {
Layout.fillWidth: true
Layout.rightMargin: Kirigami.Units.largeSpacing
Layout.leftMargin: Kirigami.Units.largeSpacing
Layout.bottomMargin: Kirigami.Units.smallSpacing * 2
font.pointSize: 9
elide: Text.ElideRight
horizontalAlignment: Text.AlignHCenter
text: root.lockScreenState.info
opacity: (root.lockScreenState.info.length === 0 || flickable.openFactor < 1) ? 0 : 1
color: 'white'
Behavior on opacity {
NumberAnimation { duration: 200 }
}
}
// scroll down icon
Kirigami.Icon {
Layout.alignment: Qt.AlignHCenter
Layout.bottomMargin: Kirigami.Units.gridUnit
implicitWidth: Kirigami.Units.iconSizes.small
implicitHeight: Kirigami.Units.iconSizes.small
Kirigami.Theme.colorSet: Kirigami.Theme.Complementary
source: "arrow-down"
opacity: Math.sin((Math.PI / 2) * flickable.openFactor + 1.5 * Math.PI) + 1
}
Keypad {
id: keypad
Layout.fillWidth: true
focus: true
lockScreenState: root.lockScreenState
swipeProgress: flickable.openFactor
}
}
// only show in last 50% of anim
opacity: (flickable.openFactor - 0.5) * 2
transform: Translate { y: (flickable.keypadHeight - flickable.position) * 0.1 }
}
}
}

View file

@ -0,0 +1,132 @@
// SPDX-FileCopyrightText: 2021-2024 Devin Lin <devin@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
import QtQuick
import QtQuick.Layouts
import QtQuick.Effects
import org.kde.kirigami 2.20 as Kirigami
import org.kde.plasma.workspace.keyboardlayout 1.0
import org.kde.notificationmanager as Notifications
import org.kde.plasma.private.mobileshell as MobileShell
Item {
id: root
required property var lockScreenState
required property bool isVertical
property var notificationsModel: []
property bool notificationsShown: false
property real fullHeight
signal passwordRequested()
// Vertical layout
ColumnLayout {
id: verticalLayout
visible: root.isVertical
spacing: 0
// center clock when no notifications are shown, otherwise move the clock upward
anchors.topMargin: !root.notificationsShown ? Math.round(root.fullHeight / 2 - (verticalLayout.implicitHeight / 2)) : Kirigami.Units.gridUnit * 5
anchors.bottomMargin: Kirigami.Units.gridUnit
anchors.fill: parent
// animate
Behavior on anchors.topMargin {
NumberAnimation {
duration: Kirigami.Units.veryLongDuration
easing.type: Easing.InOutExpo
}
}
LayoutItemProxy { target: clockAndMediaWidget }
LayoutItemProxy { target: notificationComponent }
}
// Horizontal layout (landscape on smaller devices)
Item {
id: horizontalLayout
anchors.fill: parent
visible: !root.isVertical
ColumnLayout {
id: leftLayout
width: parent.width / 2
anchors {
top: parent.top
bottom: parent.bottom
left: parent.left
leftMargin: Kirigami.Units.gridUnit * 3
}
LayoutItemProxy { target: clockAndMediaWidget }
}
ColumnLayout {
id: rightLayout
anchors {
top: parent.top
bottom: parent.bottom
left: leftLayout.right
right: parent.right
rightMargin: Kirigami.Units.gridUnit
}
LayoutItemProxy { target: notificationComponent }
}
}
// Clock and media widget column
ColumnLayout {
id: clockAndMediaWidget
Layout.fillWidth: true
Layout.fillHeight: root.isVertical
spacing: Kirigami.Units.gridUnit * 2
Clock {
layoutAlignment: root.isVertical ? Qt.AlignHCenter : Qt.AlignLeft
Layout.alignment: root.isVertical ? Qt.AlignHCenter : Qt.AlignLeft
Layout.fillWidth: true
Layout.bottomMargin: root.isVertical ? Kirigami.Units.gridUnit * 2 : 0
}
MobileShell.MediaControlsWidget {
Layout.alignment: root.isVertical ? Qt.AlignHCenter : Qt.AlignLeft
Layout.fillWidth: true
Layout.maximumWidth: Kirigami.Units.gridUnit * 25
Layout.leftMargin: root.isVertical ? Kirigami.Units.gridUnit : 0
Layout.rightMargin: root.isVertical ? Kirigami.Units.gridUnit : 0
layer.enabled: true
layer.effect: MultiEffect {
blurMax: 16
shadowEnabled: true
shadowVerticalOffset: 1
shadowOpacity: 0.5
shadowColor: Qt.lighter(Kirigami.Theme.backgroundColor, 0.1)
}
}
}
NotificationsComponent {
id: notificationComponent
lockScreenState: root.lockScreenState
notificationsModel: root.notificationsModel
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
Layout.fillHeight: true
Layout.fillWidth: true
Layout.maximumWidth: Kirigami.Units.gridUnit * (25 + 2) // clip margins
leftMargin: root.isVertical ? 0 : Kirigami.Units.gridUnit
rightMargin: root.isVertical ? 0 : Kirigami.Units.gridUnit
bottomMargin: root.isVertical ? 0 : Kirigami.Units.gridUnit
topMargin: Kirigami.Units.gridUnit
onPasswordRequested: root.passwordRequested()
onNotificationsShownChanged: root.notificationsShown = notificationsShown
}
}

View file

@ -1,93 +0,0 @@
// SPDX-FileCopyrightText: 2021-2023 Devin Lin <espidev@gmail.com>
// SPDX-License-Identifier: GPL-2.0-or-later
import QtQuick
import QtQuick.Layouts
import QtQuick.Effects
import org.kde.kirigami 2.20 as Kirigami
import org.kde.plasma.workspace.keyboardlayout 1.0
import org.kde.notificationmanager as Notifications
import org.kde.plasma.private.mobileshell as MobileShell
Loader {
id: root
required property var lockScreenState
property var notificationsModel: []
property bool notificationsShown: false
property real fullHeight
signal passwordRequested()
// avoid topMargin animation when item is being loaded
onLoaded: loadTimer.restart();
Timer {
id: loadTimer
interval: 200
}
// move while swiping up
transform: Translate { y: Math.round((1 - root.opacity) * (-root.height / 6)) }
asynchronous: true
sourceComponent: Item {
ColumnLayout {
id: column
spacing: 0
// center clock when no notifications are shown, otherwise move the clock upward
anchors.topMargin: !root.notificationsShown ? Math.round(root.fullHeight / 2 - (column.implicitHeight / 2)) : Kirigami.Units.gridUnit * 5
anchors.bottomMargin: Kirigami.Units.gridUnit
anchors.fill: parent
// animate
Behavior on anchors.topMargin {
NumberAnimation {
duration: loadTimer.running ? 0 : Kirigami.Units.veryLongDuration
easing.type: Easing.InOutExpo
}
}
Clock {
layoutAlignment: Qt.AlignHCenter
Layout.alignment: Qt.AlignHCenter
Layout.bottomMargin: Kirigami.Units.gridUnit * 2 // keep spacing even if media controls are gone
}
MobileShell.MediaControlsWidget {
Layout.alignment: Qt.AlignHCenter
Layout.fillWidth: true
Layout.maximumWidth: Kirigami.Units.gridUnit * 25
Layout.leftMargin: Kirigami.Units.gridUnit
Layout.rightMargin: Kirigami.Units.gridUnit
layer.enabled: true
layer.effect: MultiEffect {
blurMax: 16
shadowEnabled: true
shadowVerticalOffset: 1
shadowOpacity: 0.5
shadowColor: Qt.lighter(Kirigami.Theme.backgroundColor, 0.1)
}
}
NotificationsComponent {
id: notificationComponent
lockScreenState: root.lockScreenState
notificationsModel: root.notificationsModel
Layout.alignment: Qt.AlignHCenter
Layout.fillHeight: true
Layout.fillWidth: true
Layout.maximumWidth: Kirigami.Units.gridUnit * (25 + 2) // clip margins
topMargin: Kirigami.Units.gridUnit
onPasswordRequested: root.passwordRequested()
onNotificationsShownChanged: root.notificationsShown = notificationsShown
}
}
}
}

View file

@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: 2022 Devin Lin <espidev@gmail.com>
// SPDX-FileCopyrightText: 2022-2024 Devin Lin <devin@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
import QtQml
@ -8,77 +8,97 @@ import org.kde.kscreenlocker 1.0 as ScreenLocker
QtObject {
id: root
// current password being typed
property string password: ""
// whether waiting for authentication after trying password
property bool waitingForAuth: false
// the info message given
property string info: ""
// whether the lockscreen was passwordless
property bool passwordless: false // TODO true
// whether the device can log in with fingerprint
readonly property bool isFingerprintSupported: authenticator.authenticatorTypes & ScreenLocker.Authenticator.Fingerprint
// whether we are in keyboard mode (hiding the numpad)
property bool isKeyboardMode: false
property string pinLabel: enterPinLabel
readonly property string enterPinLabel: i18n("Enter PIN")
readonly property string wrongPinLabel: i18n("Wrong PIN")
signal reset()
signal unlockSucceeded()
signal unlockFailed()
Component.onCompleted: authenticator.startAuthenticating();
function tryPassword() {
if (root.password !== '') { // prevent typing lock when password is empty
waitingForAuth = true;
root.waitingForAuth = true;
}
authenticator.startAuthenticating();
authenticator.respond(root.password);
}
function resetPassword() {
password = "";
root.reset();
}
function resetPinLabel(): void {
pinLabel = enterPinLabel;
}
property var graceLockTimer: Timer {
interval: 1000
onTriggered: {
root.waitingForAuth = false;
root.password = "";
authenticator.startAuthenticating();
}
}
property var connections: Connections {
target: authenticator
function onSucceeded() {
console.log('login succeeded');
root.waitingForAuth = false;
root.unlockSucceeded();
Qt.quit();
}
function onFailed() {
function onFailed(kind: int): void {
if (kind != 0) { // if this is coming from the noninteractive authenticators
return;
}
console.log('login failed');
root.waitingForAuth = false;
root.password = "";
graceLockTimer.restart();
root.pinLabel = root.wrongPinLabel;
root.unlockFailed();
}
function onInfoMessageChanged() {
console.log('info: ' + authenticator.infoMessage);
root.info += authenticator.infoMessage + " ";
}
// TODO
function onErrorMessageChanged() {
console.log('error: ' + authenticator.errorMessage);
}
// TODO
function onPromptChanged() {
console.log('prompt: ' + authenticator.prompt);
}
function onPromptForSecretChanged() {
console.log('prompt secret: ' + authenticator.promptForSecret);
if (root.password !== "") {
authenticator.respond(root.password);
authenticator.startAuthenticating();
}
}
}
}

View file

@ -1,98 +0,0 @@
// SPDX-FileCopyrightText: 2021-2022 Devin Lin <espidev@gmail.com>
// SPDX-License-Identifier: GPL-2.0-or-later
import QtQuick
import QtQuick.Layouts
import QtQuick.Effects
import org.kde.kirigami 2.20 as Kirigami
import org.kde.plasma.workspace.keyboardlayout 1.0
import org.kde.notificationmanager as Notifications
import org.kde.plasma.private.mobileshell as MobileShell
Loader {
id: root
required property var lockScreenState
property var notificationsModel: []
property bool notificationsShown: false
signal passwordRequested()
asynchronous: true
sourceComponent: Item {
Item {
id: clock
width: parent.width / 2
anchors {
top: parent.top
bottom: parent.bottom
left: parent.left
leftMargin: Kirigami.Units.gridUnit * 3
}
ColumnLayout {
id: tabletLayout
anchors.centerIn: parent
spacing: Kirigami.Units.gridUnit
Clock {
layoutAlignment: Qt.AlignLeft
Layout.alignment: Qt.AlignLeft
Layout.fillWidth: true
Layout.minimumWidth: Kirigami.Units.gridUnit * 20
}
MobileShell.MediaControlsWidget {
Layout.alignment: Qt.AlignLeft
Layout.fillWidth: true
Layout.maximumWidth: Kirigami.Units.gridUnit * 25
layer.enabled: true
layer.effect: MultiEffect {
blurMax: 16
shadowEnabled: true
shadowVerticalOffset: 1
shadowOpacity: 0.5
shadowColor: Qt.lighter(Kirigami.Theme.backgroundColor, 0.1)
}
}
}
}
// tablet notifications list
ColumnLayout {
id: tabletNotificationsList
anchors {
top: parent.top
bottom: parent.bottom
left: clock.right
right: parent.right
rightMargin: Kirigami.Units.gridUnit
}
NotificationsComponent {
id: notificationComponent
lockScreenState: root.lockScreenState
notificationsModel: root.notificationsModel
Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter
Layout.fillWidth: true
Layout.fillHeight: true
Layout.topMargin: Kirigami.Units.gridUnit * 2
Layout.bottomMargin: Kirigami.Units.gridUnit
Layout.minimumWidth: Kirigami.Units.gridUnit * 15
Layout.maximumWidth: Kirigami.Units.gridUnit * 25
leftMargin: Kirigami.Units.gridUnit
rightMargin: Kirigami.Units.gridUnit
bottomMargin: Kirigami.Units.gridUnit
topMargin: Kirigami.Units.gridUnit
onPasswordRequested: root.passwordRequested()
onNotificationsShownChanged: root.notificationsShown = notificationsShown
}
}
}
}

View file

@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: 2020-2022 Devin Lin <espidev@gmail.com>
// SPDX-FileCopyrightText: 2020-2024 Devin Lin <devin@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
import QtQuick
@ -12,42 +12,31 @@ import org.kde.kirigami 2.12 as Kirigami
Rectangle {
id: root
implicitHeight: Kirigami.Units.gridUnit * 2.5
required property var lockScreenState
property alias textField: textField
// toggle between pin and password mode
property bool isPinMode: true
required property bool isKeypadOpen
// for displaying temporary number in pin dot display
property int previewCharIndex: -2
property string pinLabel: i18n("Enter PIN")
property bool keypadOpen
readonly property color headerTextColor: Kirigami.ColorUtils.adjustColor(Kirigami.Theme.textColor, {"alpha": 0.75*255})
readonly property color headerTextInactiveColor: Kirigami.ColorUtils.adjustColor(Kirigami.Theme.textColor, {"alpha": 0.4*255})
readonly property color headerTextColor: Qt.rgba(255, 255, 255, 1)
readonly property color headerTextInactiveColor: Qt.rgba(255, 255, 255, 0.4)
radius: Kirigami.Units.largeSpacing
color: Qt.rgba(255, 255, 255, 0.3)
// model for shown dots
// we need to use a listmodel to avoid all delegates from reloading
ListModel {
id: dotDisplayModel
}
// Listen to lockscreen state changes
Connections {
target: root.lockScreenState
function onUnlockSucceeded() {
root.pinLabel = i18n("Logging in...");
}
function onUnlockFailed() {
root.pinLabel = i18n("Wrong PIN");
}
function onPasswordChanged() {
while (root.lockScreenState.password.length < dotDisplayModel.count) {
dotDisplayModel.remove(dotDisplayModel.count - 1);
@ -57,8 +46,8 @@ Rectangle {
}
}
}
// keypad functions
// Keypad functions
function backspace() {
if (!lockScreenState.waitingForAuth) {
root.previewCharIndex = -2;
@ -72,31 +61,28 @@ Rectangle {
lockScreenState.resetPassword();
}
}
function enter() {
lockScreenState.tryPassword();
if (keypadOpen && !isPinMode) {
if (root.isKeypadOpen && root.lockScreenState.isKeyboardMode) {
// make sure keyboard doesn't close
openKeyboardTimer.restart();
}
}
function keyPress(data) {
if (!lockScreenState.waitingForAuth) {
if (root.pinLabel !== i18n("Enter PIN")) {
root.pinLabel = i18n("Enter PIN");
}
root.lockScreenState.resetPinLabel();
root.previewCharIndex = lockScreenState.password.length;
lockScreenState.password += data
// trigger turning letter into dot later
letterTimer.restart();
}
}
// HACK: we have to open the virtual keyboard after a certain amount of time or else it will close anyway
Timer {
id: openKeyboardTimer
@ -105,7 +91,7 @@ Rectangle {
repeat: false
onTriggered: Keyboards.KWinVirtualKeyboard.active = true
}
// trigger turning letter into dot after 500 milliseconds
Timer {
id: letterTimer
@ -116,24 +102,24 @@ Rectangle {
root.previewCharIndex = -2;
}
}
// hidden textfield so that the virtual keyboard shows up
TextField {
id: textField
visible: false
focus: keypadOpen && !isPinMode
focus: root.isKeypadOpen && root.lockScreenState.isKeyboardMode
z: 1
inputMethodHints: Qt.ImhNoPredictiveText
onFocusChanged: {
if (focus) {
Keyboards.KWinVirtualKeyboard.active = true;
}
}
property bool externalEdit: false
property string prevText: ""
Connections {
target: root.lockScreenState
@ -144,13 +130,13 @@ Rectangle {
}
}
}
onEditingFinished: {
if (textField.focus) {
root.enter();
}
}
onTextChanged: {
if (!externalEdit) {
if (prevText.length > text.length) { // backspace
@ -165,16 +151,16 @@ Rectangle {
externalEdit = false;
}
}
MouseArea {
anchors.fill: parent
onClicked: {
// clicking on rectangle opens keyboard if not already open
if (!isPinMode) {
if (root.lockScreenState.isKeyboardMode) {
Keyboards.KWinVirtualKeyboard.active = true;
}
}
// toggle between showing keypad and not
ToolButton {
anchors.right: parent.right
@ -182,37 +168,24 @@ Rectangle {
anchors.bottom: parent.bottom
anchors.margins: Kirigami.Units.smallSpacing
implicitWidth: height
icon.name: root.isPinMode ? "input-keyboard-virtual-symbolic" : "input-dialpad-symbolic"
icon.name: root.lockScreenState.isKeyboardMode ? "input-dialpad-symbolic" : "input-keyboard-virtual-symbolic"
onClicked: {
root.isPinMode = !root.isPinMode;
if (!root.isPinMode) {
root.lockScreenState.isKeyboardMode = !root.lockScreenState.isKeyboardMode;
if (root.lockScreenState.isKeyboardMode) {
Keyboards.KWinVirtualKeyboard.active = true;
}
}
}
// label ("wrong pin", "enter pin")
Label {
opacity: root.lockScreenState.password.length === 0 ? 1 : 0
anchors.centerIn: parent
text: root.pinLabel
font.pointSize: 12
color: root.headerTextColor
Behavior on opacity {
NumberAnimation { duration: 200 }
}
}
// pin dot display
ColumnLayout {
anchors.fill: parent
ListView {
id: dotDisplay
property int dotWidth: 6
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
Layout.bottomMargin: Math.round(dotWidth / 2)
@ -222,7 +195,7 @@ Rectangle {
orientation: ListView.Horizontal
spacing: 8
model: dotDisplayModel
Behavior on implicitWidth {
NumberAnimation { duration: 50 }
}
@ -231,7 +204,7 @@ Rectangle {
width: dotDisplay.dotWidth
height: dotDisplay.dotWidth
property bool showChar: index === root.previewCharIndex
Component.onCompleted: {
if (showChar) {
charAnimation.to = 1;
@ -242,7 +215,7 @@ Rectangle {
dotAnimation.restart();
}
}
onShowCharChanged: {
if (!showChar) {
charAnimation.to = 0;
@ -252,14 +225,14 @@ Rectangle {
dotAnimation.start();
}
}
Rectangle { // dot
id: dot
scale: 0
anchors.fill: parent
radius: width
color: lockScreenState.waitingForAuth ? root.headerTextInactiveColor : root.headerTextColor // dim when waiting for auth
PropertyAnimation {
id: dotAnimation
target: dot;
@ -267,7 +240,7 @@ Rectangle {
duration: 50
}
}
Label { // number/letter
id: charLabel
scale: 0
@ -275,7 +248,7 @@ Rectangle {
color: lockScreenState.waitingForAuth ? root.headerTextInactiveColor : root.headerTextColor // dim when waiting for auth
text: model.char
font.pointSize: 12
PropertyAnimation {
id: charAnimation
target: charLabel;

View file

@ -11,14 +11,4 @@ MultiEffect {
blurEnabled: true
blurMax: 50
blur: 1.0
property bool shouldBlur
opacity: shouldBlur ? 1 : 0
Behavior on opacity {
NumberAnimation {
duration: 1000
easing.type: Easing.InOutQuad
}
}
}