From 97e88dbd6f96074fd5085064bca2b29e1b24bfaa Mon Sep 17 00:00:00 2001 From: Polaris-Night <158275221@qq.com> Date: Sat, 17 May 2025 08:25:17 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20FluTour=E5=A2=9E=E5=8A=A0=E6=8C=87?= =?UTF-8?q?=E7=A4=BA=E5=99=A8=E5=92=8C=E5=8A=A8=E7=94=BB=E6=95=88=E6=9E=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- example/example_en_US.ts | 23 +- example/example_zh_CN.ts | 29 ++- example/qml/page/T_Tour.qml | 30 ++- src/Qt5/imports/FluentUI/Controls/FluTour.qml | 229 +++++++++++++++--- src/Qt5/imports/FluentUI/plugins.qmltypes | 2 + src/Qt6/imports/FluentUI/Controls/FluTour.qml | 229 +++++++++++++++--- 6 files changed, 449 insertions(+), 93 deletions(-) diff --git a/example/example_en_US.ts b/example/example_en_US.ts index a4886c29..15e18788 100644 --- a/example/example_en_US.ts +++ b/example/example_en_US.ts @@ -2737,49 +2737,60 @@ Some contents... T_Tour + Upload File + Put your files here. - - + + + Save + Save your changes. + Other Actions + Click to see other actions. - + Begin Tour - - + + Begin Tour with custom indicator + + + + + Upload - + More diff --git a/example/example_zh_CN.ts b/example/example_zh_CN.ts index 5d1d3c5d..f2be137d 100644 --- a/example/example_zh_CN.ts +++ b/example/example_zh_CN.ts @@ -552,7 +552,7 @@ Tour - 游览 + 漫游式引导 @@ -2938,56 +2938,67 @@ Some contents... + Upload File 上传文件 + Put your files here. 把你的文件放在这里 - - + + + Save 保存 + Save your changes. 保存更改 + Other Actions 其他操作 + Click to see other actions. 点击查看其他操作 - + Begin Tour - 开始游览 + 开始导览 - - + + Begin Tour with custom indicator + 以自定义指示器开始导览 + + + + Upload 上传 - + More 更多 Tour - 游览 + 漫游式引导 diff --git a/example/qml/page/T_Tour.qml b/example/qml/page/T_Tour.qml index e87e5827..f7ce16df 100644 --- a/example/qml/page/T_Tour.qml +++ b/example/qml/page/T_Tour.qml @@ -17,20 +17,42 @@ FluScrollablePage{ {title:qsTr("Other Actions"),description: qsTr("Click to see other actions."),target:()=>btn_more} ] } + FluTour{ + id:tour_custom_indicator + steps:[ + {title:qsTr("Upload File"),description: qsTr("Put your files here."),target:()=>btn_upload}, + {title:qsTr("Save"),description: qsTr("Save your changes."),target:()=>btn_save}, + {title:qsTr("Other Actions"),description: qsTr("Click to see other actions."),target:()=>btn_more} + ] + indicator: Component{ + FluText { + text: "%1 / %2".arg(current + 1).arg(total) + } + } + } FluFrame{ Layout.fillWidth: true Layout.preferredHeight: 130 padding: 10 - FluFilledButton{ + Row{ anchors{ top: parent.top topMargin: 14 } - text: qsTr("Begin Tour") - onClicked: { - tour.open() + spacing: 20 + FluFilledButton{ + text: qsTr("Begin Tour") + onClicked: { + tour.open() + } + } + FluFilledButton{ + text: qsTr("Begin Tour with custom indicator") + onClicked: { + tour_custom_indicator.open() + } } } diff --git a/src/Qt5/imports/FluentUI/Controls/FluTour.qml b/src/Qt5/imports/FluentUI/Controls/FluTour.qml index d8ff7027..b73b4853 100644 --- a/src/Qt5/imports/FluentUI/Controls/FluTour.qml +++ b/src/Qt5/imports/FluentUI/Controls/FluTour.qml @@ -7,8 +7,10 @@ import FluentUI 1.0 Popup{ property var steps : [] property int targetMargins: 5 + property int targetRadius: 2 property Component nextButton: com_next_button property Component prevButton: com_prev_button + property Component indicator: com_indicator property int index : 0 property string finishText: qsTr("Finish") property string nextText: qsTr("Next") @@ -22,12 +24,12 @@ Popup{ contentItem: Item{} onVisibleChanged: { if(visible){ + d.animationEnabled = false control.index = 0 + d.updatePos() + d.animationEnabled = true } } - onIndexChanged: { - canvas.requestPaint() - } Component{ id: com_next_button FluFilledButton{ @@ -50,10 +52,32 @@ Popup{ } } } + Component{ + id: com_indicator + Row{ + spacing: 10 + Repeater{ + model: total + delegate: Rectangle{ + width: 8 + height: 8 + radius: 4 + scale: current === index ? 1.2 : 1 + color:{ + if(current === index){ + return FluTheme.primaryColor + } + return FluTheme.dark ? Qt.rgba(99/255,99/255,99/255,1) : Qt.rgba(214/255,214/255,214/255,1) + } + } + } + } + } Item{ id:d property var window: Window.window property point pos: Qt.point(0,0) + property bool animationEnabled: true property var step: steps[index] property var target: { if(steps[index]){ @@ -73,15 +97,22 @@ Popup{ } return control.width } + function updatePos(){ + if(d.target && d.window){ + d.pos = d.target.mapToGlobal(0,0) + d.pos = Qt.point(d.pos.x-d.window.x,d.pos.y-d.window.y) + } + } + onTargetChanged: { + updatePos() + } } Connections{ target: d.window function onWidthChanged(){ - canvas.requestPaint() timer_delay.restart() } function onHeightChanged(){ - canvas.requestPaint() timer_delay.restart() } } @@ -89,39 +120,128 @@ Popup{ id: timer_delay interval: 200 onTriggered: { - canvas.requestPaint() + d.updatePos() } } - Canvas{ - id: canvas - anchors.fill: parent - onPaint: { - d.pos = d.target.mapToGlobal(0,0) - d.pos = Qt.point(d.pos.x-d.window.x,d.pos.y-d.window.y) - var ctx = canvas.getContext("2d") - ctx.clearRect(0, 0, canvasSize.width, canvasSize.height) - ctx.save() - ctx.fillStyle = "#88000000" - ctx.fillRect(0, 0, canvasSize.width, canvasSize.height) - ctx.globalCompositeOperation = 'destination-out' - ctx.fillStyle = 'black' - var rect = Qt.rect(d.pos.x-control.targetMargins,d.pos.y-control.targetMargins, d.target.width+control.targetMargins*2, d.target.height+control.targetMargins*2) - drawRoundedRect(rect,2,ctx) - ctx.restore() + Item{ + id: targetRect + x: d.pos.x - control.targetMargins + y: d.pos.y - control.targetMargins + width: d.target ? d.target.width + control.targetMargins * 2 : 0 + height: d.target ? d.target.height + control.targetMargins * 2 : 0 + Behavior on x { + enabled: d.animationEnabled && FluTheme.animationEnabled + NumberAnimation { + duration: 167 + } } - function drawRoundedRect(rect, r, ctx) { - ctx.beginPath(); - ctx.moveTo(rect.x + r, rect.y); - ctx.lineTo(rect.x + rect.width - r, rect.y); - ctx.arcTo(rect.x + rect.width, rect.y, rect.x + rect.width, rect.y + r, r); - ctx.lineTo(rect.x + rect.width, rect.y + rect.height - r); - ctx.arcTo(rect.x + rect.width, rect.y + rect.height, rect.x + rect.width - r, rect.y + rect.height, r); - ctx.lineTo(rect.x + r, rect.y + rect.height); - ctx.arcTo(rect.x, rect.y + rect.height, rect.x, rect.y + rect.height - r, r); - ctx.lineTo(rect.x, rect.y + r); - ctx.arcTo(rect.x, rect.y, rect.x + r, rect.y, r); - ctx.closePath(); - ctx.fill() + Behavior on y { + enabled: d.animationEnabled && FluTheme.animationEnabled + NumberAnimation { + duration: 167 + } + } + Behavior on width { + enabled: d.animationEnabled && FluTheme.animationEnabled + NumberAnimation { + duration: 167 + } + } + Behavior on height { + enabled: d.animationEnabled && FluTheme.animationEnabled + NumberAnimation { + duration: 167 + } + } + } + Shape { + anchors.fill: parent + layer.enabled: true + layer.samples: 4 + layer.smooth: true + ShapePath { + fillColor: "#88000000" + strokeWidth: 0 + strokeColor: "transparent" + + // draw background + PathMove { + x: 0 + y: 0 + } + PathLine { + x: control.width + y: 0 + } + PathLine { + x: control.width + y: control.height + } + PathLine { + x: 0 + y: control.height + } + PathLine { + x: 0 + y: 0 + } + + // draw highlight + PathMove { + x: targetRect.x + control.targetRadius + y: targetRect.y + } + PathLine { + x: targetRect.x + targetRect.width - control.targetRadius + y: targetRect.y + } + PathArc { + x: targetRect.x + targetRect.width + y: targetRect.y + control.targetRadius + radiusX: control.targetRadius + radiusY: control.targetRadius + useLargeArc: false + direction: PathArc.Clockwise + } + + PathLine { + x: targetRect.x + targetRect.width + y: targetRect.y + targetRect.height - control.targetRadius + } + PathArc { + x: targetRect.x + targetRect.width - control.targetRadius + y: targetRect.y + targetRect.height + radiusX: control.targetRadius + radiusY: control.targetRadius + useLargeArc: false + direction: PathArc.Clockwise + } + + PathLine { + x: targetRect.x + control.targetRadius + y: targetRect.y + targetRect.height + } + PathArc { + x: targetRect.x + y: targetRect.y + targetRect.height - control.targetRadius + radiusX: control.targetRadius + radiusY: control.targetRadius + useLargeArc: false + direction: PathArc.Clockwise + } + + PathLine { + x: targetRect.x + y: targetRect.y + control.targetRadius + } + PathArc { + x: targetRect.x + control.targetRadius + y: targetRect.y + radiusX: control.targetRadius + radiusY: control.targetRadius + useLargeArc: false + direction: PathArc.Clockwise + } } } FluFrame{ @@ -151,6 +271,18 @@ Popup{ return 0 } border.width: 0 + Behavior on x { + enabled: d.animationEnabled && FluTheme.animationEnabled + NumberAnimation { + duration: 167 + } + } + Behavior on y { + enabled: d.animationEnabled && FluTheme.animationEnabled + NumberAnimation { + duration: 167 + } + } FluShadow{ radius: 5 } @@ -193,10 +325,21 @@ Popup{ leftMargin: 15 } } + FluLoader{ + readonly property int total: steps.length + readonly property int current: control.index + sourceComponent: control.indicator + anchors{ + bottom: parent.bottom + left: parent.left + bottomMargin: 15 + leftMargin: 15 + } + } FluLoader{ id: loader_next property bool isEnd: control.index === steps.length-1 - sourceComponent: com_next_button + sourceComponent: control.nextButton anchors{ top: text_desc.bottom topMargin: 10 @@ -207,7 +350,7 @@ Popup{ FluLoader{ id: loader_prev visible: control.index !== 0 - sourceComponent: com_prev_button + sourceComponent: control.prevButton anchors{ right: loader_next.left top: loader_next.top @@ -246,5 +389,17 @@ Popup{ } return 0 } + Behavior on x { + enabled: d.animationEnabled && FluTheme.animationEnabled + NumberAnimation { + duration: 167 + } + } + Behavior on y { + enabled: d.animationEnabled && FluTheme.animationEnabled + NumberAnimation { + duration: 167 + } + } } } diff --git a/src/Qt5/imports/FluentUI/plugins.qmltypes b/src/Qt5/imports/FluentUI/plugins.qmltypes index ffeabf87..17c84d82 100644 --- a/src/Qt5/imports/FluentUI/plugins.qmltypes +++ b/src/Qt5/imports/FluentUI/plugins.qmltypes @@ -4328,8 +4328,10 @@ Module { defaultProperty: "contentData" Property { name: "steps"; type: "QVariant" } Property { name: "targetMargins"; type: "int" } + Property { name: "targetRadius"; type: "int" } Property { name: "nextButton"; type: "QQmlComponent"; isPointer: true } Property { name: "prevButton"; type: "QQmlComponent"; isPointer: true } + Property { name: "indicator"; type: "QQmlComponent"; isPointer: true } Property { name: "index"; type: "int" } Property { name: "finishText"; type: "string" } Property { name: "nextText"; type: "string" } diff --git a/src/Qt6/imports/FluentUI/Controls/FluTour.qml b/src/Qt6/imports/FluentUI/Controls/FluTour.qml index fea6368b..5049c9cc 100644 --- a/src/Qt6/imports/FluentUI/Controls/FluTour.qml +++ b/src/Qt6/imports/FluentUI/Controls/FluTour.qml @@ -7,8 +7,10 @@ import FluentUI Popup{ property var steps : [] property int targetMargins: 5 + property int targetRadius: 2 property Component nextButton: com_next_button property Component prevButton: com_prev_button + property Component indicator: com_indicator property int index : 0 property string finishText: qsTr("Finish") property string nextText: qsTr("Next") @@ -22,12 +24,12 @@ Popup{ contentItem: Item{} onVisibleChanged: { if(visible){ + d.animationEnabled = false control.index = 0 + d.updatePos() + d.animationEnabled = true } } - onIndexChanged: { - canvas.requestPaint() - } Component{ id: com_next_button FluFilledButton{ @@ -50,10 +52,32 @@ Popup{ } } } + Component{ + id: com_indicator + Row{ + spacing: 10 + Repeater{ + model: total + delegate: Rectangle{ + width: 8 + height: 8 + radius: 4 + scale: current === index ? 1.2 : 1 + color:{ + if(current === index){ + return FluTheme.primaryColor + } + return FluTheme.dark ? Qt.rgba(99/255,99/255,99/255,1) : Qt.rgba(214/255,214/255,214/255,1) + } + } + } + } + } Item{ id:d property var window: Window.window property point pos: Qt.point(0,0) + property bool animationEnabled: true property var step: steps[index] property var target: { if(steps[index]){ @@ -73,15 +97,22 @@ Popup{ } return control.width } + function updatePos(){ + if(d.target && d.window){ + d.pos = d.target.mapToGlobal(0,0) + d.pos = Qt.point(d.pos.x-d.window.x,d.pos.y-d.window.y) + } + } + onTargetChanged: { + updatePos() + } } Connections{ target: d.window function onWidthChanged(){ - canvas.requestPaint() timer_delay.restart() } function onHeightChanged(){ - canvas.requestPaint() timer_delay.restart() } } @@ -89,39 +120,128 @@ Popup{ id: timer_delay interval: 200 onTriggered: { - canvas.requestPaint() + d.updatePos() } } - Canvas{ - id: canvas - anchors.fill: parent - onPaint: { - d.pos = d.target.mapToGlobal(0,0) - d.pos = Qt.point(d.pos.x-d.window.x,d.pos.y-d.window.y) - var ctx = canvas.getContext("2d") - ctx.clearRect(0, 0, canvasSize.width, canvasSize.height) - ctx.save() - ctx.fillStyle = "#88000000" - ctx.fillRect(0, 0, canvasSize.width, canvasSize.height) - ctx.globalCompositeOperation = 'destination-out' - ctx.fillStyle = 'black' - var rect = Qt.rect(d.pos.x-control.targetMargins,d.pos.y-control.targetMargins, d.target.width+control.targetMargins*2, d.target.height+control.targetMargins*2) - drawRoundedRect(rect,2,ctx) - ctx.restore() + Item{ + id: targetRect + x: d.pos.x - control.targetMargins + y: d.pos.y - control.targetMargins + width: d.target ? d.target.width + control.targetMargins * 2 : 0 + height: d.target ? d.target.height + control.targetMargins * 2 : 0 + Behavior on x { + enabled: d.animationEnabled && FluTheme.animationEnabled + NumberAnimation { + duration: 167 + } } - function drawRoundedRect(rect, r, ctx) { - ctx.beginPath(); - ctx.moveTo(rect.x + r, rect.y); - ctx.lineTo(rect.x + rect.width - r, rect.y); - ctx.arcTo(rect.x + rect.width, rect.y, rect.x + rect.width, rect.y + r, r); - ctx.lineTo(rect.x + rect.width, rect.y + rect.height - r); - ctx.arcTo(rect.x + rect.width, rect.y + rect.height, rect.x + rect.width - r, rect.y + rect.height, r); - ctx.lineTo(rect.x + r, rect.y + rect.height); - ctx.arcTo(rect.x, rect.y + rect.height, rect.x, rect.y + rect.height - r, r); - ctx.lineTo(rect.x, rect.y + r); - ctx.arcTo(rect.x, rect.y, rect.x + r, rect.y, r); - ctx.closePath(); - ctx.fill() + Behavior on y { + enabled: d.animationEnabled && FluTheme.animationEnabled + NumberAnimation { + duration: 167 + } + } + Behavior on width { + enabled: d.animationEnabled && FluTheme.animationEnabled + NumberAnimation { + duration: 167 + } + } + Behavior on height { + enabled: d.animationEnabled && FluTheme.animationEnabled + NumberAnimation { + duration: 167 + } + } + } + Shape { + anchors.fill: parent + layer.enabled: true + layer.samples: 4 + layer.smooth: true + ShapePath { + fillColor: "#88000000" + strokeWidth: 0 + strokeColor: "transparent" + + // draw background + PathMove { + x: 0 + y: 0 + } + PathLine { + x: control.width + y: 0 + } + PathLine { + x: control.width + y: control.height + } + PathLine { + x: 0 + y: control.height + } + PathLine { + x: 0 + y: 0 + } + + // draw highlight + PathMove { + x: targetRect.x + control.targetRadius + y: targetRect.y + } + PathLine { + x: targetRect.x + targetRect.width - control.targetRadius + y: targetRect.y + } + PathArc { + x: targetRect.x + targetRect.width + y: targetRect.y + control.targetRadius + radiusX: control.targetRadius + radiusY: control.targetRadius + useLargeArc: false + direction: PathArc.Clockwise + } + + PathLine { + x: targetRect.x + targetRect.width + y: targetRect.y + targetRect.height - control.targetRadius + } + PathArc { + x: targetRect.x + targetRect.width - control.targetRadius + y: targetRect.y + targetRect.height + radiusX: control.targetRadius + radiusY: control.targetRadius + useLargeArc: false + direction: PathArc.Clockwise + } + + PathLine { + x: targetRect.x + control.targetRadius + y: targetRect.y + targetRect.height + } + PathArc { + x: targetRect.x + y: targetRect.y + targetRect.height - control.targetRadius + radiusX: control.targetRadius + radiusY: control.targetRadius + useLargeArc: false + direction: PathArc.Clockwise + } + + PathLine { + x: targetRect.x + y: targetRect.y + control.targetRadius + } + PathArc { + x: targetRect.x + control.targetRadius + y: targetRect.y + radiusX: control.targetRadius + radiusY: control.targetRadius + useLargeArc: false + direction: PathArc.Clockwise + } } } FluFrame{ @@ -151,6 +271,18 @@ Popup{ return 0 } border.width: 0 + Behavior on x { + enabled: d.animationEnabled && FluTheme.animationEnabled + NumberAnimation { + duration: 167 + } + } + Behavior on y { + enabled: d.animationEnabled && FluTheme.animationEnabled + NumberAnimation { + duration: 167 + } + } FluShadow{ radius: 5 } @@ -193,10 +325,21 @@ Popup{ leftMargin: 15 } } + FluLoader{ + readonly property int total: steps.length + readonly property int current: control.index + sourceComponent: control.indicator + anchors{ + bottom: parent.bottom + left: parent.left + bottomMargin: 15 + leftMargin: 15 + } + } FluLoader{ id: loader_next property bool isEnd: control.index === steps.length-1 - sourceComponent: com_next_button + sourceComponent: control.nextButton anchors{ top: text_desc.bottom topMargin: 10 @@ -207,7 +350,7 @@ Popup{ FluLoader{ id: loader_prev visible: control.index !== 0 - sourceComponent: com_prev_button + sourceComponent: control.prevButton anchors{ right: loader_next.left top: loader_next.top @@ -246,5 +389,17 @@ Popup{ } return 0 } + Behavior on x { + enabled: d.animationEnabled && FluTheme.animationEnabled + NumberAnimation { + duration: 167 + } + } + Behavior on y { + enabled: d.animationEnabled && FluTheme.animationEnabled + NumberAnimation { + duration: 167 + } + } } }