mirror of
https://github.com/noctalia-dev/noctalia-shell.git
synced 2026-05-11 17:08:27 +08:00
fix(Lockscreen): adjust password cursor so it properly follows position
Fixes: #2308
This commit is contained in:
@@ -760,166 +760,197 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
// Password dots display with selection support
|
||||
// Host for dots / plain text and the caret (caret x follows passwordInput.cursorPosition)
|
||||
Item {
|
||||
width: Math.min(passwordDisplayContent.width, 550)
|
||||
id: passwordVisualHost
|
||||
height: 20
|
||||
visible: passwordInput.text.length > 0 && !parent.parent.parent.passwordVisible
|
||||
width: passwordInputContainer.passwordVisible ? Math.min(visiblePasswordPlainText.implicitWidth, 550) : Math.min(passwordDisplayContent.width, 550)
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
clip: true
|
||||
|
||||
// Proportional selection highlight behind the dots
|
||||
Rectangle {
|
||||
id: selectionHighlight
|
||||
visible: passwordInput.selectionStart !== passwordInput.selectionEnd && passwordInput.text.length > 0
|
||||
color: Qt.alpha(Color.mPrimary, 0.8)
|
||||
height: parent.height + Style.marginS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
x: (passwordInput.selectionStart / passwordInput.text.length) * passwordDisplayContent.width
|
||||
width: ((passwordInput.selectionEnd - passwordInput.selectionStart) / passwordInput.text.length) * passwordDisplayContent.width
|
||||
readonly property real caretVisualX: {
|
||||
const len = passwordInput.text.length;
|
||||
if (len <= 0)
|
||||
return 0;
|
||||
if (passwordInputContainer.passwordVisible) {
|
||||
const adv = passwordCaretFontMetrics.advanceWidth(passwordInput.text.substring(0, passwordInput.cursorPosition));
|
||||
return Math.max(0, Math.min(adv, width));
|
||||
}
|
||||
const w = passwordDisplayContent.width;
|
||||
if (w <= 0)
|
||||
return 0;
|
||||
return Math.max(0, Math.min((passwordInput.cursorPosition / len) * w, width));
|
||||
}
|
||||
|
||||
Row {
|
||||
id: passwordDisplayContent
|
||||
spacing: Style.marginXXXS
|
||||
// Password dots display with selection support
|
||||
Item {
|
||||
width: Math.min(passwordDisplayContent.width, 550)
|
||||
height: 20
|
||||
visible: passwordInput.text.length > 0 && !passwordInputContainer.passwordVisible
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
clip: true
|
||||
|
||||
Repeater {
|
||||
id: iconRepeater
|
||||
model: ScriptModel {
|
||||
values: Array(passwordInput.text.length)
|
||||
}
|
||||
// Proportional selection highlight behind the dots
|
||||
Rectangle {
|
||||
id: selectionHighlight
|
||||
visible: passwordInput.selectionStart !== passwordInput.selectionEnd && passwordInput.text.length > 0
|
||||
color: Qt.alpha(Color.mPrimary, 0.8)
|
||||
height: parent.height + Style.marginS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
x: (passwordInput.selectionStart / passwordInput.text.length) * passwordDisplayContent.width
|
||||
width: ((passwordInput.selectionEnd - passwordInput.selectionStart) / passwordInput.text.length) * passwordDisplayContent.width
|
||||
}
|
||||
|
||||
property list<string> passwordChars: ["circle-filled", "pentagon-filled", "michelin-star-filled", "square-rounded-filled", "guitar-pick-filled", "blob-filled", "triangle-filled"]
|
||||
Row {
|
||||
id: passwordDisplayContent
|
||||
spacing: Style.marginXXXS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
NIcon {
|
||||
id: icon
|
||||
|
||||
required property int index
|
||||
// This will be called with index = -1 when the TextInput is deleted
|
||||
// So we make sur index is positive to avoid warning on array accesses
|
||||
property bool drawCustomChar: index >= 0 && Settings.data.general.passwordChars
|
||||
// Flip color when this dot falls inside the active selection range
|
||||
property bool isSelected: index >= 0 && passwordInput.selectionStart !== passwordInput.selectionEnd && index >= passwordInput.selectionStart && index < passwordInput.selectionEnd
|
||||
|
||||
icon: drawCustomChar ? iconRepeater.passwordChars[index % iconRepeater.passwordChars.length] : "circle-filled"
|
||||
pointSize: Style.fontSizeL
|
||||
color: isSelected ? Color.mOnPrimary : Color.mPrimary
|
||||
opacity: 1.0
|
||||
scale: animationsEnabled ? 0.5 : 1
|
||||
ParallelAnimation {
|
||||
id: iconAnim
|
||||
NumberAnimation {
|
||||
target: icon
|
||||
properties: "scale"
|
||||
to: 1
|
||||
duration: Style.animationFast
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Easing.OutInBounce
|
||||
}
|
||||
Repeater {
|
||||
id: iconRepeater
|
||||
model: ScriptModel {
|
||||
values: Array(passwordInput.text.length)
|
||||
}
|
||||
Component.onCompleted: {
|
||||
if (animationsEnabled) {
|
||||
iconAnim.start();
|
||||
|
||||
property list<string> passwordChars: ["circle-filled", "pentagon-filled", "michelin-star-filled", "square-rounded-filled", "guitar-pick-filled", "blob-filled", "triangle-filled"]
|
||||
|
||||
NIcon {
|
||||
id: icon
|
||||
|
||||
required property int index
|
||||
// This will be called with index = -1 when the TextInput is deleted
|
||||
// So we make sur index is positive to avoid warning on array accesses
|
||||
property bool drawCustomChar: index >= 0 && Settings.data.general.passwordChars
|
||||
// Flip color when this dot falls inside the active selection range
|
||||
property bool isSelected: index >= 0 && passwordInput.selectionStart !== passwordInput.selectionEnd && index >= passwordInput.selectionStart && index < passwordInput.selectionEnd
|
||||
|
||||
icon: drawCustomChar ? iconRepeater.passwordChars[index % iconRepeater.passwordChars.length] : "circle-filled"
|
||||
pointSize: Style.fontSizeL
|
||||
color: isSelected ? Color.mOnPrimary : Color.mPrimary
|
||||
opacity: 1.0
|
||||
scale: animationsEnabled ? 0.5 : 1
|
||||
ParallelAnimation {
|
||||
id: iconAnim
|
||||
NumberAnimation {
|
||||
target: icon
|
||||
properties: "scale"
|
||||
to: 1
|
||||
duration: Style.animationFast
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Easing.OutInBounce
|
||||
}
|
||||
}
|
||||
Component.onCompleted: {
|
||||
if (animationsEnabled) {
|
||||
iconAnim.start();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Mouse area for click-to-position and drag-to-select
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.IBeamCursor
|
||||
// Mouse area for click-to-position and drag-to-select
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.IBeamCursor
|
||||
|
||||
property int dragStartPos: 0
|
||||
property bool pendingSelectAll: false
|
||||
property int dragStartPos: 0
|
||||
property bool pendingSelectAll: false
|
||||
|
||||
// Resets double-click state if user pauses too long between clicks
|
||||
Timer {
|
||||
id: doubleClickResetTimer
|
||||
interval: 600
|
||||
onTriggered: parent.pendingSelectAll = false
|
||||
}
|
||||
|
||||
function charIndexFromX(mouseX) {
|
||||
if (passwordInput.text.length === 0)
|
||||
return 0;
|
||||
var charWidth = passwordDisplayContent.width / passwordInput.text.length;
|
||||
// floor so clicking anywhere on a dot selects that dot, not the next
|
||||
return Math.max(0, Math.min(passwordInput.text.length - 1, Math.floor(mouseX / charWidth)));
|
||||
}
|
||||
|
||||
onPressed: function (mouse) {
|
||||
doubleClickResetTimer.stop();
|
||||
passwordInput.forceActiveFocus();
|
||||
dragStartPos = charIndexFromX(mouse.x);
|
||||
passwordInput.cursorPosition = dragStartPos;
|
||||
}
|
||||
|
||||
onPositionChanged: function (mouse) {
|
||||
pendingSelectAll = false;
|
||||
var curPos = charIndexFromX(mouse.x);
|
||||
if (curPos <= dragStartPos) {
|
||||
passwordInput.select(curPos, dragStartPos + 1);
|
||||
} else {
|
||||
passwordInput.select(dragStartPos, curPos + 1);
|
||||
// Resets double-click state if user pauses too long between clicks
|
||||
Timer {
|
||||
id: doubleClickResetTimer
|
||||
interval: 600
|
||||
onTriggered: parent.pendingSelectAll = false
|
||||
}
|
||||
}
|
||||
|
||||
onDoubleClicked: function (mouse) {
|
||||
passwordInput.forceActiveFocus();
|
||||
if (pendingSelectAll) {
|
||||
passwordInput.selectAll();
|
||||
function charIndexFromX(mouseX) {
|
||||
if (passwordInput.text.length === 0)
|
||||
return 0;
|
||||
var charWidth = passwordDisplayContent.width / passwordInput.text.length;
|
||||
// floor so clicking anywhere on a dot selects that dot, not the next
|
||||
return Math.max(0, Math.min(passwordInput.text.length - 1, Math.floor(mouseX / charWidth)));
|
||||
}
|
||||
|
||||
onPressed: function (mouse) {
|
||||
doubleClickResetTimer.stop();
|
||||
passwordInput.forceActiveFocus();
|
||||
dragStartPos = charIndexFromX(mouse.x);
|
||||
passwordInput.cursorPosition = dragStartPos;
|
||||
}
|
||||
|
||||
onPositionChanged: function (mouse) {
|
||||
pendingSelectAll = false;
|
||||
} else {
|
||||
var pos = charIndexFromX(mouse.x);
|
||||
passwordInput.select(pos, Math.min(pos + 1, passwordInput.text.length));
|
||||
pendingSelectAll = true;
|
||||
doubleClickResetTimer.restart();
|
||||
var curPos = charIndexFromX(mouse.x);
|
||||
if (curPos <= dragStartPos) {
|
||||
passwordInput.select(curPos, dragStartPos + 1);
|
||||
} else {
|
||||
passwordInput.select(dragStartPos, curPos + 1);
|
||||
}
|
||||
}
|
||||
|
||||
onDoubleClicked: function (mouse) {
|
||||
passwordInput.forceActiveFocus();
|
||||
if (pendingSelectAll) {
|
||||
passwordInput.selectAll();
|
||||
pendingSelectAll = false;
|
||||
} else {
|
||||
var pos = charIndexFromX(mouse.x);
|
||||
passwordInput.select(pos, Math.min(pos + 1, passwordInput.text.length));
|
||||
pendingSelectAll = true;
|
||||
doubleClickResetTimer.restart();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NText {
|
||||
text: passwordInput.text
|
||||
color: Color.mPrimary
|
||||
pointSize: Style.fontSizeM
|
||||
visible: passwordInput.text.length > 0 && parent.parent.parent.passwordVisible
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
elide: Text.ElideRight
|
||||
width: Math.min(implicitWidth, 550)
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: 2
|
||||
height: 20
|
||||
color: Color.mPrimary
|
||||
// Hide the cursor when text is selected
|
||||
visible: passwordInput.activeFocus && passwordInput.text.length > 0 && passwordInput.selectionStart === passwordInput.selectionEnd
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
// Smooth fade animation (when animations enabled)
|
||||
SequentialAnimation on opacity {
|
||||
loops: Animation.Infinite
|
||||
running: root.animationsEnabled && passwordInput.activeFocus && passwordInput.text.length > 0 && passwordInput.selectionStart === passwordInput.selectionEnd
|
||||
NumberAnimation {
|
||||
to: 0
|
||||
duration: 530
|
||||
}
|
||||
NumberAnimation {
|
||||
to: 1
|
||||
duration: 530
|
||||
}
|
||||
NText {
|
||||
id: visiblePasswordPlainText
|
||||
text: passwordInput.text
|
||||
color: Color.mPrimary
|
||||
pointSize: Style.fontSizeM
|
||||
visible: passwordInput.text.length > 0 && passwordInputContainer.passwordVisible
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
elide: Text.ElideRight
|
||||
width: Math.min(implicitWidth, 550)
|
||||
}
|
||||
|
||||
// Simple toggle (when animations disabled) — no per-frame repaints
|
||||
Timer {
|
||||
interval: 530
|
||||
running: !root.animationsEnabled && passwordInput.activeFocus && passwordInput.text.length > 0 && passwordInput.selectionStart === passwordInput.selectionEnd
|
||||
repeat: true
|
||||
onTriggered: parent.opacity = parent.opacity > 0.5 ? 0 : 1
|
||||
FontMetrics {
|
||||
id: passwordCaretFontMetrics
|
||||
font: visiblePasswordPlainText.font
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: 2
|
||||
height: 20
|
||||
x: passwordVisualHost.caretVisualX
|
||||
color: Color.mPrimary
|
||||
// Hide the cursor when text is selected
|
||||
visible: passwordInput.activeFocus && passwordInput.text.length > 0 && passwordInput.selectionStart === passwordInput.selectionEnd
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
// Smooth fade animation (when animations enabled)
|
||||
SequentialAnimation on opacity {
|
||||
loops: Animation.Infinite
|
||||
running: root.animationsEnabled && passwordInput.activeFocus && passwordInput.text.length > 0 && passwordInput.selectionStart === passwordInput.selectionEnd
|
||||
NumberAnimation {
|
||||
to: 0
|
||||
duration: 530
|
||||
}
|
||||
NumberAnimation {
|
||||
to: 1
|
||||
duration: 530
|
||||
}
|
||||
}
|
||||
|
||||
// Simple toggle (when animations disabled) — no per-frame repaints
|
||||
Timer {
|
||||
interval: 530
|
||||
running: !root.animationsEnabled && passwordInput.activeFocus && passwordInput.text.length > 0 && passwordInput.selectionStart === passwordInput.selectionEnd
|
||||
repeat: true
|
||||
onTriggered: parent.opacity = parent.opacity > 0.5 ? 0 : 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user