完成主要交互、高性能组件、国际化和A型传感器数据包接收
This commit is contained in:
@@ -1,18 +1,56 @@
|
||||
import QtQuick
|
||||
import QtQuick.Window
|
||||
import QtQuick.Controls.Material
|
||||
import QtQuick.Layouts
|
||||
import "./"
|
||||
import TactileIPC 1.0
|
||||
|
||||
Rectangle {
|
||||
// width: Constants.width
|
||||
// height: Constants.height
|
||||
width: 360
|
||||
// minimumWidth: 800
|
||||
// minimumHeight: 600
|
||||
Item {
|
||||
id: root
|
||||
width: 1280
|
||||
height: 720
|
||||
|
||||
visible: true
|
||||
Material.theme: Backend.lightMode ? Material.Light : Material.Dark
|
||||
Material.accent: Material.Green
|
||||
Material.primary: Material.Green
|
||||
|
||||
ControlPanel {
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
spacing: 0
|
||||
|
||||
NavBar {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
spacing: 0
|
||||
|
||||
LeftPanel {
|
||||
Layout.preferredWidth: 500
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
radius: 10
|
||||
color: Qt.rgba(0, 0, 0, 0.04)
|
||||
border.color: Qt.rgba(0, 0, 0, 0.08)
|
||||
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
spacing: 8
|
||||
Label { text: "OpenGL View"; font.pixelSize: 16 }
|
||||
Label { text: "(QWidget GLWidget attached in C++)"; font.pixelSize: 12 }
|
||||
}
|
||||
}
|
||||
|
||||
RightPanel {
|
||||
Layout.preferredWidth: 320
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
117
qml/content/CollapsiblePanel.qml
Normal file
117
qml/content/CollapsiblePanel.qml
Normal file
@@ -0,0 +1,117 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Controls.Material
|
||||
import QtQuick.Layouts
|
||||
import TactileIPC 1.0
|
||||
|
||||
Item {
|
||||
id: root
|
||||
width: 350
|
||||
|
||||
property alias title: titleText.text
|
||||
property bool expanded: true
|
||||
property int contentPadding: 12
|
||||
property color panelBgColor: Backend.lightMode ? "#FFFFFF" : "#2F2F2F"
|
||||
property color panelBorderColor: Backend.lightMode ? "#E0E0E0" : "#3A3A3A"
|
||||
property color titleColor: Backend.lightMode ? "#424242" : "#E0E0E0"
|
||||
property color dividerColor: Backend.lightMode ? "#EEEEEE" : "#3A3A3A"
|
||||
|
||||
default property alias content: contentArea.data
|
||||
implicitHeight: header.height + contentWrapper.height
|
||||
Rectangle {
|
||||
id: panelBg
|
||||
anchors.fill: parent
|
||||
radius: 6
|
||||
color: root.panelBgColor
|
||||
border.color: root.panelBorderColor
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: header
|
||||
width: parent.width
|
||||
height: 44
|
||||
radius: 6
|
||||
color: "transparent"
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: root.expanded = !root.expanded
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: 12
|
||||
anchors.rightMargin: 8
|
||||
spacing: 8
|
||||
|
||||
Rectangle {
|
||||
width: 10
|
||||
height: 10
|
||||
radius: 2
|
||||
color: Material.color(Material.Green)
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
|
||||
Label {
|
||||
id: titleText
|
||||
text: "placehold"
|
||||
font.pixelSize: 16
|
||||
color: root.titleColor
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
|
||||
ToolButton {
|
||||
id: arrowBtn
|
||||
text: "\u25BE"
|
||||
font.pixelSize: 18
|
||||
background: null
|
||||
rotation: root.expanded ? 180 : 0
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
|
||||
Behavior on rotation {
|
||||
NumberAnimation {
|
||||
duration: 200;
|
||||
easing.type: Easing.InOutQuad
|
||||
}
|
||||
}
|
||||
onClicked: root.expanded = !root.expanded
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
height: 1
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
color: root.dividerColor
|
||||
visible: true
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: contentWrapper
|
||||
anchors.top: header.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
height: root.expanded ? contentArea.implicitHeight + root.contentPadding * 2 : 0
|
||||
clip: true
|
||||
|
||||
Behavior on height {
|
||||
NumberAnimation {
|
||||
duration: 220;
|
||||
easing.type: Easing.InOutQuad
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: contentArea
|
||||
anchors.fill: parent
|
||||
anchors.margins: root.contentPadding
|
||||
spacing: 10
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
import QtQuick
|
||||
import QtQuick3D
|
||||
import QtQuick.Controls.Material
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import QtQml
|
||||
import "."
|
||||
import TactileIPC 1.0
|
||||
|
||||
Pane {
|
||||
id: root
|
||||
@@ -21,11 +21,17 @@ Pane {
|
||||
|
||||
Toggle {
|
||||
id: darkModeToggle
|
||||
text: qsTr("Dark mode")
|
||||
text: Qt.binding(function() {
|
||||
I18n.retranslateToken
|
||||
return qsTr("Dark mode")
|
||||
})
|
||||
}
|
||||
|
||||
GroupBox {
|
||||
title: qsTr("Render")
|
||||
title: Qt.binding(function() {
|
||||
I18n.retranslateToken
|
||||
return qsTr("Render")
|
||||
})
|
||||
Layout.fillWidth: true
|
||||
|
||||
ColumnLayout {
|
||||
@@ -34,17 +40,23 @@ Pane {
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Label { text: qsTr("Mode"); Layout.alignment: Qt.AlignVCenter }
|
||||
Label {
|
||||
text: Qt.binding(function() {
|
||||
I18n.retranslateToken
|
||||
return qsTr("Mode")
|
||||
})
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
ComboBox {
|
||||
id: renderModeBox
|
||||
Layout.fillWidth: true
|
||||
model: ["dataViz", "realistic"]
|
||||
Component.onCompleted: currentIndex = backend.renderMode === "realistic" ? 1 : 0
|
||||
onActivated: backend.renderMode = currentText
|
||||
Component.onCompleted: currentIndex = Backend.renderMode === "realistic" ? 1 : 0
|
||||
onActivated: Backend.renderMode = currentText
|
||||
Connections {
|
||||
target: backend
|
||||
target: Backend
|
||||
function onRenderModeChanged() {
|
||||
renderModeBox.currentIndex = backend.renderMode === "realistic" ? 1 : 0
|
||||
renderModeBox.currentIndex = Backend.renderMode === "realistic" ? 1 : 0
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -52,22 +64,28 @@ Pane {
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Label { text: qsTr("Labels"); Layout.alignment: Qt.AlignVCenter }
|
||||
Label {
|
||||
text: Qt.binding(function() {
|
||||
I18n.retranslateToken
|
||||
return qsTr("Labels")
|
||||
})
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
ComboBox {
|
||||
id: labelModeBox
|
||||
Layout.fillWidth: true
|
||||
model: ["off", "hover", "always"]
|
||||
Component.onCompleted: {
|
||||
if (backend.labelMode === "always") labelModeBox.currentIndex = 2
|
||||
else if (backend.labelMode === "hover") labelModeBox.currentIndex = 1
|
||||
if (Backend.labelMode === "always") labelModeBox.currentIndex = 2
|
||||
else if (Backend.labelMode === "hover") labelModeBox.currentIndex = 1
|
||||
else labelModeBox.currentIndex = 0
|
||||
}
|
||||
onActivated: backend.labelMode = currentText
|
||||
onActivated: Backend.labelMode = currentText
|
||||
Connections {
|
||||
target: backend
|
||||
target: Backend
|
||||
function onLabelModeChanged() {
|
||||
if (backend.labelMode === "always") labelModeBox.currentIndex = 2
|
||||
else if (backend.labelMode === "hover") labelModeBox.currentIndex = 1
|
||||
if (Backend.labelMode === "always") labelModeBox.currentIndex = 2
|
||||
else if (Backend.labelMode === "hover") labelModeBox.currentIndex = 1
|
||||
else labelModeBox.currentIndex = 0
|
||||
}
|
||||
}
|
||||
@@ -76,15 +94,21 @@ Pane {
|
||||
|
||||
Toggle {
|
||||
id: legendToggle
|
||||
text: qsTr("Legend")
|
||||
checked: backend.showLegend
|
||||
onCheckedChanged: backend.showLegend = checked
|
||||
text: Qt.binding(function() {
|
||||
I18n.retranslateToken
|
||||
return qsTr("Legend")
|
||||
})
|
||||
checked: Backend.showLegend
|
||||
onCheckedChanged: Backend.showLegend = checked
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
GroupBox {
|
||||
title: qsTr("Scale")
|
||||
title: Qt.binding(function() {
|
||||
I18n.retranslateToken
|
||||
return qsTr("Scale")
|
||||
})
|
||||
Layout.fillWidth: true
|
||||
|
||||
ColumnLayout {
|
||||
@@ -93,35 +117,47 @@ Pane {
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Label { text: qsTr("Min"); Layout.alignment: Qt.AlignVCenter }
|
||||
Label {
|
||||
text: Qt.binding(function() {
|
||||
I18n.retranslateToken
|
||||
return qsTr("Min")
|
||||
})
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
SpinBox {
|
||||
id: minBox
|
||||
Layout.fillWidth: true
|
||||
from: -999999
|
||||
to: 999999
|
||||
value: backend.minValue
|
||||
onValueModified: backend.minValue = value
|
||||
value: Backend.minValue
|
||||
onValueModified: Backend.minValue = value
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Label { text: qsTr("Max"); Layout.alignment: Qt.AlignVCenter }
|
||||
Label {
|
||||
text: Qt.binding(function() {
|
||||
I18n.retranslateToken
|
||||
return qsTr("Max")
|
||||
})
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
SpinBox {
|
||||
id: maxBox
|
||||
Layout.fillWidth: true
|
||||
from: -999999
|
||||
to: 999999
|
||||
value: backend.maxValue
|
||||
onValueModified: backend.maxValue = value
|
||||
value: Backend.maxValue
|
||||
onValueModified: Backend.maxValue = value
|
||||
}
|
||||
}
|
||||
|
||||
Legend {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
visible: backend.showLegend
|
||||
minValue: backend.minValue
|
||||
maxValue: backend.maxValue
|
||||
visible: Backend.showLegend
|
||||
minValue: Backend.minValue
|
||||
maxValue: Backend.maxValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import TactileIPC 1.0
|
||||
|
||||
Slider {
|
||||
property string lableText: qsTr("Text")
|
||||
property string lableText: Qt.binding(function() {
|
||||
I18n.retranslateToken
|
||||
return qsTr("Text")
|
||||
})
|
||||
stepSize: 1
|
||||
|
||||
Label {
|
||||
|
||||
421
qml/content/LeftPanel.qml
Normal file
421
qml/content/LeftPanel.qml
Normal file
@@ -0,0 +1,421 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls.Material
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import "."
|
||||
import TactileIPC 1.0
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
width: 350
|
||||
color: Backend.lightMode ? "#F5F5F5" : "#2C2C2C"
|
||||
radius: 8
|
||||
border.color: Qt.rgba(0, 0, 0, 0.08)
|
||||
property color textColor: Backend.lightMode ? "#424242" : "#E0E0E0"
|
||||
|
||||
function formatHexByte(value) {
|
||||
const hex = Number(value).toString(16).toUpperCase()
|
||||
return "0x" + ("00" + hex).slice(-2)
|
||||
}
|
||||
|
||||
function formatHexValue(value) {
|
||||
const hex = Number(value).toString(16).toUpperCase()
|
||||
return "0x" + hex
|
||||
}
|
||||
|
||||
function parseHexValue(text, maxValue) {
|
||||
let trimmed = String(text).trim()
|
||||
if (trimmed.startsWith("0x") || trimmed.startsWith("0X"))
|
||||
trimmed = trimmed.slice(2)
|
||||
if (trimmed.length === 0)
|
||||
return NaN
|
||||
const value = parseInt(trimmed, 16)
|
||||
if (isNaN(value) || value < 0 || value > maxValue)
|
||||
return NaN
|
||||
return value
|
||||
}
|
||||
|
||||
function tr(text) {
|
||||
I18n.retranslateToken
|
||||
return qsTr(text)
|
||||
}
|
||||
|
||||
Material.theme: Backend.lightMode ? Material.Light : Material.Dark
|
||||
Material.accent: Material.Green
|
||||
Material.primary: Material.Green
|
||||
|
||||
ScrollView {
|
||||
anchors.fill: parent
|
||||
ScrollBar.vertical.policy: ScrollBar.AlwaysOff
|
||||
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
|
||||
|
||||
ColumnLayout {
|
||||
id: layout
|
||||
anchors.fill: parent
|
||||
anchors.margins: 12
|
||||
spacing: 12
|
||||
|
||||
CollapsiblePanel {
|
||||
title: root.tr("连接设置")
|
||||
expanded: true
|
||||
Layout.fillWidth: true
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 8
|
||||
|
||||
Label {
|
||||
text: root.tr("COM Port")
|
||||
Layout.preferredWidth: 90
|
||||
color: root.textColor
|
||||
}
|
||||
|
||||
ComboBox {
|
||||
id: portBox
|
||||
Layout.fillWidth: true
|
||||
model: Backend.serial.availablePorts
|
||||
Component.onCompleted: {
|
||||
for (let i = 0; i < portBox.count; i++) {
|
||||
if (portBox.textAt(i) === Backend.serial.portName) {
|
||||
currentIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
onActivated: Backend.serial.portName = currentText
|
||||
}
|
||||
|
||||
ToolButton {
|
||||
text: "\u21bb"
|
||||
onClicked: Backend.serial.refreshPorts()
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 8
|
||||
|
||||
Label {
|
||||
text: root.tr("Baud")
|
||||
Layout.preferredWidth: 90
|
||||
color: root.textColor
|
||||
}
|
||||
|
||||
ComboBox {
|
||||
Layout.fillWidth: true
|
||||
model: ["9600", "57600", "115200", "230400", "912600"]
|
||||
Component.onCompleted: {
|
||||
const idx = model.indexOf(String(Backend.serial.baudRate))
|
||||
if (idx >= 0)
|
||||
currentIndex = idx
|
||||
}
|
||||
onActivated: Backend.serial.baudRate = parseInt(currentText)
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 8
|
||||
|
||||
Label {
|
||||
text: root.tr("模式")
|
||||
Layout.preferredWidth: 90
|
||||
color: root.textColor
|
||||
}
|
||||
|
||||
ComboBox {
|
||||
Layout.fillWidth: true
|
||||
textRole: "text"
|
||||
model: [
|
||||
{ text: root.tr("从站"), value: "slave" },
|
||||
{ text: root.tr("主站"), value: "master" }
|
||||
]
|
||||
function syncModeIndex() {
|
||||
for (let i = 0; i < model.length; i++) {
|
||||
if (model[i].value === Backend.serial.mode) {
|
||||
currentIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
Component.onCompleted: syncModeIndex()
|
||||
onModelChanged: syncModeIndex()
|
||||
Connections {
|
||||
target: Backend.serial
|
||||
function onModeChanged() {
|
||||
syncModeIndex()
|
||||
}
|
||||
}
|
||||
onActivated: Backend.serial.mode = model[currentIndex].value
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 8
|
||||
|
||||
Label {
|
||||
text: root.tr("设备地址")
|
||||
Layout.preferredWidth: 90
|
||||
color: root.textColor
|
||||
}
|
||||
|
||||
TextField {
|
||||
id: addrField
|
||||
Layout.fillWidth: true
|
||||
text: root.formatHexByte(Backend.serial.deviceAddress)
|
||||
placeholderText: "0x01"
|
||||
inputMethodHints: Qt.ImhPreferUppercase
|
||||
validator: RegularExpressionValidator {
|
||||
regularExpression: /^(0x|0X)?[0-9a-fA-F]{1,2}$/
|
||||
}
|
||||
onEditingFinished: {
|
||||
const value = root.parseHexValue(text, 255)
|
||||
if (isNaN(value)) {
|
||||
text = root.formatHexByte(Backend.serial.deviceAddress)
|
||||
return
|
||||
}
|
||||
Backend.serial.deviceAddress = value
|
||||
text = root.formatHexByte(Backend.serial.deviceAddress)
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: Backend.serial
|
||||
function onDeviceAddressChanged() {
|
||||
addrField.text = root.formatHexByte(Backend.serial.deviceAddress)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 8
|
||||
|
||||
Label {
|
||||
text: root.tr("采样周期")
|
||||
Layout.preferredWidth: 90
|
||||
color: root.textColor
|
||||
}
|
||||
|
||||
SpinBox {
|
||||
Layout.fillWidth: true
|
||||
from: 1
|
||||
to: 2000
|
||||
value: Backend.serial.pollIntervalMs
|
||||
enabled: Backend.serial.mode === "slave"
|
||||
onValueModified: Backend.serial.pollIntervalMs = value
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 12
|
||||
|
||||
Button {
|
||||
Layout.fillWidth: true
|
||||
text: root.tr("连接")
|
||||
highlighted: true
|
||||
enabled: !Backend.serial.connected
|
||||
onClicked: Backend.serial.open()
|
||||
}
|
||||
Button {
|
||||
Layout.fillWidth: true
|
||||
text: root.tr("断开")
|
||||
enabled: Backend.serial.connected
|
||||
onClicked: Backend.serial.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CollapsiblePanel {
|
||||
title: root.tr("采样参数")
|
||||
expanded: true
|
||||
Layout.fillWidth: true
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 8
|
||||
Label {
|
||||
text: root.tr("功能码")
|
||||
Layout.preferredWidth: 90
|
||||
color: root.textColor
|
||||
}
|
||||
SpinBox {
|
||||
id: requestFunctionBox
|
||||
Layout.fillWidth: true
|
||||
from: 0
|
||||
to: 255
|
||||
editable: true
|
||||
value: Backend.serial.requestFunction
|
||||
textFromValue: function(value, locale) {
|
||||
return root.formatHexByte(value)
|
||||
}
|
||||
valueFromText: function(text, locale) {
|
||||
const parsed = root.parseHexValue(text, requestFunctionBox.to)
|
||||
return isNaN(parsed) ? requestFunctionBox.value : parsed
|
||||
}
|
||||
validator: RegularExpressionValidator {
|
||||
regularExpression: /^(0x|0X)?[0-9a-fA-F]{1,2}$/
|
||||
}
|
||||
onValueModified: Backend.serial.requestFunction = value
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 8
|
||||
Label {
|
||||
text: root.tr("起始地址")
|
||||
Layout.preferredWidth: 90
|
||||
color: root.textColor
|
||||
}
|
||||
SpinBox {
|
||||
id: requestStartAddressBox
|
||||
Layout.fillWidth: true
|
||||
from: 0
|
||||
to: 1000000
|
||||
editable: true
|
||||
value: Backend.serial.requestStartAddress
|
||||
textFromValue: function(value, locale) {
|
||||
return root.formatHexValue(value)
|
||||
}
|
||||
valueFromText: function(text, locale) {
|
||||
const parsed = root.parseHexValue(text, requestStartAddressBox.to)
|
||||
return isNaN(parsed) ? requestStartAddressBox.value : parsed
|
||||
}
|
||||
validator: RegularExpressionValidator {
|
||||
regularExpression: /^(0x|0X)?[0-9a-fA-F]{1,8}$/
|
||||
}
|
||||
onValueModified: Backend.serial.requestStartAddress = value
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 8
|
||||
Label {
|
||||
text: root.tr("读取长度")
|
||||
Layout.preferredWidth: 90
|
||||
color: root.textColor
|
||||
}
|
||||
SpinBox {
|
||||
Layout.fillWidth: true
|
||||
from: 0
|
||||
to: 65535
|
||||
editable: true
|
||||
value: Backend.serial.requestLength
|
||||
onValueModified: Backend.serial.requestLength = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CollapsiblePanel {
|
||||
title: root.tr("传感器规格")
|
||||
expanded: true
|
||||
Layout.fillWidth: true
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 8
|
||||
Label {
|
||||
text: root.tr("协议")
|
||||
Layout.preferredWidth: 90
|
||||
color: root.textColor
|
||||
}
|
||||
Label {
|
||||
text: Backend.serial.protocol
|
||||
Layout.fillWidth: true
|
||||
horizontalAlignment: Text.AlignRight
|
||||
color: root.textColor
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 8
|
||||
Label {
|
||||
text: root.tr("型号")
|
||||
Layout.preferredWidth: 90
|
||||
color: root.textColor
|
||||
}
|
||||
Label {
|
||||
text: Backend.serial.sensorModel
|
||||
Layout.fillWidth: true
|
||||
horizontalAlignment: Text.AlignRight
|
||||
color: root.textColor
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 8
|
||||
Label {
|
||||
text: root.tr("规格")
|
||||
Layout.preferredWidth: 90
|
||||
color: root.textColor
|
||||
}
|
||||
Label {
|
||||
text: Backend.serial.sensorGrid
|
||||
Layout.fillWidth: true
|
||||
horizontalAlignment: Text.AlignRight
|
||||
color: root.textColor
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
Layout.fillWidth: true
|
||||
text: root.tr("重新识别")
|
||||
highlighted: true
|
||||
}
|
||||
}
|
||||
|
||||
CollapsiblePanel {
|
||||
title: root.tr("显示控制")
|
||||
expanded: true
|
||||
Layout.fillWidth: true
|
||||
|
||||
CheckBox {
|
||||
text: root.tr("显示网络")
|
||||
checked: Backend.showGrid
|
||||
onToggled: Backend.showGrid = checked
|
||||
}
|
||||
CheckBox {
|
||||
text: root.tr("显示坐标轴")
|
||||
checked: false
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 12
|
||||
|
||||
Button {
|
||||
Layout.fillWidth: true
|
||||
text: root.tr("回放数据")
|
||||
highlighted: true
|
||||
}
|
||||
Button {
|
||||
Layout.fillWidth: true
|
||||
text: root.tr("导出数据")
|
||||
onClicked: exportDlg.open()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item { Layout.fillHeight: true }
|
||||
}
|
||||
}
|
||||
SaveAsExportDialog {
|
||||
id: exportDlg
|
||||
/* onSaveTo: (folder, filename, format, method) => {
|
||||
console.log("保存目录:", folder)
|
||||
console.log("文件名:", filename)
|
||||
console.log("格式:", format, "方式:", method)
|
||||
} */
|
||||
onSaveTo: (folder, filename, format, method) => {
|
||||
Backend.data.exportHandler(folder, filename, format, method)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import TactileIPC 1.0
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
123
qml/content/LiveTrendCard.qml
Normal file
123
qml/content/LiveTrendCard.qml
Normal file
@@ -0,0 +1,123 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import TactileIPC 1.0
|
||||
import LiveTrend 1.0
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
width: 260
|
||||
height: 180
|
||||
radius: 10
|
||||
color: Backend.lightMode ? "#FFFFFF" : "#2C2C2C"
|
||||
border.color: Backend.lightMode ? "#E6E6E6" : "#3A3A3A"
|
||||
border.width: 1
|
||||
|
||||
property alias plot: plot
|
||||
property string title: "Live Trend"
|
||||
|
||||
property bool follow: true
|
||||
property color accentColor: Backend.lightMode ? "#39D353" : "#5BE37A"
|
||||
property color textColor: Backend.lightMode ? "#2B2B2B" : "#E0E0E0"
|
||||
property color plotBackground: Backend.lightMode ? "#FFFFFF" : "#1F1F1F"
|
||||
property color gridColor: Backend.lightMode ? Qt.rgba(0, 0, 0, 0.08) : Qt.rgba(1, 1, 1, 0.08)
|
||||
property color plotTextColor: Backend.lightMode ? Qt.rgba(0, 0, 0, 0.65) : Qt.rgba(1, 1, 1, 0.65)
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 10
|
||||
spacing: 6
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 8
|
||||
|
||||
Rectangle {
|
||||
width: 8
|
||||
height: 8
|
||||
radius: 2
|
||||
color: root.accentColor
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
|
||||
Label {
|
||||
text: root.title
|
||||
font.pixelSize: 14
|
||||
color: root.textColor
|
||||
Layout.fillWidth: true
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
ToolButton {
|
||||
text: root.follow ? "▲" : "↺"
|
||||
font.pixelSize: 14
|
||||
onClicked: {
|
||||
root.follow = true
|
||||
plot.follow = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Plot
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
radius: 8
|
||||
color: root.plotBackground
|
||||
|
||||
SparklinePlot {
|
||||
id: plot
|
||||
anchors.fill: parent
|
||||
leftPadding: 40
|
||||
rightPadding: 6
|
||||
topPadding: 8
|
||||
bottomPadding: 18
|
||||
|
||||
yTickCount: 5
|
||||
lineColor: root.accentColor
|
||||
gridColor: root.gridColor
|
||||
textColor: root.plotTextColor
|
||||
|
||||
viewCount: 5
|
||||
follow: root.follow
|
||||
|
||||
// 60fps
|
||||
Timer {
|
||||
interval: 16
|
||||
running: true
|
||||
repeat: true
|
||||
onTriggered: plot.update()
|
||||
}
|
||||
|
||||
DragHandler {
|
||||
onActiveChanged: if (active) {
|
||||
root.follow = false
|
||||
plot.follow = false
|
||||
}
|
||||
onTranslationChanged: (t) => {
|
||||
let move = Math.round(-t.x / 12)
|
||||
if (move !== 0) {
|
||||
plot.viewStart = Math.max(0, plot.viewStart + move)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
WheelHandler {
|
||||
onWheel: (w) => {
|
||||
root.follow = false
|
||||
plot.follow = false
|
||||
let factor = w.angleDelta.y > 0 ? 0.85 : 1.15
|
||||
plot.viewCount = Math.max(10, Math.min(2000000, Math.floor(plot.viewCount * factor)))
|
||||
}
|
||||
}
|
||||
|
||||
TapHandler {
|
||||
onDoubleTapped: {
|
||||
root.follow = true
|
||||
plot.follow = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
165
qml/content/NavBar.qml
Normal file
165
qml/content/NavBar.qml
Normal file
@@ -0,0 +1,165 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Controls.Material
|
||||
import QtQuick.Layouts
|
||||
import TactileIPC 1.0
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
height: 56
|
||||
color: Backend.lightMode ? "#F5F7F5" : "#2B2F2B"
|
||||
|
||||
Material.theme: Backend.lightMode ? Material.Light : Material.Dark
|
||||
Material.accent: Material.Green
|
||||
Material.primary: Material.Green
|
||||
|
||||
Rectangle {
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
height: 1
|
||||
color: Qt.rgba(0, 0, 0, 0.08)
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: 16
|
||||
anchors.rightMargin: 16
|
||||
spacing: 14
|
||||
|
||||
Label {
|
||||
text: "Tactile IPC 3D"
|
||||
font.pixelSize: 18
|
||||
font.weight: Font.DemiBold
|
||||
color: Material.color(Material.Green)
|
||||
}
|
||||
|
||||
Item { Layout.fillWidth: true }
|
||||
|
||||
RowLayout {
|
||||
spacing: 6
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
|
||||
Rectangle {
|
||||
width: 10
|
||||
height: 10
|
||||
radius: 5
|
||||
color: Backend.connected ? "#2e7d32" : "#d32f2f"
|
||||
}
|
||||
|
||||
Label {
|
||||
text: Qt.binding(function() {
|
||||
I18n.retranslateToken
|
||||
return Backend.connected ? qsTr("CONNECTED") : qsTr("DISCONNECTED")
|
||||
})
|
||||
font.pixelSize: 12
|
||||
font.weight: Font.DemiBold
|
||||
color: Backend.connected ? "#2e7d32" : "#d32f2f"
|
||||
}
|
||||
}
|
||||
|
||||
Switch {
|
||||
id: themeSwitch
|
||||
text: Qt.binding(function() {
|
||||
I18n.retranslateToken
|
||||
return Backend.lightMode ? qsTr("Light") : qsTr("Dark")
|
||||
})
|
||||
checked: Backend.lightMode
|
||||
onCheckedChanged: Backend.lightMode = checked
|
||||
}
|
||||
|
||||
ComboBox {
|
||||
id: langBox
|
||||
implicitHeight: 32
|
||||
leftPadding: 10
|
||||
rightPadding: 26
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
model: [
|
||||
{ text: "中文", value: "zh_CN", icon: "qrc:/images/china.png" },
|
||||
{ text: "English", value: "en_US", icon: "qrc:/images/united-states.png" }
|
||||
]
|
||||
textRole: "text"
|
||||
delegate: ItemDelegate {
|
||||
width: langBox.width
|
||||
height: 28
|
||||
onClicked: {
|
||||
langBox.currentIndex = index
|
||||
langBox.popup.close()
|
||||
}
|
||||
contentItem: Row {
|
||||
spacing: 8
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 8
|
||||
Image {
|
||||
width: 18
|
||||
height: 12
|
||||
fillMode: Image.PreserveAspectFit
|
||||
source: modelData.icon
|
||||
}
|
||||
Label {
|
||||
text: modelData.text
|
||||
font: langBox.font
|
||||
}
|
||||
}
|
||||
}
|
||||
contentItem: Item {
|
||||
anchors.fill: parent
|
||||
Row {
|
||||
spacing: 8
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
Image {
|
||||
width: 18
|
||||
height: 12
|
||||
fillMode: Image.PreserveAspectFit
|
||||
source: langBox.model[langBox.currentIndex]
|
||||
? langBox.model[langBox.currentIndex].icon
|
||||
: ""
|
||||
}
|
||||
Label {
|
||||
text: langBox.displayText
|
||||
font: langBox.font
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
popup: Popup {
|
||||
y: langBox.height
|
||||
width: langBox.width
|
||||
implicitHeight: contentItem.implicitHeight
|
||||
padding: 0
|
||||
modal: false
|
||||
focus: true
|
||||
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
|
||||
popupType: Popup.Window
|
||||
contentItem: ListView {
|
||||
implicitHeight: contentHeight
|
||||
model: langBox.delegateModel
|
||||
currentIndex: langBox.highlightedIndex
|
||||
delegate: langBox.delegate
|
||||
clip: true
|
||||
}
|
||||
}
|
||||
function syncLanguage() {
|
||||
for (let i = 0; i < model.length; i++) {
|
||||
if (model[i].value === Backend.language) {
|
||||
if (currentIndex !== i)
|
||||
currentIndex = i
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
Component.onCompleted: syncLanguage()
|
||||
Connections {
|
||||
target: Backend
|
||||
function onLanguageChanged() {
|
||||
langBox.syncLanguage()
|
||||
}
|
||||
}
|
||||
onCurrentIndexChanged: {
|
||||
if (model[currentIndex])
|
||||
Backend.language = model[currentIndex].value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
198
qml/content/RightPanel.qml
Normal file
198
qml/content/RightPanel.qml
Normal file
@@ -0,0 +1,198 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls.Material
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import "."
|
||||
import TactileIPC 1.0
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
width: 340
|
||||
color: Backend.lightMode ? "#F5F5F5" : "#2C2C2C"
|
||||
radius: 8
|
||||
border.color: Qt.rgba(0, 0, 0, 0.08)
|
||||
property color accentColor: "#43a047"
|
||||
|
||||
function tr(text) {
|
||||
I18n.retranslateToken
|
||||
return qsTr(text)
|
||||
}
|
||||
|
||||
Material.theme: Backend.lightMode ? Material.Light : Material.Dark
|
||||
Material.accent: Material.Green
|
||||
Material.primary: Material.Green
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 12
|
||||
spacing: 12
|
||||
|
||||
LiveTrendCard {
|
||||
id: card
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 180
|
||||
title: root.tr("Payload Sum")
|
||||
|
||||
Connections {
|
||||
target: Backend.data
|
||||
function onMetricsChanged() {
|
||||
if (Backend.data.frameCount > 0)
|
||||
card.plot.append(Backend.data.metricSum)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CollapsiblePanel {
|
||||
title: root.tr("Live Trend")
|
||||
expanded: true
|
||||
Layout.fillWidth: true
|
||||
|
||||
|
||||
|
||||
/*
|
||||
Canvas {
|
||||
id: trendCanvas
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 180
|
||||
property var samples: [0.08, 0.24, 0.52, 0.41, 0.63, 0.47, 0.72, 0.58, 0.82, 0.69]
|
||||
|
||||
onPaint: {
|
||||
const ctx = getContext("2d")
|
||||
const w = width
|
||||
const h = height
|
||||
ctx.clearRect(0, 0, w, h)
|
||||
|
||||
ctx.strokeStyle = "#D8EAD9"
|
||||
ctx.lineWidth = 1
|
||||
for (let i = 1; i < 5; i++) {
|
||||
const y = (h / 5) * i
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(0, y)
|
||||
ctx.lineTo(w, y)
|
||||
ctx.stroke()
|
||||
}
|
||||
|
||||
ctx.strokeStyle = root.accentColor
|
||||
ctx.lineWidth = 2
|
||||
ctx.beginPath()
|
||||
for (let i = 0; i < samples.length; i++) {
|
||||
const x = (w - 12) * (i / (samples.length - 1)) + 6
|
||||
const y = h - (h - 12) * samples[i] - 6
|
||||
if (i === 0)
|
||||
ctx.moveTo(x, y)
|
||||
else
|
||||
ctx.lineTo(x, y)
|
||||
}
|
||||
ctx.stroke()
|
||||
}
|
||||
|
||||
onWidthChanged: requestPaint()
|
||||
onHeightChanged: requestPaint()
|
||||
Component.onCompleted: requestPaint()
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
CollapsiblePanel {
|
||||
title: root.tr("Metrics")
|
||||
expanded: true
|
||||
Layout.fillWidth: true
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 8
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
radius: 6
|
||||
color: Qt.rgba(0, 0, 0, 0.03)
|
||||
border.color: Qt.rgba(0, 0, 0, 0.08)
|
||||
height: 72
|
||||
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
spacing: 4
|
||||
Label { text: root.tr("峰值"); font.pixelSize: 12 }
|
||||
Label { text: Backend.data.metricPeak.toFixed(0); font.pixelSize: 18; font.bold: true }
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
radius: 6
|
||||
color: Qt.rgba(0, 0, 0, 0.03)
|
||||
border.color: Qt.rgba(0, 0, 0, 0.08)
|
||||
height: 72
|
||||
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
spacing: 4
|
||||
Label { text: root.tr("均方根"); font.pixelSize: 12 }
|
||||
Label { text: Backend.data.metricRms.toFixed(0); font.pixelSize: 18; font.bold: true }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 8
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
radius: 6
|
||||
color: Qt.rgba(0, 0, 0, 0.03)
|
||||
border.color: Qt.rgba(0, 0, 0, 0.08)
|
||||
height: 72
|
||||
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
spacing: 4
|
||||
Label { text: root.tr("平均值"); font.pixelSize: 12 }
|
||||
Label { text: Backend.data.metricAvg.toFixed(0); font.pixelSize: 18; font.bold: true }
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
radius: 6
|
||||
color: Qt.rgba(0, 0, 0, 0.03)
|
||||
border.color: Qt.rgba(0, 0, 0, 0.08)
|
||||
height: 72
|
||||
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
spacing: 4
|
||||
Label { text: root.tr("变化量"); font.pixelSize: 12 }
|
||||
Label { text: Backend.data.metricDelta.toFixed(0); font.pixelSize: 18; font.bold: true }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CollapsiblePanel {
|
||||
title: root.tr("Session")
|
||||
expanded: true
|
||||
Layout.fillWidth: true
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 8
|
||||
Label { text: root.tr("Frames"); Layout.preferredWidth: 80 }
|
||||
Label { text: Backend.data.frameCount; Layout.fillWidth: true; horizontalAlignment: Text.AlignRight }
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 8
|
||||
Label { text: root.tr("Playback"); Layout.preferredWidth: 80 }
|
||||
Label {
|
||||
text: Backend.data.playbackRunning ? root.tr("Running") : root.tr("Idle")
|
||||
Layout.fillWidth: true
|
||||
horizontalAlignment: Text.AlignRight
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item { Layout.fillHeight: true }
|
||||
}
|
||||
}
|
||||
561
qml/content/SaveAsExportDialog.qml
Normal file
561
qml/content/SaveAsExportDialog.qml
Normal file
@@ -0,0 +1,561 @@
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15
|
||||
import QtQuick.Controls.Material 2.15
|
||||
import QtQuick.Layouts 1.15
|
||||
import QtQuick.Window 2.15
|
||||
import Qt.labs.folderlistmodel 2.15
|
||||
import QtCore 6.2
|
||||
import QtQuick.Dialogs
|
||||
import TactileIPC 1.0
|
||||
|
||||
Window {
|
||||
id: root
|
||||
width: 980
|
||||
height: 640
|
||||
minimumWidth: 880
|
||||
minimumHeight: 560
|
||||
visible: false
|
||||
modality: Qt.ApplicationModal
|
||||
flags: Qt.Dialog | Qt.WindowTitleHint | Qt.WindowCloseButtonHint
|
||||
title: root.tr("导出数据")
|
||||
color: windowBg
|
||||
|
||||
readonly property bool isDark: !Backend.lightMode
|
||||
readonly property color windowBg: isDark ? "#1B1F1B" : "#F7F8F9"
|
||||
Material.accent: root.accent
|
||||
Material.primary: root.accent
|
||||
Material.theme: root.isDark ? Material.Dark : Material.Light
|
||||
|
||||
readonly property color accent: "#21A453"
|
||||
readonly property color accentSoft: root.isDark ? "#1F3A2A" : "#E6F6EC"
|
||||
readonly property color panel: root.isDark ? "#242924" : "#FFFFFF"
|
||||
readonly property color border: root.isDark ? "#343A35" : "#E1E5EA"
|
||||
readonly property color text: root.isDark ? "#E6ECE7" : "#1E2A32"
|
||||
readonly property color subText: root.isDark ? "#9AA5A0" : "#6E7A86"
|
||||
readonly property color fieldBg: root.isDark ? "#1E221E" : "#FFFFFF"
|
||||
readonly property color surfaceAlt: root.isDark ? "#202520" : "#F9FAFB"
|
||||
readonly property color hoverBg: root.isDark ? "#2C322D" : "#F3F6F8"
|
||||
readonly property color iconBg: root.isDark ? "#25362B" : "#E8F3EA"
|
||||
readonly property color iconBgAlt: root.isDark ? "#2A302A" : "#EFF2F5"
|
||||
readonly property color disabledBg: root.isDark ? "#4B544E" : "#C9D2D8"
|
||||
readonly property string uiFont: "Microsoft YaHei UI"
|
||||
|
||||
property url currentFolder: StandardPaths.writableLocation(StandardPaths.DocumentsLocation) + "/"
|
||||
property string chosenFilename: ""
|
||||
property string exportFormat: formatBox.currentValue
|
||||
property string exportMethod: methodBox.currentValue
|
||||
property string lastMethodValue: "overwrite"
|
||||
|
||||
signal saveTo(url folder, string filename, string format, string method)
|
||||
|
||||
function open() {
|
||||
centerOnScreen_()
|
||||
visible = true
|
||||
requestActivate()
|
||||
}
|
||||
|
||||
function accept() {
|
||||
visible = false
|
||||
}
|
||||
|
||||
function reject() {
|
||||
visible = false
|
||||
}
|
||||
|
||||
function centerOnScreen_() {
|
||||
x = Math.round((Screen.width - width) / 2)
|
||||
y = Math.round((Screen.height - height) / 2)
|
||||
}
|
||||
|
||||
function normalizeFolder_(path) {
|
||||
if (!path)
|
||||
return path
|
||||
if (path.endsWith("/"))
|
||||
return path
|
||||
return path + "/"
|
||||
}
|
||||
|
||||
function tr(text) {
|
||||
I18n.retranslateToken
|
||||
return qsTr(text)
|
||||
}
|
||||
|
||||
onVisibleChanged: if (visible) centerOnScreen_()
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 16
|
||||
spacing: 12
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
height: 54
|
||||
radius: 6
|
||||
color: root.panel
|
||||
border.color: root.border
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 8
|
||||
spacing: 8
|
||||
|
||||
ToolButton {
|
||||
id: backBtn
|
||||
text: "<"
|
||||
font.family: root.uiFont
|
||||
onClicked: nav.back()
|
||||
background: Rectangle {
|
||||
radius: 4
|
||||
color: backBtn.hovered ? root.accentSoft : "transparent"
|
||||
border.color: backBtn.hovered ? root.accent : root.border
|
||||
}
|
||||
}
|
||||
ToolButton {
|
||||
id: forwardBtn
|
||||
text: ">"
|
||||
font.family: root.uiFont
|
||||
onClicked: nav.forward()
|
||||
background: Rectangle {
|
||||
radius: 4
|
||||
color: forwardBtn.hovered ? root.accentSoft : "transparent"
|
||||
border.color: forwardBtn.hovered ? root.accent : root.border
|
||||
}
|
||||
}
|
||||
ToolButton {
|
||||
id: upBtn
|
||||
text: "^"
|
||||
font.family: root.uiFont
|
||||
onClicked: nav.up()
|
||||
background: Rectangle {
|
||||
radius: 4
|
||||
color: upBtn.hovered ? root.accentSoft : "transparent"
|
||||
border.color: upBtn.hovered ? root.accent : root.border
|
||||
}
|
||||
}
|
||||
|
||||
TextField {
|
||||
id: breadcrumb
|
||||
Layout.fillWidth: true
|
||||
readOnly: true
|
||||
font.family: root.uiFont
|
||||
color: root.text
|
||||
text: root.currentFolder.toString()
|
||||
background: Rectangle {
|
||||
radius: 4
|
||||
color: root.surfaceAlt
|
||||
border.color: root.border
|
||||
}
|
||||
}
|
||||
|
||||
TextField {
|
||||
id: searchField
|
||||
Layout.preferredWidth: 220
|
||||
placeholderText: root.tr("在此位置中搜索")
|
||||
font.family: root.uiFont
|
||||
background: Rectangle {
|
||||
radius: 4
|
||||
color: root.fieldBg
|
||||
border.color: root.border
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
spacing: 12
|
||||
|
||||
Rectangle {
|
||||
Layout.preferredWidth: 220
|
||||
Layout.fillHeight: true
|
||||
radius: 6
|
||||
color: root.panel
|
||||
border.color: root.border
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 10
|
||||
spacing: 8
|
||||
|
||||
Label {
|
||||
text: root.tr("位置")
|
||||
font.bold: true
|
||||
font.family: root.uiFont
|
||||
color: root.text
|
||||
}
|
||||
|
||||
ListView {
|
||||
id: places
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
clip: true
|
||||
model: [
|
||||
{ name: root.tr("此电脑"), url: "file:///", icon: root.isDark ? "qrc:/images/computer_dark.png" : "qrc:/images/computer_light.png" },
|
||||
{ name: root.tr("桌面"), url: StandardPaths.writableLocation(StandardPaths.DesktopLocation) + "/", icon: root.isDark ? "qrc:/images/desktop_dark.png" : "qrc:/images/desktop_light.png" },
|
||||
{ name: root.tr("文档"), url: StandardPaths.writableLocation(StandardPaths.DocumentsLocation) + "/", icon: root.isDark ? "qrc:/images/docs_dark.png" : "qrc:/images/docs_light.png" },
|
||||
{ name: root.tr("下载"), url: StandardPaths.writableLocation(StandardPaths.DownloadLocation) + "/", icon: root.isDark ? "qrc:/images/download_dark.png" : "qrc:/images/download_light.png" }
|
||||
]
|
||||
|
||||
delegate: ItemDelegate {
|
||||
width: ListView.view.width
|
||||
onClicked: {
|
||||
places.currentIndex = index
|
||||
root.currentFolder = normalizeFolder_(modelData.url)
|
||||
}
|
||||
|
||||
background: Rectangle {
|
||||
radius: 4
|
||||
color: places.currentIndex === index ? root.accentSoft : "transparent"
|
||||
border.color: places.currentIndex === index ? root.accent : "transparent"
|
||||
}
|
||||
|
||||
contentItem: RowLayout {
|
||||
spacing: 8
|
||||
Image {
|
||||
width: 16
|
||||
height: 16
|
||||
source: modelData.icon
|
||||
fillMode: Image.PreserveAspectFit
|
||||
smooth: true
|
||||
}
|
||||
Label {
|
||||
text: modelData.name
|
||||
font.family: root.uiFont
|
||||
color: root.text
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
radius: 6
|
||||
color: root.panel
|
||||
border.color: root.border
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 10
|
||||
spacing: 6
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Label { text: root.tr("名称"); Layout.fillWidth: true; font.bold: true; font.family: root.uiFont; color: root.text }
|
||||
Label { text: root.tr("修改时间"); Layout.preferredWidth: 160; font.bold: true; font.family: root.uiFont; color: root.text }
|
||||
}
|
||||
Rectangle { Layout.fillWidth: true; height: 1; color: root.border }
|
||||
|
||||
FolderListModel {
|
||||
id: fileModel
|
||||
folder: root.currentFolder
|
||||
showDotAndDotDot: false
|
||||
showDirs: true
|
||||
showFiles: false
|
||||
sortField: FolderListModel.Name
|
||||
nameFilters: searchField.text.trim().length > 0
|
||||
? ["*" + searchField.text.trim() + "*"]
|
||||
: ["*"]
|
||||
}
|
||||
|
||||
ListView {
|
||||
id: fileList
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
clip: true
|
||||
model: fileModel
|
||||
|
||||
delegate: ItemDelegate {
|
||||
id: fileRow
|
||||
width: ListView.view.width
|
||||
onDoubleClicked: {
|
||||
root.currentFolder = normalizeFolder_(fileModel.get(index, "filePath"))
|
||||
}
|
||||
onClicked: {
|
||||
fileList.currentIndex = index
|
||||
}
|
||||
|
||||
background: Rectangle {
|
||||
radius: 4
|
||||
color: fileRow.hovered ? root.hoverBg : "transparent"
|
||||
}
|
||||
|
||||
contentItem: RowLayout {
|
||||
spacing: 8
|
||||
Rectangle {
|
||||
width: 18
|
||||
height: 18
|
||||
radius: 3
|
||||
color: root.iconBg
|
||||
border.color: root.border
|
||||
// TODO: replace with folder icons.
|
||||
// Text {
|
||||
// anchors.centerIn: parent
|
||||
// text: "DIR"
|
||||
// font.pixelSize: 10
|
||||
// color: root.subText
|
||||
// }
|
||||
Image {
|
||||
// anchors.centerIn: parent
|
||||
width: 16
|
||||
height: 16
|
||||
source: root.isDark ? "qrc:/images/folder_dark.png" : "qrc:/images/folder_light.png"
|
||||
fillMode: Image.PreserveAspectFit
|
||||
smooth: true
|
||||
}
|
||||
}
|
||||
Label {
|
||||
Layout.fillWidth: true
|
||||
elide: Text.ElideRight
|
||||
text: fileModel.get(index, "fileName") || ""
|
||||
font.family: root.uiFont
|
||||
color: root.text
|
||||
}
|
||||
Label {
|
||||
Layout.preferredWidth: 160
|
||||
text: {
|
||||
const d = fileModel.get(index, "modified")
|
||||
return d ? Qt.formatDateTime(d, "yyyy/MM/dd hh:mm") : ""
|
||||
}
|
||||
font.family: root.uiFont
|
||||
color: root.subText
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: footerLayout.implicitHeight + 20
|
||||
Layout.preferredHeight: implicitHeight
|
||||
Layout.minimumHeight: implicitHeight
|
||||
radius: 6
|
||||
color: root.panel
|
||||
border.color: root.border
|
||||
|
||||
GridLayout {
|
||||
id: footerLayout
|
||||
anchors.fill: parent
|
||||
anchors.margins: 10
|
||||
columns: 6
|
||||
columnSpacing: 10
|
||||
rowSpacing: 8
|
||||
|
||||
Label { text: root.tr("文件名"); font.family: root.uiFont; color: root.text }
|
||||
TextField {
|
||||
id: filenameField
|
||||
Layout.columnSpan: 3
|
||||
Layout.fillWidth: true
|
||||
placeholderText: root.tr("输入要保存的文件名")
|
||||
font.family: root.uiFont
|
||||
background: Rectangle {
|
||||
radius: 4
|
||||
color: root.fieldBg
|
||||
border.color: root.border
|
||||
}
|
||||
}
|
||||
|
||||
Label { text: root.tr("文件类型"); font.family: root.uiFont; color: root.text }
|
||||
ComboBox {
|
||||
id: formatBox
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: 32
|
||||
Layout.preferredHeight: implicitHeight
|
||||
textRole: "label"
|
||||
valueRole: "value"
|
||||
font.family: root.uiFont
|
||||
topPadding: 6
|
||||
bottomPadding: 6
|
||||
contentItem: Item {
|
||||
anchors.fill: parent
|
||||
Text {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: 8
|
||||
anchors.rightMargin: 24
|
||||
text: formatBox.displayText
|
||||
font: formatBox.font
|
||||
color: root.text
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
model: [
|
||||
{ label: "CSV (*.csv)", value: "csv", suffix: ".csv" },
|
||||
{ label: "JSON (*.json)", value: "json", suffix: ".json" },
|
||||
{ label: "Excel (*.xlsx)", value: "xlsx", suffix: ".xlsx" }
|
||||
]
|
||||
background: Rectangle {
|
||||
radius: 4
|
||||
color: root.fieldBg
|
||||
border.color: root.border
|
||||
}
|
||||
}
|
||||
|
||||
Label { text: root.tr("导出方式"); font.family: root.uiFont; color: root.text }
|
||||
ComboBox {
|
||||
id: methodBox
|
||||
Layout.columnSpan: 5
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: 32
|
||||
Layout.preferredHeight: implicitHeight
|
||||
textRole: "label"
|
||||
valueRole: "value"
|
||||
font.family: root.uiFont
|
||||
topPadding: 6
|
||||
bottomPadding: 6
|
||||
contentItem: Item {
|
||||
anchors.fill: parent
|
||||
Text {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: 8
|
||||
anchors.rightMargin: 24
|
||||
text: methodBox.displayText
|
||||
font: methodBox.font
|
||||
color: root.text
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
model: [
|
||||
{ label: root.tr("覆盖导出(同名替换)"), value: "overwrite" },
|
||||
{ label: root.tr("追加导出(写入同一文件)"), value: "append" },
|
||||
{ label: root.tr("压缩导出(zip)"), value: "zip" }
|
||||
]
|
||||
function syncMethodIndex() {
|
||||
for (let i = 0; i < model.length; i++) {
|
||||
if (model[i].value === root.lastMethodValue) {
|
||||
currentIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
Component.onCompleted: syncMethodIndex()
|
||||
onModelChanged: syncMethodIndex()
|
||||
onCurrentValueChanged: root.lastMethodValue = currentValue
|
||||
background: Rectangle {
|
||||
radius: 4
|
||||
color: root.fieldBg
|
||||
border.color: root.border
|
||||
}
|
||||
}
|
||||
|
||||
Item { Layout.columnSpan: 4; Layout.fillWidth: true }
|
||||
|
||||
RowLayout {
|
||||
Layout.columnSpan: 2
|
||||
Layout.alignment: Qt.AlignRight
|
||||
spacing: 10
|
||||
|
||||
Button {
|
||||
id: cancelBtn
|
||||
text: root.tr("取消")
|
||||
font.family: root.uiFont
|
||||
background: Rectangle {
|
||||
radius: 4
|
||||
color: "transparent"
|
||||
border.color: root.border
|
||||
}
|
||||
contentItem: Text {
|
||||
text: cancelBtn.text
|
||||
font.family: root.uiFont
|
||||
color: root.text
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
onClicked: root.reject()
|
||||
}
|
||||
Button {
|
||||
id: saveBtn
|
||||
text: root.tr("保存")
|
||||
enabled: filenameField.text.trim().length > 0
|
||||
font.family: root.uiFont
|
||||
background: Rectangle {
|
||||
radius: 4
|
||||
color: saveBtn.enabled ? root.accent : root.disabledBg
|
||||
border.color: saveBtn.enabled ? root.accent : root.disabledBg
|
||||
}
|
||||
contentItem: Text {
|
||||
text: saveBtn.text
|
||||
font.family: root.uiFont
|
||||
color: "#FFFFFF"
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
onClicked: doSave()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MessageDialog {
|
||||
id: overwriteDlg
|
||||
title: root.tr("文件已存在")
|
||||
text: root.tr("目标文件已存在,是否覆盖?")
|
||||
buttons: MessageDialog.Yes | MessageDialog.No
|
||||
onButtonClicked: function (button, role) {
|
||||
switch (button) {
|
||||
case MessageDialog.Yes:
|
||||
finalizeSave(true)
|
||||
break
|
||||
case MessageDialog.No:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function finalizeSave(forceOverwrite) {
|
||||
root.saveTo(root.currentFolder, chosenFilename, exportFormat, exportMethod)
|
||||
root.accept()
|
||||
}
|
||||
function doSave() {
|
||||
let name = filenameField.text.trim()
|
||||
if (name.length === 0) return
|
||||
|
||||
const suffix = formatBox.model[formatBox.currentIndex].suffix
|
||||
if (!name.endsWith(suffix)) name += suffix
|
||||
chosenFilename = name
|
||||
|
||||
// TODO: check file existence and show overwriteDlg when needed.
|
||||
finalizeSave(false)
|
||||
}
|
||||
|
||||
QtObject {
|
||||
id: nav
|
||||
property var backStack: []
|
||||
property var forwardStack: []
|
||||
|
||||
function push(url) {
|
||||
backStack.push(url)
|
||||
forwardStack = []
|
||||
}
|
||||
function back() {
|
||||
if (backStack.length === 0) return
|
||||
forwardStack.push(root.currentFolder)
|
||||
root.currentFolder = backStack.pop()
|
||||
}
|
||||
function forward() {
|
||||
if (forwardStack.length === 0) return
|
||||
backStack.push(root.currentFolder)
|
||||
root.currentFolder = forwardStack.pop()
|
||||
}
|
||||
function up() {
|
||||
let s = root.currentFolder.toString()
|
||||
if (s.endsWith("/")) s = s.slice(0, -1)
|
||||
const idx = s.lastIndexOf("/")
|
||||
if (idx > 8) root.currentFolder = s.slice(0, idx + 1)
|
||||
}
|
||||
}
|
||||
|
||||
onCurrentFolderChanged: {
|
||||
if (breadcrumb.text && breadcrumb.text !== root.currentFolder.toString())
|
||||
nav.push(breadcrumb.text)
|
||||
breadcrumb.text = root.currentFolder.toString()
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import TactileIPC 1.0
|
||||
|
||||
Item {
|
||||
property string text
|
||||
|
||||
Reference in New Issue
Block a user