/*SPDX-FileCopyrightText: 2024 Joshua Goins <josh@redstrate.com>SPDX-License-Identifier: GPL-2.0-or-later*/import QtQuickimport QtQuick.Windowimport QtQuick.Layoutsimport QtQuick.Shapesimport QtQuick.Controls as QQC2import org.kde.kirigami as Kirigamiimport org.kde.plasma.tablet.kcm as KCMQQC2.ApplicationWindow {id: rootrequired property KCM.InputDevice devicerequired property KCM.TabletEvents tabletEventsproperty KCM.CalibrationTool tool: KCM.CalibrationTool {onCalibrationCreated: matrix => tool.setCalibrationMatrix(device, matrix)onResetFromSaved: root.device.resetCalibrationMatrix()}property real currentPenToolX: -1property real currentPenToolY: -1property var recordedResiduals: ({})property string testingActionSummary: ""title: i18nc("@title", "Pen Calibration")onWidthChanged: tool.width = widthonHeightChanged: tool.height = heightonClosing: {// Make sure to reset it back to default if the user closes the window for any reason and hasn't confirmedif (tool.state === KCM.CalibrationTool.Confirming) {root.device.resetCalibrationMatrix()}}HoverHandler {cursorShape: Qt.BlankCursoracceptedDevices: PointerDevice.Stylusenabled: tool.state !== KCM.CalibrationTool.Testing}function targetCenter(target: Item): point {return target.mapToItem(root.contentItem, target.width / 2, target.height / 2);}function resetTestingFeedback(): void {root.recordedResiduals = ({});root.testingActionSummary = "";}function testingTargets(): var {return [topLeftAnchor, topRightAnchor, bottomLeftAnchor, bottomRightAnchor, centerAnchor];}function residualSummary(): string {const order = ["topLeft", "topRight", "bottomLeft", "bottomRight", "center"];const parts = [];for (const key of order) {const residual = root.recordedResiduals[key];if (!residual) {continue;}parts.push(i18nc("@info", "%1: (%2 px, %3 px)", residual.label, Math.round(residual.x), Math.round(residual.y)));}return parts.join("\n");}Connections {target: tabletEventsfunction onToolDown(hardware_serial_hi: int, hardware_serial_lo: int, x: real, y: real): void {if (tool.state === KCM.CalibrationTool.Testing) {root.currentPenToolX = x;root.currentPenToolY = y;let closestTarget = null;let closestDistance = Infinity;for (const target of root.testingTargets()) {const targetPoint = root.targetCenter(target);const targetDistance = Math.sqrt(Math.pow(x - targetPoint.x, 2) + Math.pow(y - targetPoint.y, 2));if (targetDistance < closestDistance) {closestDistance = targetDistance;closestTarget = target;}}if (closestTarget && closestDistance <= 100) {const targetPoint = root.targetCenter(closestTarget);const residualX = x - targetPoint.x;const residualY = y - targetPoint.y;if (closestTarget === centerAnchor) {tool.applyCenterCorrection(device, residualX, residualY);root.recordedResiduals = ({});root.testingActionSummary = i18nc("@info", "Applied center correction (%1 px, %2 px). Retap the targets to redo the correction.", Math.round(residualX), Math.round(residualY));return;}const updatedResiduals = Object.assign({}, root.recordedResiduals);updatedResiduals[closestTarget.diagnosticKey] = {label: closestTarget.diagnosticLabel,x: residualX,y: residualY,};root.recordedResiduals = updatedResiduals;}return;}const centerX = root.width / 2;const centerY = root.height / 2;let targetItem = null;if (x < centerX && y < centerY) {targetItem = topLeftAnchor;} else if (x < centerX && y > centerY) {targetItem = bottomLeftAnchor;} else if (x > centerX && y < centerY) {targetItem = topRightAnchor;} else {targetItem = bottomRightAnchor;}const targetCenterPoint = root.targetCenter(targetItem);// Calculate the distance between the target center and the point.const distance = Math.sqrt(Math.pow(x - targetCenterPoint.x, 2) + Math.pow(y - targetCenterPoint.y, 2));// Ensure that large distances are culled because they are probably not clicking on the targetif (distance > 100) {return;}tool.calibrate(x, y, targetCenterPoint.x, targetCenterPoint.y);}function onToolUp(hardware_serial_hi: int, hardware_serial_lo: int, x: real, y: real): void {root.currentPenToolX = -1;root.currentPenToolY = -1;}}ColumnLayout {anchors {top: parent.toptopMargin: Kirigami.Units.gridUnit * 7horizontalCenter: parent.horizontalCenter}spacing: Kirigami.Units.largeSpacingQQC2.Label {Layout.maximumWidth: root.width - (Kirigami.Units.gridUnit * 8)Layout.fillWidth: truehorizontalAlignment: Text.AlignHCenterwrapMode: Text.Wraptext: {if (tool.state === KCM.CalibrationTool.Confirming) {return xi18ncp("@info", "Tap the targets again to confirm the new calibration.<nl/><nl/><emphasis strong='true'>Will revert to default calibration in %1 second unless further action is taken.</emphasis>", "Tap the targets again to confirm the new calibration.<nl/><nl/><emphasis strong='true'>Will revert to default calibration in %1 seconds unless further action is taken.</emphasis>", tool.resetSecondsLeft)}if (tool.state === KCM.CalibrationTool.Testing) {return i18nc("@info", "Calibration is completed and saved.\n\nTap the center target once to fine-tune alignment. You can retap the other targets to verify the result.");}return i18nc("@info", "Tap the center of each target.");}}QQC2.Label {visible: tool.state === KCM.CalibrationTool.Testing && (root.testingActionSummary.length > 0 || root.residualSummary().length > 0)Layout.maximumWidth: root.width - (Kirigami.Units.gridUnit * 8)Layout.fillWidth: truehorizontalAlignment: Text.AlignHCenterwrapMode: Text.WraptextFormat: Text.PlainTexttext: {const sections = [];if (root.testingActionSummary.length > 0) {sections.push(root.testingActionSummary);}const residualSummary = root.residualSummary();if (residualSummary.length > 0) {sections.push(i18nc("@info", "Offset Correction:\n%1", residualSummary));}return sections.join("\n\n");}}RowLayout {spacing: Kirigami.Units.largeSpacingLayout.alignment: Qt.AlignHCenterQQC2.Button {visible: tool.state === KCM.CalibrationTool.Testingtext: i18nc("@action:button", "Refine Existing Calibration")icon.name: "edit-redo"onClicked: {root.resetTestingFeedback();tool.reset();}Layout.alignment: Qt.AlignHCenter}QQC2.Button {text: tool.state === KCM.CalibrationTool.Confirming ? i18nc("@action:button", "Reset and Try Again") : i18nc("@action:button", "Calibrate from Scratch")icon.name: "kt-restore-defaults"focus: truevisible: tool.state === KCM.CalibrationTool.Confirming || tool.state === KCM.CalibrationTool.TestingonClicked: {root.resetTestingFeedback();tool.restoreDefaults(root.device);tool.reset();}Layout.alignment: Qt.AlignHCenter}QQC2.Button {text: tool.state === KCM.CalibrationTool.Testing ? i18nc("@action:button", "Close") : i18nc("@action:button", "Cancel")icon.name: "dialog-cancel"onClicked: root.close()Layout.alignment: Qt.AlignHCenter}}}component Target: QQC2.ToolButton {property string diagnosticKey: ""property string diagnosticLabel: ""readonly property real positionX: Kirigami.ScenePosition.x + width / 2readonly property real positionY: Kirigami.ScenePosition.y + height / 2icon.name: "crosshairs"focusPolicy: Qt.NoFocushoverEnabled: falseHoverHandler {cursorShape: Qt.BlankCursoracceptedDevices: PointerDevice.Stylusenabled: tool.state !== KCM.CalibrationTool.Testing}down: {const isX = root.currentPenToolX >= Kirigami.ScenePosition.x && root.currentPenToolX <= Kirigami.ScenePosition.x + width;const isY = root.currentPenToolY >= Kirigami.ScenePosition.y && root.currentPenToolY <= Kirigami.ScenePosition.y + height;return isX && isY;}}component TargetPath: Shape {required property real sourceXrequired property real sourceYrequired property real targetXrequired property real targetYKirigami.Theme.colorSet: Kirigami.Theme.HeaderKirigami.Theme.inherit: falseShapePath {strokeWidth: 1strokeColor: Qt.lighter(Kirigami.Theme.backgroundColor, 1.5)startX: sourceX; startY: sourceYPathLine { x: targetX; y: targetY }}}component PairTargetPath: Item {required property Item anchorvisible: anchor.visibleTargetPath {sourceX: anchor.positionXsourceY: 0targetX: anchor.positionXtargetY: root.height}TargetPath {sourceX: 0sourceY: anchor.positionYtargetX: root.widthtargetY: anchor.positionY}}Target {id: topLeftAnchordiagnosticKey: "topLeft"diagnosticLabel: i18nc("@info Name used in the diagnostic log", "Top-left")visible: tool.state === KCM.CalibrationTool.Testing || tool.currentTarget === 0anchors {left: parent.leftleftMargin: Kirigami.Units.gridUnit * 4top: parent.toptopMargin: Kirigami.Units.gridUnit * 4}}PairTargetPath {anchor: topLeftAnchor}Target {id: topRightAnchordiagnosticKey: "topRight"diagnosticLabel: i18nc("@info Name used in the diagnostic log", "Top-right")visible: tool.state === KCM.CalibrationTool.Testing || tool.currentTarget === 1anchors {right: parent.rightrightMargin: Kirigami.Units.gridUnit * 4top: parent.toptopMargin: Kirigami.Units.gridUnit * 4}}PairTargetPath {anchor: topRightAnchor}Target {id: bottomLeftAnchordiagnosticKey: "bottomLeft"diagnosticLabel: i18nc("@info Name used in the diagnostic log", "Bottom-left")visible: tool.state === KCM.CalibrationTool.Testing || tool.currentTarget === 2anchors {left: parent.leftleftMargin: Kirigami.Units.gridUnit * 4bottom: parent.bottombottomMargin: Kirigami.Units.gridUnit * 4}}PairTargetPath {anchor: bottomLeftAnchor}Target {id: bottomRightAnchordiagnosticKey: "bottomRight"diagnosticLabel: i18nc("@info Name used in the diagnostic log", "Bottom-right")visible: tool.state === KCM.CalibrationTool.Testing || tool.currentTarget === 3anchors {right: parent.rightrightMargin: Kirigami.Units.gridUnit * 4bottom: parent.bottombottomMargin: Kirigami.Units.gridUnit * 4}}PairTargetPath {anchor: bottomRightAnchor}Target {id: centerAnchordiagnosticKey: "center"diagnosticLabel: i18nc("@info Name used in the diagnostic log", "Center")visible: tool.state === KCM.CalibrationTool.Testinganchors.centerIn: parent}}