Files
tactileipc3d/qml/content/LeftPanel.qml

708 lines
26 KiB
QML

import QtQuick
import QtQuick.Controls.Material
import QtQuick.Controls
import QtQuick.Layouts
import QtQuick.Dialogs
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
Layout.topMargin: 4
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", "921600"]
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
property bool _internalUpdate: false
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._internalUpdate = true
addrField.text = root.formatHexByte(Backend.serial.deviceAddress)
addrField._internalUpdate = false
}
}
}
}
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: 8
Label {
text: root.tr("宽")
Layout.preferredWidth: 90
color: root.textColor
}
SpinBox {
Layout.fillWidth: true
from: 1
to: 20
value: Backend.sensorCol
enabled: Backend.serial.connected === false
onValueModified: Backend.sensorCol = value
}
}
RowLayout {
Layout.fillWidth: true
spacing: 8
Label {
text: root.tr("高")
Layout.preferredWidth: 90
color: root.textColor
}
SpinBox {
Layout.fillWidth: true
from: 1
to: 20
value: Backend.sensorRow
enabled: Backend.serial.connected === false
onValueModified: Backend.sensorRow = 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
RowLayout {
Layout.fillWidth: true
spacing: 8
Label {
text: root.tr("最小值")
Layout.preferredWidth: 90
color: root.textColor
}
SpinBox {
id: rangeMinBox
Layout.fillWidth: true
from: -999999
to: 999999
editable: true
value: Backend.rangeMin
function applyTextEdit() {
if (!contentItem) return
const parsed = valueFromText(contentItem.text, locale)
if (isNaN(parsed)) return
const clamped = Math.max(from, Math.min(to, parsed))
if (Backend.rangeMin !== clamped) {
Backend.rangeMin = clamped
}
}
onValueModified: Backend.rangeMin = value
Connections {
target: rangeMinBox.contentItem
function onTextEdited() { rangeMinBox.applyTextEdit() }
function onEditingFinished() { rangeMinBox.applyTextEdit() }
function onAccepted() {
rangeMinBox.applyTextEdit()
rangeMinBox.focus = false
}
}
}
}
RowLayout {
Layout.fillWidth: true
spacing: 8
Label {
text: root.tr("最大值")
Layout.preferredWidth: 90
color: root.textColor
}
SpinBox {
id: rangeMaxBox
Layout.fillWidth: true
from: -999999
to: 999999
editable: true
value: Backend.rangeMax
function applyTextEdit() {
if (!contentItem) return
const parsed = valueFromText(contentItem.text, locale)
if (isNaN(parsed)) return
const clamped = Math.max(from, Math.min(to, parsed))
if (Backend.rangeMax !== clamped) {
Backend.rangeMax = clamped
}
}
onValueModified: Backend.rangeMax = value
Connections {
target: rangeMaxBox.contentItem
function onTextEdited() { rangeMaxBox.applyTextEdit() }
function onEditingFinished() { rangeMaxBox.applyTextEdit() }
function onAccepted() {
rangeMaxBox.applyTextEdit()
rangeMaxBox.focus = false
}
}
}
}
RowLayout {
Layout.fillWidth: true
spacing: 8
Label {
text: root.tr("零色")
Layout.preferredWidth: 90
color: root.textColor
}
Rectangle {
width: 22
height: 22
radius: 4
color: Backend.colorZero
border.width: 1
border.color: Qt.rgba(0, 0, 0, 0.2)
Layout.alignment: Qt.AlignVCenter
MouseArea {
anchors.fill: parent
onClicked: {
zeroColorDialog.openWith(Backend.colorZero)
}
}
}
Button {
text: root.tr("选择")
Layout.fillWidth: true
onClicked: {
zeroColorDialog.openWith(Backend.colorZero)
}
}
}
RowLayout {
Layout.fillWidth: true
spacing: 8
Label {
text: root.tr("低色")
Layout.preferredWidth: 90
color: root.textColor
}
Rectangle {
width: 22
height: 22
radius: 4
color: Backend.colorLow
border.width: 1
border.color: Qt.rgba(0, 0, 0, 0.2)
Layout.alignment: Qt.AlignVCenter
MouseArea {
anchors.fill: parent
onClicked: {
lowColorDialog.openWith(Backend.colorLow)
}
}
}
Button {
text: root.tr("选择")
Layout.fillWidth: true
onClicked: {
lowColorDialog.openWith(Backend.colorLow)
}
}
}
RowLayout {
Layout.fillWidth: true
spacing: 8
Label {
text: root.tr("中色")
Layout.preferredWidth: 90
color: root.textColor
}
Rectangle {
width: 22
height: 22
radius: 4
color: Backend.colorMid
border.width: 1
border.color: Qt.rgba(0, 0, 0, 0.2)
Layout.alignment: Qt.AlignVCenter
MouseArea {
anchors.fill: parent
onClicked: {
midColorDialog.openWith(Backend.colorMid)
}
}
}
Button {
text: root.tr("选择")
Layout.fillWidth: true
onClicked: {
midColorDialog.openWith(Backend.colorMid)
}
}
}
RowLayout {
Layout.fillWidth: true
spacing: 8
Label {
text: root.tr("高色")
Layout.preferredWidth: 90
color: root.textColor
}
Rectangle {
width: 22
height: 22
radius: 4
color: Backend.colorHigh
border.width: 1
border.color: Qt.rgba(0, 0, 0, 0.2)
Layout.alignment: Qt.AlignVCenter
MouseArea {
anchors.fill: parent
onClicked: {
highColorDialog.openWith(Backend.colorHigh)
}
}
}
Button {
text: root.tr("选择")
Layout.fillWidth: true
onClicked: {
highColorDialog.openWith(Backend.colorHigh)
}
}
}
}
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: Backend.useHeatmap
onToggled: Backend.useHeatmap = 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: {
if (Backend.data.frameCount != 0) {
exportDlg.open()
}
else {
console.log("Backend.data.frameCount() === 0")
}
}
}
}
}
Item { Layout.fillHeight: true }
}
}
ColorPickerDialog {
id: zeroColorDialog
title: root.tr("选择零色")
onAccepted: Backend.colorZero = c
}
ColorPickerDialog {
id: lowColorDialog
title: root.tr("选择低色")
onAccepted: Backend.colorLow = c
}
ColorPickerDialog {
id: midColorDialog
title: root.tr("选择中色")
onAccepted: Backend.colorMid = c
}
ColorPickerDialog {
id: highColorDialog
title: root.tr("选择高色")
onAccepted: Backend.colorHigh = c
}
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)
}
}
}