Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5b319cb76d | |||
| a1f7f337c2 | |||
| c86c24488c | |||
| 98dcfa1520 | |||
| aedba5813f | |||
| fb1a30fc94 | |||
| 0ec07218ab | |||
| b2350a3b35 | |||
| 7517f79c07 | |||
| 221f7303ee | |||
| a07ff7d6b7 | |||
| 6ed795a2b6 | |||
| f411ab21cb | |||
| c6cef3d89d | |||
| c50b44efe2 |
153
.clang-format
153
.clang-format
@@ -1,14 +1,147 @@
|
|||||||
|
# 基础样式:LLVM,Google,Chromium,Mozilla,WebKit
|
||||||
BasedOnStyle: LLVM
|
BasedOnStyle: LLVM
|
||||||
|
# 类似于下面这行还没有实现,未来希望clang提供分号;后不会强制换行的功能
|
||||||
|
# BreakAfterSemicolon: false
|
||||||
|
---
|
||||||
|
# 要使用的预处理器指令缩进样式
|
||||||
|
IndentPPDirectives: AfterHash
|
||||||
|
# 缩进宽度
|
||||||
IndentWidth: 4
|
IndentWidth: 4
|
||||||
|
# 标准: Cpp03, Cpp11, Auto
|
||||||
Language: Cpp
|
Standard: Latest
|
||||||
|
# tab宽度
|
||||||
|
TabWidth: 2
|
||||||
|
# 使用ObjC块时缩进宽度
|
||||||
|
ObjCBlockIndentWidth: 2
|
||||||
|
# 构造函数的初始化列表的缩进宽度
|
||||||
|
ConstructorInitializerIndentWidth: 2
|
||||||
|
# 延续的行的缩进宽度
|
||||||
|
ContinuationIndentWidth: 0
|
||||||
|
# 在ObjC的@property后添加一个空格
|
||||||
|
ObjCSpaceAfterProperty: false
|
||||||
|
# 在ObjC的protocol列表前添加一个空格
|
||||||
|
ObjCSpaceBeforeProtocolList: true
|
||||||
|
# 访问说明符的偏移
|
||||||
|
AccessModifierOffset: -2
|
||||||
|
# 连续的空行保留几行
|
||||||
|
MaxEmptyLinesToKeep: 2
|
||||||
|
# 调整连续行中的分配操作符(对齐等号)
|
||||||
|
AlignConsecutiveAssignments: true
|
||||||
|
# 校准连续的声明(对齐局部变量)
|
||||||
|
AlignConsecutiveDeclarations: true
|
||||||
|
# 允许排序#include
|
||||||
|
SortIncludes: false
|
||||||
|
# 允许排序 using 声明
|
||||||
|
SortUsingDeclarations: false
|
||||||
|
# 继承的符号后是否换行 类别:AfterComma,AfterColon
|
||||||
|
BreakInheritanceList: AfterComma
|
||||||
|
# 总是在多行string字面量前换行
|
||||||
|
AlwaysBreakBeforeMultilineStrings: false
|
||||||
|
# 使用反斜杠换行对齐 Right,DontAlign
|
||||||
|
AlignEscapedNewlines: DontAlign
|
||||||
|
# Align, DontAlign, AlwaysBreak(总是在开括号后换行)
|
||||||
|
AlignAfterOpenBracket: DontAlign
|
||||||
|
# 左对齐换行(使用反斜杠换行)的反斜杠
|
||||||
|
AlignEscapedNewlinesLeft: true
|
||||||
|
# 对齐连续的尾随的注释
|
||||||
|
AlignTrailingComments: true
|
||||||
|
# 允许短的case标签放在同一行
|
||||||
|
AllowShortCaseLabelsOnASingleLine: false
|
||||||
|
# 允许函数声明的所有参数在放在下一行
|
||||||
|
AllowAllParametersOfDeclarationOnNextLine: false
|
||||||
|
# 允许短的块放在同一行Empty
|
||||||
|
AllowShortBlocksOnASingleLine: true
|
||||||
|
# 在构造函数的初始化列表的逗号前换行
|
||||||
|
BreakConstructorInitializersBeforeComma: false
|
||||||
|
# 在构造函数的初始化列表的冒号后换行
|
||||||
|
BreakConstructorInitializers: AfterColon
|
||||||
|
# 在圆括号的(后和)前添加空格
|
||||||
|
SpacesInParentheses: false
|
||||||
|
# 允许在单行上使用短枚举
|
||||||
|
AllowShortEnumsOnASingleLine: true
|
||||||
|
# 允许短的函数放在同一行: None, InlineOnly(定义在类中), Empty(空函数), Inline(定义在类中,空函数), All
|
||||||
|
AllowShortFunctionsOnASingleLine: All
|
||||||
|
# 去除C++11的列表初始化的大括号{后和}前的空格
|
||||||
|
Cpp11BracedListStyle: false
|
||||||
|
# 继承最常用的指针和引用的对齐方式
|
||||||
DerivePointerAlignment: false
|
DerivePointerAlignment: false
|
||||||
|
# 指针的*的位置
|
||||||
PointerAlignment: Left
|
PointerAlignment: Left
|
||||||
|
# 允许在单行上使用简短的If语句
|
||||||
|
AllowShortIfStatementsOnASingleLine: WithoutElse
|
||||||
CompileFlags:
|
# 中括号两边空格 []
|
||||||
Add: []
|
SpacesInSquareBrackets: false
|
||||||
Remove: [-mno-direct-extern-access, -mdirect-extern-access]
|
# 等号两边的空格
|
||||||
|
SpaceBeforeAssignmentOperators: true
|
||||||
|
# 容器类的空格
|
||||||
|
SpacesInContainerLiterals: false
|
||||||
|
# 缩进包装函数名
|
||||||
|
IndentWrappedFunctionNames: false
|
||||||
|
# 在块的开头保留空行
|
||||||
|
KeepEmptyLinesAtTheStartOfBlocks: true
|
||||||
|
# 括号后添加空格
|
||||||
|
SpaceAfterCStyleCast: false
|
||||||
|
# 缩进case 标签
|
||||||
|
IndentCaseLabels: true
|
||||||
|
# 允许短的循环保持在同一行
|
||||||
|
AllowShortLoopsOnASingleLine: true
|
||||||
|
# 在模板声明“template<...>”后总是换行
|
||||||
|
AlwaysBreakTemplateDeclarations: Yes
|
||||||
|
# 二进制运算符之前的中断:非赋值NonAssignment
|
||||||
|
BreakBeforeBinaryOperators: NonAssignment
|
||||||
|
# 三元运算符将被放置在换行后
|
||||||
|
BreakBeforeTernaryOperators: false
|
||||||
|
# 每行字符的限制,0表示没有限制
|
||||||
|
ColumnLimit: 0
|
||||||
|
# 只有定义成Custom,下面的大括号才会生效
|
||||||
|
BreakBeforeBraces: Custom
|
||||||
|
# 大括号后的分行
|
||||||
|
BraceWrapping :
|
||||||
|
# class定义后面
|
||||||
|
AfterClass: false
|
||||||
|
# 控制语句后面
|
||||||
|
AfterControlStatement: false
|
||||||
|
# enum定义后面
|
||||||
|
AfterEnum: false
|
||||||
|
# 函数定义后面
|
||||||
|
AfterFunction: false
|
||||||
|
# 命名空间定义后面
|
||||||
|
AfterNamespace: false
|
||||||
|
# ObjC定义后面
|
||||||
|
AfterObjCDeclaration: false
|
||||||
|
# struct定义后面
|
||||||
|
AfterStruct: false
|
||||||
|
# union定义后面
|
||||||
|
AfterUnion: false
|
||||||
|
# catch之前
|
||||||
|
BeforeCatch: true
|
||||||
|
# else之前
|
||||||
|
BeforeElse: true
|
||||||
|
# 缩进大括号
|
||||||
|
IndentBraces: false
|
||||||
|
# 语言: Cpp, Java, JavaScript, ObjC, Proto
|
||||||
|
Language: Cpp
|
||||||
|
# 模板关键字后的空格:false
|
||||||
|
SpaceAfterTemplateKeyword: false
|
||||||
|
# 指针限定符周围的空格:之后
|
||||||
|
SpaceAroundPointerQualifiers: After
|
||||||
|
# 大小写冒号前的空格:false
|
||||||
|
SpaceBeforeCaseColon: false
|
||||||
|
# C到r初始值设定项冒号前的空格:false
|
||||||
|
SpaceBeforeCtorInitializerColon: false
|
||||||
|
# 继承冒号前的空格:false
|
||||||
|
SpaceBeforeInheritanceColon: false
|
||||||
|
# 开圆括号之前添加一个空格: Never, ControlStatements, Always
|
||||||
|
SpaceBeforeParens: ControlStatements
|
||||||
|
# 基于范围的循环冒号前的空格:false
|
||||||
|
SpaceBeforeRangeBasedForLoopColon: false
|
||||||
|
# 在尖括号的<>后和前添加空格
|
||||||
|
SpacesInAngles: false
|
||||||
|
# 收拾格子参数
|
||||||
|
BinPackArguments : false
|
||||||
|
# 纸盒包装参数
|
||||||
|
BinPackParameters : false
|
||||||
|
# 当格式化时,总是对字面量字符串换行
|
||||||
|
BreakStringLiterals : false
|
||||||
|
# Never, ForIndentation, ForContinuationAndIndentation, Always
|
||||||
|
UseTab: Never
|
||||||
|
|||||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -2,8 +2,11 @@
|
|||||||
.vscode/
|
.vscode/
|
||||||
.vs/
|
.vs/
|
||||||
.idea/
|
.idea/
|
||||||
|
.VSCodeCounter/
|
||||||
|
mingw-build/
|
||||||
cmake-build-*/
|
cmake-build-*/
|
||||||
|
clion-build-*/
|
||||||
|
build-demo*/
|
||||||
build/
|
build/
|
||||||
output/
|
output/
|
||||||
AppDir/
|
AppDir/
|
||||||
|
|||||||
113
CMakeLists.txt
113
CMakeLists.txt
@@ -15,8 +15,7 @@ set(CMAKE_AUTORCC ON)
|
|||||||
add_compile_options(-Os -O3)
|
add_compile_options(-Os -O3)
|
||||||
|
|
||||||
list(APPEND CMAKE_PREFIX_PATH
|
list(APPEND CMAKE_PREFIX_PATH
|
||||||
"D:/Environment/include"
|
"C:/msys64/mingw64/include"
|
||||||
"D:/Environment/lib"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
find_package(${QT_VERSION} REQUIRED COMPONENTS Widgets Network PrintSupport)
|
find_package(${QT_VERSION} REQUIRED COMPONENTS Widgets Network PrintSupport)
|
||||||
@@ -25,18 +24,16 @@ find_package(Eigen3 REQUIRED)
|
|||||||
qt_standard_project_setup()
|
qt_standard_project_setup()
|
||||||
|
|
||||||
file(
|
file(
|
||||||
GLOB_RECURSE MODERN_QT_SOURCES
|
GLOB_RECURSE creeper_QT_SOURCES
|
||||||
CONFIGURE_DEPENDS
|
CONFIGURE_DEPENDS
|
||||||
"modern-qt/*.cc"
|
"creeper-qt/*.cc"
|
||||||
)
|
)
|
||||||
set(MODERN_QT_HEADERS
|
set(creeper_QT_HEADERS
|
||||||
modern-qt/widget/select.hh
|
creeper-qt/widget/sliders.hh
|
||||||
modern-qt/widget/select.impl.hh
|
|
||||||
modern-qt/widget/sliders.hh
|
|
||||||
)
|
)
|
||||||
add_library(modern-qt SHARED ${MODERN_QT_SOURCES} ${MODERN_QT_HEADERS})
|
add_library(creeper-qt SHARED ${creeper_QT_SOURCES} ${creeper_QT_HEADERS})
|
||||||
target_include_directories(modern-qt PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
|
target_include_directories(creeper-qt PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
|
||||||
target_link_libraries(modern-qt
|
target_link_libraries(creeper-qt
|
||||||
PUBLIC
|
PUBLIC
|
||||||
${QT_VERSION}::Widgets
|
${QT_VERSION}::Widgets
|
||||||
${QT_VERSION}::Network
|
${QT_VERSION}::Network
|
||||||
@@ -69,45 +66,67 @@ file(
|
|||||||
"dlog/*.cc"
|
"dlog/*.cc"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
file(
|
||||||
|
GLOB_RECURSE BASE_SOURCES
|
||||||
|
CONFIGURE_DEPENDS
|
||||||
|
"base/*.cc"
|
||||||
|
)
|
||||||
|
|
||||||
set(FFMSEP_SOURCES
|
set(FFMSEP_SOURCES
|
||||||
components/ffmsep/cpdecoder.cc
|
components/ffmsep/cpdecoder.cc
|
||||||
components/ffmsep/cpstream_core.cc
|
components/ffmsep/cpstream_core.cc
|
||||||
|
components/ffmsep/presist/presist.cc
|
||||||
components/ffmsep/tactile/tacdec.cc
|
components/ffmsep/tactile/tacdec.cc
|
||||||
)
|
)
|
||||||
set(FFMSEP_HEADERS
|
set(FFMSEP_HEADERS
|
||||||
components/ffmsep/cpdecoder.hh
|
components/ffmsep/cpdecoder.hh
|
||||||
components/ffmsep/cpstream_core.hh
|
components/ffmsep/cpstream_core.hh
|
||||||
|
components/ffmsep/presist/presist.hh
|
||||||
components/ffmsep/tactile/tacdec.hh
|
components/ffmsep/tactile/tacdec.hh
|
||||||
)
|
)
|
||||||
set(FFMSEP_INCLUDE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/components/ffmsep")
|
set(FFMSEP_INCLUDE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/components/ffmsep")
|
||||||
|
set(BASE_INCLUDE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/base")
|
||||||
|
|
||||||
set(TOUCHSENSOR_HEADERS
|
set(TOUCHSENSOR_HEADERS
|
||||||
component.hh
|
component.hh
|
||||||
components/charts/heatmap.hh
|
components/charts/heatmap.hh
|
||||||
components/charts/heatmap.impl.hh
|
components/charts/heatmap.impl.hh
|
||||||
|
components/charts/vector_field.hh
|
||||||
|
components/charts/line_chart.hh
|
||||||
dlog/dlog.hh
|
dlog/dlog.hh
|
||||||
${FFMSEP_HEADERS}
|
${FFMSEP_HEADERS}
|
||||||
|
components/setting.cc
|
||||||
)
|
)
|
||||||
|
|
||||||
qt6_add_resources(APP_RESOURCES resources.qrc)
|
qt6_add_resources(APP_RESOURCES resources.qrc)
|
||||||
|
|
||||||
add_executable(${PROJECT_NAME}
|
add_executable(${PROJECT_NAME} WIN32
|
||||||
${COMPONENT_SOURCES}
|
${COMPONENT_SOURCES}
|
||||||
${UTILITY_SOURCES}
|
${UTILITY_SOURCES}
|
||||||
${TOUCHSENSOR_HEADERS}
|
${TOUCHSENSOR_HEADERS}
|
||||||
|
${BASE_SOURCES}
|
||||||
main.cc
|
main.cc
|
||||||
)
|
)
|
||||||
|
|
||||||
|
#add_executable(${PROJECT_NAME}
|
||||||
|
# ${COMPONENT_SOURCES}
|
||||||
|
# ${UTILITY_SOURCES}
|
||||||
|
# ${TOUCHSENSOR_HEADERS}
|
||||||
|
# ${BASE_SOURCES}
|
||||||
|
# main.cc
|
||||||
|
#)
|
||||||
target_sources(${PROJECT_NAME} PRIVATE ${APP_RESOURCES})
|
target_sources(${PROJECT_NAME} PRIVATE ${APP_RESOURCES})
|
||||||
target_include_directories(${PROJECT_NAME}
|
target_include_directories(${PROJECT_NAME}
|
||||||
PRIVATE
|
PRIVATE
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}
|
${CMAKE_CURRENT_SOURCE_DIR}
|
||||||
${FFMSEP_INCLUDE_DIR}
|
${FFMSEP_INCLUDE_DIR}
|
||||||
|
${BASE_INCLUDE_DIR}
|
||||||
)
|
)
|
||||||
target_link_libraries(${PROJECT_NAME}
|
target_link_libraries(${PROJECT_NAME}
|
||||||
PRIVATE
|
PRIVATE
|
||||||
${QT_VERSION}::Widgets
|
${QT_VERSION}::Widgets
|
||||||
${QT_VERSION}::Network
|
${QT_VERSION}::Network
|
||||||
modern-qt
|
creeper-qt
|
||||||
qcustomplot
|
qcustomplot
|
||||||
serial
|
serial
|
||||||
setupapi
|
setupapi
|
||||||
@@ -127,3 +146,71 @@ if(BUILD_EXAMPLE)
|
|||||||
target_link_libraries(cpstream_demo PRIVATE serial)
|
target_link_libraries(cpstream_demo PRIVATE serial)
|
||||||
target_link_libraries(cpstream_demo PRIVATE setupapi)
|
target_link_libraries(cpstream_demo PRIVATE setupapi)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
set(CMAKE_INSTALL_PREFIX "${CMAKE_SOURCE_DIR}/deploy" CACHE PATH "" FORCE)
|
||||||
|
|
||||||
|
include(GNUInstallDirs)
|
||||||
|
install(TARGETS
|
||||||
|
touchsensor
|
||||||
|
creeper-qt
|
||||||
|
qcustomplot
|
||||||
|
RUNTIME DESTINATION .
|
||||||
|
LIBRARY DESTINATION .
|
||||||
|
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
|
||||||
|
)
|
||||||
|
|
||||||
|
if(BUILD_EXAMPLE)
|
||||||
|
install(TARGETS cpstream_demo
|
||||||
|
RUNTIME DESTINATION .
|
||||||
|
)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if(WIN32)
|
||||||
|
# 利用 QtCore 目标找到 Qt bin 目录
|
||||||
|
get_target_property(_qt_core_location ${QT_VERSION}::Core LOCATION)
|
||||||
|
get_filename_component(_qt_bin_dir "${_qt_core_location}" DIRECTORY)
|
||||||
|
|
||||||
|
find_program(WINDEPLOYQT_EXECUTABLE
|
||||||
|
NAMES windeployqt windeployqt.exe
|
||||||
|
HINTS "${_qt_bin_dir}"
|
||||||
|
)
|
||||||
|
|
||||||
|
if(WINDEPLOYQT_EXECUTABLE)
|
||||||
|
message(STATUS "Found windeployqt: ${WINDEPLOYQT_EXECUTABLE}")
|
||||||
|
|
||||||
|
# 安装完之后,对 deploy/touchsensor.exe 跑 windeployqt
|
||||||
|
install(CODE
|
||||||
|
"execute_process(
|
||||||
|
COMMAND \"${WINDEPLOYQT_EXECUTABLE}\"
|
||||||
|
--dir \"${CMAKE_INSTALL_PREFIX}\"
|
||||||
|
--no-translations
|
||||||
|
\"${CMAKE_INSTALL_PREFIX}/touchsensor.exe\"
|
||||||
|
RESULT_VARIABLE _windeployqt_result
|
||||||
|
)
|
||||||
|
if(NOT _windeployqt_result EQUAL 0)
|
||||||
|
message(FATAL_ERROR \"windeployqt failed with exit code: \${_windeployqt_result}\")
|
||||||
|
endif()"
|
||||||
|
)
|
||||||
|
else()
|
||||||
|
message(WARNING "windeployqt not found, Qt 相关 dll 需要你手动处理")
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if(MINGW)
|
||||||
|
get_filename_component(MINGW_BIN_DIR "${CMAKE_CXX_COMPILER}" DIRECTORY)
|
||||||
|
|
||||||
|
set(MINGW_RUNTIME_DLLS
|
||||||
|
libstdc++-6.dll
|
||||||
|
libgcc_s_seh-1.dll
|
||||||
|
libwinpthread-1.dll
|
||||||
|
)
|
||||||
|
|
||||||
|
foreach(dll ${MINGW_RUNTIME_DLLS})
|
||||||
|
if(EXISTS "${MINGW_BIN_DIR}/${dll}")
|
||||||
|
message(STATUS "Will install MinGW runtime DLL: ${dll}")
|
||||||
|
install(FILES "${MINGW_BIN_DIR}/${dll}" DESTINATION .)
|
||||||
|
else()
|
||||||
|
message(WARNING "MinGW runtime DLL not found: ${MINGW_BIN_DIR}/${dll}")
|
||||||
|
endif()
|
||||||
|
endforeach()
|
||||||
|
endif()
|
||||||
|
|||||||
119
README.md
119
README.md
@@ -1,41 +1,49 @@
|
|||||||
# TouchSensor 2.0
|
# TouchSensor 2.0
|
||||||
|
|
||||||
> Real-time tactile sensor exploration UI powered by Qt 6 and a custom Modern Qt component toolkit.
|
> 基于 Qt 6 与自研 Modern Qt 组件库打造的实时触觉传感器探索界面。
|
||||||
|
|
||||||

|

|
||||||

|

|
||||||

|

|
||||||

|

|
||||||

|

|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## Highlights
|
## 项目亮点
|
||||||
- Modern, material-inspired desktop shell built on the in-repo `modern-qt` library.
|
- 基于仓库内置的 `modern-qt` 库构建现代化、贴近 Material 风格的桌面壳层。
|
||||||
- Modular component system (`NavComponent`, `ViewComponent`) for quick UI experiments.
|
- 左侧导航栏搭配卡片式主界面,集中承载串口控制面板与双热力图视图。
|
||||||
- Real-time tactile matrix visualisation via the `HeatMapPlot` wrapper around QCustomPlot.
|
- 实时触觉数据流水线:`ffmsep::CPStreamCore` 负责串口 I/O、编解码调度与帧缓存。
|
||||||
- Codec infrastructure (`components/ffmsep`) ready for custom tactile packet decoding.
|
- 内置触觉编解码模块(`ffmsep::tactile`)自动解析载荷、推断矩阵尺寸并输出压力量表。
|
||||||
- Serial transport module scaffolded for COM port discovery and streaming.
|
- 两套 `HeatMapPlot` 组件通过响应式数据源实时刷新,并与主题色梯度联动。
|
||||||
|
|
||||||
## Architecture At A Glance
|
## 当前进展
|
||||||
- **Entry point**: `main.cc` composes the themed window, navigation rail, and card-based layout.
|
- UI 内可完成串口的搜索、连接与断开,同时支持周期性从站轮询指令。
|
||||||
- **Components**: `components/` hosts UI widgets, charts, and the tactile decoder pipeline.
|
- 编解码注册流程已接入触觉解码器,控制台输出原始帧内容与矩阵提示,便于调试。
|
||||||
- **Modern Qt toolkit**: `modern-qt/` provides declarative wrappers, theming, and Material icon helpers.
|
- 双热力图面板会根据解码结果动态调整矩阵尺寸,停止串流时自动回落至默认状态。
|
||||||
- **Data layer**: `components/ffmsep` implements codec registration, packet decoding, and tactile frame processing.
|
- 同时提供 `cpstream_demo` CLI,用于在无界面环境校验串流核心逻辑。
|
||||||
- **Visualisation**: `components/charts/heatmap.*` exposes a themable heatmap control for sensor grids.
|
|
||||||
|
|
||||||
## Getting Started
|
## 架构速览
|
||||||
|
- **入口**:`main.cc` 负责加载主题、导航栏、遮罩动画以及传感器面板。
|
||||||
|
- **组件层**:`components/` 收纳导航/视图界面、图表组件以及触觉串流管线。
|
||||||
|
- **Modern Qt 工具集**:`modern-qt/` 提供声明式封装、主题系统与 Material 图标工具。
|
||||||
|
- **串流与编解码**:`components/ffmsep` 打包编解码注册、`CPStreamCore` 与触觉帧解析工具。
|
||||||
|
- **可视化**:`components/charts/heatmap.*` 提供可主题化的热力图控件,支持梯度和矩阵重设。
|
||||||
|
- **示例**:`examples/` 下的 `cpstream_demo.cc` 可单独运行验证串流核心。
|
||||||
|
|
||||||
### Prerequisites
|
## 快速开始
|
||||||
|
|
||||||
|
### 环境要求
|
||||||
- CMake 3.20+
|
- CMake 3.20+
|
||||||
- A C++23-capable compiler (MSVC 19.3x, Clang 16+, or GCC 13+)
|
- 支持 C++23 的编译器(MSVC 19.3x、Clang 16+ 或 GCC 13+)
|
||||||
- Qt 6 (Widgets, Network, PrintSupport modules)
|
- Qt 6(至少包含 Widgets、Network、PrintSupport 模块)
|
||||||
- Eigen3
|
- Eigen3
|
||||||
- `spdlog` (fetched via package manager or provided to CMake)
|
- `spdlog`(可通过包管理器安装或手动提供给 CMake)
|
||||||
|
- [`serial`](https://github.com/wjwwood/serial)(跨平台串口库,可包管理器安装或自行构建)
|
||||||
|
|
||||||
Ensure `Qt6_DIR` (or `CMAKE_PREFIX_PATH`) points to the Qt install so CMake can locate the required modules.
|
确保 `Qt6_DIR` 或 `CMAKE_PREFIX_PATH` 指向 Qt 安装路径,以便 CMake 正确找到依赖模块。
|
||||||
|
|
||||||
### Configure & Build
|
### 配置与构建
|
||||||
|
|
||||||

|

|
||||||
```powershell
|
```powershell
|
||||||
@@ -52,42 +60,51 @@ cd build && mingw32-make install
|
|||||||
cat install_manifest.txt
|
cat install_manifest.txt
|
||||||
```
|
```
|
||||||
|
|
||||||
To run from the build directory:
|
从构建目录直接运行:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
.\touchsensor.exe
|
.\touchsensor.exe
|
||||||
```
|
```
|
||||||
|
|
||||||
On Linux/macOS adjust the Qt path and executable name accordingly.
|
在 Linux/macOS 上使用对应的 Qt 安装路径与可执行文件名。
|
||||||
|
|
||||||
## Project Layout
|
### 运行传感器界面
|
||||||
|
- 通过 USB 接入触觉设备,确认系统识别到的串口号。
|
||||||
|
- 启动 `touchsensor.exe`(或平台对应的可执行文件)。
|
||||||
|
- 在串口下拉列表中选择端口,可按需刷新;若需更换波特率请同步调整。
|
||||||
|
- 点击链接图标开始串流,双热力图将实时更新,控制台亦会打印原始载荷。
|
||||||
|
- 再次点击链接图标即可断开连接,界面会回落到默认示例数据。
|
||||||
|
|
||||||
|
当 `BUILD_EXAMPLE` 选项开启时会同步构建 `cpstream_demo`,可在终端中运行验证串流与编解码流程。
|
||||||
|
|
||||||
|
## 项目结构
|
||||||
|
|
||||||
```text
|
```text
|
||||||
.
|
.
|
||||||
|-- components/
|
|-- components/
|
||||||
| |-- charts/ # QCustomPlot-based visualisations (heatmaps, etc.)
|
| |-- charts/ # 基于 QCustomPlot 的热力图等可视化组件
|
||||||
| |-- ffmsep/ # Codec system and tactile decoder experiments
|
| |-- ffmsep/ # 触觉串流核心与编解码实现
|
||||||
| |-- view.cc # Main dashboard composition
|
| |-- nav.cc # 导航栏与主题切换逻辑
|
||||||
| `-- ... # Additional UI widgets
|
| |-- view.cc # 传感器控制面板与双热力图
|
||||||
|-- modern-qt/ # In-house declarative Qt UI framework
|
| `-- ... # 其他 UI 组件
|
||||||
|-- serial/ # Serial communication helper library
|
|-- examples/ # cpstream_demo 命令行示例
|
||||||
|-- images/logo.png # Current app branding
|
|-- modern-qt/ # 自研 Qt 声明式 UI 框架
|
||||||
|-- main.cc # Application bootstrap
|
|-- serial/ # 串口通信辅助库
|
||||||
`-- CMakeLists.txt # Build script (adds Qt, Eigen, Modern Qt, Serial, SPDLOG)
|
|-- images/logo.png # 项目标识
|
||||||
|
|-- main.cc # 程序入口
|
||||||
|
`-- CMakeLists.txt # 构建脚本,聚合 Qt、Eigen、Modern Qt、Serial、SPDLOG
|
||||||
```
|
```
|
||||||
|
|
||||||
## Roadmap (WIP)
|
## 路线图(进行中)
|
||||||
- Flesh out tactile codec implementations and connect them to live serial streams.
|
- **已完成** 串口串流管线(轮询、编解码回调、热力图数据绑定)已连通。
|
||||||
- Replace placeholder random data with decoded sensor frames.
|
- **已完成** HeatMapPlot 支持矩阵提示与梯度调节,能随解码结果刷新。
|
||||||
- Expand navigation targets beyond the current demo cards.
|
- **规划中** 引入内置诊断/历史面板,替换当前的标准输出日志。
|
||||||
- Capture screenshots or recordings for documentation.
|
- **规划中** 保存串口偏好、提供手动矩阵覆写,并补充文档素材。
|
||||||
- Polish theming, animation masks, and landing experience.
|
- **规划中** 扩展导航目标,完善非传感器场景。
|
||||||
|
|
||||||
## Contributing
|
## 致谢
|
||||||
This repository is in active development; feel free to open issues or PRs once guidelines land. Until then, keep discussions in the project chat or issues board.
|
- [Qt](https://www.qt.io/) 提供核心 UI 框架。
|
||||||
|
- [QCustomPlot](https://www.qcustomplot.com/) 支撑热力图组件。
|
||||||
## Acknowledgements
|
- [spdlog](https://github.com/gabime/spdlog) 用于日志记录(已在 CMake 中接入)。
|
||||||
- [Qt](https://www.qt.io/) for the core UI framework.
|
- [serial](https://github.com/wjwwood/serial) 提供跨平台串口能力。
|
||||||
- [QCustomPlot](https://www.qcustomplot.com/) powering the heatmap widget.
|
- 内部 **Modern Qt** 工具集基于 creeper-qt 生态构建。
|
||||||
- [spdlog](https://github.com/gabime/spdlog) for logging (wired via CMake).
|
|
||||||
- Internal **Modern Qt** toolkit built on top of creeper-qt utilities.
|
|
||||||
|
|||||||
159
base/globalhelper.cc
Normal file
159
base/globalhelper.cc
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
#include "globalhelper.hh"
|
||||||
|
#include "qdir.h"
|
||||||
|
#include "qsettings.h"
|
||||||
|
#include "qstandardpaths.h"
|
||||||
|
#include <array>
|
||||||
|
#include <QDir>
|
||||||
|
#include <QStandardPaths>
|
||||||
|
#include <QSettings>
|
||||||
|
#include <qt6/QtCore/qcontainerfwd.h>
|
||||||
|
#include <algorithm>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
const QString IPC_CONFIG_BASEDIR = QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation) + "/tactile";
|
||||||
|
|
||||||
|
const QString IPC_CONFIG = "sensor_ipc_config.ini";
|
||||||
|
|
||||||
|
const QString APP_VERSION = "0.2.0";
|
||||||
|
const QString PROFILES_GROUP = "profile";
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
QString tactileTypeToString(Tactile_TYPE type) {
|
||||||
|
switch (type) {
|
||||||
|
case Tactile_TYPE::PiezoresistiveA:
|
||||||
|
return "压阻A型";
|
||||||
|
case Tactile_TYPE::PiezoresistiveB:
|
||||||
|
return "压阻B型";
|
||||||
|
case Tactile_TYPE::Hall:
|
||||||
|
return "霍尔型";
|
||||||
|
}
|
||||||
|
return "霍尔型";
|
||||||
|
}
|
||||||
|
|
||||||
|
Tactile_TYPE tactileTypeFromString(const QString& type) {
|
||||||
|
if (type == "压阻A型") {
|
||||||
|
return Tactile_TYPE::PiezoresistiveA;
|
||||||
|
}
|
||||||
|
if (type == "压阻B型") {
|
||||||
|
return Tactile_TYPE::PiezoresistiveB;
|
||||||
|
}
|
||||||
|
return Tactile_TYPE::Hall;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
GlobalHelper& GlobalHelper::instance() {
|
||||||
|
static GlobalHelper instance;
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
GlobalHelper::GlobalHelper() {
|
||||||
|
QDir().mkpath(IPC_CONFIG_BASEDIR);
|
||||||
|
// qDebug() << "QDir: " << IPC_CONFIG_BASEDIR;
|
||||||
|
// ConfigProfile cfg{
|
||||||
|
// .name = "default",
|
||||||
|
// .type = Tactile_TYPE::Hall,
|
||||||
|
// .matrix_width = 3,
|
||||||
|
// .matrix_height = 4,
|
||||||
|
// .range_left = 200,
|
||||||
|
// .range_right = 300,
|
||||||
|
// .baud_rate = 115200
|
||||||
|
// };
|
||||||
|
// save_profile(cfg, 0);
|
||||||
|
load_profiles();
|
||||||
|
}
|
||||||
|
void GlobalHelper::load_profiles() {
|
||||||
|
QString str_ipc_config_filename = IPC_CONFIG_BASEDIR + "/" + IPC_CONFIG;
|
||||||
|
QSettings settings(str_ipc_config_filename, QSettings::IniFormat);
|
||||||
|
|
||||||
|
config_vec.clear();
|
||||||
|
|
||||||
|
settings.beginGroup(PROFILES_GROUP);
|
||||||
|
const auto profile_names = settings.childGroups();
|
||||||
|
for (const auto& profile_group_name : profile_names) {
|
||||||
|
settings.beginGroup(profile_group_name);
|
||||||
|
|
||||||
|
ConfigProfile profile;
|
||||||
|
profile.name = settings.value("profile_name", profile_group_name).toString();
|
||||||
|
profile.type = tactileTypeFromString(settings.value("type").toString());
|
||||||
|
profile.matrix_width = settings.value("matrix_width", 0).toInt();
|
||||||
|
profile.matrix_height = settings.value("matrix_height", 0).toInt();
|
||||||
|
profile.range_left = settings.value("range_left", 0).toInt();
|
||||||
|
profile.range_right = settings.value("range_right", 0).toInt();
|
||||||
|
profile.baud_rate = settings.value("baud_rate", 0).toInt();
|
||||||
|
|
||||||
|
config_vec.push_back(profile);
|
||||||
|
|
||||||
|
settings.endGroup();
|
||||||
|
}
|
||||||
|
settings.endGroup();
|
||||||
|
qDebug() << "profiles: " << config_vec.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
void GlobalHelper::reload_profiles() {
|
||||||
|
load_profiles();
|
||||||
|
}
|
||||||
|
|
||||||
|
void GlobalHelper::save_profile(const ConfigProfile& profile, int is_default) {
|
||||||
|
QString str_ipc_config_filename = IPC_CONFIG_BASEDIR + "/" + IPC_CONFIG;
|
||||||
|
QSettings settings(str_ipc_config_filename, QSettings::IniFormat);
|
||||||
|
settings.beginGroup(PROFILES_GROUP);
|
||||||
|
settings.beginGroup(profile.name);
|
||||||
|
settings.setValue("profile_name", profile.name);
|
||||||
|
settings.setValue("type", tactileTypeToString(profile.type));
|
||||||
|
settings.setValue("matrix_width", profile.matrix_width);
|
||||||
|
settings.setValue("matrix_height", profile.matrix_height);
|
||||||
|
settings.setValue("range_left", profile.range_left);
|
||||||
|
settings.setValue("range_right", profile.range_right);
|
||||||
|
settings.setValue("baud_rate", profile.baud_rate);
|
||||||
|
settings.setValue("is_default", is_default);
|
||||||
|
settings.endGroup();
|
||||||
|
settings.endGroup();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool GlobalHelper::add_new_profile(QString name, Tactile_TYPE type, int width, int height, int rl, int rr, int baud) {
|
||||||
|
ConfigProfile cfg{
|
||||||
|
.name = name,
|
||||||
|
.type = type,
|
||||||
|
.matrix_width = width,
|
||||||
|
.matrix_height = height,
|
||||||
|
.range_left = rl,
|
||||||
|
.range_right = rr,
|
||||||
|
.baud_rate = baud
|
||||||
|
};
|
||||||
|
|
||||||
|
return add_new_profile(cfg);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool GlobalHelper::add_new_profile(const ConfigProfile& profile) {
|
||||||
|
auto item_find = std::find_if(config_vec.begin(), config_vec.end(), [profile](const ConfigProfile& p) {
|
||||||
|
return p.name == profile.name;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (item_find == config_vec.end()) {
|
||||||
|
save_profile(profile);
|
||||||
|
config_vec.push_back(profile);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void GlobalHelper::remove_profile(const QString& name) {
|
||||||
|
auto item_find = std::find_if(config_vec.begin(), config_vec.end(), [name](const ConfigProfile& p){
|
||||||
|
return p.name == name;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (item_find != config_vec.end()) {
|
||||||
|
config_vec.erase(item_find);
|
||||||
|
QString str_ipc_config_filename = IPC_CONFIG_BASEDIR + "/" + IPC_CONFIG;
|
||||||
|
QSettings settings(str_ipc_config_filename, QSettings::IniFormat);
|
||||||
|
settings.beginGroup(PROFILES_GROUP);
|
||||||
|
settings.remove(name);
|
||||||
|
settings.endGroup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<ConfigProfile>& GlobalHelper::get_all_profile() {
|
||||||
|
return config_vec;
|
||||||
|
}
|
||||||
46
base/globalhelper.hh
Normal file
46
base/globalhelper.hh
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <QStringList>
|
||||||
|
#include <array>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
// 热力图宽
|
||||||
|
// 热力图高
|
||||||
|
// 量程
|
||||||
|
// baud
|
||||||
|
enum class Tactile_TYPE {
|
||||||
|
PiezoresistiveA,
|
||||||
|
PiezoresistiveB,
|
||||||
|
Hall,
|
||||||
|
};
|
||||||
|
|
||||||
|
typedef struct ConfigProfile {
|
||||||
|
QString name;
|
||||||
|
Tactile_TYPE type;
|
||||||
|
int matrix_width;
|
||||||
|
int matrix_height;
|
||||||
|
int range_left;
|
||||||
|
int range_right;
|
||||||
|
int baud_rate;
|
||||||
|
} ConfigProfile;
|
||||||
|
|
||||||
|
class GlobalHelper {
|
||||||
|
|
||||||
|
public:
|
||||||
|
static GlobalHelper& instance();
|
||||||
|
|
||||||
|
GlobalHelper(const GlobalHelper&) = delete;
|
||||||
|
GlobalHelper& operator=(const GlobalHelper&) = delete;
|
||||||
|
GlobalHelper(GlobalHelper&&) = delete;
|
||||||
|
GlobalHelper& operator=(GlobalHelper&&) = delete;
|
||||||
|
bool add_new_profile(QString name, Tactile_TYPE type, int width, int height, int rl, int rr, int baud);
|
||||||
|
bool add_new_profile(const ConfigProfile& profile);
|
||||||
|
void remove_profile(const QString& name);
|
||||||
|
|
||||||
|
void save_profile(const ConfigProfile& profile, int is_default = 0);
|
||||||
|
std::vector<ConfigProfile>& get_all_profile();
|
||||||
|
void reload_profiles();
|
||||||
|
private:
|
||||||
|
GlobalHelper();
|
||||||
|
void load_profiles();
|
||||||
|
std::vector<ConfigProfile> config_vec;
|
||||||
|
};
|
||||||
25
component.hh
25
component.hh
@@ -1,7 +1,13 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <modern-qt/utility/theme/theme.hh>
|
#include <creeper-qt/utility/theme/theme.hh>
|
||||||
|
#include <creeper-qt/utility/wrapper/mutable-value.hh>
|
||||||
|
#include <functional>
|
||||||
|
#include <memory>
|
||||||
#include <qwidget.h>
|
#include <qwidget.h>
|
||||||
|
#include <string_view>
|
||||||
|
#include <tuple>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
template <typename T>
|
template <typename T>
|
||||||
using raw_pointer = T*;
|
using raw_pointer = T*;
|
||||||
@@ -9,8 +15,9 @@ using raw_pointer = T*;
|
|||||||
struct NavComponentState {
|
struct NavComponentState {
|
||||||
creeper::ThemeManager& manager;
|
creeper::ThemeManager& manager;
|
||||||
std::function<void(int, const std::string_view&)> switch_callback;
|
std::function<void(int, const std::string_view&)> switch_callback;
|
||||||
|
|
||||||
std::vector<std::tuple<std::string_view, std::string_view>> buttons_context;
|
std::vector<std::tuple<std::string_view, std::string_view>> buttons_context;
|
||||||
|
std::function<void(int)> stacked_callback;
|
||||||
|
std::shared_ptr<creeper::MutableValue<bool>> heatmap_show_numbers;
|
||||||
};
|
};
|
||||||
|
|
||||||
auto NavComponent(NavComponentState&) noexcept -> raw_pointer<QWidget>;
|
auto NavComponent(NavComponentState&) noexcept -> raw_pointer<QWidget>;
|
||||||
@@ -19,3 +26,17 @@ struct ViewComponentState {
|
|||||||
creeper::ThemeManager& manager;
|
creeper::ThemeManager& manager;
|
||||||
};
|
};
|
||||||
auto ViewComponent(ViewComponentState&) noexcept -> raw_pointer<QWidget>;
|
auto ViewComponent(ViewComponentState&) noexcept -> raw_pointer<QWidget>;
|
||||||
|
|
||||||
|
struct SettingComponentState {
|
||||||
|
creeper::ThemeManager& manager;
|
||||||
|
};
|
||||||
|
auto SettingComponent(SettingComponentState&) noexcept -> raw_pointer<QWidget>;
|
||||||
|
|
||||||
|
struct HandViewComponentState {
|
||||||
|
creeper::ThemeManager& manager;
|
||||||
|
};
|
||||||
|
auto HandViewComponent(HandViewComponentState&) noexcept -> raw_pointer<QWidget>;
|
||||||
|
|
||||||
|
// 让其他模块可触发视图层的串口/配置刷新
|
||||||
|
void RefreshProfilesForView();
|
||||||
|
std::shared_ptr<creeper::MutableValue<bool>> HeatmapNumberVisibilityContext();
|
||||||
|
|||||||
@@ -52,6 +52,14 @@ void BasicPlot::set_data(const QVector<PointData>& data)const {
|
|||||||
pimpl->set_data(data);
|
pimpl->set_data(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void BasicPlot::set_labels_visible(bool visible) const {
|
||||||
|
pimpl->set_labels_visible(visible);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool BasicPlot::labels_visible() const {
|
||||||
|
return pimpl->labels_visible();
|
||||||
|
}
|
||||||
|
|
||||||
bool BasicPlot::is_initialized() const {
|
bool BasicPlot::is_initialized() const {
|
||||||
return pimpl->is_plot_initialized();
|
return pimpl->is_plot_initialized();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,12 +5,12 @@
|
|||||||
#ifndef TOUCHSENSOR_HEATMAP_H
|
#ifndef TOUCHSENSOR_HEATMAP_H
|
||||||
#define TOUCHSENSOR_HEATMAP_H
|
#define TOUCHSENSOR_HEATMAP_H
|
||||||
|
|
||||||
#include "modern-qt/utility/theme/theme.hh"
|
#include "creeper-qt/utility/theme/theme.hh"
|
||||||
#include "modern-qt/utility/wrapper/common.hh"
|
#include "creeper-qt/utility/wrapper/common.hh"
|
||||||
#include "modern-qt/utility/wrapper/pimpl.hh"
|
#include "creeper-qt/utility/wrapper/pimpl.hh"
|
||||||
#include "modern-qt/utility/wrapper/property.hh"
|
#include "creeper-qt/utility/wrapper/property.hh"
|
||||||
#include "qcustomplot/qcustomplot.h"
|
#include "qcustomplot/qcustomplot.h"
|
||||||
#include "modern-qt/utility/wrapper/widget.hh"
|
#include "creeper-qt/utility/wrapper/widget.hh"
|
||||||
#include <concepts>
|
#include <concepts>
|
||||||
#include <qcontainerfwd.h>
|
#include <qcontainerfwd.h>
|
||||||
#include <qvector.h>
|
#include <qvector.h>
|
||||||
@@ -43,7 +43,9 @@ public:
|
|||||||
void set_matrix_size(const int& w, const int& h)const;
|
void set_matrix_size(const int& w, const int& h)const;
|
||||||
void set_data(const QVector<PointData>& data)const;
|
void set_data(const QVector<PointData>& data)const;
|
||||||
void set_color_gradient_range(const double& min, const double& max)const;
|
void set_color_gradient_range(const double& min, const double& max)const;
|
||||||
|
void set_labels_visible(bool visible) const;
|
||||||
QSize get_matrix_size() const;
|
QSize get_matrix_size() const;
|
||||||
|
bool labels_visible() const;
|
||||||
bool is_initialized() const;
|
bool is_initialized() const;
|
||||||
|
|
||||||
public slots:
|
public slots:
|
||||||
@@ -82,8 +84,11 @@ namespace plot_widget::pro {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
using Data = common::pro::Vector<Token, PointData,
|
// using Data = common::pro::Vector<Token, PointData,
|
||||||
[](auto& self, const auto& data) {
|
// [](auto& self, const auto& data) {
|
||||||
|
// self.set_data(data);
|
||||||
|
// }>;
|
||||||
|
using Data = DerivedProp<Token, QVector<QString>, [](auto& self, const auto& data) {
|
||||||
self.set_data(data);
|
self.set_data(data);
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
@@ -107,11 +112,15 @@ namespace plot_widget::pro {
|
|||||||
template<class PlotWidget>
|
template<class PlotWidget>
|
||||||
concept trait = std::derived_from<PlotWidget, Token>;
|
concept trait = std::derived_from<PlotWidget, Token>;
|
||||||
|
|
||||||
using PlotData = common::pro::Vector<Token, PointData, [](auto& self, const auto& vec) {
|
// using PlotData = common::pro::Vector<Token, PointData, [](auto& self, const auto& vec) {
|
||||||
self.set_data(vec);
|
// self.set_data(vec);
|
||||||
}>;
|
// }>;
|
||||||
|
using PlotData = DerivedProp<Token, QVector<PointData>, [](auto& self, const auto& vec){self.set_data(vec);}>;
|
||||||
|
|
||||||
using DataRange = common::pro::Array<Token, int, 2, [](auto& self, const auto& arr) {
|
// using DataRange = common::pro::Array<Token, int, 2, [](auto& self, const auto& arr) {
|
||||||
|
// self.set_color_gradient_range(arr[0], arr[1]);
|
||||||
|
// }>;
|
||||||
|
using DataRange = DerivedProp<Token, std::array<int, 2>, [](auto& self, const auto& arr) {
|
||||||
self.set_color_gradient_range(arr[0], arr[1]);
|
self.set_color_gradient_range(arr[0], arr[1]);
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
|||||||
@@ -6,17 +6,29 @@
|
|||||||
#define TOUCHSENSOR_HEATMAP_IMPL_HH
|
#define TOUCHSENSOR_HEATMAP_IMPL_HH
|
||||||
|
|
||||||
#include "heatmap.hh"
|
#include "heatmap.hh"
|
||||||
#include "modern-qt/utility/theme/theme.hh"
|
#include "creeper-qt/utility/theme/theme.hh"
|
||||||
#include "modern-qt/widget/sliders.hh"
|
#include "creeper-qt/widget/sliders.hh"
|
||||||
|
#include "qcustomplot/qcustomplot.h"
|
||||||
|
#include <algorithm>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
#include <optional>
|
||||||
#include <qcolor.h>
|
#include <qcolor.h>
|
||||||
#include <qdebug.h>
|
#include <qdebug.h>
|
||||||
|
#include <qfont.h>
|
||||||
|
#include <vector>
|
||||||
using namespace creeper::plot_widget::internal;
|
using namespace creeper::plot_widget::internal;
|
||||||
|
|
||||||
struct BasicPlot::Impl {
|
struct BasicPlot::Impl {
|
||||||
explicit Impl(BasicPlot& self) noexcept : self{self}, initialized(false), matrix_size(QSize{3, 4}) {}
|
explicit Impl(BasicPlot& self) noexcept
|
||||||
|
: matrix_size(QSize{3, 4})
|
||||||
|
, color_min(0.0)
|
||||||
|
, color_max(800.0)
|
||||||
|
, show_labels(false)
|
||||||
|
, initialized(false)
|
||||||
|
, self{self} { }
|
||||||
|
|
||||||
public:
|
public:
|
||||||
|
std::optional<creeper::ColorScheme> scheme;
|
||||||
auto set_xlabel_text(const QString& text) -> void {
|
auto set_xlabel_text(const QString& text) -> void {
|
||||||
xlabel = text;
|
xlabel = text;
|
||||||
if (initialized) {
|
if (initialized) {
|
||||||
@@ -35,18 +47,23 @@ public:
|
|||||||
|
|
||||||
auto set_matrix_size(const QSize& size) -> void {
|
auto set_matrix_size(const QSize& size) -> void {
|
||||||
matrix_size = size;
|
matrix_size = size;
|
||||||
|
const int expected = std::max(0, matrix_size.width() * matrix_size.height());
|
||||||
|
last_values.assign(static_cast<std::size_t>(expected), 0.0);
|
||||||
if (initialized) {
|
if (initialized) {
|
||||||
// 重新初始化热力图以适应新的矩阵大小
|
|
||||||
reset_plot();
|
reset_plot();
|
||||||
if (!data_points.isEmpty()) {
|
if (!data_points.isEmpty()) {
|
||||||
set_data(data_points);
|
set_data(data_points);
|
||||||
|
} else if (show_labels) {
|
||||||
|
sync_labels(last_values);
|
||||||
|
self.replot();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
auto load_theme_manager(ThemeManager& mgr) -> void {
|
auto load_theme_manager(ThemeManager& mgr) -> void {
|
||||||
mgr.append_handler(&self, [this](const ThemeManager& mgr) {
|
mgr.append_handler(&self, [this](const ThemeManager& mgr) {
|
||||||
// 可以根据主题更新颜色渐变等
|
scheme = mgr.color_scheme();
|
||||||
|
apply_color_scheme();
|
||||||
if (initialized) {
|
if (initialized) {
|
||||||
self.replot();
|
self.replot();
|
||||||
}
|
}
|
||||||
@@ -54,9 +71,8 @@ public:
|
|||||||
}
|
}
|
||||||
|
|
||||||
auto set_color_gradient_range(const double& min, const double& max) -> void {
|
auto set_color_gradient_range(const double& min, const double& max) -> void {
|
||||||
if (initialized && self.plottableCount() > 0) {
|
if (initialized && color_map) {
|
||||||
auto* cpmp = static_cast<QCPColorMap*>(self.plottable(0));
|
color_map->setDataRange(QCPRange(min, max));
|
||||||
cpmp->setDataRange(QCPRange(min, max));
|
|
||||||
self.replot();
|
self.replot();
|
||||||
}
|
}
|
||||||
color_min = min;
|
color_min = min;
|
||||||
@@ -65,21 +81,32 @@ public:
|
|||||||
|
|
||||||
auto set_data(const QVector<PointData>& data) -> void {
|
auto set_data(const QVector<PointData>& data) -> void {
|
||||||
data_points = data;
|
data_points = data;
|
||||||
if (initialized) {
|
if (initialized && color_map) {
|
||||||
update_plot_data();
|
update_plot_data();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
auto set_labels_visible(bool visible) -> void {
|
||||||
|
show_labels = visible;
|
||||||
|
if (initialized) {
|
||||||
|
sync_labels(last_values);
|
||||||
|
self.replot();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto labels_visible() const -> bool {
|
||||||
|
return show_labels;
|
||||||
|
}
|
||||||
|
|
||||||
auto initialize_plot() -> void {
|
auto initialize_plot() -> void {
|
||||||
if (initialized) return;
|
if (initialized) return;
|
||||||
|
|
||||||
// 创建颜色映射
|
color_map = new QCPColorMap(self.xAxis, self.yAxis);
|
||||||
QCPColorMap* cpmp = new QCPColorMap(self.xAxis, self.yAxis);
|
auto* cpmp = color_map;
|
||||||
cpmp->data()->setSize(matrix_size.width(), matrix_size.height());
|
cpmp->data()->setSize(matrix_size.width(), matrix_size.height());
|
||||||
cpmp->data()->setRange(QCPRange(0.5, matrix_size.width() - 0.5),
|
cpmp->data()->setRange(QCPRange(0.5, matrix_size.width() - 0.5),
|
||||||
QCPRange(0.5, matrix_size.height() - 0.5));
|
QCPRange(0.5, matrix_size.height() - 0.5));
|
||||||
|
|
||||||
// 配置坐标轴
|
|
||||||
QSharedPointer<QCPAxisTickerText> xticker(new QCPAxisTickerText);
|
QSharedPointer<QCPAxisTickerText> xticker(new QCPAxisTickerText);
|
||||||
QSharedPointer<QCPAxisTickerText> yticker(new QCPAxisTickerText);
|
QSharedPointer<QCPAxisTickerText> yticker(new QCPAxisTickerText);
|
||||||
xticker->setSubTickCount(1);
|
xticker->setSubTickCount(1);
|
||||||
@@ -89,7 +116,6 @@ public:
|
|||||||
self.xAxis->setTicker(xticker);
|
self.xAxis->setTicker(xticker);
|
||||||
self.yAxis->setTicker(yticker);
|
self.yAxis->setTicker(yticker);
|
||||||
|
|
||||||
// 设置网格
|
|
||||||
self.xAxis->grid()->setPen(Qt::NoPen);
|
self.xAxis->grid()->setPen(Qt::NoPen);
|
||||||
self.yAxis->grid()->setPen(Qt::NoPen);
|
self.yAxis->grid()->setPen(Qt::NoPen);
|
||||||
self.xAxis->grid()->setSubGridVisible(true);
|
self.xAxis->grid()->setSubGridVisible(true);
|
||||||
@@ -101,38 +127,44 @@ public:
|
|||||||
self.xAxis->setSubTickLength(6);
|
self.xAxis->setSubTickLength(6);
|
||||||
self.yAxis->setSubTickLength(6);
|
self.yAxis->setSubTickLength(6);
|
||||||
|
|
||||||
// 设置范围
|
|
||||||
self.xAxis->setRange(0, matrix_size.width());
|
self.xAxis->setRange(0, matrix_size.width());
|
||||||
self.yAxis->setRange(0, matrix_size.height());
|
self.yAxis->setRange(0, matrix_size.height());
|
||||||
|
|
||||||
// 设置标签
|
|
||||||
if (!xlabel.isEmpty()) self.xAxis->setLabel(xlabel);
|
if (!xlabel.isEmpty()) self.xAxis->setLabel(xlabel);
|
||||||
if (!ylabel.isEmpty()) self.yAxis->setLabel(ylabel);
|
if (!ylabel.isEmpty()) self.yAxis->setLabel(ylabel);
|
||||||
|
|
||||||
// 添加颜色刻度
|
if (!color_scale) {
|
||||||
QCPColorScale* color_scale = new QCPColorScale(&self);
|
color_scale = new QCPColorScale(&self);
|
||||||
color_scale->setType(QCPAxis::atBottom);
|
color_scale->setType(QCPAxis::atBottom);
|
||||||
|
}
|
||||||
|
if (self.plotLayout()) {
|
||||||
|
auto existing = self.plotLayout()->element(1, 0);
|
||||||
|
if (existing && existing != color_scale) {
|
||||||
|
self.plotLayout()->take(existing);
|
||||||
|
}
|
||||||
|
if (!existing || existing != color_scale) {
|
||||||
self.plotLayout()->addElement(1, 0, color_scale);
|
self.plotLayout()->addElement(1, 0, color_scale);
|
||||||
|
}
|
||||||
|
}
|
||||||
cpmp->setColorScale(color_scale);
|
cpmp->setColorScale(color_scale);
|
||||||
|
|
||||||
// 设置颜色渐变
|
|
||||||
QCPColorGradient gradient;
|
QCPColorGradient gradient;
|
||||||
gradient.setColorStopAt(0.0, QColor(246, 239, 166)); // F6EFA6
|
gradient.setColorStopAt(0.0, QColor(0, 176, 80));
|
||||||
gradient.setColorStopAt(1.0, QColor(191, 68, 76)); // BF444C
|
gradient.setColorStopAt(0.5, QColor(255, 214, 10));
|
||||||
|
gradient.setColorStopAt(1.0, QColor(204, 0, 0));
|
||||||
cpmp->setGradient(gradient);
|
cpmp->setGradient(gradient);
|
||||||
|
|
||||||
// 设置数据范围
|
|
||||||
cpmp->setDataRange(QCPRange(color_min, color_max));
|
cpmp->setDataRange(QCPRange(color_min, color_max));
|
||||||
cpmp->setInterpolate(false);
|
cpmp->setInterpolate(false);
|
||||||
|
|
||||||
// 配置边距
|
|
||||||
QCPMarginGroup *margin_group = new QCPMarginGroup(&self);
|
QCPMarginGroup *margin_group = new QCPMarginGroup(&self);
|
||||||
self.axisRect()->setMarginGroup(QCP::msLeft | QCP::msRight, margin_group);
|
self.axisRect()->setMarginGroup(QCP::msLeft | QCP::msRight, margin_group);
|
||||||
color_scale->setMarginGroup(QCP::msLeft | QCP::msRight, margin_group);
|
color_scale->setMarginGroup(QCP::msLeft | QCP::msRight, margin_group);
|
||||||
|
|
||||||
initialized = true;
|
initialized = true;
|
||||||
|
apply_color_scheme();
|
||||||
|
|
||||||
// 如果已有数据,更新图表
|
|
||||||
if (!data_points.isEmpty()) {
|
if (!data_points.isEmpty()) {
|
||||||
update_plot_data();
|
update_plot_data();
|
||||||
}
|
}
|
||||||
@@ -140,29 +172,41 @@ public:
|
|||||||
|
|
||||||
auto reset_plot() -> void {
|
auto reset_plot() -> void {
|
||||||
// 清除所有绘图元素
|
// 清除所有绘图元素
|
||||||
|
clear_labels();
|
||||||
self.clearPlottables();
|
self.clearPlottables();
|
||||||
self.clearGraphs();
|
self.clearGraphs();
|
||||||
self.clearItems();
|
self.clearItems();
|
||||||
self.clearFocus();
|
self.clearFocus();
|
||||||
|
color_map = nullptr;
|
||||||
|
|
||||||
// 重新初始化
|
// 重新初始化
|
||||||
initialized = false;
|
initialized = false;
|
||||||
initialize_plot();
|
initialize_plot();
|
||||||
}
|
}
|
||||||
|
|
||||||
auto update_plot_data() -> void {
|
auto update_plot_data() -> void {
|
||||||
if (!initialized || self.plottableCount() == 0) return;
|
if (!initialized || !color_map) return;
|
||||||
|
|
||||||
auto* cpmp = static_cast<QCPColorMap*>(self.plottable(0));
|
// ensure_labels();
|
||||||
|
|
||||||
|
const int width = matrix_size.width();
|
||||||
|
const int height = matrix_size.height();
|
||||||
|
const int expected = width * height;
|
||||||
|
std::vector<double> values(static_cast<std::size_t>(expected), 0.0);
|
||||||
|
|
||||||
// 设置新数据
|
// 设置新数据
|
||||||
for (const auto& item : data_points) {
|
for (const auto& item : data_points) {
|
||||||
if (item.x >= 0 && item.x < matrix_size.width() &&
|
if (item.x >= 0 && item.x < matrix_size.width() &&
|
||||||
item.y >= 0 && item.y < matrix_size.height()) {
|
item.y >= 0 && item.y < matrix_size.height()) {
|
||||||
cpmp->data()->setCell(item.x, item.y, item.z);
|
color_map->data()->setCell(item.x, item.y, item.z);
|
||||||
|
const int idx = static_cast<int>(item.y) * width + static_cast<int>(item.x);
|
||||||
|
if (idx >= 0 && idx < expected) {
|
||||||
|
values[static_cast<std::size_t>(idx)] = item.z;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
last_values = values;
|
||||||
|
sync_labels(last_values);
|
||||||
|
|
||||||
// 重绘
|
// 重绘
|
||||||
self.replot();
|
self.replot();
|
||||||
@@ -181,10 +225,127 @@ private:
|
|||||||
QString ylabel;
|
QString ylabel;
|
||||||
QSize matrix_size;
|
QSize matrix_size;
|
||||||
QVector<PointData> data_points;
|
QVector<PointData> data_points;
|
||||||
|
std::vector<double> last_values;
|
||||||
double color_min = 0.0;
|
double color_min = 0.0;
|
||||||
double color_max = 15.0;
|
double color_max = 800.0;
|
||||||
|
bool show_labels = false;
|
||||||
bool initialized;
|
bool initialized;
|
||||||
BasicPlot& self;
|
BasicPlot& self;
|
||||||
|
QCPColorScale* color_scale = nullptr;
|
||||||
|
QCPColorMap* color_map = nullptr;
|
||||||
|
QVector<QCPItemText*> cell_labels;
|
||||||
|
QColor label_text_color = QColor(0, 0, 0);
|
||||||
|
|
||||||
|
void apply_color_scheme() {
|
||||||
|
QColor text_color = QColor(30, 30, 30);
|
||||||
|
if (scheme.has_value()) {
|
||||||
|
if (scheme->on_surface.isValid()) {
|
||||||
|
text_color = scheme->on_surface;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
label_text_color = text_color;
|
||||||
|
|
||||||
|
const auto pen = QPen(text_color);
|
||||||
|
|
||||||
|
self.xAxis->setTickLabelColor(text_color);
|
||||||
|
self.yAxis->setTickLabelColor(text_color);
|
||||||
|
self.xAxis->setLabelColor(text_color);
|
||||||
|
self.yAxis->setLabelColor(text_color);
|
||||||
|
self.xAxis->setBasePen(pen);
|
||||||
|
self.yAxis->setBasePen(pen);
|
||||||
|
self.xAxis->setTickPen(pen);
|
||||||
|
self.yAxis->setTickPen(pen);
|
||||||
|
if (color_scale && color_scale->axis()) {
|
||||||
|
color_scale->axis()->setTickLabelColor(text_color);
|
||||||
|
color_scale->axis()->setLabelColor(text_color);
|
||||||
|
color_scale->axis()->setBasePen(pen);
|
||||||
|
color_scale->axis()->setTickPen(pen);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 已有标签更新
|
||||||
|
for (auto* label : cell_labels) {
|
||||||
|
if (!label) continue;
|
||||||
|
label->setColor(label_text_color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void clear_labels() {
|
||||||
|
for (auto* label : cell_labels) {
|
||||||
|
if (label) {
|
||||||
|
self.removeItem(label);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cell_labels.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ensure_labels() {
|
||||||
|
const int width = matrix_size.width();
|
||||||
|
const int height = matrix_size.height();
|
||||||
|
const int expected = width * height;
|
||||||
|
if (expected <= 0) {
|
||||||
|
clear_labels();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (cell_labels.size() == expected) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
clear_labels();
|
||||||
|
cell_labels.reserve(expected);
|
||||||
|
for (int y = 0; y < height; ++y) {
|
||||||
|
for (int x = 0; x < width; ++x) {
|
||||||
|
auto* label = new QCPItemText(&self);
|
||||||
|
label->position->setType(QCPItemPosition::ptPlotCoords);
|
||||||
|
label->setClipToAxisRect(true);
|
||||||
|
label->setClipAxisRect(self.axisRect());
|
||||||
|
label->setPositionAlignment(Qt::AlignCenter);
|
||||||
|
label->position->setCoords(x + 0.5, y + 0.5);
|
||||||
|
label->setBrush(Qt::NoBrush);
|
||||||
|
label->setPen(Qt::NoPen);
|
||||||
|
QFont font = label->font();
|
||||||
|
if (font.pointSize() > 0) {
|
||||||
|
font.setPointSize(std::max(font.pointSize() - 1, 6));
|
||||||
|
} else {
|
||||||
|
font.setPointSize(8);
|
||||||
|
}
|
||||||
|
label->setFont(font);
|
||||||
|
label->setColor(label_text_color);
|
||||||
|
label->setSelectable(false);
|
||||||
|
cell_labels.push_back(label);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void update_label_values(const std::vector<double>& values) {
|
||||||
|
const int width = matrix_size.width();
|
||||||
|
const int height = matrix_size.height();
|
||||||
|
const int expected = width * height;
|
||||||
|
for (int idx = 0; idx < expected && idx < cell_labels.size(); ++idx) {
|
||||||
|
auto* label = cell_labels[idx];
|
||||||
|
if (!label) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const double value = values.size() > static_cast<std::size_t>(idx)
|
||||||
|
? values[static_cast<std::size_t>(idx)]
|
||||||
|
: 0.0;
|
||||||
|
label->setText(QString::number(value, 'f', 0));
|
||||||
|
const int x = idx % width;
|
||||||
|
const int y = idx / width;
|
||||||
|
label->position->setCoords(x + 0.5, y + 0.5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void sync_labels(const std::vector<double>& values) {
|
||||||
|
if (!initialized) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (show_labels) {
|
||||||
|
ensure_labels();
|
||||||
|
update_label_values(values);
|
||||||
|
} else if (!cell_labels.isEmpty()) {
|
||||||
|
clear_labels();
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // TOUCHSENSOR_HEATMAP_IMPL_HH
|
#endif // TOUCHSENSOR_HEATMAP_IMPL_HH
|
||||||
|
|||||||
254
components/charts/line_chart.cc
Normal file
254
components/charts/line_chart.cc
Normal file
@@ -0,0 +1,254 @@
|
|||||||
|
//
|
||||||
|
// Created by Codex on 2025/12/10.
|
||||||
|
//
|
||||||
|
|
||||||
|
#include "line_chart.hh"
|
||||||
|
#include <QLinearGradient>
|
||||||
|
#include <QMargins>
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
#include <limits>
|
||||||
|
|
||||||
|
using creeper::line_widget::internal::LinePlot;
|
||||||
|
|
||||||
|
LinePlot::LinePlot() {
|
||||||
|
setBackground(Qt::transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
LinePlot::~LinePlot() = default;
|
||||||
|
|
||||||
|
void LinePlot::load_theme_manager(creeper::ThemeManager& mgr) {
|
||||||
|
mgr.append_handler(this, [this](const creeper::ThemeManager& manager) {
|
||||||
|
scheme_ = manager.color_scheme();
|
||||||
|
apply_theme();
|
||||||
|
if (initialized_) {
|
||||||
|
replot();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void LinePlot::set_data(const QVector<QPointF>& points) {
|
||||||
|
points_ = points;
|
||||||
|
trim_points();
|
||||||
|
if (initialized_) {
|
||||||
|
update_graph();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void LinePlot::set_max_points(int count) {
|
||||||
|
const int clamped = std::max(1, count);
|
||||||
|
if (clamped == max_points_) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
max_points_ = clamped;
|
||||||
|
trim_points();
|
||||||
|
if (initialized_) {
|
||||||
|
update_graph();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void LinePlot::paintEvent(QPaintEvent* event) {
|
||||||
|
if (!initialized_) {
|
||||||
|
initialize_plot();
|
||||||
|
}
|
||||||
|
QCustomPlot::paintEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
void LinePlot::initialize_plot() {
|
||||||
|
if (initialized_) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
axisRect()->setAutoMargins(QCP::msNone);
|
||||||
|
axisRect()->setMargins(QMargins(40, 12, 12, 18));
|
||||||
|
axisRect()->setBackground(QBrush(QColor(246, 249, 255)));
|
||||||
|
|
||||||
|
legend->setVisible(false);
|
||||||
|
xAxis->setVisible(true);
|
||||||
|
yAxis->setVisible(true);
|
||||||
|
xAxis->setRange(0.0, std::max(10, max_points_));
|
||||||
|
yAxis->setRange(0.0, default_y_range_);
|
||||||
|
|
||||||
|
xAxis->grid()->setVisible(false);
|
||||||
|
yAxis->grid()->setVisible(false);
|
||||||
|
xAxis->setTicks(false);
|
||||||
|
yAxis->setTicks(true);
|
||||||
|
xAxis->setSubTicks(false);
|
||||||
|
yAxis->setSubTicks(false);
|
||||||
|
xAxis->setTickLength(0);
|
||||||
|
yAxis->setTickLength(0);
|
||||||
|
yAxis->setTickLabelPadding(6);
|
||||||
|
yAxis->setLabelPadding(8);
|
||||||
|
|
||||||
|
graph_ = addGraph();
|
||||||
|
graph_->setLineStyle(QCPGraph::lsLine);
|
||||||
|
graph_->setScatterStyle(QCPScatterStyle(QCPScatterStyle::ssCircle, QPen(Qt::NoPen), QBrush(Qt::white), 6));
|
||||||
|
graph_->setBrush(QColor(16, 54, 128, 42));
|
||||||
|
graph_->setAntialiased(true);
|
||||||
|
graph_->setAdaptiveSampling(true);
|
||||||
|
|
||||||
|
current_label_ = new QCPItemText(this);
|
||||||
|
current_label_->position->setType(QCPItemPosition::ptAxisRectRatio);
|
||||||
|
current_label_->setPositionAlignment(Qt::AlignRight | Qt::AlignTop);
|
||||||
|
current_label_->position->setCoords(0.98, 0.04);
|
||||||
|
current_label_->setPadding(QMargins(6, 3, 6, 3));
|
||||||
|
current_label_->setBrush(QColor(0, 0, 0, 50));
|
||||||
|
current_label_->setPen(Qt::NoPen);
|
||||||
|
current_label_->setLayer("overlay");
|
||||||
|
current_label_->setClipToAxisRect(true);
|
||||||
|
|
||||||
|
initialized_ = true;
|
||||||
|
apply_theme();
|
||||||
|
update_graph();
|
||||||
|
}
|
||||||
|
|
||||||
|
void LinePlot::reset_graph_range() {
|
||||||
|
xAxis->setRange(0.0, std::max(10, max_points_));
|
||||||
|
yAxis->setRange(0.0, default_y_range_);
|
||||||
|
}
|
||||||
|
|
||||||
|
void LinePlot::apply_theme() {
|
||||||
|
QColor line_color{ 16, 54, 128 };
|
||||||
|
QColor text_color{ 30, 30, 30 };
|
||||||
|
QColor bg_color{ 232, 238, 248 };
|
||||||
|
QColor label_bg = QColor(0, 0, 0, 50);
|
||||||
|
if (scheme_.has_value()) {
|
||||||
|
if (scheme_->primary.isValid()) {
|
||||||
|
line_color = scheme_->primary;
|
||||||
|
}
|
||||||
|
if (scheme_->on_surface.isValid()) {
|
||||||
|
text_color = scheme_->on_surface;
|
||||||
|
}
|
||||||
|
if (scheme_->surface_container_high.isValid()) {
|
||||||
|
bg_color = scheme_->surface_container_high;
|
||||||
|
}
|
||||||
|
else if (scheme_->surface_container.isValid()) {
|
||||||
|
bg_color = scheme_->surface_container;
|
||||||
|
}
|
||||||
|
else if (scheme_->surface.isValid()) {
|
||||||
|
bg_color = scheme_->surface;
|
||||||
|
}
|
||||||
|
if (scheme_->surface_container.isValid()) {
|
||||||
|
label_bg = QColor(scheme_->surface_container.red(),
|
||||||
|
scheme_->surface_container.green(),
|
||||||
|
scheme_->surface_container.blue(), 90);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QColor grid_color = bg_color;
|
||||||
|
|
||||||
|
QPen axis_pen(bg_color);
|
||||||
|
axis_pen.setWidthF(1.0);
|
||||||
|
xAxis->setBasePen(axis_pen);
|
||||||
|
yAxis->setBasePen(axis_pen);
|
||||||
|
xAxis->setTickPen(axis_pen);
|
||||||
|
yAxis->setTickPen(axis_pen);
|
||||||
|
xAxis->setTickLabelColor(Qt::transparent);
|
||||||
|
yAxis->setTickLabelColor(text_color);
|
||||||
|
xAxis->setLabelColor(Qt::transparent);
|
||||||
|
yAxis->setLabelColor(text_color);
|
||||||
|
axisRect()->setBackground(bg_color);
|
||||||
|
|
||||||
|
if (graph_) {
|
||||||
|
QPen pen(line_color);
|
||||||
|
pen.setWidthF(3.0);
|
||||||
|
pen.setCapStyle(Qt::RoundCap);
|
||||||
|
graph_->setPen(pen);
|
||||||
|
|
||||||
|
QLinearGradient fill_grad(0, 0, 0, 1);
|
||||||
|
fill_grad.setCoordinateMode(QGradient::CoordinateMode::ObjectBoundingMode);
|
||||||
|
fill_grad.setColorAt(0.0, QColor(line_color.red(), line_color.green(), line_color.blue(), 70));
|
||||||
|
fill_grad.setColorAt(1.0, QColor(line_color.red(), line_color.green(), line_color.blue(), 18));
|
||||||
|
graph_->setBrush(QBrush(fill_grad));
|
||||||
|
|
||||||
|
auto scatter = graph_->scatterStyle();
|
||||||
|
scatter.setPen(QPen(line_color, 1.5));
|
||||||
|
scatter.setBrush(QBrush(QColor(bg_color).lighter(104)));
|
||||||
|
scatter.setSize(7);
|
||||||
|
graph_->setScatterStyle(scatter);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (current_label_) {
|
||||||
|
current_label_->setColor(text_color);
|
||||||
|
current_label_->setBrush(label_bg);
|
||||||
|
QFont f = current_label_->font();
|
||||||
|
f.setBold(true);
|
||||||
|
current_label_->setFont(f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void LinePlot::update_graph() {
|
||||||
|
if (!initialized_) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!graph_) {
|
||||||
|
graph_ = addGraph();
|
||||||
|
apply_theme();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (points_.isEmpty()) {
|
||||||
|
graph_->data()->clear();
|
||||||
|
reset_graph_range();
|
||||||
|
if (current_label_) {
|
||||||
|
current_label_->setText(QStringLiteral("--"));
|
||||||
|
}
|
||||||
|
replot();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
QVector<double> keys(points_.size());
|
||||||
|
QVector<double> values(points_.size());
|
||||||
|
double min_key = std::numeric_limits<double>::max();
|
||||||
|
double max_key = std::numeric_limits<double>::lowest();
|
||||||
|
double min_val = std::numeric_limits<double>::max();
|
||||||
|
double max_val = std::numeric_limits<double>::lowest();
|
||||||
|
|
||||||
|
for (int i = 0; i < points_.size(); ++i) {
|
||||||
|
const auto& pt = points_[i];
|
||||||
|
keys[i] = pt.x();
|
||||||
|
values[i] = pt.y();
|
||||||
|
min_key = std::min(min_key, pt.x());
|
||||||
|
max_key = std::max(max_key, pt.x());
|
||||||
|
min_val = std::min(min_val, pt.y());
|
||||||
|
max_val = std::max(max_val, pt.y());
|
||||||
|
}
|
||||||
|
|
||||||
|
graph_->setData(keys, values, true);
|
||||||
|
|
||||||
|
if (min_key == std::numeric_limits<double>::max()) {
|
||||||
|
reset_graph_range();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
const double key_span = std::max(1e-3, max_key - min_key);
|
||||||
|
xAxis->setRange(min_key, max_key + key_span * 0.02);
|
||||||
|
|
||||||
|
double value_span = max_val - min_val;
|
||||||
|
if (value_span < 1e-3) {
|
||||||
|
value_span = std::max(std::abs(max_val), 1.0);
|
||||||
|
min_val = max_val - value_span * 0.5;
|
||||||
|
}
|
||||||
|
const double padding = std::max(value_span * 0.25, 1.0);
|
||||||
|
yAxis->setRange(min_val - padding, max_val + padding);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (current_label_) {
|
||||||
|
const double last_val = points_.back().y();
|
||||||
|
current_label_->setText(QString::number(last_val, 'f', 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
replot();
|
||||||
|
}
|
||||||
|
|
||||||
|
void LinePlot::trim_points() {
|
||||||
|
if (max_points_ <= 0 || points_.size() <= max_points_) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const int start = points_.size() - max_points_;
|
||||||
|
points_ = points_.mid(start);
|
||||||
|
}
|
||||||
|
|
||||||
|
using namespace creeper;
|
||||||
|
|
||||||
|
void SumLinePlot::paintEvent(QPaintEvent* event) {
|
||||||
|
line_widget::internal::LinePlot::paintEvent(event);
|
||||||
|
}
|
||||||
81
components/charts/line_chart.hh
Normal file
81
components/charts/line_chart.hh
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
//
|
||||||
|
// Created by Codex on 2025/12/10.
|
||||||
|
//
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "creeper-qt/utility/theme/theme.hh"
|
||||||
|
#include "creeper-qt/utility/wrapper/common.hh"
|
||||||
|
#include "creeper-qt/utility/wrapper/property.hh"
|
||||||
|
#include "creeper-qt/utility/wrapper/widget.hh"
|
||||||
|
#include "qcustomplot/qcustomplot.h"
|
||||||
|
#include <QPaintEvent>
|
||||||
|
#include <QPointF>
|
||||||
|
#include <QString>
|
||||||
|
#include <concepts>
|
||||||
|
#include <optional>
|
||||||
|
#include <qsize.h>
|
||||||
|
#include <qvector.h>
|
||||||
|
|
||||||
|
namespace creeper {
|
||||||
|
class SumLinePlot;
|
||||||
|
|
||||||
|
namespace line_widget::internal {
|
||||||
|
class LinePlot: public QCustomPlot {
|
||||||
|
public:
|
||||||
|
LinePlot();
|
||||||
|
~LinePlot() override;
|
||||||
|
|
||||||
|
void load_theme_manager(ThemeManager& mgr);
|
||||||
|
void set_data(const QVector<QPointF>& points);
|
||||||
|
void set_max_points(int count);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void paintEvent(QPaintEvent* event) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void initialize_plot();
|
||||||
|
void update_graph();
|
||||||
|
void apply_theme();
|
||||||
|
void reset_graph_range();
|
||||||
|
void trim_points();
|
||||||
|
|
||||||
|
QVector<QPointF> points_;
|
||||||
|
bool initialized_ = false;
|
||||||
|
std::optional<ColorScheme> scheme_;
|
||||||
|
QCPGraph* graph_ = nullptr;
|
||||||
|
QCPItemText* current_label_ = nullptr;
|
||||||
|
int max_points_ = 240;
|
||||||
|
double default_y_range_ = 100.0;
|
||||||
|
};
|
||||||
|
} // namespace line_widget::internal
|
||||||
|
|
||||||
|
namespace line_widget::pro {
|
||||||
|
using Token = common::Token<internal::LinePlot>;
|
||||||
|
|
||||||
|
struct MaxPoints: Token {
|
||||||
|
int count;
|
||||||
|
explicit MaxPoints(int c): count{ c } { }
|
||||||
|
void apply(auto& self) const { self.set_max_points(count); }
|
||||||
|
};
|
||||||
|
|
||||||
|
using PlotData = DerivedProp<Token, QVector<QPointF>, [](auto& self, const auto& vec) {
|
||||||
|
self.set_data(vec);
|
||||||
|
}>;
|
||||||
|
|
||||||
|
template<class PlotWidget>
|
||||||
|
concept trait = std::derived_from<PlotWidget, Token>;
|
||||||
|
|
||||||
|
CREEPER_DEFINE_CHECKER(trait);
|
||||||
|
|
||||||
|
using namespace widget::pro;
|
||||||
|
using namespace theme::pro;
|
||||||
|
} // namespace line_widget::pro
|
||||||
|
|
||||||
|
struct SumLinePlot
|
||||||
|
: public Declarative<line_widget::internal::LinePlot,
|
||||||
|
CheckerOr<line_widget::pro::checker, widget::pro::checker, theme::pro::checker>> {
|
||||||
|
using Declarative::Declarative;
|
||||||
|
void paintEvent(QPaintEvent* event) override;
|
||||||
|
};
|
||||||
|
} // namespace creeper
|
||||||
318
components/charts/vector_field.cc
Normal file
318
components/charts/vector_field.cc
Normal file
@@ -0,0 +1,318 @@
|
|||||||
|
//
|
||||||
|
// Created by Codex on 2025/12/05.
|
||||||
|
//
|
||||||
|
|
||||||
|
#include "vector_field.hh"
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
#include <vector>
|
||||||
|
#include <QLinearGradient>
|
||||||
|
#include <QPainter>
|
||||||
|
#include <QPainterPath>
|
||||||
|
#include <QPointF>
|
||||||
|
|
||||||
|
using creeper::vector_widget::internal::VectorPlot;
|
||||||
|
|
||||||
|
VectorPlot::VectorPlot() {
|
||||||
|
setBackground(Qt::transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
VectorPlot::~VectorPlot() = default;
|
||||||
|
|
||||||
|
void VectorPlot::load_theme_manager(creeper::ThemeManager& mgr) {
|
||||||
|
mgr.append_handler(this, [this](const creeper::ThemeManager& manager) {
|
||||||
|
scheme_ = manager.color_scheme();
|
||||||
|
apply_color_scheme();
|
||||||
|
if (initialized_) {
|
||||||
|
replot();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void VectorPlot::set_matrix_size(const QSize& size) {
|
||||||
|
if (size == matrix_size_) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
matrix_size_ = size;
|
||||||
|
reset_plot();
|
||||||
|
}
|
||||||
|
|
||||||
|
void VectorPlot::set_data(const QVector<PointData>& data) {
|
||||||
|
data_points_ = data;
|
||||||
|
if (initialized_) {
|
||||||
|
update_vectors();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void VectorPlot::paintEvent(QPaintEvent* event) {
|
||||||
|
if (!initialized_) {
|
||||||
|
initialize_plot();
|
||||||
|
}
|
||||||
|
QCustomPlot::paintEvent(event);
|
||||||
|
|
||||||
|
// Custom glossy arrow rendering
|
||||||
|
const auto rect = axisRect();
|
||||||
|
if (!rect) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
QPainter painter(this);
|
||||||
|
painter.setRenderHint(QPainter::Antialiasing, true);
|
||||||
|
|
||||||
|
const QRectF plot_rect = rect->rect();
|
||||||
|
const double side_pad = plot_rect.width() * 0.24;
|
||||||
|
QRectF area = plot_rect.adjusted(side_pad, 8.0, -side_pad, -8.0);
|
||||||
|
const double cx_canvas = area.center().x();
|
||||||
|
const double cy_canvas = area.center().y();
|
||||||
|
const double scale = 0.72; // overall shrink to make it slimmer
|
||||||
|
area.setWidth(area.width() * scale);
|
||||||
|
area.setHeight(area.height() * scale);
|
||||||
|
area.moveCenter(QPointF(cx_canvas, cy_canvas));
|
||||||
|
if (area.width() <= 0.0 || area.height() <= 0.0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const double w = area.width();
|
||||||
|
const double h = area.height();
|
||||||
|
const double cx = area.center().x();
|
||||||
|
const double shaft_w = w * 0.18;
|
||||||
|
const double shaft_r = shaft_w * 0.25;
|
||||||
|
const double head_h = h * 0.36;
|
||||||
|
const double head_w = w * 0.52;
|
||||||
|
const double shaft_h = h - head_h;
|
||||||
|
const double shaft_x = cx - shaft_w * 0.5;
|
||||||
|
const double shaft_y = area.top();
|
||||||
|
const double head_base_y = shaft_y + shaft_h - shaft_r * 0.4; // overlap slightly to avoid seam
|
||||||
|
|
||||||
|
QPainterPath shaft;
|
||||||
|
shaft.addRoundedRect(QRectF(shaft_x, shaft_y, shaft_w, shaft_h), shaft_r, shaft_r);
|
||||||
|
|
||||||
|
QPainterPath head;
|
||||||
|
head.moveTo(cx - head_w * 0.5, head_base_y);
|
||||||
|
head.lineTo(cx + head_w * 0.5, head_base_y);
|
||||||
|
head.lineTo(cx, area.bottom());
|
||||||
|
head.closeSubpath();
|
||||||
|
|
||||||
|
QPainterPath arrow = shaft.united(head);
|
||||||
|
|
||||||
|
// Vibrant orange palette (fixed)
|
||||||
|
QColor base_color(255, 140, 0);
|
||||||
|
QColor highlight = base_color.lighter(165);
|
||||||
|
QColor mid = base_color;
|
||||||
|
QColor shadow = base_color.darker(180);
|
||||||
|
|
||||||
|
QLinearGradient body_grad(area.topLeft(), QPointF(area.left(), area.bottom()));
|
||||||
|
body_grad.setColorAt(0.0, highlight);
|
||||||
|
body_grad.setColorAt(0.28, mid);
|
||||||
|
body_grad.setColorAt(0.72, base_color.darker(110));
|
||||||
|
body_grad.setColorAt(1.0, shadow);
|
||||||
|
|
||||||
|
const double angle_deg = std::atan2(arrow_dir_.y(), arrow_dir_.x()) * 180.0 / 3.14159265358979323846 - 90.0;
|
||||||
|
QTransform transform;
|
||||||
|
transform.translate(cx, (shaft_y + head_base_y + area.bottom()) / 3.0); // approximate center
|
||||||
|
transform.rotate(angle_deg);
|
||||||
|
transform.translate(-cx, -(shaft_y + head_base_y + area.bottom()) / 3.0);
|
||||||
|
|
||||||
|
auto draw_with_transform = [&](const QPainterPath& path, const QBrush& brush, const QPen* pen = nullptr) {
|
||||||
|
const QPainterPath rotated = transform.map(path);
|
||||||
|
painter.setBrush(brush);
|
||||||
|
if (pen) {
|
||||||
|
painter.setPen(*pen);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
painter.setPen(Qt::NoPen);
|
||||||
|
}
|
||||||
|
painter.drawPath(rotated);
|
||||||
|
};
|
||||||
|
|
||||||
|
draw_with_transform(arrow, QBrush(body_grad));
|
||||||
|
|
||||||
|
// Gloss highlight
|
||||||
|
QPainterPath gloss;
|
||||||
|
const double gloss_w = shaft_w * 0.42;
|
||||||
|
QRectF gloss_rect(cx - gloss_w * 0.5, shaft_y + h * 0.04, gloss_w, shaft_h * 0.5);
|
||||||
|
gloss.addRoundedRect(gloss_rect, gloss_w * 0.4, gloss_w * 0.4);
|
||||||
|
QLinearGradient gloss_grad(gloss_rect.topLeft(), gloss_rect.bottomLeft());
|
||||||
|
QColor gloss_hi = Qt::white;
|
||||||
|
gloss_hi.setAlpha(190);
|
||||||
|
QColor gloss_lo = Qt::white;
|
||||||
|
gloss_lo.setAlpha(40);
|
||||||
|
gloss_grad.setColorAt(0.0, gloss_hi);
|
||||||
|
gloss_grad.setColorAt(1.0, gloss_lo);
|
||||||
|
draw_with_transform(gloss, QBrush(gloss_grad));
|
||||||
|
|
||||||
|
// Head specular highlights
|
||||||
|
const double spec_w = head_w * 0.18;
|
||||||
|
QRectF spec_left(cx - head_w * 0.32, head_base_y + head_h * 0.08, spec_w, head_h * 0.2);
|
||||||
|
QRectF spec_right(cx + head_w * 0.14, head_base_y + head_h * 0.1, spec_w * 0.8, head_h * 0.2);
|
||||||
|
auto paint_spec = [&](const QRectF& r) {
|
||||||
|
QPainterPath p;
|
||||||
|
p.addRoundedRect(r, r.width() * 0.4, r.height() * 0.6);
|
||||||
|
QLinearGradient g(r.topLeft(), r.bottomLeft());
|
||||||
|
QColor hi = Qt::white;
|
||||||
|
hi.setAlpha(180);
|
||||||
|
QColor lo = Qt::white;
|
||||||
|
lo.setAlpha(30);
|
||||||
|
g.setColorAt(0.0, hi);
|
||||||
|
g.setColorAt(1.0, lo);
|
||||||
|
draw_with_transform(p, QBrush(g));
|
||||||
|
};
|
||||||
|
paint_spec(spec_left);
|
||||||
|
paint_spec(spec_right);
|
||||||
|
}
|
||||||
|
|
||||||
|
void VectorPlot::initialize_plot() {
|
||||||
|
if (initialized_) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
xAxis->setVisible(false);
|
||||||
|
yAxis->setVisible(false);
|
||||||
|
xAxis->grid()->setPen(Qt::NoPen);
|
||||||
|
yAxis->grid()->setPen(Qt::NoPen);
|
||||||
|
xAxis->setRange(0.0, std::max(1, matrix_size_.width()));
|
||||||
|
yAxis->setRange(0.0, std::max(1, matrix_size_.height()));
|
||||||
|
|
||||||
|
xAxis->setSubTicks(true);
|
||||||
|
yAxis->setSubTicks(true);
|
||||||
|
xAxis->setTickLength(0);
|
||||||
|
yAxis->setTickLength(0);
|
||||||
|
xAxis->setSubTickLength(4);
|
||||||
|
yAxis->setSubTickLength(4);
|
||||||
|
|
||||||
|
initialized_ = true;
|
||||||
|
apply_color_scheme();
|
||||||
|
ensure_arrows();
|
||||||
|
update_vectors();
|
||||||
|
}
|
||||||
|
|
||||||
|
void VectorPlot::reset_plot() {
|
||||||
|
clearItems();
|
||||||
|
primary_arrow_ = nullptr;
|
||||||
|
initialized_ = false;
|
||||||
|
initialize_plot();
|
||||||
|
}
|
||||||
|
|
||||||
|
void VectorPlot::ensure_arrows() {
|
||||||
|
// no-op: legacy multi-arrow support removed
|
||||||
|
}
|
||||||
|
|
||||||
|
void VectorPlot::ensure_primary_arrow() {
|
||||||
|
if (primary_arrow_) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
primary_arrow_ = new QCPItemLine(this);
|
||||||
|
primary_arrow_->start->setType(QCPItemPosition::ptPlotCoords);
|
||||||
|
primary_arrow_->end->setType(QCPItemPosition::ptPlotCoords);
|
||||||
|
primary_arrow_->setClipToAxisRect(true);
|
||||||
|
primary_arrow_->setClipAxisRect(axisRect());
|
||||||
|
primary_arrow_->setHead(QCPLineEnding(QCPLineEnding::esSpikeArrow, 14, 8));
|
||||||
|
primary_arrow_->setTail(QCPLineEnding(QCPLineEnding::esNone));
|
||||||
|
apply_color_scheme();
|
||||||
|
}
|
||||||
|
|
||||||
|
void VectorPlot::apply_color_scheme() {
|
||||||
|
QColor pen_color = arrow_color_;
|
||||||
|
if (scheme_.has_value() && scheme_->primary.isValid()) {
|
||||||
|
pen_color = scheme_->primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
QPen strong_pen(pen_color);
|
||||||
|
strong_pen.setWidthF(8.0); // 稍细一点但仍然饱满
|
||||||
|
strong_pen.setCapStyle(Qt::FlatCap);
|
||||||
|
strong_pen.setJoinStyle(Qt::MiterJoin);
|
||||||
|
if (primary_arrow_) {
|
||||||
|
primary_arrow_->setPen(strong_pen);
|
||||||
|
primary_arrow_->setVisible(false); // 使用自绘 3D 箭头
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void VectorPlot::update_vectors() {
|
||||||
|
if (!initialized_) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ensure_primary_arrow();
|
||||||
|
|
||||||
|
const int width = std::max(1, matrix_size_.width());
|
||||||
|
const int height = std::max(1, matrix_size_.height());
|
||||||
|
const int expected = width * height;
|
||||||
|
if (expected <= 0 || !primary_arrow_) {
|
||||||
|
replot();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<double> values(static_cast<std::size_t>(expected), 0.0);
|
||||||
|
for (const auto& item: data_points_) {
|
||||||
|
const int x = static_cast<int>(item.x);
|
||||||
|
const int y = static_cast<int>(item.y);
|
||||||
|
if (x >= 0 && x < width && y >= 0 && y < height) {
|
||||||
|
const int idx = y * width + x;
|
||||||
|
values[static_cast<std::size_t>(idx)] = item.z;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto value_at = [&](int x, int y) -> double {
|
||||||
|
if (x < 0 || x >= width || y < 0 || y >= height) {
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
return values[static_cast<std::size_t>(y * width + x)];
|
||||||
|
};
|
||||||
|
|
||||||
|
std::vector<std::pair<double, double>> grads(static_cast<std::size_t>(expected), { 0.0, 0.0 });
|
||||||
|
double max_mag = 0.0;
|
||||||
|
double sum_gx = 0.0;
|
||||||
|
double sum_gy = 0.0;
|
||||||
|
for (int y = 0; y < height; ++y) {
|
||||||
|
for (int x = 0; x < width; ++x) {
|
||||||
|
const double gx = 0.5 * (value_at(x + 1, y) - value_at(x - 1, y));
|
||||||
|
const double gy = 0.5 * (value_at(x, y + 1) - value_at(x, y - 1));
|
||||||
|
const double mag = std::sqrt(gx * gx + gy * gy);
|
||||||
|
grads[static_cast<std::size_t>(y * width + x)] = { gx, gy };
|
||||||
|
max_mag = std::max(max_mag, mag);
|
||||||
|
sum_gx += gx;
|
||||||
|
sum_gy += gy;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const double scale = (max_mag > 1e-6) ? (0.35 / max_mag) : 0.0;
|
||||||
|
const bool fallback = max_mag <= 1e-6;
|
||||||
|
const double mid_x = static_cast<double>(width) * 0.5;
|
||||||
|
const double mid_y = static_cast<double>(height) * 0.5;
|
||||||
|
|
||||||
|
double dir_x = sum_gx;
|
||||||
|
double dir_y = sum_gy;
|
||||||
|
if (fallback || std::abs(dir_x) + std::abs(dir_y) < 1e-6) {
|
||||||
|
dir_x = 0.0;
|
||||||
|
dir_y = -1.0; // 默认向上指,保证可见
|
||||||
|
}
|
||||||
|
double dir_len = std::sqrt(dir_x * dir_x + dir_y * dir_y);
|
||||||
|
if (dir_len < 1e-6) {
|
||||||
|
dir_x = 0.0;
|
||||||
|
dir_y = -1.0;
|
||||||
|
dir_len = 1.0;
|
||||||
|
}
|
||||||
|
dir_x /= dir_len;
|
||||||
|
dir_y /= dir_len;
|
||||||
|
arrow_dir_ = QPointF(dir_x, dir_y);
|
||||||
|
const double arrow_len = 0.48 * std::min(width, height); // 稍长的指针
|
||||||
|
const double tail_ratio = 0.25; // 穿过中心的尾巴略长
|
||||||
|
const double tail_len = arrow_len * tail_ratio;
|
||||||
|
const double head_len = arrow_len - tail_len;
|
||||||
|
const double backoff = std::max(0.5, head_len * 0.12); // 轻微回缩,避免方头顶出
|
||||||
|
const double head_base = std::max(0.0, head_len - backoff);
|
||||||
|
const double cx = static_cast<double>(width) * 0.5;
|
||||||
|
const double cy = static_cast<double>(height) * 0.5;
|
||||||
|
if (primary_arrow_) {
|
||||||
|
primary_arrow_->start->setCoords(cx - dir_x * tail_len, cy - dir_y * tail_len);
|
||||||
|
primary_arrow_->end->setCoords(cx + dir_x * head_base, cy + dir_y * head_base);
|
||||||
|
primary_arrow_->setVisible(false); // 位置保留以备需要,但当前使用自绘 3D 箭头
|
||||||
|
}
|
||||||
|
|
||||||
|
replot();
|
||||||
|
}
|
||||||
|
|
||||||
|
using namespace creeper;
|
||||||
|
|
||||||
|
void VectorFieldPlot::paintEvent(QPaintEvent* event) {
|
||||||
|
vector_widget::internal::VectorPlot::paintEvent(event);
|
||||||
|
}
|
||||||
84
components/charts/vector_field.hh
Normal file
84
components/charts/vector_field.hh
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
//
|
||||||
|
// Created by Codex on 2025/12/05.
|
||||||
|
//
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "components/charts/heatmap.hh" // for PointData definition
|
||||||
|
#include "creeper-qt/utility/theme/theme.hh"
|
||||||
|
#include "creeper-qt/utility/wrapper/common.hh"
|
||||||
|
#include "creeper-qt/utility/wrapper/property.hh"
|
||||||
|
#include "creeper-qt/utility/wrapper/widget.hh"
|
||||||
|
#include "qcustomplot/qcustomplot.h"
|
||||||
|
#include <optional>
|
||||||
|
#include <QPaintEvent>
|
||||||
|
#include <concepts>
|
||||||
|
#include <qsize.h>
|
||||||
|
#include <qpoint.h>
|
||||||
|
#include <qvector.h>
|
||||||
|
|
||||||
|
namespace creeper {
|
||||||
|
class VectorFieldPlot;
|
||||||
|
|
||||||
|
namespace vector_widget::internal {
|
||||||
|
class VectorPlot : public QCustomPlot {
|
||||||
|
public:
|
||||||
|
VectorPlot();
|
||||||
|
~VectorPlot() override;
|
||||||
|
|
||||||
|
void load_theme_manager(ThemeManager& mgr);
|
||||||
|
void set_matrix_size(const QSize& size);
|
||||||
|
void set_data(const QVector<PointData>& data);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void paintEvent(QPaintEvent* event) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void initialize_plot();
|
||||||
|
void reset_plot();
|
||||||
|
void update_vectors();
|
||||||
|
void ensure_arrows();
|
||||||
|
void apply_color_scheme();
|
||||||
|
|
||||||
|
QSize matrix_size_{ 3, 4 };
|
||||||
|
QVector<PointData> data_points_;
|
||||||
|
bool initialized_ = false;
|
||||||
|
std::optional<ColorScheme> scheme_;
|
||||||
|
QColor arrow_color_{ 16, 54, 128 }; // 深蓝色
|
||||||
|
QPointF arrow_dir_{ 0.0, 1.0 };
|
||||||
|
QCPItemLine* primary_arrow_ = nullptr;
|
||||||
|
|
||||||
|
void ensure_primary_arrow();
|
||||||
|
};
|
||||||
|
} // namespace vector_widget::internal
|
||||||
|
|
||||||
|
namespace vector_widget::pro {
|
||||||
|
using Token = common::Token<internal::VectorPlot>;
|
||||||
|
|
||||||
|
struct MatrixSize : Token {
|
||||||
|
QSize size;
|
||||||
|
explicit MatrixSize(const QSize& s) : size{ s } { }
|
||||||
|
explicit MatrixSize(int w, int h) : size{ w, h } { }
|
||||||
|
void apply(auto& self) const { self.set_matrix_size(size); }
|
||||||
|
};
|
||||||
|
|
||||||
|
using PlotData = DerivedProp<Token, QVector<PointData>, [](auto& self, const auto& vec) {
|
||||||
|
self.set_data(vec);
|
||||||
|
}>;
|
||||||
|
|
||||||
|
template<class PlotWidget>
|
||||||
|
concept trait = std::derived_from<PlotWidget, Token>;
|
||||||
|
|
||||||
|
CREEPER_DEFINE_CHECKER(trait);
|
||||||
|
|
||||||
|
using namespace widget::pro;
|
||||||
|
using namespace theme::pro;
|
||||||
|
} // namespace vector_widget::pro
|
||||||
|
|
||||||
|
struct VectorFieldPlot
|
||||||
|
: public Declarative<vector_widget::internal::VectorPlot,
|
||||||
|
CheckerOr<vector_widget::pro::checker, widget::pro::checker, theme::pro::checker>> {
|
||||||
|
using Declarative::Declarative;
|
||||||
|
void paintEvent(QPaintEvent* event) override;
|
||||||
|
};
|
||||||
|
} // namespace creeper
|
||||||
@@ -173,10 +173,6 @@ int cpcodec_close(CPCodecContext *ctx) {
|
|||||||
ctx->codec->close(ctx);
|
ctx->codec->close(ctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ctx->codec && ctx->codec->close) {
|
|
||||||
ctx->codec->close(ctx);
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx->is_open = false;
|
ctx->is_open = false;
|
||||||
ctx->release_priv_storage();
|
ctx->release_priv_storage();
|
||||||
ctx->codec_type = CPMediaType::Unknow;
|
ctx->codec_type = CPMediaType::Unknow;
|
||||||
|
|||||||
@@ -1,14 +1,11 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "components/ffmsep/cpdecoder.hh"
|
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <cstddef>
|
#include <cstddef>
|
||||||
#include <mutex>
|
#include <future>
|
||||||
#include <optional>
|
|
||||||
#include <string>
|
|
||||||
#include <string_view>
|
#include <string_view>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include <initializer_list>
|
#include <initializer_list>
|
||||||
|
#include <nlohmann/json.hpp>
|
||||||
|
|
||||||
namespace ffmsep {
|
namespace ffmsep {
|
||||||
|
|
||||||
@@ -26,7 +23,8 @@ enum class CPMediaType : std::uint8_t {
|
|||||||
|
|
||||||
enum class CPCodecID : std::uint32_t {
|
enum class CPCodecID : std::uint32_t {
|
||||||
Unknow = 0,
|
Unknow = 0,
|
||||||
Tactile = 0x54514354u // 'T','Q','C','T':触觉传感器协议标识 Tactile Quick Codec Type
|
Tactile = 0x54514354u, // 'T','Q','C','T':触觉传感器协议标识 Tactile Quick Codec Type
|
||||||
|
PiezoresistiveB = 0x54514342u // 'T','Q','C','B':压阻B测试协议
|
||||||
};
|
};
|
||||||
|
|
||||||
struct CPPacket {
|
struct CPPacket {
|
||||||
@@ -64,6 +62,7 @@ struct CPCodec {
|
|||||||
using CloseFn = void(*)(CPCodecContext*);
|
using CloseFn = void(*)(CPCodecContext*);
|
||||||
using SendPacketFn = int(*)(CPCodecContext*, const CPPacket&);
|
using SendPacketFn = int(*)(CPCodecContext*, const CPPacket&);
|
||||||
using ReceiveFrameFn = int(*)(CPCodecContext*, CPFrame&);
|
using ReceiveFrameFn = int(*)(CPCodecContext*, CPFrame&);
|
||||||
|
using PresistFrameFn = int(*)(CPCodecContext*);
|
||||||
|
|
||||||
const char* name = nullptr;
|
const char* name = nullptr;
|
||||||
const char* long_name = nullptr;
|
const char* long_name = nullptr;
|
||||||
@@ -74,11 +73,13 @@ struct CPCodec {
|
|||||||
CloseFn close = nullptr;
|
CloseFn close = nullptr;
|
||||||
SendPacketFn send_packet = nullptr;
|
SendPacketFn send_packet = nullptr;
|
||||||
ReceiveFrameFn receive_frame = nullptr;
|
ReceiveFrameFn receive_frame = nullptr;
|
||||||
|
PresistFrameFn presistend = nullptr;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct CPCodecContext {
|
struct CPCodecContext {
|
||||||
const CPCodec* codec = nullptr;
|
const CPCodec* codec = nullptr;
|
||||||
void* priv_data = nullptr;
|
void* priv_data = nullptr;
|
||||||
|
void* record_data = nullptr;
|
||||||
CPMediaType codec_type = CPMediaType::Unknow;
|
CPMediaType codec_type = CPMediaType::Unknow;
|
||||||
bool is_open = false;
|
bool is_open = false;
|
||||||
|
|
||||||
@@ -109,7 +110,6 @@ private:
|
|||||||
friend int cpcodec_open(CPCodecContext*, const CPCodec*);
|
friend int cpcodec_open(CPCodecContext*, const CPCodec*);
|
||||||
friend int cpcodec_close(CPCodecContext*);
|
friend int cpcodec_close(CPCodecContext*);
|
||||||
};
|
};
|
||||||
|
|
||||||
void cpcodec_register(const CPCodec* codec);
|
void cpcodec_register(const CPCodec* codec);
|
||||||
void cpcodec_register_many(std::initializer_list<const CPCodec*> codecs);
|
void cpcodec_register_many(std::initializer_list<const CPCodec*> codecs);
|
||||||
const CPCodec* cpcodec_find_decoder(CPCodecID id);
|
const CPCodec* cpcodec_find_decoder(CPCodecID id);
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
#include "components/ffmsep/cpstream_core.hh"
|
#include "components/ffmsep/cpstream_core.hh"
|
||||||
|
|
||||||
|
#include "components/ffmsep/presist/presist.hh"
|
||||||
|
#include "dlog/dlog.hh"
|
||||||
|
#include <iostream>
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
@@ -7,19 +10,26 @@
|
|||||||
#include <cstddef>
|
#include <cstddef>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <deque>
|
#include <deque>
|
||||||
|
#include <future>
|
||||||
|
#include <ios>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
|
#include <qlogging.h>
|
||||||
#include <thread>
|
#include <thread>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
#include <qdebug.h>
|
||||||
|
#include <iostream>
|
||||||
|
using namespace std::chrono_literals;
|
||||||
|
|
||||||
|
|
||||||
namespace ffmsep {
|
namespace ffmsep {
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
constexpr auto kReaderIdleSleep = std::chrono::milliseconds(5);
|
constexpr auto kReaderIdleSleep = 5ms;
|
||||||
constexpr auto kDecoderIdleSleep = std::chrono::milliseconds(1);
|
constexpr auto kDecoderIdleSleep = 1ms;
|
||||||
|
|
||||||
const CPCodec* resolve_requested_codec(const CPStreamConfig& config) {
|
const CPCodec* resolve_requested_codec(const CPStreamConfig& config) {
|
||||||
if (!config.codec_name.empty()) {
|
if (!config.codec_name.empty()) {
|
||||||
@@ -45,9 +55,9 @@ struct CPStreamCore::Impl {
|
|||||||
bool flush = false;
|
bool flush = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
explicit Impl(CPStreamConfig config)
|
explicit Impl(CPStreamConfig config): config_(std::move(config)) {
|
||||||
: config_(std::move(config)) {
|
|
||||||
normalize_config();
|
normalize_config();
|
||||||
|
frame_writer_ = std::make_unique<persist::JsonWritter>();
|
||||||
}
|
}
|
||||||
|
|
||||||
~Impl() = default;
|
~Impl() = default;
|
||||||
@@ -62,6 +72,9 @@ struct CPStreamCore::Impl {
|
|||||||
if (config_.frame_queue_capacity == 0U) {
|
if (config_.frame_queue_capacity == 0U) {
|
||||||
config_.frame_queue_capacity = 1U;
|
config_.frame_queue_capacity = 1U;
|
||||||
}
|
}
|
||||||
|
if (config_.slave_request_interval.count() < 0) {
|
||||||
|
config_.slave_request_interval = 0ms;
|
||||||
|
}
|
||||||
frame_queue_capacity_ = config_.frame_queue_capacity;
|
frame_queue_capacity_ = config_.frame_queue_capacity;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,26 +119,31 @@ struct CPStreamCore::Impl {
|
|||||||
config_.parity,
|
config_.parity,
|
||||||
config_.stopbits,
|
config_.stopbits,
|
||||||
config_.flowcontrol);
|
config_.flowcontrol);
|
||||||
|
if (!serial->isOpen()) {
|
||||||
serial->open();
|
serial->open();
|
||||||
|
}
|
||||||
serial->flush();
|
serial->flush();
|
||||||
|
|
||||||
{
|
{
|
||||||
std::lock_guard lock(serial_mutex_);
|
std::lock_guard<std::mutex> lock(serial_mutex_);
|
||||||
serial_ = std::move(serial);
|
serial_ = std::move(serial);
|
||||||
}
|
}
|
||||||
} catch (const serial::IOException& ex) {
|
}
|
||||||
|
catch (const serial::IOException& ex) {
|
||||||
set_last_error(ex.what() ? ex.what() : "serial IO exception");
|
set_last_error(ex.what() ? ex.what() : "serial IO exception");
|
||||||
cpcodec_close(codec_ctx_);
|
cpcodec_close(codec_ctx_);
|
||||||
cpcodec_free_context(&codec_ctx_);
|
cpcodec_free_context(&codec_ctx_);
|
||||||
codec_ctx_ = nullptr;
|
codec_ctx_ = nullptr;
|
||||||
return false;
|
return false;
|
||||||
} catch (const serial::SerialException& ex) {
|
}
|
||||||
|
catch (const serial::SerialException& ex) {
|
||||||
set_last_error(ex.what() ? ex.what() : "serial exception");
|
set_last_error(ex.what() ? ex.what() : "serial exception");
|
||||||
cpcodec_close(codec_ctx_);
|
cpcodec_close(codec_ctx_);
|
||||||
cpcodec_free_context(&codec_ctx_);
|
cpcodec_free_context(&codec_ctx_);
|
||||||
codec_ctx_ = nullptr;
|
codec_ctx_ = nullptr;
|
||||||
return false;
|
return false;
|
||||||
} catch (const std::exception& ex) {
|
}
|
||||||
|
catch (const std::exception& ex) {
|
||||||
set_last_error(ex.what());
|
set_last_error(ex.what());
|
||||||
cpcodec_close(codec_ctx_);
|
cpcodec_close(codec_ctx_);
|
||||||
cpcodec_free_context(&codec_ctx_);
|
cpcodec_free_context(&codec_ctx_);
|
||||||
@@ -134,11 +152,11 @@ struct CPStreamCore::Impl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
std::lock_guard lock(packet_mutex_);
|
std::lock_guard<std::mutex> lock(packet_mutex_);
|
||||||
packet_queue_.clear();
|
packet_queue_.clear();
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
std::lock_guard lock(frame_mutex_);
|
std::lock_guard<std::mutex> lock(frame_mutex_);
|
||||||
frame_queue_.clear();
|
frame_queue_.clear();
|
||||||
}
|
}
|
||||||
pts_counter_.store(0, std::memory_order_relaxed);
|
pts_counter_.store(0, std::memory_order_relaxed);
|
||||||
@@ -159,13 +177,14 @@ struct CPStreamCore::Impl {
|
|||||||
stop();
|
stop();
|
||||||
|
|
||||||
{
|
{
|
||||||
std::lock_guard lock(serial_mutex_);
|
std::lock_guard<std::mutex> lock(serial_mutex_);
|
||||||
if (serial_) {
|
if (serial_) {
|
||||||
try {
|
try {
|
||||||
if (serial_->isOpen()) {
|
if (serial_->isOpen()) {
|
||||||
serial_->close();
|
serial_->close();
|
||||||
}
|
}
|
||||||
} catch (...) {
|
}
|
||||||
|
catch (...) {
|
||||||
// Ignore close errors.
|
// Ignore close errors.
|
||||||
}
|
}
|
||||||
serial_.reset();
|
serial_.reset();
|
||||||
@@ -179,12 +198,13 @@ struct CPStreamCore::Impl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
std::lock_guard lock(packet_mutex_);
|
std::lock_guard<std::mutex> lock(packet_mutex_);
|
||||||
packet_queue_.clear();
|
packet_queue_.clear();
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
std::lock_guard lock(frame_mutex_);
|
std::lock_guard<std::mutex> lock(frame_mutex_);
|
||||||
frame_queue_.clear();
|
frame_queue_.clear();
|
||||||
|
frame_record_queue_.clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -195,7 +215,7 @@ struct CPStreamCore::Impl {
|
|||||||
|
|
||||||
std::shared_ptr<serial::Serial> serial_copy;
|
std::shared_ptr<serial::Serial> serial_copy;
|
||||||
{
|
{
|
||||||
std::lock_guard lock(serial_mutex_);
|
std::lock_guard<std::mutex> lock(serial_mutex_);
|
||||||
serial_copy = serial_;
|
serial_copy = serial_;
|
||||||
}
|
}
|
||||||
if (!serial_copy || !serial_copy->isOpen()) {
|
if (!serial_copy || !serial_copy->isOpen()) {
|
||||||
@@ -213,6 +233,9 @@ struct CPStreamCore::Impl {
|
|||||||
|
|
||||||
reader_thread_ = std::thread(&Impl::reader_loop, this);
|
reader_thread_ = std::thread(&Impl::reader_loop, this);
|
||||||
decoder_thread_ = std::thread(&Impl::decoder_loop, this);
|
decoder_thread_ = std::thread(&Impl::decoder_loop, this);
|
||||||
|
if (!config_.slave_request_command.empty()) {
|
||||||
|
slave_thread_ = std::thread(&Impl::slave_loop, this);
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -227,6 +250,9 @@ struct CPStreamCore::Impl {
|
|||||||
if (reader_thread_.joinable()) {
|
if (reader_thread_.joinable()) {
|
||||||
reader_thread_.join();
|
reader_thread_.join();
|
||||||
}
|
}
|
||||||
|
if (slave_thread_.joinable()) {
|
||||||
|
slave_thread_.join();
|
||||||
|
}
|
||||||
|
|
||||||
signal_decoder_flush(true);
|
signal_decoder_flush(true);
|
||||||
packet_cv_.notify_all();
|
packet_cv_.notify_all();
|
||||||
@@ -238,7 +264,7 @@ struct CPStreamCore::Impl {
|
|||||||
stop_requested_.store(false, std::memory_order_release);
|
stop_requested_.store(false, std::memory_order_release);
|
||||||
|
|
||||||
{
|
{
|
||||||
std::lock_guard lock(packet_mutex_);
|
std::lock_guard<std::mutex> lock(packet_mutex_);
|
||||||
packet_queue_.clear();
|
packet_queue_.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -248,7 +274,7 @@ struct CPStreamCore::Impl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool is_open() const {
|
bool is_open() const {
|
||||||
std::lock_guard lock(serial_mutex_);
|
std::lock_guard<std::mutex> lock(serial_mutex_);
|
||||||
return serial_ && serial_->isOpen();
|
return serial_ && serial_->isOpen();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -267,7 +293,7 @@ struct CPStreamCore::Impl {
|
|||||||
|
|
||||||
std::shared_ptr<serial::Serial> serial_copy;
|
std::shared_ptr<serial::Serial> serial_copy;
|
||||||
{
|
{
|
||||||
std::lock_guard lock(serial_mutex_);
|
std::lock_guard<std::mutex> lock(serial_mutex_);
|
||||||
serial_copy = serial_;
|
serial_copy = serial_;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -279,27 +305,30 @@ struct CPStreamCore::Impl {
|
|||||||
try {
|
try {
|
||||||
const auto written = serial_copy->write(data, size);
|
const auto written = serial_copy->write(data, size);
|
||||||
return written == size;
|
return written == size;
|
||||||
} catch (const serial::IOException& ex) {
|
}
|
||||||
|
catch (const serial::IOException& ex) {
|
||||||
set_last_error(ex.what() ? ex.what() : "serial IO exception");
|
set_last_error(ex.what() ? ex.what() : "serial IO exception");
|
||||||
} catch (const serial::SerialException& ex) {
|
}
|
||||||
|
catch (const serial::SerialException& ex) {
|
||||||
set_last_error(ex.what() ? ex.what() : "serial exception");
|
set_last_error(ex.what() ? ex.what() : "serial exception");
|
||||||
} catch (const std::exception& ex) {
|
}
|
||||||
|
catch (const std::exception& ex) {
|
||||||
set_last_error(ex.what());
|
set_last_error(ex.what());
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::optional<DecodedFrame> try_pop_frame() {
|
std::optional<std::shared_ptr<DecodedFrame>> try_pop_frame() {
|
||||||
std::lock_guard lock(frame_mutex_);
|
std::lock_guard<std::mutex> lock(frame_mutex_);
|
||||||
if (frame_queue_.empty()) {
|
if (frame_queue_.empty()) {
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
DecodedFrame frame = std::move(frame_queue_.front());
|
std::shared_ptr<DecodedFrame> frame = std::move(frame_queue_.front());
|
||||||
frame_queue_.pop_front();
|
frame_queue_.pop_front();
|
||||||
return frame;
|
return frame;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool wait_for_frame(DecodedFrame& frame, std::chrono::milliseconds timeout) {
|
bool wait_for_frame(std::shared_ptr<DecodedFrame>& frame, std::chrono::milliseconds timeout) {
|
||||||
std::unique_lock lock(frame_mutex_);
|
std::unique_lock lock(frame_mutex_);
|
||||||
if (!frame_cv_.wait_for(lock, timeout, [&] {
|
if (!frame_cv_.wait_for(lock, timeout, [&] {
|
||||||
return !frame_queue_.empty();
|
return !frame_queue_.empty();
|
||||||
@@ -312,7 +341,7 @@ struct CPStreamCore::Impl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void clear_frames() {
|
void clear_frames() {
|
||||||
std::lock_guard lock(frame_mutex_);
|
std::lock_guard<std::mutex> lock(frame_mutex_);
|
||||||
frame_queue_.clear();
|
frame_queue_.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -321,7 +350,7 @@ struct CPStreamCore::Impl {
|
|||||||
capacity = 1U;
|
capacity = 1U;
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
std::lock_guard lock(frame_mutex_);
|
std::lock_guard<std::mutex> lock(frame_mutex_);
|
||||||
frame_queue_capacity_ = capacity;
|
frame_queue_capacity_ = capacity;
|
||||||
config_.frame_queue_capacity = capacity;
|
config_.frame_queue_capacity = capacity;
|
||||||
while (frame_queue_.size() > frame_queue_capacity_) {
|
while (frame_queue_.size() > frame_queue_capacity_) {
|
||||||
@@ -330,8 +359,45 @@ struct CPStreamCore::Impl {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void clear_recorded_frames() {
|
||||||
|
std::lock_guard<std::mutex> lock(frame_mutex_);
|
||||||
|
frame_record_queue_.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::size_t recorded_frame_count() const {
|
||||||
|
std::lock_guard<std::mutex> lock(frame_mutex_);
|
||||||
|
return frame_record_queue_.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::future<persist::WriteResult> export_recorded_frames(const std::string& path, bool clear_after_export) {
|
||||||
|
if (!frame_writer_) {
|
||||||
|
frame_writer_ = std::make_unique<persist::JsonWritter>();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::deque<std::shared_ptr<DecodedFrame>> snapshot;
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(frame_mutex_);
|
||||||
|
snapshot = frame_record_queue_;
|
||||||
|
if (clear_after_export) {
|
||||||
|
frame_record_queue_.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (snapshot.empty()) {
|
||||||
|
std::promise<persist::WriteResult> promise;
|
||||||
|
auto future = promise.get_future();
|
||||||
|
promise.set_value(persist::WriteResult{
|
||||||
|
false,
|
||||||
|
"no recorded frames available",
|
||||||
|
path });
|
||||||
|
return future;
|
||||||
|
}
|
||||||
|
|
||||||
|
return frame_writer_->enqueue(path, std::move(snapshot));
|
||||||
|
}
|
||||||
|
|
||||||
void set_frame_callback(FrameCallback callback) {
|
void set_frame_callback(FrameCallback callback) {
|
||||||
std::lock_guard lock(callback_mutex_);
|
std::lock_guard<std::mutex> lock(callback_mutex_);
|
||||||
frame_callback_ = std::move(callback);
|
frame_callback_ = std::move(callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -340,7 +406,7 @@ struct CPStreamCore::Impl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
std::string last_error() const {
|
std::string last_error() const {
|
||||||
std::lock_guard lock(last_error_mutex_);
|
std::lock_guard<std::mutex> lock(last_error_mutex_);
|
||||||
return last_error_;
|
return last_error_;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -352,9 +418,10 @@ struct CPStreamCore::Impl {
|
|||||||
std::vector<std::uint8_t> buffer(config_.read_chunk_size);
|
std::vector<std::uint8_t> buffer(config_.read_chunk_size);
|
||||||
|
|
||||||
while (!stop_requested_.load(std::memory_order_acquire)) {
|
while (!stop_requested_.load(std::memory_order_acquire)) {
|
||||||
|
|
||||||
std::shared_ptr<serial::Serial> serial_copy;
|
std::shared_ptr<serial::Serial> serial_copy;
|
||||||
{
|
{
|
||||||
std::lock_guard lock(serial_mutex_);
|
std::lock_guard<std::mutex> lock(serial_mutex_);
|
||||||
serial_copy = serial_;
|
serial_copy = serial_;
|
||||||
}
|
}
|
||||||
if (!serial_copy || !serial_copy->isOpen()) {
|
if (!serial_copy || !serial_copy->isOpen()) {
|
||||||
@@ -365,15 +432,19 @@ struct CPStreamCore::Impl {
|
|||||||
std::size_t bytes_read = 0;
|
std::size_t bytes_read = 0;
|
||||||
try {
|
try {
|
||||||
bytes_read = serial_copy->read(buffer.data(), buffer.size());
|
bytes_read = serial_copy->read(buffer.data(), buffer.size());
|
||||||
} catch (const serial::IOException& ex) {
|
qDebug() << "bytes_read: " << bytes_read;
|
||||||
|
}
|
||||||
|
catch (const serial::IOException& ex) {
|
||||||
set_last_error(ex.what() ? ex.what() : "serial IO exception");
|
set_last_error(ex.what() ? ex.what() : "serial IO exception");
|
||||||
std::this_thread::sleep_for(kReaderIdleSleep);
|
std::this_thread::sleep_for(kReaderIdleSleep);
|
||||||
continue;
|
continue;
|
||||||
} catch (const serial::SerialException& ex) {
|
}
|
||||||
|
catch (const serial::SerialException& ex) {
|
||||||
set_last_error(ex.what() ? ex.what() : "serial exception");
|
set_last_error(ex.what() ? ex.what() : "serial exception");
|
||||||
std::this_thread::sleep_for(kReaderIdleSleep);
|
std::this_thread::sleep_for(kReaderIdleSleep);
|
||||||
continue;
|
continue;
|
||||||
} catch (const std::exception& ex) {
|
}
|
||||||
|
catch (const std::exception& ex) {
|
||||||
set_last_error(ex.what());
|
set_last_error(ex.what());
|
||||||
std::this_thread::sleep_for(kReaderIdleSleep);
|
std::this_thread::sleep_for(kReaderIdleSleep);
|
||||||
continue;
|
continue;
|
||||||
@@ -383,13 +454,30 @@ struct CPStreamCore::Impl {
|
|||||||
std::this_thread::sleep_for(kReaderIdleSleep);
|
std::this_thread::sleep_for(kReaderIdleSleep);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
const auto format_command =
|
||||||
|
[](const std::vector<std::uint8_t>& data) -> std::string {
|
||||||
|
if (data.empty()) {
|
||||||
|
return "[]";
|
||||||
|
}
|
||||||
|
std::ostringstream oss;
|
||||||
|
oss << '[' << std::uppercase << std::setfill('0');
|
||||||
|
for (std::size_t idx = 0; idx < data.size(); ++idx) {
|
||||||
|
if (idx != 0U) {
|
||||||
|
oss << ' ';
|
||||||
|
}
|
||||||
|
oss << std::setw(2) << std::hex << static_cast<unsigned int>(data[idx]);
|
||||||
|
}
|
||||||
|
oss << ']';
|
||||||
|
return oss.str();
|
||||||
|
};
|
||||||
Packet packet;
|
Packet packet;
|
||||||
packet.payload.assign(buffer.begin(), buffer.begin() + static_cast<std::ptrdiff_t>(bytes_read));
|
packet.payload.assign(buffer.begin(), buffer.begin() + static_cast<std::ptrdiff_t>(bytes_read));
|
||||||
|
// std::cout << "======payload======" << std::endl;
|
||||||
|
// std::cout << format_command(packet.payload) << std::endl;
|
||||||
packet.pts = pts_counter_.fetch_add(1, std::memory_order_relaxed);
|
packet.pts = pts_counter_.fetch_add(1, std::memory_order_relaxed);
|
||||||
|
|
||||||
{
|
{
|
||||||
std::lock_guard lock(packet_mutex_);
|
std::lock_guard<std::mutex> lock(packet_mutex_);
|
||||||
if (packet_queue_.size() >= config_.packet_queue_capacity) {
|
if (packet_queue_.size() >= config_.packet_queue_capacity) {
|
||||||
packet_queue_.pop_front();
|
packet_queue_.pop_front();
|
||||||
}
|
}
|
||||||
@@ -399,6 +487,33 @@ struct CPStreamCore::Impl {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void slave_loop() {
|
||||||
|
const auto command = config_.slave_request_command;
|
||||||
|
auto interval = config_.slave_request_interval;
|
||||||
|
if (interval.count() < 0) {
|
||||||
|
interval = 0ms;
|
||||||
|
}
|
||||||
|
const bool repeat = interval.count() > 0;
|
||||||
|
|
||||||
|
while (!stop_requested_.load(std::memory_order_acquire)) {
|
||||||
|
const bool success = send(command);
|
||||||
|
if (!success) {
|
||||||
|
std::this_thread::sleep_for(kReaderIdleSleep);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!repeat) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto remaining = interval;
|
||||||
|
while (remaining.count() > 0 && !stop_requested_.load(std::memory_order_acquire)) {
|
||||||
|
const auto step = std::min(remaining, kReaderIdleSleep);
|
||||||
|
std::this_thread::sleep_for(step);
|
||||||
|
remaining -= step;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void decoder_loop() {
|
void decoder_loop() {
|
||||||
while (true) {
|
while (true) {
|
||||||
Packet packet;
|
Packet packet;
|
||||||
@@ -446,14 +561,28 @@ struct CPStreamCore::Impl {
|
|||||||
CPFrame frame;
|
CPFrame frame;
|
||||||
rc = cpcodec_receive_frame(codec_ctx_, &frame);
|
rc = cpcodec_receive_frame(codec_ctx_, &frame);
|
||||||
if (rc == CP_SUCCESS) {
|
if (rc == CP_SUCCESS) {
|
||||||
DecodedFrame decoded;
|
auto decoded = std::make_shared<DecodedFrame>();
|
||||||
decoded.pts = frame.pts;
|
decoded->pts = frame.pts;
|
||||||
decoded.received_at = std::chrono::steady_clock::now();
|
decoded->received_at = std::chrono::steady_clock::now();
|
||||||
decoded.frame = std::move(frame);
|
decoded->frame = std::move(frame);
|
||||||
|
decoded->id = codec_descriptor_ ? codec_descriptor_->id : CPCodecID::Unknow;
|
||||||
|
if (decoded->id == CPCodecID::Tactile) {
|
||||||
|
if (auto parsed = tactile::parse_frame(decoded->frame)) {
|
||||||
|
decoded->tactile = parsed;
|
||||||
|
decoded->tactile_pressures = tactile::parse_pressure_values(*parsed);
|
||||||
|
if (auto matrix = tactile::parse_matrix_size_payload(*parsed)) {
|
||||||
|
decoded->tactile_matrix_size = matrix;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (decoded->id == CPCodecID::PiezoresistiveB) {
|
||||||
|
decoded->tactile_pressures =
|
||||||
|
tactile::parse_piezoresistive_b_pressures(decoded->frame);
|
||||||
|
}
|
||||||
|
|
||||||
FrameCallback callback_copy;
|
FrameCallback callback_copy;
|
||||||
{
|
{
|
||||||
std::lock_guard lock(callback_mutex_);
|
std::lock_guard<std::mutex> lock(callback_mutex_);
|
||||||
callback_copy = frame_callback_;
|
callback_copy = frame_callback_;
|
||||||
}
|
}
|
||||||
if (callback_copy) {
|
if (callback_copy) {
|
||||||
@@ -461,16 +590,21 @@ struct CPStreamCore::Impl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
std::lock_guard lock(frame_mutex_);
|
std::lock_guard<std::mutex> lock(frame_mutex_);
|
||||||
if (frame_queue_.size() >= frame_queue_capacity_) {
|
if (frame_queue_.size() >= frame_queue_capacity_) {
|
||||||
frame_queue_.pop_front();
|
frame_queue_.pop_front();
|
||||||
}
|
}
|
||||||
frame_queue_.push_back(std::move(decoded));
|
frame_queue_.push_back(decoded);
|
||||||
|
if (decoded->id == CPCodecID::Tactile || decoded->id == CPCodecID::PiezoresistiveB) {
|
||||||
|
frame_record_queue_.push_back(decoded);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
frame_cv_.notify_one();
|
frame_cv_.notify_one();
|
||||||
} else if (rc == CP_ERROR_EAGAIN) {
|
}
|
||||||
|
else if (rc == CP_ERROR_EAGAIN) {
|
||||||
break;
|
break;
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
if (rc == CP_ERROR_EOF && packet.end_of_stream) {
|
if (rc == CP_ERROR_EOF && packet.end_of_stream) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -489,7 +623,7 @@ struct CPStreamCore::Impl {
|
|||||||
packet.flush = true;
|
packet.flush = true;
|
||||||
packet.end_of_stream = end_of_stream;
|
packet.end_of_stream = end_of_stream;
|
||||||
{
|
{
|
||||||
std::lock_guard lock(packet_mutex_);
|
std::lock_guard<std::mutex> lock(packet_mutex_);
|
||||||
packet_queue_.push_back(std::move(packet));
|
packet_queue_.push_back(std::move(packet));
|
||||||
}
|
}
|
||||||
packet_cv_.notify_one();
|
packet_cv_.notify_one();
|
||||||
@@ -507,7 +641,7 @@ struct CPStreamCore::Impl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void set_last_error(std::string message) {
|
void set_last_error(std::string message) {
|
||||||
std::lock_guard lock(last_error_mutex_);
|
std::lock_guard<std::mutex> lock(last_error_mutex_);
|
||||||
last_error_ = std::move(message);
|
last_error_ = std::move(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -520,15 +654,19 @@ struct CPStreamCore::Impl {
|
|||||||
CPCodecContext* codec_ctx_ = nullptr;
|
CPCodecContext* codec_ctx_ = nullptr;
|
||||||
|
|
||||||
std::thread reader_thread_;
|
std::thread reader_thread_;
|
||||||
|
std::thread slave_thread_;
|
||||||
std::thread decoder_thread_;
|
std::thread decoder_thread_;
|
||||||
|
|
||||||
std::mutex packet_mutex_;
|
std::mutex packet_mutex_;
|
||||||
std::condition_variable packet_cv_;
|
std::condition_variable packet_cv_;
|
||||||
std::deque<Packet> packet_queue_;
|
std::deque<Packet> packet_queue_;
|
||||||
|
|
||||||
std::mutex frame_mutex_;
|
mutable std::mutex frame_mutex_;
|
||||||
std::condition_variable frame_cv_;
|
std::condition_variable frame_cv_;
|
||||||
std::deque<DecodedFrame> frame_queue_;
|
// std::deque<DecodedFrame> frame_queue_;
|
||||||
|
// 更新为智能指针,我们需要更长的生命周期😊
|
||||||
|
std::deque<std::shared_ptr<DecodedFrame>> frame_queue_;
|
||||||
|
std::deque<std::shared_ptr<DecodedFrame>> frame_record_queue_;
|
||||||
std::size_t frame_queue_capacity_ = 16;
|
std::size_t frame_queue_capacity_ = 16;
|
||||||
|
|
||||||
FrameCallback frame_callback_;
|
FrameCallback frame_callback_;
|
||||||
@@ -540,10 +678,11 @@ struct CPStreamCore::Impl {
|
|||||||
|
|
||||||
std::string last_error_;
|
std::string last_error_;
|
||||||
mutable std::mutex last_error_mutex_;
|
mutable std::mutex last_error_mutex_;
|
||||||
|
|
||||||
|
std::unique_ptr<persist::JsonWritter> frame_writer_;
|
||||||
};
|
};
|
||||||
|
|
||||||
CPStreamCore::CPStreamCore(CPStreamConfig config)
|
CPStreamCore::CPStreamCore(CPStreamConfig config): impl_(std::make_unique<Impl>(std::move(config))) {}
|
||||||
: impl_(std::make_unique<Impl>(std::move(config))) {}
|
|
||||||
|
|
||||||
CPStreamCore::~CPStreamCore() {
|
CPStreamCore::~CPStreamCore() {
|
||||||
if (impl_) {
|
if (impl_) {
|
||||||
@@ -592,11 +731,11 @@ bool CPStreamCore::send(const std::uint8_t* data, std::size_t size) {
|
|||||||
return impl_->send(data, size);
|
return impl_->send(data, size);
|
||||||
}
|
}
|
||||||
|
|
||||||
std::optional<DecodedFrame> CPStreamCore::try_pop_frame() {
|
std::optional<std::shared_ptr<DecodedFrame>> CPStreamCore::try_pop_frame() {
|
||||||
return impl_->try_pop_frame();
|
return impl_->try_pop_frame();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool CPStreamCore::wait_for_frame(DecodedFrame& frame, std::chrono::milliseconds timeout) {
|
bool CPStreamCore::wait_for_frame(std::shared_ptr<DecodedFrame>& frame, std::chrono::milliseconds timeout) {
|
||||||
return impl_->wait_for_frame(frame, timeout);
|
return impl_->wait_for_frame(frame, timeout);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -608,6 +747,18 @@ void CPStreamCore::set_frame_queue_capacity(std::size_t capacity) {
|
|||||||
impl_->set_frame_queue_capacity(capacity);
|
impl_->set_frame_queue_capacity(capacity);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void CPStreamCore::clear_recorded_frames() {
|
||||||
|
impl_->clear_recorded_frames();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::size_t CPStreamCore::recorded_frame_count() const {
|
||||||
|
return impl_->recorded_frame_count();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::future<persist::WriteResult> CPStreamCore::export_recorded_frames(const std::string& path, bool clear_after_export) {
|
||||||
|
return impl_->export_recorded_frames(path, clear_after_export);
|
||||||
|
}
|
||||||
|
|
||||||
void CPStreamCore::set_frame_callback(FrameCallback callback) {
|
void CPStreamCore::set_frame_callback(FrameCallback callback) {
|
||||||
impl_->set_frame_callback(std::move(callback));
|
impl_->set_frame_callback(std::move(callback));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +1,32 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "components/ffmsep/cpdecoder.hh"
|
#include "components/ffmsep/cpdecoder.hh"
|
||||||
|
#include "components/ffmsep/tactile/tacdec.hh"
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <functional>
|
#include <functional>
|
||||||
|
#include <future>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
#include <serial/serial.h>
|
#include <serial/serial.h>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
using namespace std::chrono_literals;
|
||||||
|
|
||||||
namespace ffmsep {
|
namespace ffmsep {
|
||||||
|
|
||||||
|
namespace persist {
|
||||||
|
struct WriteResult;
|
||||||
|
} // namespace persist
|
||||||
|
|
||||||
struct DecodedFrame {
|
struct DecodedFrame {
|
||||||
CPFrame frame;
|
CPFrame frame;
|
||||||
|
CPCodecID id = CPCodecID::Unknow;
|
||||||
std::chrono::steady_clock::time_point received_at{};
|
std::chrono::steady_clock::time_point received_at{};
|
||||||
std::int64_t pts = 0;
|
std::int64_t pts = 0;
|
||||||
|
std::optional<tactile::TactileFrame> tactile;
|
||||||
|
std::vector<std::uint16_t> tactile_pressures;
|
||||||
|
std::optional<tactile::MatrixSize> tactile_matrix_size;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct CPStreamConfig {
|
struct CPStreamConfig {
|
||||||
@@ -31,11 +42,13 @@ struct CPStreamConfig {
|
|||||||
std::size_t frame_queue_capacity = 16;
|
std::size_t frame_queue_capacity = 16;
|
||||||
CPCodecID codec_id = CPCodecID::Unknow;
|
CPCodecID codec_id = CPCodecID::Unknow;
|
||||||
std::string codec_name;
|
std::string codec_name;
|
||||||
|
std::vector<std::uint8_t> slave_request_command{};
|
||||||
|
std::chrono::milliseconds slave_request_interval{200ms};
|
||||||
};
|
};
|
||||||
|
|
||||||
class CPStreamCore {
|
class CPStreamCore {
|
||||||
public:
|
public:
|
||||||
using FrameCallback = std::function<void(const DecodedFrame&)>;
|
using FrameCallback = std::function<void(std::shared_ptr<DecodedFrame>)>;
|
||||||
|
|
||||||
explicit CPStreamCore(CPStreamConfig config = {});
|
explicit CPStreamCore(CPStreamConfig config = {});
|
||||||
~CPStreamCore();
|
~CPStreamCore();
|
||||||
@@ -57,10 +70,14 @@ public:
|
|||||||
bool send(const std::vector<std::uint8_t>& data);
|
bool send(const std::vector<std::uint8_t>& data);
|
||||||
bool send(const std::uint8_t* data, std::size_t size);
|
bool send(const std::uint8_t* data, std::size_t size);
|
||||||
|
|
||||||
std::optional<DecodedFrame> try_pop_frame();
|
std::optional<std::shared_ptr<DecodedFrame>> try_pop_frame();
|
||||||
bool wait_for_frame(DecodedFrame& frame, std::chrono::milliseconds timeout);
|
bool wait_for_frame(std::shared_ptr<DecodedFrame>& frame, std::chrono::milliseconds timeout);
|
||||||
void clear_frames();
|
void clear_frames();
|
||||||
void set_frame_queue_capacity(std::size_t capacity);
|
void set_frame_queue_capacity(std::size_t capacity);
|
||||||
|
void clear_recorded_frames();
|
||||||
|
[[nodiscard]] std::size_t recorded_frame_count() const;
|
||||||
|
std::future<persist::WriteResult> export_recorded_frames(const std::string& path,
|
||||||
|
bool clear_after_export = false);
|
||||||
|
|
||||||
void set_frame_callback(FrameCallback callback);
|
void set_frame_callback(FrameCallback callback);
|
||||||
|
|
||||||
@@ -69,6 +86,8 @@ public:
|
|||||||
|
|
||||||
static std::vector<serial::PortInfo> list_available_ports();
|
static std::vector<serial::PortInfo> list_available_ports();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
struct Impl;
|
struct Impl;
|
||||||
std::unique_ptr<Impl> impl_;
|
std::unique_ptr<Impl> impl_;
|
||||||
|
|||||||
314
components/ffmsep/presist/presist.cc
Normal file
314
components/ffmsep/presist/presist.cc
Normal file
@@ -0,0 +1,314 @@
|
|||||||
|
//
|
||||||
|
// Created by Lenn on 2025/10/31.
|
||||||
|
//
|
||||||
|
|
||||||
|
#include "components/ffmsep/presist/presist.hh"
|
||||||
|
|
||||||
|
#include "components/ffmsep/cpstream_core.hh"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <chrono>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <fstream>
|
||||||
|
#include <iomanip>
|
||||||
|
#include <nlohmann/json_fwd.hpp>
|
||||||
|
#include <sstream>
|
||||||
|
#include <system_error>
|
||||||
|
#include <nlohmann/json.hpp>
|
||||||
|
|
||||||
|
namespace ffmsep::persist {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
using nlohmann::json;
|
||||||
|
|
||||||
|
// 旧的 JSON 导出实现保留在此,避免直接删除,便于回退。
|
||||||
|
// bool is_simple_array(const json& value) { ... }
|
||||||
|
// void dump_compact_json(...)
|
||||||
|
// json serialize_tactile_frame(const DecodedFrame& frame) { ... }
|
||||||
|
|
||||||
|
std::string payload_to_csv_row(const std::string& timestamp,
|
||||||
|
const std::vector<std::uint8_t>& payload) {
|
||||||
|
// First column: receive local time (YYYYMMDDHHMMSS). Then payload every 2 bytes -> uint16.
|
||||||
|
std::ostringstream oss;
|
||||||
|
oss << timestamp;
|
||||||
|
bool first = false; // timestamp already placed
|
||||||
|
for (std::size_t idx = 0; idx + 1U < payload.size(); idx += 2U) {
|
||||||
|
const auto value =
|
||||||
|
static_cast<std::uint16_t>(payload[idx]) | static_cast<std::uint16_t>(payload[idx + 1U] << 8U);
|
||||||
|
if (!first) {
|
||||||
|
oss << ',';
|
||||||
|
}
|
||||||
|
first = false;
|
||||||
|
oss << value;
|
||||||
|
}
|
||||||
|
return oss.str();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
using nlohmann::json;
|
||||||
|
|
||||||
|
std::string format_receive_time(const std::chrono::steady_clock::time_point& received) {
|
||||||
|
// Map steady_clock timestamp to system_clock using the current offset, then format as YYYYMMDDHHMMSS.
|
||||||
|
const auto now_sys = std::chrono::system_clock::now();
|
||||||
|
const auto now_steady = std::chrono::steady_clock::now();
|
||||||
|
const auto steady_delta = received - now_steady;
|
||||||
|
const auto sys_tp = now_sys + steady_delta;
|
||||||
|
const auto sys_ms_tp = std::chrono::time_point_cast<std::chrono::milliseconds>(sys_tp);
|
||||||
|
const auto ms_part = std::chrono::duration_cast<std::chrono::milliseconds>(sys_ms_tp.time_since_epoch()) % 1000;
|
||||||
|
const auto sys_sec_tp = std::chrono::time_point_cast<std::chrono::seconds>(sys_ms_tp);
|
||||||
|
const std::time_t tt = std::chrono::system_clock::to_time_t(sys_sec_tp);
|
||||||
|
std::tm tm{};
|
||||||
|
#if defined(_WIN32)
|
||||||
|
localtime_s(&tm, &tt);
|
||||||
|
#else
|
||||||
|
localtime_r(&tt, &tm);
|
||||||
|
#endif
|
||||||
|
std::ostringstream oss;
|
||||||
|
oss << std::put_time(&tm, "%Y%m%d%H%M%S")
|
||||||
|
<< std::setw(3) << std::setfill('0') << ms_part.count();
|
||||||
|
return oss.str();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool is_simple_array(const json& value) {
|
||||||
|
if (!value.is_array()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return std::all_of(value.begin(), value.end(), [](const json& item) {
|
||||||
|
return item.is_primitive();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void dump_compact_json(std::ostream& out,
|
||||||
|
const json& value,
|
||||||
|
int indent = 0,
|
||||||
|
int indent_step = 2) {
|
||||||
|
const auto indent_str = std::string(static_cast<std::size_t>(indent), ' ');
|
||||||
|
const auto child_indent = indent + indent_step;
|
||||||
|
const auto child_indent_str = std::string(static_cast<std::size_t>(child_indent), ' ');
|
||||||
|
|
||||||
|
if (value.is_object()) {
|
||||||
|
out << "{\n";
|
||||||
|
bool first = true;
|
||||||
|
for (auto it = value.begin(); it != value.end(); ++it) {
|
||||||
|
if (!first) {
|
||||||
|
out << ",\n";
|
||||||
|
}
|
||||||
|
first = false;
|
||||||
|
out << child_indent_str << json(it.key()).dump() << ": ";
|
||||||
|
dump_compact_json(out, it.value(), child_indent, indent_step);
|
||||||
|
}
|
||||||
|
out << '\n'
|
||||||
|
<< indent_str << '}';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value.is_array()) {
|
||||||
|
if (value.empty()) {
|
||||||
|
out << "[]";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_simple_array(value)) {
|
||||||
|
out << '[';
|
||||||
|
for (std::size_t idx = 0; idx < value.size(); ++idx) {
|
||||||
|
if (idx != 0U) {
|
||||||
|
out << ", ";
|
||||||
|
}
|
||||||
|
out << value[static_cast<json::size_type>(idx)].dump();
|
||||||
|
}
|
||||||
|
out << ']';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
out << "[\n";
|
||||||
|
bool first = true;
|
||||||
|
for (const auto& item: value) {
|
||||||
|
if (!first) {
|
||||||
|
out << ",\n";
|
||||||
|
}
|
||||||
|
first = false;
|
||||||
|
out << child_indent_str;
|
||||||
|
dump_compact_json(out, item, child_indent, indent_step);
|
||||||
|
}
|
||||||
|
out << '\n'
|
||||||
|
<< indent_str << ']';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
out << value.dump();
|
||||||
|
}
|
||||||
|
|
||||||
|
json serialize_tactile_frame(const DecodedFrame& frame) {
|
||||||
|
json result = {
|
||||||
|
{ "pts", frame.pts },
|
||||||
|
{ "raw_frame", frame.frame.data },
|
||||||
|
{ "pressures", frame.tactile_pressures },
|
||||||
|
};
|
||||||
|
|
||||||
|
const auto received = frame.received_at.time_since_epoch();
|
||||||
|
result["received_at_ns"] =
|
||||||
|
std::chrono::duration_cast<std::chrono::nanoseconds>(received).count();
|
||||||
|
|
||||||
|
if (frame.tactile_matrix_size) {
|
||||||
|
result["matrix"] = {
|
||||||
|
{ "long_edge", frame.tactile_matrix_size->long_edge },
|
||||||
|
{ "short_edge", frame.tactile_matrix_size->short_edge },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (frame.tactile) {
|
||||||
|
const auto& tactile = *frame.tactile;
|
||||||
|
result["tactile"] = {
|
||||||
|
{ "device_address", tactile.device_address },
|
||||||
|
{ "response_function", tactile.response_function },
|
||||||
|
{ "function", static_cast<std::uint8_t>(tactile.function) },
|
||||||
|
{ "start_address", tactile.start_address },
|
||||||
|
{ "return_byte_count", tactile.return_byte_count },
|
||||||
|
{ "status", tactile.status },
|
||||||
|
{ "payload", tactile.payload },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
bool WriteQueue::push(WriteRequest&& req) {
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mutex_);
|
||||||
|
if (stopped_) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
queue_.push(std::move(req));
|
||||||
|
}
|
||||||
|
cond_.notify_one();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool WriteQueue::pop(WriteRequest& out) {
|
||||||
|
std::unique_lock lock(mutex_);
|
||||||
|
cond_.wait(lock, [&] {
|
||||||
|
return stopped_ || !queue_.empty();
|
||||||
|
});
|
||||||
|
|
||||||
|
if (queue_.empty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
out = std::move(queue_.front());
|
||||||
|
queue_.pop();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void WriteQueue::stop() {
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mutex_);
|
||||||
|
stopped_ = true;
|
||||||
|
}
|
||||||
|
cond_.notify_all();
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonWritter::JsonWritter(): write_thread_([this] { run(); }) {}
|
||||||
|
|
||||||
|
JsonWritter::~JsonWritter() {
|
||||||
|
stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::future<WriteResult> JsonWritter::enqueue(std::string path,
|
||||||
|
std::deque<std::shared_ptr<DecodedFrame>> frames) {
|
||||||
|
std::promise<WriteResult> promise;
|
||||||
|
auto future = promise.get_future();
|
||||||
|
|
||||||
|
WriteRequest request{ std::move(path), std::move(frames), std::move(promise) };
|
||||||
|
if (!write_queue_.push(std::move(request))) {
|
||||||
|
WriteResult result{ false, "writer has been stopped", request.path };
|
||||||
|
request.promise.set_value(std::move(result));
|
||||||
|
}
|
||||||
|
|
||||||
|
return future;
|
||||||
|
}
|
||||||
|
|
||||||
|
void JsonWritter::run() {
|
||||||
|
WriteRequest request;
|
||||||
|
while (write_queue_.pop(request)) {
|
||||||
|
try {
|
||||||
|
auto result = write_once(request.path, std::move(request.frames));
|
||||||
|
request.promise.set_value(std::move(result));
|
||||||
|
}
|
||||||
|
catch (const std::exception& ex) {
|
||||||
|
request.promise.set_value(WriteResult{ false, ex.what(), request.path });
|
||||||
|
}
|
||||||
|
catch (...) {
|
||||||
|
request.promise.set_value(WriteResult{ false, "unknown error", request.path });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
WriteResult JsonWritter::write_once(const std::string& path,
|
||||||
|
std::deque<std::shared_ptr<DecodedFrame>> frames) {
|
||||||
|
if (path.empty()) {
|
||||||
|
return { false, "export path is empty", path };
|
||||||
|
}
|
||||||
|
|
||||||
|
std::filesystem::path fs_path(path);
|
||||||
|
if (fs_path.has_parent_path()) {
|
||||||
|
std::error_code ec;
|
||||||
|
std::filesystem::create_directories(fs_path.parent_path(), ec);
|
||||||
|
if (ec) {
|
||||||
|
return { false, "failed to create export directory: " + ec.message(), path };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::ofstream stream(path, std::ios::binary | std::ios::trunc);
|
||||||
|
if (!stream.is_open()) {
|
||||||
|
return { false, "failed to open export file", path };
|
||||||
|
}
|
||||||
|
|
||||||
|
bool wrote_any = false;
|
||||||
|
for (const auto& frame: frames) {
|
||||||
|
if (!frame) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::uint8_t> payload;
|
||||||
|
if (frame->id == CPCodecID::Tactile && frame->tactile) {
|
||||||
|
payload = frame->tactile->payload;
|
||||||
|
}
|
||||||
|
else if (frame->id == CPCodecID::PiezoresistiveB) {
|
||||||
|
payload = tactile::extract_piezoresistive_b_payload(frame->frame);
|
||||||
|
}
|
||||||
|
if (payload.empty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto timestamp = format_receive_time(frame->received_at);
|
||||||
|
const auto row = payload_to_csv_row(timestamp, payload);
|
||||||
|
stream << row << '\n';
|
||||||
|
wrote_any = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
stream.flush();
|
||||||
|
if (!stream.good()) {
|
||||||
|
return { false, "failed to write export file", path };
|
||||||
|
}
|
||||||
|
if (!wrote_any) {
|
||||||
|
return { false, "no tactile frames available for export", path };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { true, {}, path };
|
||||||
|
}
|
||||||
|
|
||||||
|
void JsonWritter::stop() {
|
||||||
|
if (!stopped_.exchange(true)) {
|
||||||
|
write_queue_.stop();
|
||||||
|
if (write_thread_.joinable()) {
|
||||||
|
write_thread_.join();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace ffmsep::persist
|
||||||
86
components/ffmsep/presist/presist.hh
Normal file
86
components/ffmsep/presist/presist.hh
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
//
|
||||||
|
// Created by Lenn on 2025/10/31.
|
||||||
|
//
|
||||||
|
|
||||||
|
#ifndef TOUCHSENSOR_PRESIST_HH
|
||||||
|
#define TOUCHSENSOR_PRESIST_HH
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
|
#include <condition_variable>
|
||||||
|
#include <deque>
|
||||||
|
#include <future>
|
||||||
|
#include <memory>
|
||||||
|
#include <mutex>
|
||||||
|
#include <queue>
|
||||||
|
#include <string>
|
||||||
|
#include <thread>
|
||||||
|
|
||||||
|
namespace ffmsep {
|
||||||
|
|
||||||
|
struct DecodedFrame;
|
||||||
|
|
||||||
|
namespace persist {
|
||||||
|
|
||||||
|
struct WriteResult {
|
||||||
|
bool ok = false;
|
||||||
|
std::string error;
|
||||||
|
std::string path;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct WriteRequest {
|
||||||
|
WriteRequest() = default;
|
||||||
|
WriteRequest(std::string p,
|
||||||
|
std::deque<std::shared_ptr<DecodedFrame>> f,
|
||||||
|
std::promise<WriteResult>&& pr)
|
||||||
|
: path(std::move(p))
|
||||||
|
, frames(std::move(f))
|
||||||
|
, promise(std::move(pr)) {}
|
||||||
|
|
||||||
|
WriteRequest(const WriteRequest&) = delete;
|
||||||
|
WriteRequest& operator=(const WriteRequest&) = delete;
|
||||||
|
WriteRequest(WriteRequest&&) noexcept = default;
|
||||||
|
WriteRequest& operator=(WriteRequest&&) noexcept = default;
|
||||||
|
|
||||||
|
std::string path;
|
||||||
|
std::deque<std::shared_ptr<DecodedFrame>> frames;
|
||||||
|
std::promise<WriteResult> promise;
|
||||||
|
};
|
||||||
|
|
||||||
|
class WriteQueue {
|
||||||
|
public:
|
||||||
|
bool push(WriteRequest&& req);
|
||||||
|
bool pop(WriteRequest& out);
|
||||||
|
void stop();
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::mutex mutex_;
|
||||||
|
std::condition_variable cond_;
|
||||||
|
std::queue<WriteRequest> queue_;
|
||||||
|
bool stopped_ = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
class JsonWritter {
|
||||||
|
public:
|
||||||
|
JsonWritter();
|
||||||
|
~JsonWritter();
|
||||||
|
|
||||||
|
std::future<WriteResult> enqueue(std::string path,
|
||||||
|
std::deque<std::shared_ptr<DecodedFrame>> frames);
|
||||||
|
|
||||||
|
void stop();
|
||||||
|
|
||||||
|
private:
|
||||||
|
void run();
|
||||||
|
WriteResult write_once(const std::string& path,
|
||||||
|
std::deque<std::shared_ptr<DecodedFrame>> frames);
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::thread write_thread_;
|
||||||
|
WriteQueue write_queue_;
|
||||||
|
std::atomic_bool stopped_{false};
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace persist
|
||||||
|
} // namespace ffmsep
|
||||||
|
|
||||||
|
#endif // TOUCHSENSOR_PRESIST_HH
|
||||||
@@ -1,60 +1,140 @@
|
|||||||
#include "tacdec.hh"
|
#include "tacdec.hh"
|
||||||
#include "components/ffmsep/cpdecoder.hh"
|
#include "components/ffmsep/cpdecoder.hh"
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
#include <array>
|
||||||
|
#include <atomic>
|
||||||
#include <cstddef>
|
#include <cstddef>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <new>
|
#include <new>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
|
#include <qlogging.h>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
#include <qdebug.h>
|
||||||
|
#include <iostream>
|
||||||
|
|
||||||
namespace ffmsep::tactile {
|
namespace ffmsep::tactile {
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
constexpr std::size_t kMinimumFrameSize = 1
|
constexpr std::size_t kHeaderSize = 4U; // start bytes + length field
|
||||||
+ 1
|
constexpr std::size_t kFixedSectionSize = 1U + 1U + 1U + 4U + 2U + 1U; // address..status
|
||||||
+ 1
|
constexpr std::size_t kMinimumFrameSize = kHeaderSize + kFixedSectionSize + 1U; // + CRC byte
|
||||||
+ 1
|
constexpr std::uint8_t kCrcPolynomial = 0x07U;
|
||||||
+ 0
|
constexpr std::uint8_t kCrcInitial = 0x00U;
|
||||||
+ 2
|
constexpr std::uint8_t kCrcXorOut = 0xA9U;
|
||||||
+ 2;
|
constexpr std::array<std::uint8_t, 2> kStartSequence{
|
||||||
|
kStartByteFirst,
|
||||||
constexpr std::uint16_t kCrcInitial = 0xFFFF;
|
kStartByteSecond
|
||||||
constexpr std::uint16_t kCrcPolynomial = 0xA001;
|
};
|
||||||
|
constexpr std::size_t kAbsoluteMaxPayloadBytes = 4096U; // 硬上限,防止异常配置撑爆内存
|
||||||
|
constexpr std::array<std::uint8_t, 2> kPiezoresistiveBStartSequence{
|
||||||
|
kPiezoresistiveBStartByteFirst,
|
||||||
|
kPiezoresistiveBStartByteSecond
|
||||||
|
};
|
||||||
|
constexpr std::array<std::uint8_t, 2> kPiezoresistiveBEndSequence{
|
||||||
|
kPiezoresistiveBEndByteFirst,
|
||||||
|
kPiezoresistiveBEndByteSecond
|
||||||
|
};
|
||||||
|
constexpr std::size_t kPiezoresistiveBPayloadSize =
|
||||||
|
kPiezoresistiveBValueCount * 2U;
|
||||||
|
constexpr std::size_t kPiezoresistiveBFrameSize =
|
||||||
|
kPiezoresistiveBStartSequence.size() + kPiezoresistiveBPayloadSize + kPiezoresistiveBEndSequence.size();
|
||||||
|
|
||||||
struct TactileDecoderContext {
|
struct TactileDecoderContext {
|
||||||
std::vector<std::uint8_t> fifo;
|
std::vector<std::uint8_t> fifo;
|
||||||
bool end_of_stream = false;
|
bool end_of_stream = false;
|
||||||
std::int64_t next_pts = 0;
|
std::int64_t next_pts = 0;
|
||||||
};
|
CPCodecID codec_id = CPCodecID::Unknow;
|
||||||
|
std::size_t max_payload_bytes = kPiezoresistiveBPayloadSize;
|
||||||
|
std::size_t max_frame_bytes = kHeaderSize + kFixedSectionSize + kPiezoresistiveBPayloadSize + 1U;
|
||||||
|
std::size_t max_fifo_bytes = (kHeaderSize + kFixedSectionSize + kPiezoresistiveBPayloadSize + 1U) * 2U;
|
||||||
|
|
||||||
std::size_t frame_length_from_payload(std::uint8_t payload_length) {
|
void update_limits(std::size_t payload_bytes) {
|
||||||
return 1U + 1U + 1U + 1U + payload_length + 2U + 2U;
|
const auto clamped_payload = std::min<std::size_t>(
|
||||||
|
std::max<std::size_t>(payload_bytes, 2U),
|
||||||
|
kAbsoluteMaxPayloadBytes);
|
||||||
|
max_payload_bytes = clamped_payload;
|
||||||
|
max_frame_bytes = kHeaderSize + kFixedSectionSize + max_payload_bytes + 1U;
|
||||||
|
max_fifo_bytes = max_frame_bytes * 2U;
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const std::uint8_t* buffer_data(const std::vector<std::uint8_t>& buf) {
|
const std::uint8_t* buffer_data(const std::vector<std::uint8_t>& buf) {
|
||||||
return buf.empty() ? nullptr : buf.data();
|
return buf.empty() ? nullptr : buf.data();
|
||||||
}
|
}
|
||||||
|
|
||||||
std::uint16_t crc16_modbus(const std::uint8_t* data, std::size_t length) {
|
std::uint8_t crc8_with_xorout(const std::uint8_t* data, std::size_t length) {
|
||||||
std::uint16_t crc = kCrcInitial;
|
#if 0
|
||||||
|
std::uint8_t reg = kCrcInitial;
|
||||||
for (std::size_t i = 0; i < length; ++i) {
|
for (std::size_t i = 0; i < length; ++i) {
|
||||||
crc ^= static_cast<std::uint16_t>(data[i]);
|
reg ^= data[i];
|
||||||
for (int bit = 0; bit < 8; ++bit) {
|
for (int bit = 0; bit < 8; ++bit) {
|
||||||
if ((crc & 0x0001U) != 0U) {
|
if ((reg & 0x80U) != 0U) {
|
||||||
crc = static_cast<std::uint16_t>((crc >> 1U) ^ kCrcPolynomial);
|
reg = static_cast<std::uint8_t>((reg << 1U) ^ kCrcPolynomial);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
crc = static_cast<std::uint16_t>(crc >> 1U);
|
reg = static_cast<std::uint8_t>(reg << 1U);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return crc;
|
return static_cast<std::uint8_t>(reg ^ kCrcXorOut);
|
||||||
|
#endif
|
||||||
|
constexpr std::uint8_t kPolynomial = 0x07;
|
||||||
|
constexpr std::uint8_t kInitial = 0x00;
|
||||||
|
constexpr std::uint8_t kXorOut =
|
||||||
|
0x55; // CRC-8/ITU params match device expectation
|
||||||
|
|
||||||
|
std::uint8_t reg = kInitial;
|
||||||
|
for (std::size_t idx = 0; idx < length; ++idx) {
|
||||||
|
reg = static_cast<std::uint8_t>(reg ^ data[idx]);
|
||||||
|
for (int bit = 0; bit < 8; ++bit) {
|
||||||
|
if ((reg & 0x80U) != 0U) {
|
||||||
|
reg = static_cast<std::uint8_t>((reg << 1U) ^ kPolynomial);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
reg = static_cast<std::uint8_t>(reg << 1U);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return static_cast<std::uint8_t>(reg ^ kXorOut);
|
||||||
}
|
}
|
||||||
|
|
||||||
TactileDecoderContext* get_priv(CPCodecContext* ctx) {
|
TactileDecoderContext* get_priv(CPCodecContext* ctx) {
|
||||||
return ctx ? ctx->priv_as<TactileDecoderContext>() : nullptr;
|
return ctx ? ctx->priv_as<TactileDecoderContext>() : nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
template<std::size_t N>
|
||||||
|
void keep_partial_start_prefix(std::vector<std::uint8_t>& buf, const std::array<std::uint8_t, N>& start_sequence) {
|
||||||
|
if (buf.empty() || N == 0U) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const std::size_t max_prefix = std::min<std::size_t>(N - 1U, buf.size());
|
||||||
|
for (std::size_t len = max_prefix; len > 0; --len) {
|
||||||
|
const auto seq_begin = start_sequence.begin();
|
||||||
|
const auto seq_end = seq_begin + static_cast<std::ptrdiff_t>(len);
|
||||||
|
const auto buf_begin =
|
||||||
|
buf.end() - static_cast<std::ptrdiff_t>(len);
|
||||||
|
if (std::equal(seq_begin, seq_end, buf_begin)) {
|
||||||
|
std::vector<std::uint8_t> tail(buf_begin, buf.end());
|
||||||
|
buf.swap(tail);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
buf.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
void trim_fifo_if_needed(std::vector<std::uint8_t>& buf, std::size_t max_fifo_bytes) {
|
||||||
|
if (buf.size() <= max_fifo_bytes) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const auto excess = buf.size() - max_fifo_bytes;
|
||||||
|
buf.erase(buf.begin(), buf.begin() + static_cast<std::ptrdiff_t>(excess));
|
||||||
|
}
|
||||||
|
|
||||||
|
std::atomic<std::size_t>& expected_payload_bytes_for_tactile() {
|
||||||
|
static std::atomic<std::size_t> expected{kPiezoresistiveBPayloadSize};
|
||||||
|
return expected;
|
||||||
|
}
|
||||||
|
|
||||||
int tactile_init(CPCodecContext* ctx) {
|
int tactile_init(CPCodecContext* ctx) {
|
||||||
if (!ctx) {
|
if (!ctx) {
|
||||||
return CP_ERROR_INVALID_ARGUMENT;
|
return CP_ERROR_INVALID_ARGUMENT;
|
||||||
@@ -64,6 +144,14 @@ int tactile_init(CPCodecContext* ctx) {
|
|||||||
}
|
}
|
||||||
auto* storage = static_cast<TactileDecoderContext*>(ctx->priv_data);
|
auto* storage = static_cast<TactileDecoderContext*>(ctx->priv_data);
|
||||||
new (storage) TactileDecoderContext();
|
new (storage) TactileDecoderContext();
|
||||||
|
storage->codec_id = ctx->codec ? ctx->codec->id : CPCodecID::Unknow;
|
||||||
|
if (storage->codec_id == CPCodecID::Tactile) {
|
||||||
|
const auto expected = expected_payload_bytes_for_tactile().load(std::memory_order_relaxed);
|
||||||
|
storage->update_limits(expected);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
storage->update_limits(kPiezoresistiveBPayloadSize);
|
||||||
|
}
|
||||||
return CP_SUCCESS;
|
return CP_SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,6 +177,7 @@ int tactile_send_packet(CPCodecContext* ctx, const CPPacket& packet) {
|
|||||||
|
|
||||||
if (!packet.payload.empty()) {
|
if (!packet.payload.empty()) {
|
||||||
priv->fifo.insert(priv->fifo.end(), packet.payload.begin(), packet.payload.end());
|
priv->fifo.insert(priv->fifo.end(), packet.payload.begin(), packet.payload.end());
|
||||||
|
trim_fifo_if_needed(priv->fifo, priv->max_fifo_bytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (packet.end_of_stream) {
|
if (packet.end_of_stream) {
|
||||||
@@ -115,9 +204,9 @@ int tactile_receive_frame(CPCodecContext* ctx, CPFrame& frame) {
|
|||||||
return CP_ERROR_EAGAIN;
|
return CP_ERROR_EAGAIN;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto start_it = std::find(buf.begin(), buf.end(), kStartByte);
|
const auto start_it = std::search(buf.begin(), buf.end(), kStartSequence.begin(), kStartSequence.end());
|
||||||
if (start_it == buf.end()) {
|
if (start_it == buf.end()) {
|
||||||
buf.clear();
|
keep_partial_start_prefix(buf, kStartSequence);
|
||||||
if (priv->end_of_stream) {
|
if (priv->end_of_stream) {
|
||||||
priv->end_of_stream = false;
|
priv->end_of_stream = false;
|
||||||
return CP_ERROR_EOF;
|
return CP_ERROR_EOF;
|
||||||
@@ -129,7 +218,7 @@ int tactile_receive_frame(CPCodecContext* ctx, CPFrame& frame) {
|
|||||||
buf.erase(buf.begin(), start_it);
|
buf.erase(buf.begin(), start_it);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (buf.size() < kMinimumFrameSize) {
|
if (buf.size() < kHeaderSize) {
|
||||||
if (priv->end_of_stream) {
|
if (priv->end_of_stream) {
|
||||||
buf.clear();
|
buf.clear();
|
||||||
priv->end_of_stream = false;
|
priv->end_of_stream = false;
|
||||||
@@ -139,11 +228,24 @@ int tactile_receive_frame(CPCodecContext* ctx, CPFrame& frame) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const std::uint8_t* data = buffer_data(buf);
|
const std::uint8_t* data = buffer_data(buf);
|
||||||
const std::uint8_t address = data[1U];
|
if (!data) {
|
||||||
const FunctionCode function = static_cast<FunctionCode>(data[2U]);
|
buf.clear();
|
||||||
const std::uint8_t payload_length = data[3U];
|
continue;
|
||||||
const std::size_t total_frame_length = frame_length_from_payload(payload_length);
|
}
|
||||||
|
|
||||||
|
const std::uint16_t data_length =
|
||||||
|
static_cast<std::uint16_t>(data[2]) | static_cast<std::uint16_t>(static_cast<std::uint16_t>(data[3]) << 8U);
|
||||||
|
|
||||||
|
if (data_length < kFixedSectionSize) {
|
||||||
|
buf.erase(buf.begin());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::size_t total_frame_length = kHeaderSize + static_cast<std::size_t>(data_length) + 1U;
|
||||||
|
if (total_frame_length > priv->max_frame_bytes) {
|
||||||
|
buf.erase(buf.begin());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (buf.size() < total_frame_length) {
|
if (buf.size() < total_frame_length) {
|
||||||
if (priv->end_of_stream) {
|
if (priv->end_of_stream) {
|
||||||
buf.clear();
|
buf.clear();
|
||||||
@@ -153,33 +255,15 @@ int tactile_receive_frame(CPCodecContext* ctx, CPFrame& frame) {
|
|||||||
return CP_ERROR_EAGAIN;
|
return CP_ERROR_EAGAIN;
|
||||||
}
|
}
|
||||||
|
|
||||||
const std::size_t payload_offset = 4U;
|
const auto crc_offset = total_frame_length - 1U;
|
||||||
const std::size_t crc_offset = payload_offset + payload_length;
|
const std::uint8_t computed_crc =
|
||||||
const std::size_t end_offset = crc_offset + 2U;
|
crc8_with_xorout(data, crc_offset); // header..last payload byte (excludes CRC)
|
||||||
|
const std::uint8_t frame_crc = data[crc_offset];
|
||||||
const std::uint8_t crc_lo = data[crc_offset];
|
if (computed_crc != frame_crc) {
|
||||||
const std::uint8_t crc_hi = data[crc_offset + 1U];
|
|
||||||
const std::uint16_t crc_value = static_cast<std::uint16_t>(crc_lo) |
|
|
||||||
static_cast<std::uint16_t>(crc_hi << 8U);
|
|
||||||
|
|
||||||
const std::uint8_t end_first = data[end_offset];
|
|
||||||
const std::uint8_t end_second = data[end_offset + 1U];
|
|
||||||
|
|
||||||
if (end_first != kEndByteFirst || end_second != kEndByteSecond) {
|
|
||||||
buf.erase(buf.begin());
|
buf.erase(buf.begin());
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const std::size_t crc_region_length = 3U + payload_length;
|
|
||||||
const std::uint16_t computed_crc = crc16_modbus(data + 1U, crc_region_length);
|
|
||||||
if (computed_crc != crc_value) {
|
|
||||||
buf.erase(buf.begin());
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
(void)address;
|
|
||||||
(void)function;
|
|
||||||
|
|
||||||
frame.data.assign(buf.begin(), buf.begin() + static_cast<std::ptrdiff_t>(total_frame_length));
|
frame.data.assign(buf.begin(), buf.begin() + static_cast<std::ptrdiff_t>(total_frame_length));
|
||||||
frame.pts = priv->next_pts++;
|
frame.pts = priv->next_pts++;
|
||||||
frame.key_frame = true;
|
frame.key_frame = true;
|
||||||
@@ -190,6 +274,70 @@ int tactile_receive_frame(CPCodecContext* ctx, CPFrame& frame) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int tactile_b_receive_frame(CPCodecContext* ctx, CPFrame& frame) {
|
||||||
|
auto* priv = get_priv(ctx);
|
||||||
|
if (!priv) {
|
||||||
|
return CP_ERROR_INVALID_STATE;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto& buf = priv->fifo;
|
||||||
|
while (true) {
|
||||||
|
if (buf.size() < kPiezoresistiveBStartSequence.size()) {
|
||||||
|
if (priv->end_of_stream) {
|
||||||
|
buf.clear();
|
||||||
|
priv->end_of_stream = false;
|
||||||
|
return CP_ERROR_EOF;
|
||||||
|
}
|
||||||
|
return CP_ERROR_EAGAIN;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto start_it = std::search(buf.begin(),
|
||||||
|
buf.end(),
|
||||||
|
kPiezoresistiveBStartSequence.begin(),
|
||||||
|
kPiezoresistiveBStartSequence.end());
|
||||||
|
if (start_it == buf.end()) {
|
||||||
|
keep_partial_start_prefix(buf, kPiezoresistiveBStartSequence);
|
||||||
|
if (priv->end_of_stream) {
|
||||||
|
priv->end_of_stream = false;
|
||||||
|
return CP_ERROR_EOF;
|
||||||
|
}
|
||||||
|
return CP_ERROR_EAGAIN;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (start_it != buf.begin()) {
|
||||||
|
buf.erase(buf.begin(), start_it);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (buf.size() < kPiezoresistiveBFrameSize) {
|
||||||
|
if (priv->end_of_stream) {
|
||||||
|
buf.clear();
|
||||||
|
priv->end_of_stream = false;
|
||||||
|
return CP_ERROR_EOF;
|
||||||
|
}
|
||||||
|
return CP_ERROR_EAGAIN;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto end_offset = kPiezoresistiveBFrameSize - kPiezoresistiveBEndSequence.size();
|
||||||
|
const auto end_it = buf.begin() + static_cast<std::ptrdiff_t>(end_offset);
|
||||||
|
if (!std::equal(end_it,
|
||||||
|
end_it + static_cast<std::ptrdiff_t>(kPiezoresistiveBEndSequence.size()),
|
||||||
|
kPiezoresistiveBEndSequence.begin())) {
|
||||||
|
buf.erase(buf.begin());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
frame.data.assign(buf.begin(),
|
||||||
|
buf.begin() + static_cast<std::ptrdiff_t>(kPiezoresistiveBFrameSize));
|
||||||
|
frame.pts = priv->next_pts++;
|
||||||
|
frame.key_frame = true;
|
||||||
|
frame.valid = true;
|
||||||
|
|
||||||
|
buf.erase(buf.begin(),
|
||||||
|
buf.begin() + static_cast<std::ptrdiff_t>(kPiezoresistiveBFrameSize));
|
||||||
|
return CP_SUCCESS;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const CPCodec kTactileCodec{
|
const CPCodec kTactileCodec{
|
||||||
.name = "tactile_serial",
|
.name = "tactile_serial",
|
||||||
.long_name = "Framed tactile sensor serial protocol decoder",
|
.long_name = "Framed tactile sensor serial protocol decoder",
|
||||||
@@ -201,53 +349,93 @@ const CPCodec kTactileCodec {
|
|||||||
.send_packet = &tactile_send_packet,
|
.send_packet = &tactile_send_packet,
|
||||||
.receive_frame = &tactile_receive_frame
|
.receive_frame = &tactile_receive_frame
|
||||||
};
|
};
|
||||||
}
|
|
||||||
|
const CPCodec kTactileBCodec{
|
||||||
|
.name = "tactile_serial_b",
|
||||||
|
.long_name = "Piezoresistive B tactile serial protocol decoder",
|
||||||
|
.type = CPMediaType::Data,
|
||||||
|
.id = CPCodecID::PiezoresistiveB,
|
||||||
|
.priv_data_size = sizeof(TactileDecoderContext),
|
||||||
|
.init = &tactile_init,
|
||||||
|
.close = &tactile_close,
|
||||||
|
.send_packet = &tactile_send_packet,
|
||||||
|
.receive_frame = &tactile_b_receive_frame
|
||||||
|
};
|
||||||
|
} // namespace
|
||||||
|
|
||||||
std::optional<TactileFrame> parse_frame(const CPFrame& frame) {
|
std::optional<TactileFrame> parse_frame(const CPFrame& frame) {
|
||||||
if (!frame.valid || frame.data.size() < kMinimumFrameSize) {
|
// if (!frame.valid || frame.data.size() < kMinimumFrameSize) {
|
||||||
return std::nullopt;
|
// return std::nullopt;
|
||||||
}
|
// }
|
||||||
|
std::cout << "frame valid:" << frame.valid << ", frame.data.size:" << frame.data.size() << std::endl;
|
||||||
|
|
||||||
const auto* bytes = frame.data.data();
|
const auto* bytes = frame.data.data();
|
||||||
const std::size_t size = frame.data.size();
|
const std::size_t size = frame.data.size();
|
||||||
|
|
||||||
if (bytes[0] != kStartByte) {
|
if (bytes[0] != kStartByteFirst || bytes[1] != kStartByteSecond) {
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
|
std::cout << "frame valid1:" << frame.valid << ", frame.data.size1:" << frame.data.size() << std::endl;
|
||||||
if (bytes[size - 2] != kEndByteFirst || bytes[size - 1] != kEndByteSecond) {
|
const std::uint16_t data_length =
|
||||||
|
static_cast<std::uint16_t>(bytes[2]) | static_cast<std::uint16_t>(static_cast<std::uint16_t>(bytes[3]) << 8U);
|
||||||
|
qDebug() << "data_length: " << data_length;
|
||||||
|
if (data_length < kFixedSectionSize) {
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
|
std::cout << "frame valid2:" << frame.valid << ", frame.data.size1:" << frame.data.size() << std::endl;
|
||||||
if (size < 4U) {
|
const std::size_t expected_size = kHeaderSize + static_cast<std::size_t>(data_length) + 1U;
|
||||||
|
if (size != expected_size) {
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
|
std::cout << "frame valid3:" << frame.valid << ", frame.data.size1:" << frame.data.size() << std::endl;
|
||||||
const std::uint8_t length = bytes[3];
|
const std::uint8_t crc_byte = bytes[expected_size - 1U];
|
||||||
if (frame_length_from_payload(length) != size) {
|
const std::uint8_t computed_crc =
|
||||||
|
crc8_with_xorout(bytes, expected_size - 1U); // header..last payload byte
|
||||||
|
if (computed_crc != crc_byte) {
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
const std::uint8_t address = bytes[1];
|
std::cout << "frame valid4:" << frame.valid << ", frame.data.size1:" << frame.data.size() << std::endl;
|
||||||
const FunctionCode function = static_cast<FunctionCode>(bytes[2]);
|
const std::uint8_t device_address = bytes[4];
|
||||||
const std::size_t payload_offset = 4U;
|
const std::uint8_t reserved = bytes[5];
|
||||||
|
const std::uint8_t response_function = bytes[6];
|
||||||
|
const std::uint32_t start_address =
|
||||||
|
static_cast<std::uint32_t>(bytes[7]) | (static_cast<std::uint32_t>(bytes[8]) << 8U) | (static_cast<std::uint32_t>(bytes[9]) << 16U) | (static_cast<std::uint32_t>(bytes[10]) << 24U);
|
||||||
|
const std::uint16_t return_byte_count =
|
||||||
|
static_cast<std::uint16_t>(bytes[11]) | (static_cast<std::uint16_t>(bytes[12]) << 8U);
|
||||||
|
const std::uint8_t status = bytes[13];
|
||||||
|
|
||||||
|
const std::size_t payload_offset = kHeaderSize + kFixedSectionSize;
|
||||||
|
const std::size_t payload_available =
|
||||||
|
data_length > kFixedSectionSize ? static_cast<std::size_t>(data_length) - kFixedSectionSize : 0U;
|
||||||
|
const std::size_t requested_payload = static_cast<std::size_t>(return_byte_count);
|
||||||
|
if (payload_available < requested_payload) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
std::cout << "frame valid5:" << frame.valid << ", frame.data.size1:" << frame.data.size() << std::endl;
|
||||||
TactileFrame parsed{};
|
TactileFrame parsed{};
|
||||||
parsed.device_address = address;
|
parsed.device_address = device_address;
|
||||||
parsed.function = function;
|
parsed.reserved = reserved;
|
||||||
parsed.data_length = length;
|
parsed.response_function = response_function;
|
||||||
parsed.payload.assign(bytes + payload_offset, bytes + payload_offset + length);
|
parsed.function = static_cast<FunctionCode>(response_function & 0x7FU);
|
||||||
|
parsed.start_address = start_address;
|
||||||
|
parsed.return_byte_count = return_byte_count;
|
||||||
|
parsed.status = status;
|
||||||
|
parsed.payload.assign(bytes + payload_offset, bytes + payload_offset + requested_payload);
|
||||||
return parsed;
|
return parsed;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<std::uint16_t> parse_pressure_values(const TactileFrame& frame) {
|
std::vector<std::uint16_t> parse_pressure_values(const TactileFrame& frame) {
|
||||||
if (frame.payload.empty() || (frame.payload.size() % 2U != 0U)) {
|
std::cout << "parse_pressure_values" << std::endl;
|
||||||
|
const auto requested_bytes = static_cast<std::size_t>(frame.return_byte_count);
|
||||||
|
const auto usable_bytes = std::min(requested_bytes, frame.payload.size());
|
||||||
|
if (usable_bytes == 0U || (usable_bytes % 2U != 0U)) {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
std::vector<std::uint16_t> values;
|
std::vector<std::uint16_t> values;
|
||||||
values.reserve(frame.payload.size() / 2U);
|
values.reserve(usable_bytes / 2U);
|
||||||
for (std::size_t idx = 0; idx + 1U < frame.payload.size(); idx += 2U) {
|
for (std::size_t idx = 0; idx + 1U < usable_bytes; idx += 2U) {
|
||||||
const std::uint16_t value = static_cast<std::uint16_t>(
|
const std::uint16_t value = static_cast<std::uint16_t>(
|
||||||
static_cast<std::uint16_t>(frame.payload[idx]) |
|
static_cast<std::uint16_t>(frame.payload[idx]) | static_cast<std::uint16_t>(frame.payload[idx + 1U] << 8U));
|
||||||
static_cast<std::uint16_t>(frame.payload[idx + 1U] << 8U));
|
|
||||||
values.push_back(value);
|
values.push_back(value);
|
||||||
}
|
}
|
||||||
return values;
|
return values;
|
||||||
@@ -263,6 +451,71 @@ std::optional<MatrixSize> parse_matrix_size_payload(const TactileFrame& frame) {
|
|||||||
return size;
|
return size;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::vector<std::uint16_t> parse_piezoresistive_b_pressures(const CPFrame& frame) {
|
||||||
|
// if (!frame.valid) {
|
||||||
|
// return {};
|
||||||
|
// }
|
||||||
|
// if (frame.data.size() != kPiezoresistiveBFrameSize) {
|
||||||
|
// return {};
|
||||||
|
// }
|
||||||
|
// if (frame.data.size() < kPiezoresistiveBFrameSize) {
|
||||||
|
// return {};
|
||||||
|
// }
|
||||||
|
// if (frame.data[0] != kPiezoresistiveBStartByteFirst || frame.data[1] != kPiezoresistiveBStartByteSecond) {
|
||||||
|
// return {};
|
||||||
|
// }
|
||||||
|
const auto end_offset = kPiezoresistiveBFrameSize - kPiezoresistiveBEndSequence.size();
|
||||||
|
// if (frame.data[end_offset] != kPiezoresistiveBEndByteFirst || frame.data[end_offset + 1U] != kPiezoresistiveBEndByteSecond) {
|
||||||
|
// return {};
|
||||||
|
// }
|
||||||
|
|
||||||
|
std::vector<std::uint16_t> values;
|
||||||
|
values.reserve(kPiezoresistiveBValueCount);
|
||||||
|
std::cout << "valuessize:" << values.size() << std::endl;
|
||||||
|
const auto payload_offset = kPiezoresistiveBStartSequence.size();
|
||||||
|
for (std::size_t idx = 0; idx < kPiezoresistiveBValueCount; ++idx) {
|
||||||
|
const auto base = payload_offset + idx * 2U;
|
||||||
|
if (base + 1U >= frame.data.size()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const auto hi = static_cast<std::uint16_t>(frame.data[base]);
|
||||||
|
const auto lo = static_cast<std::uint16_t>(frame.data[base + 1U]);
|
||||||
|
values.push_back(static_cast<std::uint16_t>((hi << 8U) | lo));
|
||||||
|
}
|
||||||
|
return values;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::uint8_t> extract_piezoresistive_b_payload(const CPFrame& frame) {
|
||||||
|
if (!frame.valid) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
if (frame.data.size() != kPiezoresistiveBFrameSize) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
if (frame.data[0] != kPiezoresistiveBStartByteFirst || frame.data[1] != kPiezoresistiveBStartByteSecond) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
const auto payload_offset = kPiezoresistiveBStartSequence.size();
|
||||||
|
const auto payload_end = payload_offset + kPiezoresistiveBPayloadSize;
|
||||||
|
if (frame.data.size() < payload_end + kPiezoresistiveBEndSequence.size()) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
if (frame.data[payload_end] != kPiezoresistiveBEndByteFirst || frame.data[payload_end + 1U] != kPiezoresistiveBEndByteSecond) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
return std::vector<std::uint8_t>(
|
||||||
|
frame.data.begin() + static_cast<std::ptrdiff_t>(payload_offset),
|
||||||
|
frame.data.begin() + static_cast<std::ptrdiff_t>(payload_end));
|
||||||
|
}
|
||||||
|
|
||||||
|
void set_tactile_expected_payload_bytes(std::size_t bytes) {
|
||||||
|
const auto clamped = std::min<std::size_t>(
|
||||||
|
std::max<std::size_t>(bytes, 2U),
|
||||||
|
kAbsoluteMaxPayloadBytes);
|
||||||
|
expected_payload_bytes_for_tactile().store(clamped, std::memory_order_relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
const CPCodec* tactile_codec() {
|
const CPCodec* tactile_codec() {
|
||||||
return &kTactileCodec;
|
return &kTactileCodec;
|
||||||
}
|
}
|
||||||
@@ -270,4 +523,12 @@ const CPCodec* tactile_codec() {
|
|||||||
void register_tactile_codec() {
|
void register_tactile_codec() {
|
||||||
cpcodec_register(&kTactileCodec);
|
cpcodec_register(&kTactileCodec);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const CPCodec* tactile_b_codec() {
|
||||||
|
return &kTactileBCodec;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void register_tactile_b_codec() {
|
||||||
|
cpcodec_register(&kTactileBCodec);
|
||||||
|
}
|
||||||
|
} // namespace ffmsep::tactile
|
||||||
|
|||||||
@@ -6,9 +6,13 @@
|
|||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
namespace ffmsep::tactile {
|
namespace ffmsep::tactile {
|
||||||
inline constexpr std::uint8_t kStartByte = 0x3A;
|
inline constexpr std::uint8_t kStartByteFirst = 0xAA;
|
||||||
inline constexpr std::uint8_t kEndByteFirst = 0x0D;
|
inline constexpr std::uint8_t kStartByteSecond = 0x55;
|
||||||
inline constexpr std::uint8_t kEndByteSecond = 0x0A;
|
inline constexpr std::uint8_t kPiezoresistiveBStartByteFirst = 0xF0;
|
||||||
|
inline constexpr std::uint8_t kPiezoresistiveBStartByteSecond = 0xF1;
|
||||||
|
inline constexpr std::uint8_t kPiezoresistiveBEndByteFirst = 0xF1;
|
||||||
|
inline constexpr std::uint8_t kPiezoresistiveBEndByteSecond = 0xF0;
|
||||||
|
inline constexpr std::size_t kPiezoresistiveBValueCount = 200;
|
||||||
|
|
||||||
enum class FunctionCode : std::uint8_t {
|
enum class FunctionCode : std::uint8_t {
|
||||||
Unknown = 0x00,
|
Unknown = 0x00,
|
||||||
@@ -27,16 +31,27 @@ struct MatrixSize {
|
|||||||
|
|
||||||
struct TactileFrame {
|
struct TactileFrame {
|
||||||
std::uint8_t device_address = 0;
|
std::uint8_t device_address = 0;
|
||||||
|
std::uint8_t reserved = 0;
|
||||||
|
std::uint8_t response_function = 0;
|
||||||
FunctionCode function = FunctionCode::Unknown;
|
FunctionCode function = FunctionCode::Unknown;
|
||||||
std::uint8_t data_length = 0;
|
std::uint32_t start_address = 0;
|
||||||
|
std::uint16_t return_byte_count = 0;
|
||||||
|
std::uint8_t status = 0;
|
||||||
std::vector<std::uint8_t> payload;
|
std::vector<std::uint8_t> payload;
|
||||||
};
|
};
|
||||||
|
|
||||||
std::optional<TactileFrame> parse_frame(const CPFrame &frame);
|
std::optional<TactileFrame> parse_frame(const CPFrame &frame);
|
||||||
std::vector<std::uint16_t> parse_pressure_values(const TactileFrame &frame);
|
std::vector<std::uint16_t> parse_pressure_values(const TactileFrame &frame);
|
||||||
std::optional<MatrixSize> parse_matrix_size_payload(const TactileFrame &frame);
|
std::optional<MatrixSize> parse_matrix_size_payload(const TactileFrame &frame);
|
||||||
std::optional<MatrixSize> parse_patrix_coordinate_payload(const TactileFrame& frame);
|
std::optional<MatrixSize>
|
||||||
|
parse_patrix_coordinate_payload(const TactileFrame &frame);
|
||||||
|
std::vector<std::uint16_t> parse_piezoresistive_b_pressures(const CPFrame &frame);
|
||||||
|
std::vector<std::uint8_t> extract_piezoresistive_b_payload(const CPFrame &frame);
|
||||||
|
// 配置触觉 A 类型预期的 payload 字节数(点数 * 2),用于限制解码 FIFO。
|
||||||
|
void set_tactile_expected_payload_bytes(std::size_t bytes);
|
||||||
|
|
||||||
const CPCodec *tactile_codec();
|
const CPCodec *tactile_codec();
|
||||||
void register_tactile_codec();
|
void register_tactile_codec();
|
||||||
}
|
const CPCodec *tactile_b_codec();
|
||||||
|
void register_tactile_b_codec();
|
||||||
|
} // namespace ffmsep::tactile
|
||||||
|
|||||||
44
components/hand-view.cc
Normal file
44
components/hand-view.cc
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
//
|
||||||
|
// Created by Lenn on 2025/11/24.
|
||||||
|
//
|
||||||
|
#include "component.hh"
|
||||||
|
#include "base/globalhelper.hh"
|
||||||
|
#include "creeper-qt/utility/theme/theme.hh"
|
||||||
|
#include "creeper-qt/utility/wrapper/layout.hh"
|
||||||
|
#include "creeper-qt/utility/wrapper/widget.hh"
|
||||||
|
#include "creeper-qt/layout/flow.hh"
|
||||||
|
#include "creeper-qt/layout/linear.hh"
|
||||||
|
#include "creeper-qt/widget/cards/basic-card.hh"
|
||||||
|
#include <concepts>
|
||||||
|
#include <creeper-qt/utility/material-icon.hh>
|
||||||
|
#include <creeper-qt/utility/wrapper/mutable-value.hh>
|
||||||
|
#include <creeper-qt/widget/buttons/icon-button.hh>
|
||||||
|
#include <creeper-qt/widget/cards/filled-card.hh>
|
||||||
|
#include <creeper-qt/widget/cards/outlined-card.hh>
|
||||||
|
#include <creeper-qt/widget/dropdown-menu.hh>
|
||||||
|
#include <creeper-qt/widget/image.hh>
|
||||||
|
#include <creeper-qt/widget/shape/wave-circle.hh>
|
||||||
|
#include <creeper-qt/widget/sliders.hh>
|
||||||
|
#include <creeper-qt/widget/switch.hh>
|
||||||
|
#include <creeper-qt/widget/text-fields.hh>
|
||||||
|
#include <creeper-qt/widget/text.hh>
|
||||||
|
#include <creeper-qt/widget/buttons/icon-button.hh>
|
||||||
|
#include <cstddef>
|
||||||
|
#include <iterator>
|
||||||
|
#include <qcontainerfwd.h>
|
||||||
|
#include <qlogging.h>
|
||||||
|
#include <qnamespace.h>
|
||||||
|
#include <ranges>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
using namespace creeper;
|
||||||
|
namespace capro = card::pro;
|
||||||
|
namespace lnpro = linear::pro;
|
||||||
|
namespace ibpro = icon_button::pro;
|
||||||
|
|
||||||
|
auto HandViewComponent(HandViewComponentState& state) noexcept -> raw_pointer<QWidget> {
|
||||||
|
return new FilledCard {
|
||||||
|
capro::ThemeManager{state.manager},
|
||||||
|
capro::SizePolicy {QSizePolicy::Expanding},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,14 +1,14 @@
|
|||||||
#include "component.hh"
|
#include "component.hh"
|
||||||
|
|
||||||
#include "modern-qt/core/application.hh"
|
#include "creeper-qt/core/application.hh"
|
||||||
#include "modern-qt/layout/group.hh"
|
#include "creeper-qt/layout/group.hh"
|
||||||
#include "modern-qt/layout/linear.hh"
|
#include "creeper-qt/layout/linear.hh"
|
||||||
#include "modern-qt/layout/mutual-exclusion-group.hh"
|
#include "creeper-qt/layout/mutual-exclusion-group.hh"
|
||||||
#include "modern-qt/utility/material-icon.hh"
|
#include "creeper-qt/utility/material-icon.hh"
|
||||||
#include "modern-qt/utility/theme/theme.hh"
|
#include "creeper-qt/utility/theme/theme.hh"
|
||||||
#include "modern-qt/widget/buttons/icon-button.hh"
|
#include "creeper-qt/widget/buttons/icon-button.hh"
|
||||||
#include "modern-qt/widget/cards/filled-card.hh"
|
#include "creeper-qt/widget/cards/filled-card.hh"
|
||||||
#include "modern-qt/widget/image.hh"
|
#include "creeper-qt/widget/image.hh"
|
||||||
|
|
||||||
using namespace creeper;
|
using namespace creeper;
|
||||||
namespace fc = filled_card::pro;
|
namespace fc = filled_card::pro;
|
||||||
@@ -26,7 +26,6 @@ auto NavComponent(NavComponentState& state) noexcept -> raw_pointer<QWidget> {
|
|||||||
im::BorderWidth {3},
|
im::BorderWidth {3},
|
||||||
im::PainterResource {
|
im::PainterResource {
|
||||||
":/images/images/logo.png",
|
":/images/images/logo.png",
|
||||||
// "./images/logo.png",
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
state.manager.append_handler(AvatarComponent, [AvatarComponent](const ThemeManager& manager) {
|
state.manager.append_handler(AvatarComponent, [AvatarComponent](const ThemeManager& manager) {
|
||||||
@@ -74,7 +73,10 @@ auto NavComponent(NavComponentState& state) noexcept -> raw_pointer<QWidget> {
|
|||||||
navigation_icons_config,
|
navigation_icons_config,
|
||||||
status,
|
status,
|
||||||
ic::FontIcon(icon.data()),
|
ic::FontIcon(icon.data()),
|
||||||
ic::Clickable {[=]{state.switch_callback(index, name);}},
|
ic::Clickable {[=] {
|
||||||
|
// state.switch_callback(index, name);
|
||||||
|
state.stacked_callback(index);
|
||||||
|
}},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
Qt::AlignHCenter,
|
Qt::AlignHCenter,
|
||||||
@@ -90,6 +92,23 @@ auto NavComponent(NavComponentState& state) noexcept -> raw_pointer<QWidget> {
|
|||||||
ic::FontIcon {material::icon::kLogout},
|
ic::FontIcon {material::icon::kLogout},
|
||||||
ic::Clickable {&app::quit},
|
ic::Clickable {&app::quit},
|
||||||
},
|
},
|
||||||
|
ln::Item<IconButton> {
|
||||||
|
{0, Qt::AlignHCenter},
|
||||||
|
navigation_icons_config,
|
||||||
|
ic::ColorFilled,
|
||||||
|
ic::FontIcon {material::icon::k123},
|
||||||
|
MutableTransform{ [](auto& self, bool show_numbers) {
|
||||||
|
self.set_types(show_numbers
|
||||||
|
? icon_button::internal::IconButton::Types::TOGGLE_SELECTED
|
||||||
|
: icon_button::internal::IconButton::Types::TOGGLE_UNSELECTED);
|
||||||
|
},
|
||||||
|
state.heatmap_show_numbers },
|
||||||
|
ic::Clickable{ [ctx = state.heatmap_show_numbers] {
|
||||||
|
if (ctx) {
|
||||||
|
ctx->set(!ctx->get());
|
||||||
|
}
|
||||||
|
} },
|
||||||
|
},
|
||||||
ln::Item<IconButton> {
|
ln::Item<IconButton> {
|
||||||
{0, Qt::AlignHCenter},
|
{0, Qt::AlignHCenter},
|
||||||
navigation_icons_config,
|
navigation_icons_config,
|
||||||
@@ -100,4 +119,3 @@ auto NavComponent(NavComponentState& state) noexcept -> raw_pointer<QWidget> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
636
components/setting.cc
Normal file
636
components/setting.cc
Normal file
@@ -0,0 +1,636 @@
|
|||||||
|
//
|
||||||
|
// Created by Lenn on 2025/11/21.
|
||||||
|
//
|
||||||
|
#include "component.hh"
|
||||||
|
#include "base/globalhelper.hh"
|
||||||
|
#include "creeper-qt/utility/theme/theme.hh"
|
||||||
|
#include "creeper-qt/utility/wrapper/layout.hh"
|
||||||
|
#include "creeper-qt/utility/wrapper/widget.hh"
|
||||||
|
#include "creeper-qt/layout/flow.hh"
|
||||||
|
#include "creeper-qt/layout/linear.hh"
|
||||||
|
#include "creeper-qt/layout/scroll.hh"
|
||||||
|
#include "creeper-qt/widget/buttons/filled-button.hh"
|
||||||
|
#include "creeper-qt/widget/cards/basic-card.hh"
|
||||||
|
#include <concepts>
|
||||||
|
#include <creeper-qt/utility/material-icon.hh>
|
||||||
|
#include <creeper-qt/utility/wrapper/mutable-value.hh>
|
||||||
|
#include <creeper-qt/widget/buttons/icon-button.hh>
|
||||||
|
#include <creeper-qt/widget/cards/filled-card.hh>
|
||||||
|
#include <creeper-qt/widget/cards/outlined-card.hh>
|
||||||
|
#include <creeper-qt/widget/dropdown-menu.hh>
|
||||||
|
#include <creeper-qt/widget/image.hh>
|
||||||
|
#include <creeper-qt/widget/shape/wave-circle.hh>
|
||||||
|
#include <creeper-qt/widget/sliders.hh>
|
||||||
|
#include <creeper-qt/widget/switch.hh>
|
||||||
|
#include <creeper-qt/widget/text-fields.hh>
|
||||||
|
#include <creeper-qt/widget/text.hh>
|
||||||
|
#include <creeper-qt/widget/buttons/icon-button.hh>
|
||||||
|
#include <cstddef>
|
||||||
|
#include <iterator>
|
||||||
|
#include <qcontainerfwd.h>
|
||||||
|
#include <qlogging.h>
|
||||||
|
#include <qnamespace.h>
|
||||||
|
#include <ranges>
|
||||||
|
#include <utility>
|
||||||
|
#include "globalhelper.hh"
|
||||||
|
#include <functional>
|
||||||
|
#include <memory>
|
||||||
|
#include <QComboBox>
|
||||||
|
#include <QDialog>
|
||||||
|
#include <QDialogButtonBox>
|
||||||
|
#include <QFormLayout>
|
||||||
|
#include <QLineEdit>
|
||||||
|
#include <QSpinBox>
|
||||||
|
#include <QVBoxLayout>
|
||||||
|
#include <QFileDialog>
|
||||||
|
#include <QFile>
|
||||||
|
#include <QJsonArray>
|
||||||
|
#include <QJsonDocument>
|
||||||
|
#include <QJsonObject>
|
||||||
|
#include <QMessageBox>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
|
||||||
|
namespace repest_literals {
|
||||||
|
template<class F>
|
||||||
|
concept IndexInvocable = std::invocable<F, std::size_t>;
|
||||||
|
template<IndexInvocable F>
|
||||||
|
void operator*(F&& f, std::size_t n) {
|
||||||
|
std::ranges::for_each(std::views::iota(std::size_t{ 0 }, n), std::forward<F>(f));
|
||||||
|
}
|
||||||
|
template<IndexInvocable F>
|
||||||
|
void operator*(std::size_t n, F&& f) {
|
||||||
|
std::ranges::for_each(std::views::iota(std::size_t{ 0 }, n), std::forward<F>(f));
|
||||||
|
}
|
||||||
|
} // namespace repest_literals
|
||||||
|
|
||||||
|
using namespace creeper;
|
||||||
|
namespace capro = card::pro;
|
||||||
|
namespace lnpro = linear::pro;
|
||||||
|
namespace ibpro = icon_button::pro;
|
||||||
|
namespace fbpro = filled_button::pro;
|
||||||
|
|
||||||
|
static std::weak_ptr<MutableValue<std::vector<ConfigProfile>>> g_profiles_store;
|
||||||
|
static std::function<void()> g_profiles_refresh;
|
||||||
|
|
||||||
|
static QString TactileTypeToJsonString(Tactile_TYPE type)
|
||||||
|
{
|
||||||
|
switch (type) {
|
||||||
|
case Tactile_TYPE::PiezoresistiveA:
|
||||||
|
return QStringLiteral("PiezoresistiveA");
|
||||||
|
case Tactile_TYPE::PiezoresistiveB:
|
||||||
|
return QStringLiteral("PiezoresistiveB");
|
||||||
|
case Tactile_TYPE::Hall:
|
||||||
|
default:
|
||||||
|
return QStringLiteral("Hall");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static Tactile_TYPE TactileTypeFromJsonString(const QString& str)
|
||||||
|
{
|
||||||
|
if (str == QStringLiteral("PiezoresistiveA")) {
|
||||||
|
return Tactile_TYPE::PiezoresistiveA;
|
||||||
|
}
|
||||||
|
if (str == QStringLiteral("PiezoresistiveB")) {
|
||||||
|
return Tactile_TYPE::PiezoresistiveB;
|
||||||
|
}
|
||||||
|
return Tactile_TYPE::Hall;
|
||||||
|
}
|
||||||
|
|
||||||
|
static QJsonObject ConfigProfileToJson(const ConfigProfile& profile)
|
||||||
|
{
|
||||||
|
QJsonObject obj;
|
||||||
|
obj.insert(QStringLiteral("name"), profile.name);
|
||||||
|
obj.insert(QStringLiteral("type"), TactileTypeToJsonString(profile.type));
|
||||||
|
obj.insert(QStringLiteral("matrix_width"), profile.matrix_width);
|
||||||
|
obj.insert(QStringLiteral("matrix_height"), profile.matrix_height);
|
||||||
|
obj.insert(QStringLiteral("range_left"), profile.range_left);
|
||||||
|
obj.insert(QStringLiteral("range_right"), profile.range_right);
|
||||||
|
obj.insert(QStringLiteral("baud_rate"), profile.baud_rate);
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool ConfigProfileFromJson(const QJsonObject& obj, ConfigProfile& out_profile)
|
||||||
|
{
|
||||||
|
const auto name = obj.value(QStringLiteral("name")).toString();
|
||||||
|
if (name.isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
out_profile.name = name;
|
||||||
|
out_profile.type = TactileTypeFromJsonString(obj.value(QStringLiteral("type")).toString());
|
||||||
|
out_profile.matrix_width = obj.value(QStringLiteral("matrix_width")).toInt(0);
|
||||||
|
out_profile.matrix_height = obj.value(QStringLiteral("matrix_height")).toInt(0);
|
||||||
|
out_profile.range_left = obj.value(QStringLiteral("range_left")).toInt(0);
|
||||||
|
out_profile.range_right = obj.value(QStringLiteral("range_right")).toInt(0);
|
||||||
|
out_profile.baud_rate = obj.value(QStringLiteral("baud_rate")).toInt(0);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void ShowEditProfileDialog(
|
||||||
|
const ConfigProfile& current,
|
||||||
|
const std::shared_ptr<MutableValue<std::vector<ConfigProfile>>>& profiles_store) {
|
||||||
|
QDialog dialog;
|
||||||
|
dialog.setWindowTitle(QStringLiteral("修改配置"));
|
||||||
|
auto* layout = new QVBoxLayout(&dialog);
|
||||||
|
auto* form = new QFormLayout();
|
||||||
|
|
||||||
|
auto* name_edit = new QLineEdit(&dialog);
|
||||||
|
name_edit->setText(current.name);
|
||||||
|
|
||||||
|
auto* type_combo = new QComboBox(&dialog);
|
||||||
|
type_combo->addItem(QStringLiteral("压阻A型"));
|
||||||
|
type_combo->addItem(QStringLiteral("压阻B型"));
|
||||||
|
type_combo->addItem(QStringLiteral("霍尔型"));
|
||||||
|
switch (current.type) {
|
||||||
|
case Tactile_TYPE::PiezoresistiveA:
|
||||||
|
type_combo->setCurrentIndex(0);
|
||||||
|
break;
|
||||||
|
case Tactile_TYPE::PiezoresistiveB:
|
||||||
|
type_combo->setCurrentIndex(1);
|
||||||
|
break;
|
||||||
|
case Tactile_TYPE::Hall:
|
||||||
|
type_combo->setCurrentIndex(2);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto* width_spin = new QSpinBox(&dialog);
|
||||||
|
auto* height_spin = new QSpinBox(&dialog);
|
||||||
|
width_spin->setRange(1, 64);
|
||||||
|
height_spin->setRange(1, 64);
|
||||||
|
width_spin->setValue(current.matrix_width);
|
||||||
|
height_spin->setValue(current.matrix_height);
|
||||||
|
|
||||||
|
auto* left_spin = new QSpinBox(&dialog);
|
||||||
|
auto* right_spin = new QSpinBox(&dialog);
|
||||||
|
left_spin->setRange(-100000, 100000);
|
||||||
|
right_spin->setRange(-100000, 100000);
|
||||||
|
left_spin->setValue(current.range_left);
|
||||||
|
right_spin->setValue(current.range_right);
|
||||||
|
|
||||||
|
auto* baud_spin = new QSpinBox(&dialog);
|
||||||
|
baud_spin->setRange(1200, 10000000);
|
||||||
|
baud_spin->setValue(current.baud_rate);
|
||||||
|
|
||||||
|
form->addRow(QStringLiteral("名称"), name_edit);
|
||||||
|
form->addRow(QStringLiteral("类型"), type_combo);
|
||||||
|
form->addRow(QStringLiteral("宽"), width_spin);
|
||||||
|
form->addRow(QStringLiteral("高"), height_spin);
|
||||||
|
form->addRow(QStringLiteral("量程左"), left_spin);
|
||||||
|
form->addRow(QStringLiteral("量程右"), right_spin);
|
||||||
|
form->addRow(QStringLiteral("波特率"), baud_spin);
|
||||||
|
|
||||||
|
layout->addLayout(form);
|
||||||
|
|
||||||
|
auto* buttons =
|
||||||
|
new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, Qt::Horizontal, &dialog);
|
||||||
|
layout->addWidget(buttons);
|
||||||
|
QObject::connect(buttons, &QDialogButtonBox::accepted, &dialog, &QDialog::accept);
|
||||||
|
QObject::connect(buttons, &QDialogButtonBox::rejected, &dialog, &QDialog::reject);
|
||||||
|
|
||||||
|
if (dialog.exec() == QDialog::Accepted) {
|
||||||
|
ConfigProfile updated = current;
|
||||||
|
updated.name = name_edit->text();
|
||||||
|
updated.type = [idx = type_combo->currentIndex()] {
|
||||||
|
switch (idx) {
|
||||||
|
case 0:
|
||||||
|
return Tactile_TYPE::PiezoresistiveA;
|
||||||
|
case 1:
|
||||||
|
return Tactile_TYPE::PiezoresistiveB;
|
||||||
|
default:
|
||||||
|
return Tactile_TYPE::Hall;
|
||||||
|
}
|
||||||
|
}();
|
||||||
|
updated.matrix_width = width_spin->value();
|
||||||
|
updated.matrix_height = height_spin->value();
|
||||||
|
updated.range_left = left_spin->value();
|
||||||
|
updated.range_right = right_spin->value();
|
||||||
|
updated.baud_rate = baud_spin->value();
|
||||||
|
|
||||||
|
GlobalHelper::instance().remove_profile(current.name);
|
||||||
|
GlobalHelper::instance().add_new_profile(updated);
|
||||||
|
GlobalHelper::instance().reload_profiles();
|
||||||
|
if (profiles_store) {
|
||||||
|
profiles_store->set(GlobalHelper::instance().get_all_profile());
|
||||||
|
}
|
||||||
|
RefreshProfilesForView();
|
||||||
|
if (g_profiles_refresh) {
|
||||||
|
g_profiles_refresh();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void ShowAddProfileDialog() {
|
||||||
|
QDialog dialog;
|
||||||
|
dialog.setWindowTitle(QStringLiteral("添加配置"));
|
||||||
|
auto* layout = new QVBoxLayout(&dialog);
|
||||||
|
auto* form = new QFormLayout();
|
||||||
|
|
||||||
|
auto* name_edit = new QLineEdit(&dialog);
|
||||||
|
|
||||||
|
auto* type_combo = new QComboBox(&dialog);
|
||||||
|
type_combo->addItem(QStringLiteral("压阻A型"));
|
||||||
|
type_combo->addItem(QStringLiteral("压阻B型"));
|
||||||
|
type_combo->addItem(QStringLiteral("霍尔型"));
|
||||||
|
|
||||||
|
auto* width_spin = new QSpinBox(&dialog);
|
||||||
|
auto* height_spin = new QSpinBox(&dialog);
|
||||||
|
width_spin->setRange(1, 64);
|
||||||
|
height_spin->setRange(1, 64);
|
||||||
|
|
||||||
|
auto* left_spin = new QSpinBox(&dialog);
|
||||||
|
auto* right_spin = new QSpinBox(&dialog);
|
||||||
|
left_spin->setRange(-100000, 100000);
|
||||||
|
right_spin->setRange(-100000, 100000);
|
||||||
|
|
||||||
|
auto* baud_spin = new QSpinBox(&dialog);
|
||||||
|
baud_spin->setRange(1200, 10000000);
|
||||||
|
baud_spin->setValue(115200);
|
||||||
|
|
||||||
|
form->addRow(QStringLiteral("名称"), name_edit);
|
||||||
|
form->addRow(QStringLiteral("类型"), type_combo);
|
||||||
|
form->addRow(QStringLiteral("宽"), width_spin);
|
||||||
|
form->addRow(QStringLiteral("高"), height_spin);
|
||||||
|
form->addRow(QStringLiteral("量程左"), left_spin);
|
||||||
|
form->addRow(QStringLiteral("量程右"), right_spin);
|
||||||
|
form->addRow(QStringLiteral("波特率"), baud_spin);
|
||||||
|
|
||||||
|
layout->addLayout(form);
|
||||||
|
|
||||||
|
auto* buttons =
|
||||||
|
new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, Qt::Horizontal, &dialog);
|
||||||
|
layout->addWidget(buttons);
|
||||||
|
QObject::connect(buttons, &QDialogButtonBox::accepted, &dialog, &QDialog::accept);
|
||||||
|
QObject::connect(buttons, &QDialogButtonBox::rejected, &dialog, &QDialog::reject);
|
||||||
|
|
||||||
|
if (dialog.exec() == QDialog::Accepted) {
|
||||||
|
ConfigProfile profile;
|
||||||
|
profile.name = name_edit->text();
|
||||||
|
profile.type = [idx = type_combo->currentIndex()] {
|
||||||
|
switch (idx) {
|
||||||
|
case 0:
|
||||||
|
return Tactile_TYPE::PiezoresistiveA;
|
||||||
|
case 1:
|
||||||
|
return Tactile_TYPE::PiezoresistiveB;
|
||||||
|
default:
|
||||||
|
return Tactile_TYPE::Hall;
|
||||||
|
}
|
||||||
|
}();
|
||||||
|
profile.matrix_width = width_spin->value();
|
||||||
|
profile.matrix_height = height_spin->value();
|
||||||
|
profile.range_left = left_spin->value();
|
||||||
|
profile.range_right = right_spin->value();
|
||||||
|
profile.baud_rate = baud_spin->value();
|
||||||
|
|
||||||
|
GlobalHelper::instance().add_new_profile(profile);
|
||||||
|
GlobalHelper::instance().reload_profiles();
|
||||||
|
if (auto store = g_profiles_store.lock()) {
|
||||||
|
store->set(GlobalHelper::instance().get_all_profile());
|
||||||
|
}
|
||||||
|
RefreshProfilesForView();
|
||||||
|
if (g_profiles_refresh) {
|
||||||
|
g_profiles_refresh();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static auto AddProfileLongItem(creeper::ThemeManager& manager) {
|
||||||
|
return new FilledButton {
|
||||||
|
fbpro::ThemeManager {manager},
|
||||||
|
fbpro::Text {QStringLiteral("添加配置")},
|
||||||
|
widget::pro::SizePolicy {QSizePolicy::Fixed, QSizePolicy::Expanding},
|
||||||
|
widget::pro::MinimumHeight {40},
|
||||||
|
widget::pro::MinimumWidth {320},
|
||||||
|
fbpro::Radius {12},
|
||||||
|
fbpro::Clickable {[]{ ShowAddProfileDialog(); }},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
static auto ImportProfileLongItem(creeper::ThemeManager& manager) {
|
||||||
|
return new FilledButton {
|
||||||
|
fbpro::ThemeManager {manager},
|
||||||
|
fbpro::Text {QStringLiteral("导入配置")},
|
||||||
|
widget::pro::SizePolicy {QSizePolicy::Fixed, QSizePolicy::Expanding},
|
||||||
|
widget::pro::MinimumHeight {40},
|
||||||
|
widget::pro::MinimumWidth {320},
|
||||||
|
fbpro::Radius {12},
|
||||||
|
fbpro::Clickable {[]{
|
||||||
|
const QString file_name = QFileDialog::getOpenFileName(
|
||||||
|
nullptr,
|
||||||
|
QStringLiteral("导入配置"),
|
||||||
|
QString(),
|
||||||
|
QStringLiteral("配置文件 (*.conf);;所有文件 (*.*)"));
|
||||||
|
if (file_name.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
QFile file(file_name);
|
||||||
|
if (!file.open(QIODevice::ReadOnly)) {
|
||||||
|
QMessageBox::warning(nullptr, QStringLiteral("导入配置"),
|
||||||
|
QStringLiteral("无法打开配置文件。"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QByteArray data = file.readAll();
|
||||||
|
file.close();
|
||||||
|
|
||||||
|
QJsonParseError parse_error {};
|
||||||
|
const QJsonDocument doc = QJsonDocument::fromJson(data, &parse_error);
|
||||||
|
if (doc.isNull() || parse_error.error != QJsonParseError::NoError || !doc.isArray()) {
|
||||||
|
QMessageBox::warning(nullptr, QStringLiteral("导入配置"),
|
||||||
|
QStringLiteral("配置文件格式不正确。"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QJsonArray array = doc.array();
|
||||||
|
std::vector<ConfigProfile> imported_profiles;
|
||||||
|
imported_profiles.reserve(static_cast<std::size_t>(array.size()));
|
||||||
|
for (const auto& value : array) {
|
||||||
|
if (!value.isObject()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
ConfigProfile profile {};
|
||||||
|
if (ConfigProfileFromJson(value.toObject(), profile)) {
|
||||||
|
imported_profiles.push_back(profile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (imported_profiles.empty()) {
|
||||||
|
QMessageBox::warning(nullptr, QStringLiteral("导入配置"),
|
||||||
|
QStringLiteral("配置文件中没有有效的配置。"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto& helper = GlobalHelper::instance();
|
||||||
|
|
||||||
|
// 清空现有配置
|
||||||
|
const auto existing = helper.get_all_profile();
|
||||||
|
for (const auto& p : existing) {
|
||||||
|
helper.remove_profile(p.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 写入新配置到 ini
|
||||||
|
for (const auto& p : imported_profiles) {
|
||||||
|
helper.add_new_profile(p);
|
||||||
|
}
|
||||||
|
helper.reload_profiles();
|
||||||
|
|
||||||
|
if (auto store = g_profiles_store.lock()) {
|
||||||
|
store->set(helper.get_all_profile());
|
||||||
|
}
|
||||||
|
RefreshProfilesForView();
|
||||||
|
if (g_profiles_refresh) {
|
||||||
|
g_profiles_refresh();
|
||||||
|
}
|
||||||
|
}},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
static auto ExportProfileLongItem(creeper::ThemeManager& manager) {
|
||||||
|
return new FilledButton {
|
||||||
|
fbpro::ThemeManager {manager},
|
||||||
|
fbpro::Text {QStringLiteral("导出配置")},
|
||||||
|
widget::pro::SizePolicy {QSizePolicy::Fixed, QSizePolicy::Expanding},
|
||||||
|
widget::pro::MinimumHeight {40},
|
||||||
|
widget::pro::MinimumWidth {320},
|
||||||
|
fbpro::Radius {12},
|
||||||
|
fbpro::Clickable {[]{
|
||||||
|
QString file_name = QFileDialog::getSaveFileName(
|
||||||
|
nullptr,
|
||||||
|
QStringLiteral("导出配置"),
|
||||||
|
QString(),
|
||||||
|
QStringLiteral("配置文件 (*.conf);;所有文件 (*.*)"));
|
||||||
|
if (file_name.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!file_name.endsWith(QStringLiteral(".conf"), Qt::CaseInsensitive)) {
|
||||||
|
file_name.append(QStringLiteral(".conf"));
|
||||||
|
}
|
||||||
|
|
||||||
|
auto& helper = GlobalHelper::instance();
|
||||||
|
helper.reload_profiles();
|
||||||
|
const auto& profiles = helper.get_all_profile();
|
||||||
|
|
||||||
|
QJsonArray array;
|
||||||
|
// array.reserve(static_cast<int>(profiles.size()));
|
||||||
|
for (const auto& p : profiles) {
|
||||||
|
array.append(ConfigProfileToJson(p));
|
||||||
|
}
|
||||||
|
|
||||||
|
const QJsonDocument doc(array);
|
||||||
|
|
||||||
|
QFile file(file_name);
|
||||||
|
if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
|
||||||
|
QMessageBox::warning(nullptr, QStringLiteral("导出配置"),
|
||||||
|
QStringLiteral("无法写入配置文件。"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
file.write(doc.toJson(QJsonDocument::Indented));
|
||||||
|
file.close();
|
||||||
|
|
||||||
|
QMessageBox::information(nullptr, QStringLiteral("导出配置"),
|
||||||
|
QStringLiteral("配置导出完成。"));
|
||||||
|
}},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
static auto ProfileItemComponent(creeper::ThemeManager& manager, ConfigProfile& profile,
|
||||||
|
const std::shared_ptr<MutableValue<std::vector<ConfigProfile>>>& profiles_store) {
|
||||||
|
QString matrix_size = "规格:" + QString{ "%1 * %2" }.arg(profile.matrix_width).arg(profile.matrix_height);
|
||||||
|
QString value_range = "量程:" + QString{ "%1 ~ %2" }.arg(profile.range_left).arg(profile.range_right);
|
||||||
|
QString tactile_type = [profile]() -> QString {
|
||||||
|
switch (profile.type) {
|
||||||
|
case Tactile_TYPE::PiezoresistiveA:
|
||||||
|
return "类型:压阻A型";
|
||||||
|
case Tactile_TYPE::PiezoresistiveB:
|
||||||
|
return "类型:压阻B型";
|
||||||
|
case Tactile_TYPE::Hall:
|
||||||
|
return "类型:霍尔型";
|
||||||
|
}
|
||||||
|
return "错误";
|
||||||
|
}();
|
||||||
|
QString baud_rate = "波特率:" + QString::number(profile.baud_rate);
|
||||||
|
return new FilledCard{
|
||||||
|
card::pro::SizePolicy {
|
||||||
|
QSizePolicy::Expanding,
|
||||||
|
},
|
||||||
|
card::pro::MinimumSize {
|
||||||
|
300, 50
|
||||||
|
},
|
||||||
|
// card::pro::FixedSize {
|
||||||
|
// 200, 100
|
||||||
|
// },
|
||||||
|
card::pro::ThemeManager{ manager },
|
||||||
|
card::pro::SizePolicy { QSizePolicy::Expanding },
|
||||||
|
card::pro::LevelLow,
|
||||||
|
card::pro::Layout<Col>{
|
||||||
|
lnpro::Item<Row>{
|
||||||
|
// row::pro::Stretch {1},
|
||||||
|
row::pro::Item<Text>{
|
||||||
|
text::pro::AdjustSize {},
|
||||||
|
text::pro::Text{ QString{ profile.name } },
|
||||||
|
text::pro::Apply{
|
||||||
|
[&manager](Text& self) {
|
||||||
|
manager.append_handler(&self, [&](const ThemeManager& manager) {
|
||||||
|
const auto scheme = manager.color_scheme();
|
||||||
|
self.set_color(scheme.primary);
|
||||||
|
});
|
||||||
|
} },
|
||||||
|
},
|
||||||
|
row::pro::Item<IconButton>{
|
||||||
|
ibpro::ThemeManager{ manager },
|
||||||
|
ibpro::FixedSize{ 30, 30 },
|
||||||
|
ibpro::Font{ material::kRegularExtraSmallFont },
|
||||||
|
icon_button::pro::FontIcon{ material::icon::kBorderColor },
|
||||||
|
ibpro::Clickable{ [profiles_store, current = profile] {
|
||||||
|
ShowEditProfileDialog(current, profiles_store);
|
||||||
|
} },
|
||||||
|
},
|
||||||
|
row::pro::Item<IconButton>{
|
||||||
|
ibpro::ThemeManager{ manager },
|
||||||
|
ibpro::FixedSize{ 30, 30 },
|
||||||
|
ibpro::Font{ material::kRegularExtraSmallFont },
|
||||||
|
icon_button::pro::FontIcon{ material::icon::kDelete },
|
||||||
|
ibpro::Clickable{ [profiles_store, name = profile.name, &manager] {
|
||||||
|
GlobalHelper::instance().remove_profile(name);
|
||||||
|
GlobalHelper::instance().reload_profiles();
|
||||||
|
if (profiles_store) {
|
||||||
|
profiles_store->set(GlobalHelper::instance().get_all_profile());
|
||||||
|
}
|
||||||
|
RefreshProfilesForView();
|
||||||
|
manager.apply_theme();
|
||||||
|
} },
|
||||||
|
}
|
||||||
|
},
|
||||||
|
lnpro::Item<Text>{
|
||||||
|
text::pro::Text{ tactile_type },
|
||||||
|
text::pro::Apply{
|
||||||
|
[&manager](Text& self) {
|
||||||
|
manager.append_handler(&self, [&](const ThemeManager& manager) {
|
||||||
|
const auto scheme = manager.color_scheme();
|
||||||
|
self.set_color(scheme.primary);
|
||||||
|
});
|
||||||
|
} } },
|
||||||
|
lnpro::Item<Text>{
|
||||||
|
text::pro::AdjustSize {},
|
||||||
|
text::pro::Text{ matrix_size },
|
||||||
|
text::pro::Apply{
|
||||||
|
[&manager](Text& self) {
|
||||||
|
manager.append_handler(&self, [&](const ThemeManager& manager) {
|
||||||
|
const auto scheme = manager.color_scheme();
|
||||||
|
self.set_color(scheme.primary);
|
||||||
|
});
|
||||||
|
} }
|
||||||
|
},
|
||||||
|
lnpro::Item<Text>{
|
||||||
|
text::pro::Text{ value_range },
|
||||||
|
text::pro::Apply{
|
||||||
|
[&manager](Text& self) {
|
||||||
|
manager.append_handler(&self, [&](const ThemeManager& manager) {
|
||||||
|
const auto scheme = manager.color_scheme();
|
||||||
|
self.set_color(scheme.primary);
|
||||||
|
});
|
||||||
|
} } },
|
||||||
|
lnpro::Item<Text>{
|
||||||
|
text::pro::Text{ baud_rate },
|
||||||
|
text::pro::Apply{
|
||||||
|
[&manager](Text& self) {
|
||||||
|
manager.append_handler(&self, [&](const ThemeManager& manager) {
|
||||||
|
const auto scheme = manager.color_scheme();
|
||||||
|
self.set_color(scheme.primary);
|
||||||
|
});
|
||||||
|
} }
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static void PopulateProfiles(Flow& flow, creeper::ThemeManager& manager,
|
||||||
|
const std::vector<ConfigProfile>& profiles,
|
||||||
|
const std::shared_ptr<MutableValue<std::vector<ConfigProfile>>>& profiles_store) {
|
||||||
|
while (auto item = flow.takeAt(0)) {
|
||||||
|
if (auto* w = item->widget()) {
|
||||||
|
w->deleteLater();
|
||||||
|
}
|
||||||
|
delete item;
|
||||||
|
}
|
||||||
|
using namespace repest_literals;
|
||||||
|
profiles.size() * [&](auto i) {
|
||||||
|
flow.addWidget(ProfileItemComponent(manager, const_cast<ConfigProfile&>(profiles[i]), profiles_store));
|
||||||
|
};
|
||||||
|
flow.update();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void AttachProfilesObserver(const std::shared_ptr<MutableValue<std::vector<ConfigProfile>>>& store,
|
||||||
|
Flow& flow, creeper::ThemeManager& manager) {
|
||||||
|
struct Functor : creeper::MutableValue<std::vector<ConfigProfile>>::Functor {
|
||||||
|
Flow& flow;
|
||||||
|
creeper::ThemeManager& manager;
|
||||||
|
std::weak_ptr<MutableValue<std::vector<ConfigProfile>>> store_ptr;
|
||||||
|
Functor(Flow& f, creeper::ThemeManager& m,
|
||||||
|
std::weak_ptr<MutableValue<std::vector<ConfigProfile>>> s) noexcept
|
||||||
|
: flow(f)
|
||||||
|
, manager(m)
|
||||||
|
, store_ptr(std::move(s)) { }
|
||||||
|
void update(const std::vector<ConfigProfile>& value) override {
|
||||||
|
PopulateProfiles(flow, manager, value, store_ptr.lock());
|
||||||
|
manager.apply_theme();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
auto functor = std::make_unique<Functor>(flow, manager, store);
|
||||||
|
store->callbacks[&flow] = std::move(functor);
|
||||||
|
auto alive = std::weak_ptr { store->alive };
|
||||||
|
QObject::connect(&flow, &QObject::destroyed, [store, alive](auto* key) {
|
||||||
|
if (alive.lock()) store->callbacks.erase(key);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
auto SettingComponent(SettingComponentState& state) noexcept -> raw_pointer<QWidget> {
|
||||||
|
auto profiles_ctx = std::make_shared<MutableValue<std::vector<ConfigProfile>>>(
|
||||||
|
GlobalHelper::instance().get_all_profile()
|
||||||
|
);
|
||||||
|
g_profiles_store = profiles_ctx;
|
||||||
|
g_profiles_refresh = [profiles_ctx]() {
|
||||||
|
if (auto store = profiles_ctx) {
|
||||||
|
store->set(GlobalHelper::instance().get_all_profile());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
auto* profiles_flow = new Flow {
|
||||||
|
flow::pro::RowSpacing{ 10 },
|
||||||
|
flow::pro::ColSpacing{ 10 },
|
||||||
|
flow::pro::RowLimit{ 10 },
|
||||||
|
};
|
||||||
|
PopulateProfiles(*profiles_flow, state.manager, profiles_ctx->get(), profiles_ctx);
|
||||||
|
AttachProfilesObserver(profiles_ctx, *profiles_flow, state.manager);
|
||||||
|
|
||||||
|
return new FilledCard {
|
||||||
|
capro::ThemeManager{ state.manager },
|
||||||
|
capro::SizePolicy{ QSizePolicy::Expanding },
|
||||||
|
capro::Layout<Col>{
|
||||||
|
lnpro::Alignment {Qt::AlignTop},
|
||||||
|
lnpro::Margin {10},
|
||||||
|
lnpro::Spacing {10},
|
||||||
|
lnpro::Item<Row> {
|
||||||
|
lnpro::Item{
|
||||||
|
AddProfileLongItem(state.manager)
|
||||||
|
},
|
||||||
|
lnpro::Item {
|
||||||
|
ImportProfileLongItem(state.manager)
|
||||||
|
},
|
||||||
|
lnpro::Item {
|
||||||
|
ExportProfileLongItem(state.manager)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
col::pro::Item<ScrollArea>{
|
||||||
|
scroll::pro::ThemeManager { state.manager },
|
||||||
|
scroll::pro::HorizontalScrollBarPolicy { Qt::ScrollBarAlwaysOff },
|
||||||
|
scroll::pro::Item {
|
||||||
|
profiles_flow
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
1122
components/view.cc
1122
components/view.cc
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "modern-qt/utility/wrapper/common.hh"
|
#include "creeper-qt/utility/wrapper/common.hh"
|
||||||
#include "modern-qt/utility/wrapper/property.hh"
|
#include "creeper-qt/utility/wrapper/property.hh"
|
||||||
#include <qapplication.h>
|
#include <qapplication.h>
|
||||||
#include <qcoreapplication.h>
|
#include <qcoreapplication.h>
|
||||||
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include "modern-qt/utility/wrapper/common.hh"
|
#include "creeper-qt/utility/wrapper/common.hh"
|
||||||
#include "modern-qt/utility/wrapper/layout.hh"
|
#include "creeper-qt/utility/wrapper/layout.hh"
|
||||||
#include "modern-qt/utility/wrapper/pimpl.hh"
|
#include "creeper-qt/utility/wrapper/pimpl.hh"
|
||||||
#include <qlayout.h>
|
#include <qlayout.h>
|
||||||
|
|
||||||
namespace creeper::flow::internal {
|
namespace creeper::flow::internal {
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "modern-qt/utility/wrapper/common.hh"
|
#include "creeper-qt/utility/wrapper/common.hh"
|
||||||
#include "modern-qt/utility/wrapper/property.hh"
|
#include "creeper-qt/utility/wrapper/property.hh"
|
||||||
|
|
||||||
#include <qgridlayout.h>
|
#include <qgridlayout.h>
|
||||||
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include "modern-qt/utility/trait/widget.hh"
|
#include "creeper-qt/utility/trait/widget.hh"
|
||||||
#include "modern-qt/utility/wrapper/common.hh"
|
#include "creeper-qt/utility/wrapper/common.hh"
|
||||||
#include "modern-qt/utility/wrapper/property.hh"
|
#include "creeper-qt/utility/wrapper/property.hh"
|
||||||
|
|
||||||
namespace creeper::group::internal {
|
namespace creeper::group::internal {
|
||||||
|
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "modern-qt/utility/trait/widget.hh"
|
#include "creeper-qt/utility/trait/widget.hh"
|
||||||
#include "modern-qt/utility/wrapper/common.hh"
|
#include "creeper-qt/utility/wrapper/common.hh"
|
||||||
#include "modern-qt/utility/wrapper/layout.hh"
|
#include "creeper-qt/utility/wrapper/layout.hh"
|
||||||
#include "modern-qt/utility/wrapper/property.hh"
|
#include "creeper-qt/utility/wrapper/property.hh"
|
||||||
|
|
||||||
#include <qboxlayout.h>
|
#include <qboxlayout.h>
|
||||||
|
#include <qstackedlayout.h>
|
||||||
|
|
||||||
namespace creeper::linear::pro {
|
namespace creeper::linear::pro {
|
||||||
|
|
||||||
@@ -98,10 +99,18 @@ using Col = BoxLayout<QVBoxLayout>;
|
|||||||
namespace row = linear;
|
namespace row = linear;
|
||||||
namespace col = linear;
|
namespace col = linear;
|
||||||
|
|
||||||
|
using HBoxLayout = Row;
|
||||||
|
using VBoxLayout = Col;
|
||||||
|
|
||||||
|
namespace h_box_layout = linear;
|
||||||
|
namespace v_box_layout = linear;
|
||||||
|
|
||||||
namespace internal {
|
namespace internal {
|
||||||
inline auto use_the_namespace_alias_to_eliminate_warnings() {
|
inline auto use_the_namespace_alias_to_eliminate_warnings() {
|
||||||
std::ignore = row::pro::Token {};
|
std::ignore = row::pro::Token {};
|
||||||
std::ignore = col::pro::Token {};
|
std::ignore = col::pro::Token {};
|
||||||
|
std::ignore = h_box_layout::pro::Token {};
|
||||||
|
std::ignore = v_box_layout::pro::Token {};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3,10 +3,10 @@
|
|||||||
#include <qpainter.h>
|
#include <qpainter.h>
|
||||||
#include <qpainterpath.h>
|
#include <qpainterpath.h>
|
||||||
|
|
||||||
#include "modern-qt/utility/animation/animatable.hh"
|
#include "creeper-qt/utility/animation/animatable.hh"
|
||||||
#include "modern-qt/utility/animation/state/pid.hh"
|
#include "creeper-qt/utility/animation/state/pid.hh"
|
||||||
#include "modern-qt/utility/animation/transition.hh"
|
#include "creeper-qt/utility/animation/transition.hh"
|
||||||
#include "modern-qt/utility/wrapper/widget.hh"
|
#include "creeper-qt/utility/wrapper/widget.hh"
|
||||||
|
|
||||||
namespace creeper::mixer::internal {
|
namespace creeper::mixer::internal {
|
||||||
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include <modern-qt/layout/group.hh>
|
#include <creeper-qt/layout/group.hh>
|
||||||
#include <modern-qt/utility/wrapper/layout.hh>
|
#include <creeper-qt/utility/wrapper/layout.hh>
|
||||||
#include <ranges>
|
#include <ranges>
|
||||||
|
|
||||||
namespace creeper::mutual_exclusion_group::internal {
|
namespace creeper::mutual_exclusion_group::internal {
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include "modern-qt/utility/theme/theme.hh"
|
#include "creeper-qt/utility/theme/theme.hh"
|
||||||
#include "modern-qt/utility/trait/widget.hh"
|
#include "creeper-qt/utility/trait/widget.hh"
|
||||||
#include "modern-qt/utility/wrapper/common.hh"
|
#include "creeper-qt/utility/wrapper/common.hh"
|
||||||
#include "modern-qt/utility/wrapper/property.hh"
|
#include "creeper-qt/utility/wrapper/property.hh"
|
||||||
#include "modern-qt/widget/widget.hh"
|
#include "creeper-qt/widget/widget.hh"
|
||||||
#include <qscrollarea.h>
|
#include <qscrollarea.h>
|
||||||
#include <qscrollbar.h>
|
#include <qscrollbar.h>
|
||||||
|
|
||||||
@@ -13,8 +13,8 @@ namespace creeper::scroll::internal {
|
|||||||
class ScrollArea : public QScrollArea {
|
class ScrollArea : public QScrollArea {
|
||||||
public:
|
public:
|
||||||
explicit ScrollArea() noexcept {
|
explicit ScrollArea() noexcept {
|
||||||
viewport()->setStyleSheet("background:transparent;");
|
viewport()->setStyleSheet("background:transparent;border:none;");
|
||||||
setStyleSheet("QScrollArea{background:transparent;}");
|
setStyleSheet("QScrollArea{background:transparent;border:none;}");
|
||||||
|
|
||||||
setWidgetResizable(true);
|
setWidgetResizable(true);
|
||||||
}
|
}
|
||||||
53
creeper-qt/layout/stacked.hh
Normal file
53
creeper-qt/layout/stacked.hh
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
#pragma once
|
||||||
|
#include "creeper-qt/utility/trait/widget.hh"
|
||||||
|
#include "creeper-qt/utility/wrapper/common.hh"
|
||||||
|
#include "creeper-qt/utility/wrapper/layout.hh"
|
||||||
|
#include "creeper-qt/utility/wrapper/property.hh"
|
||||||
|
|
||||||
|
#include <concepts>
|
||||||
|
#include <qstackedlayout.h>
|
||||||
|
|
||||||
|
namespace creeper::stacked::internal {
|
||||||
|
class Stacked : public QStackedLayout { };
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace creeper::stacked::pro {
|
||||||
|
|
||||||
|
using Token = common::Token<internal::Stacked>;
|
||||||
|
|
||||||
|
/// @note: currentChanged(int index)
|
||||||
|
template <typename F>
|
||||||
|
using IndexChanged = common::pro::SignalInjection<F, Token, &internal::Stacked::currentChanged>;
|
||||||
|
|
||||||
|
using CurrentIndex =
|
||||||
|
SetterProp<Token, int, [](auto& self, auto index) { self.setCurrentIndex(index); }>;
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
concept trait = std::derived_from<T, Token> || layout::pro::trait<T>;
|
||||||
|
|
||||||
|
template <item_trait T>
|
||||||
|
struct Item : Token {
|
||||||
|
T* item_pointer = nullptr;
|
||||||
|
|
||||||
|
explicit Item(T* pointer) noexcept
|
||||||
|
: item_pointer { pointer } { }
|
||||||
|
|
||||||
|
explicit Item(auto&&... args) noexcept
|
||||||
|
requires std::constructible_from<T, decltype(args)...>
|
||||||
|
: item_pointer { new T { std::forward<decltype(args)>(args)... } } { }
|
||||||
|
|
||||||
|
void apply(stacked_trait auto& layout) const {
|
||||||
|
if constexpr (widget_trait<T>) {
|
||||||
|
layout.addWidget(item_pointer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
CREEPER_DEFINE_CHECKER(trait);
|
||||||
|
using namespace layout::pro;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace creeper {
|
||||||
|
using Stacked = Declarative<stacked::internal::Stacked, stacked::pro::checker>;
|
||||||
|
using NavHost = Stacked;
|
||||||
|
}
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
#include "animatable.hh"
|
#include "animatable.hh"
|
||||||
#include <qdebug.h>
|
|
||||||
using namespace creeper;
|
using namespace creeper;
|
||||||
|
|
||||||
#include <qtimer.h>
|
#include <qtimer.h>
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "modern-qt/utility/wrapper/pimpl.hh"
|
#include "creeper-qt/utility/wrapper/pimpl.hh"
|
||||||
#include <qwidget.h>
|
#include <qwidget.h>
|
||||||
|
|
||||||
namespace creeper {
|
namespace creeper {
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include "modern-qt/utility/animation/math.hh"
|
#include "creeper-qt/utility/animation/math.hh"
|
||||||
#include "modern-qt/utility/animation/state/accessor.hh"
|
#include "creeper-qt/utility/animation/state/accessor.hh"
|
||||||
|
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include "modern-qt/utility/animation/math.hh"
|
#include "creeper-qt/utility/animation/math.hh"
|
||||||
#include "modern-qt/utility/animation/state/accessor.hh"
|
#include "creeper-qt/utility/animation/state/accessor.hh"
|
||||||
|
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include "modern-qt/utility/animation/math.hh"
|
#include "creeper-qt/utility/animation/math.hh"
|
||||||
#include "modern-qt/utility/animation/state/accessor.hh"
|
#include "creeper-qt/utility/animation/state/accessor.hh"
|
||||||
|
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "modern-qt/utility/animation/state/accessor.hh"
|
#include "creeper-qt/utility/animation/state/accessor.hh"
|
||||||
#include "modern-qt/utility/animation/transition.hh"
|
#include "creeper-qt/utility/animation/transition.hh"
|
||||||
|
|
||||||
#include <QColor>
|
#include <QColor>
|
||||||
#include <QPainter>
|
#include <QPainter>
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include <qdebug.h>
|
|
||||||
#include <qpixmap.h>
|
#include <qpixmap.h>
|
||||||
|
|
||||||
namespace creeper {
|
namespace creeper {
|
||||||
@@ -27,15 +27,12 @@ namespace material {
|
|||||||
private:
|
private:
|
||||||
static QString get_font_family(int fontId, const QString& fallback) {
|
static QString get_font_family(int fontId, const QString& fallback) {
|
||||||
if (fontId == -1) {
|
if (fontId == -1) {
|
||||||
qWarning() << "Failed to load font:" << fallback;
|
|
||||||
return fallback;
|
return fallback;
|
||||||
}
|
}
|
||||||
QStringList families = QFontDatabase::applicationFontFamilies(fontId);
|
QStringList families = QFontDatabase::applicationFontFamilies(fontId);
|
||||||
if (families.isEmpty()) {
|
if (families.isEmpty()) {
|
||||||
qWarning() << "No families found for font:" << fallback;
|
|
||||||
return fallback;
|
return fallback;
|
||||||
}
|
}
|
||||||
qDebug() << "families found for font:" << families;
|
|
||||||
return families.first();
|
return families.first();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -99,6 +96,13 @@ namespace material {
|
|||||||
inline const auto kOutlinedLargeFont = outlined::font_3;
|
inline const auto kOutlinedLargeFont = outlined::font_3;
|
||||||
inline const auto kOutlinedExtraLargeFont = outlined::font_4;
|
inline const auto kOutlinedExtraLargeFont = outlined::font_4;
|
||||||
|
|
||||||
|
constexpr auto kRegularFontName = regular::font;
|
||||||
|
inline const auto kRegularExtraSmallFont = regular::font_0;
|
||||||
|
inline const auto kRegularSmallFont = regular::font_1;
|
||||||
|
inline const auto kRegularMediumFont = regular::font_2;
|
||||||
|
inline const auto kRegularLargeFont = regular::font_3;
|
||||||
|
inline const auto kRegularExtraLargeFont = regular::font_4;
|
||||||
|
|
||||||
namespace icon {
|
namespace icon {
|
||||||
|
|
||||||
// Function
|
// Function
|
||||||
@@ -114,6 +118,7 @@ namespace material {
|
|||||||
constexpr auto kNotifications = "notifications";
|
constexpr auto kNotifications = "notifications";
|
||||||
constexpr auto kDashboard = "dashboard";
|
constexpr auto kDashboard = "dashboard";
|
||||||
constexpr auto kExtension = "extension";
|
constexpr auto kExtension = "extension";
|
||||||
|
constexpr auto kPets = "pets";
|
||||||
|
|
||||||
// Shape
|
// Shape
|
||||||
constexpr auto kFavorite = "favorite";
|
constexpr auto kFavorite = "favorite";
|
||||||
@@ -141,8 +146,10 @@ namespace material {
|
|||||||
constexpr auto kCancel = "cancel";
|
constexpr auto kCancel = "cancel";
|
||||||
constexpr auto kOpenInNew = "open_in_new";
|
constexpr auto kOpenInNew = "open_in_new";
|
||||||
constexpr auto kLogout = "logout";
|
constexpr auto kLogout = "logout";
|
||||||
|
constexpr auto k123 = "123";
|
||||||
constexpr auto kRoutine = "routine";
|
constexpr auto kRoutine = "routine";
|
||||||
constexpr auto kDarkMode = "dark_mode";
|
constexpr auto kDarkMode = "dark_mode";
|
||||||
|
constexpr auto kFileExport = "file_export";
|
||||||
|
|
||||||
// File
|
// File
|
||||||
constexpr auto kFolder = "folder";
|
constexpr auto kFolder = "folder";
|
||||||
@@ -164,6 +171,9 @@ namespace material {
|
|||||||
|
|
||||||
// sensor icon
|
// sensor icon
|
||||||
constexpr auto kTouchSensor = "touch_app";
|
constexpr auto kTouchSensor = "touch_app";
|
||||||
|
|
||||||
|
// setting page
|
||||||
|
constexpr auto kBorderColor = "border_color";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include "modern-qt/utility/wrapper/property.hh"
|
#include "creeper-qt/utility/wrapper/property.hh"
|
||||||
|
|
||||||
#include <qpainter.h>
|
#include <qpainter.h>
|
||||||
|
|
||||||
@@ -14,6 +14,7 @@ using align = Qt::Alignment;
|
|||||||
using string = QString;
|
using string = QString;
|
||||||
using font = QFont;
|
using font = QFont;
|
||||||
using text_option = QTextOption;
|
using text_option = QTextOption;
|
||||||
|
using icon = QIcon;
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace creeper::painter {
|
namespace creeper::painter {
|
||||||
@@ -1,14 +1,12 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include "modern-qt/utility/painter/common.hh"
|
#include "creeper-qt/utility/painter/common.hh"
|
||||||
|
|
||||||
namespace creeper::painter {
|
namespace creeper::painter {
|
||||||
|
|
||||||
// 核心容器结构体,现在继承自 Impl,使其满足 drawable_trait (假设 Impl 继承了所需的属性)
|
|
||||||
template <class Impl, drawable_trait... Ts>
|
template <class Impl, drawable_trait... Ts>
|
||||||
struct Container : public Impl {
|
struct Container : public Impl {
|
||||||
std::tuple<std::decay_t<Ts>...> drawable;
|
std::tuple<std::decay_t<Ts>...> drawable;
|
||||||
|
|
||||||
// 唯一构造函数:接受 Impl 实例和可变参数包
|
|
||||||
constexpr explicit Container(const Impl& impl, Ts&&... drawable)
|
constexpr explicit Container(const Impl& impl, Ts&&... drawable)
|
||||||
: Impl { impl }
|
: Impl { impl }
|
||||||
, drawable { std::make_tuple(std::forward<Ts>(drawable)...) } { }
|
, drawable { std::make_tuple(std::forward<Ts>(drawable)...) } { }
|
||||||
@@ -47,7 +45,7 @@ struct MakeLayoutFunction {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// ----------------------------------------------------------------------
|
// ----------------------------------------------------------------------
|
||||||
// SurfaceImpl (仅平移)
|
// SurfaceImpl
|
||||||
// ----------------------------------------------------------------------
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
struct SurfaceImpl : public MakeLayoutFunction, ContainerProps {
|
struct SurfaceImpl : public MakeLayoutFunction, ContainerProps {
|
||||||
@@ -162,13 +160,12 @@ struct BoxImpl : public MakeLayoutFunction, ContainerProps {
|
|||||||
// ----------------------------------------------------------------------
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
struct RowImpl : public MakeLayoutFunction, ContainerProps {
|
struct RowImpl : public MakeLayoutFunction, ContainerProps {
|
||||||
// 主轴对齐 (Horizontal)
|
|
||||||
const qt::align main_align;
|
const qt::align main_align;
|
||||||
|
|
||||||
constexpr explicit RowImpl(
|
constexpr explicit RowImpl(
|
||||||
const qt::size& size,
|
const qt::size& size,
|
||||||
const qt::align& main_align = Qt::AlignLeft, // 主轴对齐:AlignLeft/AlignRight/AlignHCenter
|
const qt::align& main_align = Qt::AlignLeft,
|
||||||
const qt::align& cross_align = Qt::AlignVCenter, // 非主轴对齐:AlignTop/AlignBottom/AlignVCenter
|
const qt::align& cross_align = Qt::AlignVCenter,
|
||||||
const qt::point& origin = {})
|
const qt::point& origin = {})
|
||||||
: ContainerProps {
|
: ContainerProps {
|
||||||
.size = size,
|
.size = size,
|
||||||
@@ -207,7 +204,7 @@ struct RowImpl : public MakeLayoutFunction, ContainerProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
auto make(drawable_trait auto& drawable) {
|
auto make(drawable_trait auto& drawable) {
|
||||||
const auto container_cross_align = align; // 非主轴对齐 (垂直)
|
const auto container_cross_align = align;
|
||||||
const auto container_size = size;
|
const auto container_size = size;
|
||||||
const auto container_origin = origin;
|
const auto container_origin = origin;
|
||||||
|
|
||||||
@@ -235,17 +232,16 @@ struct RowImpl : public MakeLayoutFunction, ContainerProps {
|
|||||||
// ----------------------------------------------------------------------
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
struct ColImpl : public MakeLayoutFunction, ContainerProps {
|
struct ColImpl : public MakeLayoutFunction, ContainerProps {
|
||||||
// 主轴对齐 (Vertical)
|
|
||||||
const qt::align main_align;
|
const qt::align main_align;
|
||||||
|
|
||||||
constexpr explicit ColImpl(
|
constexpr explicit ColImpl(
|
||||||
const qt::size& size,
|
const qt::size& size,
|
||||||
const qt::align& main_align = Qt::AlignTop, // 主轴对齐:AlignTop/AlignBottom/AlignVCenter
|
const qt::align& main_align = Qt::AlignTop,
|
||||||
const qt::align& cross_align = Qt::AlignHCenter, // 非主轴对齐:AlignLeft/AlignRight/AlignHCenter
|
const qt::align& cross_align = Qt::AlignHCenter,
|
||||||
const qt::point& origin = {})
|
const qt::point& origin = {})
|
||||||
: ContainerProps {
|
: ContainerProps {
|
||||||
.size = size,
|
.size = size,
|
||||||
.align = cross_align, // ContainerProps::align 存储非主轴对齐
|
.align = cross_align,
|
||||||
.origin = origin,
|
.origin = origin,
|
||||||
}
|
}
|
||||||
, main_align(main_align) // 存储主轴对齐
|
, main_align(main_align) // 存储主轴对齐
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
#include "modern-qt/utility/painter/helper.hh"
|
#include "creeper-qt/utility/painter/helper.hh"
|
||||||
#include <qdebug.h>
|
#include <qdebug.h>
|
||||||
|
|
||||||
namespace creeper::util {
|
namespace creeper::util {
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include "modern-qt/utility/painter/common.hh"
|
#include "creeper-qt/utility/painter/common.hh"
|
||||||
|
#include <qicon.h>
|
||||||
#include <qpainterpath.h>
|
#include <qpainterpath.h>
|
||||||
|
|
||||||
namespace creeper::painter::internal {
|
namespace creeper::painter::internal {
|
||||||
@@ -160,11 +161,52 @@ struct Text : CommonProps {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct Icon : CommonProps {
|
||||||
|
using IconSource = qt::icon;
|
||||||
|
using FontSource = std::tuple<qt::string, qt::string>;
|
||||||
|
using MutiSource = std::variant<FontSource, IconSource>;
|
||||||
|
|
||||||
|
MutiSource source;
|
||||||
|
qt::color color = Qt::black;
|
||||||
|
|
||||||
|
auto operator()(qt::painter& painter) const noexcept {
|
||||||
|
if (size.isEmpty()) return;
|
||||||
|
|
||||||
|
painter.save();
|
||||||
|
|
||||||
|
const auto rect = qt::rect { origin, size };
|
||||||
|
std::visit(
|
||||||
|
[&](auto& value) {
|
||||||
|
using T = std::decay_t<decltype(value)>;
|
||||||
|
|
||||||
|
if constexpr (std::is_same_v<T, FontSource>) {
|
||||||
|
const auto& [font_family, code] = value;
|
||||||
|
|
||||||
|
auto font = qt::font { font_family };
|
||||||
|
font.setPointSizeF(std::min(size.height(), size.width()));
|
||||||
|
|
||||||
|
auto option = qt::text_option {};
|
||||||
|
option.setAlignment(Qt::AlignCenter);
|
||||||
|
|
||||||
|
painter.setFont(font);
|
||||||
|
painter.setPen(color);
|
||||||
|
painter.setBrush(Qt::NoBrush);
|
||||||
|
painter.drawText(rect, code, option);
|
||||||
|
} else if constexpr (std::is_same_v<T, IconSource>) {
|
||||||
|
const auto& icon_source = value;
|
||||||
|
icon_source.paint(&painter, rect.toRect());
|
||||||
|
}
|
||||||
|
},
|
||||||
|
source);
|
||||||
|
|
||||||
|
painter.restore();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
namespace creeper::painter {
|
namespace creeper::painter {
|
||||||
|
|
||||||
/// Export Rounded Rectangle
|
/// Export Rounded Rectangle
|
||||||
|
|
||||||
using RadiusTL = SetterProp<common::pro::Token, double,
|
using RadiusTL = SetterProp<common::pro::Token, double,
|
||||||
[](auto& self, auto radius) { self.radius_tl = radius; }>;
|
[](auto& self, auto radius) { self.radius_tl = radius; }>;
|
||||||
using RadiusTR = SetterProp<common::pro::Token, double,
|
using RadiusTR = SetterProp<common::pro::Token, double,
|
||||||
@@ -189,12 +231,27 @@ using Scale = SetterProp<common::pro::Token, qt::real,
|
|||||||
using TextOption = DerivedProp<common::pro::Token, qt::text_option,
|
using TextOption = DerivedProp<common::pro::Token, qt::text_option,
|
||||||
[](auto& self, const auto& option) { self.text_option = option; }>;
|
[](auto& self, const auto& option) { self.text_option = option; }>;
|
||||||
|
|
||||||
|
/// Export Icon
|
||||||
|
struct Icon : common::pro::Token {
|
||||||
|
using T = internal::Icon;
|
||||||
|
T::MutiSource source;
|
||||||
|
|
||||||
|
constexpr explicit Icon(const qt::string& font, const qt::string& code) noexcept
|
||||||
|
: source { T::FontSource { font, code } } { }
|
||||||
|
|
||||||
|
constexpr explicit Icon(const qt::icon& icon) noexcept
|
||||||
|
: source { T::IconSource { icon } } { }
|
||||||
|
|
||||||
|
auto apply(auto& self) const noexcept { self.source = source; }
|
||||||
|
};
|
||||||
|
|
||||||
namespace Paint {
|
namespace Paint {
|
||||||
using EraseRectangle = Declarative<internal::EraseRectangle, CheckerOr<common::pro::checker>>;
|
using EraseRectangle = Declarative<internal::EraseRectangle, CheckerOr<common::pro::checker>>;
|
||||||
using Rectangle = Declarative<internal::Rectangle, CheckerOr<common::pro::checker>>;
|
using Rectangle = Declarative<internal::Rectangle, CheckerOr<common::pro::checker>>;
|
||||||
using RoundedRectangle =
|
using RoundedRectangle =
|
||||||
Declarative<internal::RoundedRectangle, CheckerOr<common::pro::checker>>;
|
Declarative<internal::RoundedRectangle, CheckerOr<common::pro::checker>>;
|
||||||
using Text = Declarative<internal::Text, CheckerOr<common::pro::checker>>;
|
using Text = Declarative<internal::Text, CheckerOr<common::pro::checker>>;
|
||||||
|
using Icon = Declarative<internal::Icon, CheckerOr<common::pro::checker>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "modern-qt/utility/theme/theme.hh"
|
#include "creeper-qt/utility/theme/theme.hh"
|
||||||
|
|
||||||
namespace creeper {
|
namespace creeper {
|
||||||
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "modern-qt/utility/theme/theme.hh"
|
#include "creeper-qt/utility/theme/theme.hh"
|
||||||
|
|
||||||
namespace creeper {
|
namespace creeper {
|
||||||
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "modern-qt/utility/theme/theme.hh"
|
#include "creeper-qt/utility/theme/theme.hh"
|
||||||
|
|
||||||
namespace creeper {
|
namespace creeper {
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
#include "modern-qt/utility/theme/theme.hh"
|
#include "creeper-qt/utility/theme/theme.hh"
|
||||||
|
|
||||||
using namespace creeper::theme;
|
using namespace creeper::theme;
|
||||||
using Handler = ThemeManager::Handler;
|
using Handler = ThemeManager::Handler;
|
||||||
@@ -2,15 +2,10 @@
|
|||||||
|
|
||||||
#include <qwidget.h>
|
#include <qwidget.h>
|
||||||
|
|
||||||
// #include "utility/theme/color-scheme.hh"
|
#include "creeper-qt/utility/theme/color-scheme.hh"
|
||||||
// #include "utility/wrapper/common.hh"
|
#include "creeper-qt/utility/wrapper/common.hh"
|
||||||
// #include "utility/wrapper/pimpl.hh"
|
#include "creeper-qt/utility/wrapper/pimpl.hh"
|
||||||
// #include "utility/wrapper/property.hh"
|
#include "creeper-qt/utility/wrapper/property.hh"
|
||||||
#include "modern-qt/utility/theme/color-scheme.hh"
|
|
||||||
#include "modern-qt/utility/wrapper/common.hh"
|
|
||||||
#include "modern-qt/utility/wrapper/pimpl.hh"
|
|
||||||
#include "modern-qt/utility/wrapper/property.hh"
|
|
||||||
#include <memory>
|
|
||||||
|
|
||||||
namespace creeper::theme {
|
namespace creeper::theme {
|
||||||
|
|
||||||
@@ -32,17 +27,8 @@ struct ThemePack {
|
|||||||
};
|
};
|
||||||
|
|
||||||
class ThemeManager {
|
class ThemeManager {
|
||||||
// CREEPER_PIMPL_DEFINITION(ThemeManager)
|
CREEPER_PIMPL_DEFINITION(ThemeManager)
|
||||||
public:
|
public:
|
||||||
ThemeManager();
|
|
||||||
~ThemeManager();
|
|
||||||
ThemeManager(const ThemeManager&) = delete;
|
|
||||||
ThemeManager& operator=(const ThemeManager&) = delete;
|
|
||||||
private:
|
|
||||||
struct Impl;
|
|
||||||
std::unique_ptr<Impl> pimpl;
|
|
||||||
public:
|
|
||||||
|
|
||||||
explicit ThemeManager(const ThemePack& pack, ColorMode mode = ColorMode::LIGHT);
|
explicit ThemeManager(const ThemePack& pack, ColorMode mode = ColorMode::LIGHT);
|
||||||
|
|
||||||
void apply_theme() const;
|
void apply_theme() const;
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include <concepts>
|
#include <concepts>
|
||||||
#include <qwidget.h>
|
#include <qwidget.h>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
namespace creeper {
|
namespace creeper {
|
||||||
|
|
||||||
@@ -25,6 +26,12 @@ concept linear_trait = requires(T t) {
|
|||||||
{ t.addLayout(std::declval<QLayout*>(), int {}) };
|
{ t.addLayout(std::declval<QLayout*>(), int {}) };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
template <class T>
|
||||||
|
concept stacked_trait = requires(T t) {
|
||||||
|
{t.addWidget(std::declval<QWidget*>())};
|
||||||
|
{t.insertWidget(int {}, std::declval<QWidget*>())};
|
||||||
|
};
|
||||||
|
|
||||||
template <class T>
|
template <class T>
|
||||||
concept area_trait = requires(T t) {
|
concept area_trait = requires(T t) {
|
||||||
{ t.setWidget(std::declval<QWidget*>()) };
|
{ t.setWidget(std::declval<QWidget*>()) };
|
||||||
@@ -1,26 +1,22 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "property.hh"
|
#include "property.hh"
|
||||||
|
|
||||||
#include <qbitmap.h>
|
|
||||||
#include <qvector.h>
|
|
||||||
#include <qdebug.h>
|
|
||||||
#include <qicon.h>
|
|
||||||
#include <qwidget.h>
|
#include <qwidget.h>
|
||||||
|
|
||||||
namespace creeper::common {
|
namespace creeper::common {
|
||||||
|
|
||||||
template <typename Instance>
|
template <typename Instance>
|
||||||
struct Token {
|
struct Token {
|
||||||
void apply(auto& self) const {
|
void apply(auto& self) const {
|
||||||
const auto self_name = typeid(self).name();
|
const auto self_name = typeid(self).name();
|
||||||
const auto prop_name = typeid(this).name();
|
const auto prop_name = typeid(this).name();
|
||||||
qDebug() << "Unimplemented" << prop_name << "is called by" << self_name;
|
// qDebug() << "Unimplemented" << prop_name << "is called by" << self_name;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
namespace pro {
|
namespace pro {
|
||||||
|
|
||||||
// 设置组建透明度
|
// 设置组件透明度
|
||||||
template <class Token>
|
template <class Token>
|
||||||
using Opacity = SetterProp<Token, double, [](auto& self, double v) { self.set_opacity(v); }>;
|
using Opacity = SetterProp<Token, double, [](auto& self, double v) { self.set_opacity(v); }>;
|
||||||
|
|
||||||
@@ -115,44 +111,6 @@ namespace pro {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
template <class Token, typename T, auto setter>
|
|
||||||
struct Vector : public QVector<T>, Token {
|
|
||||||
using QVector<T>::QVector;
|
|
||||||
|
|
||||||
explicit Vector(const QVector<T>& vec) noexcept
|
|
||||||
: QVector<T> { vec } { }
|
|
||||||
explicit Vector(const std::vector<T>& vec) noexcept
|
|
||||||
: QVector<T> { vec } { }
|
|
||||||
|
|
||||||
void apply(auto& self) const
|
|
||||||
requires requires {setter(self, *this); }
|
|
||||||
{
|
|
||||||
setter(self, *this);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
template <class Token, typename T, std::size_t N, auto setter>
|
|
||||||
struct Array : public std::array<T, N>, Token {
|
|
||||||
using std::array<T, N>::array;
|
|
||||||
explicit Array(const std::array<T, N>& arr) noexcept
|
|
||||||
: std::array<T, N> { arr } { }
|
|
||||||
|
|
||||||
template <typename... Args>
|
|
||||||
requires (sizeof...(Args) == N)
|
|
||||||
explicit Array(Args&&... args) noexcept
|
|
||||||
: std::array<T, N> {
|
|
||||||
std::forward<Args>(args)...} {}
|
|
||||||
|
|
||||||
void apply(auto& self) const
|
|
||||||
requires requires {setter(self, *this);}
|
|
||||||
{
|
|
||||||
setter(self, *this);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// template<class Token, typename T, auto setter>
|
|
||||||
// Vector<Token, T, setter>::Vector(const QVector<T> &vec) noexcept:QVector { vec } { }
|
|
||||||
|
|
||||||
template <class Token>
|
template <class Token>
|
||||||
using Text = String<Token, [](auto& self, const auto& string) { self.setText(string); }>;
|
using Text = String<Token, [](auto& self, const auto& string) { self.setText(string); }>;
|
||||||
|
|
||||||
@@ -182,22 +140,7 @@ namespace pro {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
template <typename Callback, class Token>
|
|
||||||
struct IndexChanged : Token {
|
|
||||||
Callback callback;
|
|
||||||
explicit IndexChanged(Callback callback) noexcept
|
|
||||||
: callback {std::move(callback)} {}
|
|
||||||
auto apply(auto& self) const noexcept -> void
|
|
||||||
requires std::invocable<Callback, decltype(self)> || std::invocable<Callback> {
|
|
||||||
using widget_t = std::remove_cvref_t<decltype(self)>;
|
|
||||||
QObject::connect(&self, &widget_t::currentIndexChanged, [function = callback, &self] {
|
|
||||||
if constexpr (std::invocable<Callback, decltype(self)>) function(self);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 自定义信号回调注册
|
// 自定义信号回调注册
|
||||||
|
|
||||||
namespace internal {
|
namespace internal {
|
||||||
template <typename T>
|
template <typename T>
|
||||||
struct FunctionArgs;
|
struct FunctionArgs;
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include "modern-qt/utility/qt_wrapper/margin_setter.hh"
|
#include "creeper-qt/utility/qt_wrapper/margin-setter.hh"
|
||||||
#include "modern-qt/utility/trait/widget.hh"
|
#include "creeper-qt/utility/trait/widget.hh"
|
||||||
#include "modern-qt/utility/wrapper/common.hh"
|
#include "creeper-qt/utility/wrapper/common.hh"
|
||||||
|
|
||||||
namespace creeper::layout::pro {
|
namespace creeper::layout::pro {
|
||||||
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include "modern-qt/utility/wrapper/widget.hh"
|
#include "creeper-qt/utility/wrapper/widget.hh"
|
||||||
#include <qobject.h>
|
#include <qobject.h>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
|
|
||||||
@@ -45,7 +45,7 @@ struct SetterProp : Token {
|
|||||||
requires std::assignable_from<T&, O>
|
requires std::assignable_from<T&, O>
|
||||||
{
|
{
|
||||||
value = std::forward<O>(other);
|
value = std::forward<O>(other);
|
||||||
return *this; // x= y =6
|
return *this;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto apply(auto& self) const noexcept -> void
|
auto apply(auto& self) const noexcept -> void
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "modern-qt/utility/wrapper/common.hh"
|
#include "creeper-qt/utility/wrapper/common.hh"
|
||||||
#include "modern-qt/utility/wrapper/property.hh"
|
#include "creeper-qt/utility/wrapper/property.hh"
|
||||||
|
|
||||||
#include <qgraphicseffect.h>
|
#include <qgraphicseffect.h>
|
||||||
#include <qscreen.h>
|
#include <qscreen.h>
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "modern-qt/utility/wrapper/common.hh"
|
#include "creeper-qt/utility/wrapper/common.hh"
|
||||||
#include "modern-qt/utility/wrapper/property.hh"
|
#include "creeper-qt/utility/wrapper/property.hh"
|
||||||
|
|
||||||
namespace creeper::button::pro {
|
namespace creeper::button::pro {
|
||||||
|
|
||||||
@@ -1,11 +1,13 @@
|
|||||||
#include "filled-button.hh"
|
#include "filled-button.hh"
|
||||||
|
|
||||||
#include "modern-qt/utility/animation/math.hh"
|
#include "creeper-qt/utility/animation/math.hh"
|
||||||
#include "modern-qt/utility/animation/state/pid.hh"
|
#include "creeper-qt/utility/animation/state/pid.hh"
|
||||||
#include "modern-qt/utility/animation/transition.hh"
|
#include "creeper-qt/utility/animation/transition.hh"
|
||||||
#include "modern-qt/utility/animation/water-ripple.hh"
|
#include "creeper-qt/utility/animation/water-ripple.hh"
|
||||||
#include "modern-qt/utility/painter/helper.hh"
|
#include "creeper-qt/utility/painter/helper.hh"
|
||||||
#include "modern-qt/utility/theme/theme.hh"
|
#include "creeper-qt/utility/theme/theme.hh"
|
||||||
|
|
||||||
|
#include <qevent.h>
|
||||||
|
|
||||||
namespace creeper::filled_button::internal {
|
namespace creeper::filled_button::internal {
|
||||||
|
|
||||||
@@ -16,7 +18,7 @@ public:
|
|||||||
bool enable_water_ripple = true;
|
bool enable_water_ripple = true;
|
||||||
double water_ripple_step = 5.;
|
double water_ripple_step = 5.;
|
||||||
|
|
||||||
double radius = 0;
|
double radius = -1;
|
||||||
QColor text_color = Qt::black;
|
QColor text_color = Qt::black;
|
||||||
QColor background = Qt::white;
|
QColor background = Qt::white;
|
||||||
|
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "modern-qt/utility/qt_wrapper/enter_event.hh"
|
#include "creeper-qt/utility/qt_wrapper/enter-event.hh"
|
||||||
#include "modern-qt/utility/theme/theme.hh"
|
#include "creeper-qt/utility/theme/theme.hh"
|
||||||
#include "modern-qt/utility/wrapper/pimpl.hh"
|
#include "creeper-qt/utility/wrapper/pimpl.hh"
|
||||||
#include "modern-qt/utility/wrapper/property.hh"
|
#include "creeper-qt/utility/wrapper/property.hh"
|
||||||
#include "modern-qt/utility/wrapper/widget.hh"
|
#include "creeper-qt/utility/wrapper/widget.hh"
|
||||||
#include "modern-qt/widget/buttons/button.hh"
|
#include "creeper-qt/widget/buttons/button.hh"
|
||||||
#include "qabstractbutton.h"
|
#include "qabstractbutton.h"
|
||||||
|
|
||||||
namespace creeper::filled_button::internal {
|
namespace creeper::filled_button::internal {
|
||||||
@@ -3,12 +3,12 @@
|
|||||||
#include <qabstractbutton.h>
|
#include <qabstractbutton.h>
|
||||||
#include <qpainter.h>
|
#include <qpainter.h>
|
||||||
|
|
||||||
#include "modern-qt/utility/qt_wrapper/enter_event.hh"
|
#include "creeper-qt/utility/qt_wrapper/enter-event.hh"
|
||||||
#include "modern-qt/utility/theme/theme.hh"
|
#include "creeper-qt/utility/theme/theme.hh"
|
||||||
#include "modern-qt/utility/wrapper/common.hh"
|
#include "creeper-qt/utility/wrapper/common.hh"
|
||||||
#include "modern-qt/utility/wrapper/pimpl.hh"
|
#include "creeper-qt/utility/wrapper/pimpl.hh"
|
||||||
#include "modern-qt/utility/wrapper/property.hh"
|
#include "creeper-qt/utility/wrapper/property.hh"
|
||||||
#include "modern-qt/utility/wrapper/widget.hh"
|
#include "creeper-qt/utility/wrapper/widget.hh"
|
||||||
|
|
||||||
namespace creeper::icon_button::internal {
|
namespace creeper::icon_button::internal {
|
||||||
class IconButton : public QAbstractButton {
|
class IconButton : public QAbstractButton {
|
||||||
@@ -2,13 +2,12 @@
|
|||||||
|
|
||||||
#include "icon-button.hh"
|
#include "icon-button.hh"
|
||||||
|
|
||||||
#include "modern-qt/utility/animation/animatable.hh"
|
#include "creeper-qt/utility/animation/animatable.hh"
|
||||||
#include "modern-qt/utility/animation/state/pid.hh"
|
#include "creeper-qt/utility/animation/state/pid.hh"
|
||||||
#include "modern-qt/utility/animation/state/spring.hh"
|
#include "creeper-qt/utility/animation/state/spring.hh"
|
||||||
#include "modern-qt/utility/animation/transition.hh"
|
#include "creeper-qt/utility/animation/transition.hh"
|
||||||
#include "modern-qt/utility/animation/water-ripple.hh"
|
#include "creeper-qt/utility/animation/water-ripple.hh"
|
||||||
#include "modern-qt/utility/material-icon.hh"
|
#include "creeper-qt/utility/painter/helper.hh"
|
||||||
#include "modern-qt/utility/painter/helper.hh"
|
|
||||||
|
|
||||||
using namespace creeper::icon_button::internal;
|
using namespace creeper::icon_button::internal;
|
||||||
|
|
||||||
@@ -35,7 +35,7 @@ public:
|
|||||||
namespace creeper {
|
namespace creeper {
|
||||||
|
|
||||||
namespace outlined_button::pro {
|
namespace outlined_button::pro {
|
||||||
using namespace filled_button;
|
using namespace filled_button::pro;
|
||||||
}
|
}
|
||||||
|
|
||||||
using OutlinedButton =
|
using OutlinedButton =
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include "modern-qt/utility/theme/theme.hh"
|
#include "creeper-qt/utility/theme/theme.hh"
|
||||||
#include "modern-qt/widget/shape/rounded-rect.hh"
|
#include "creeper-qt/widget/shape/rounded-rect.hh"
|
||||||
|
|
||||||
namespace creeper::card::internal {
|
namespace creeper::card::internal {
|
||||||
|
|
||||||
61
creeper-qt/widget/dropdown-menu.cc
Normal file
61
creeper-qt/widget/dropdown-menu.cc
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
#include "dropdown-menu.impl.hh"
|
||||||
|
|
||||||
|
DropdownMenu::DropdownMenu()
|
||||||
|
: pimpl { std::make_unique<Impl>(*this) } { }
|
||||||
|
|
||||||
|
DropdownMenu::~DropdownMenu() = default;
|
||||||
|
|
||||||
|
void DropdownMenu::set_color_scheme(const ColorScheme& scheme) { pimpl->set_color_scheme(scheme); }
|
||||||
|
|
||||||
|
void DropdownMenu::load_theme_manager(ThemeManager& manager) { pimpl->load_theme_manager(manager); }
|
||||||
|
|
||||||
|
void DropdownMenu::set_label_text(const QString& text) { pimpl->set_label_text(text); }
|
||||||
|
|
||||||
|
void DropdownMenu::set_leading_icon(const QIcon&) { }
|
||||||
|
|
||||||
|
void DropdownMenu::set_leading_icon(const QString& code, const QString& font) {
|
||||||
|
pimpl->set_leading_icon(code, font);
|
||||||
|
}
|
||||||
|
|
||||||
|
void DropdownMenu::resizeEvent(QResizeEvent* event) { QComboBox::resizeEvent(event); }
|
||||||
|
|
||||||
|
void DropdownMenu::enterEvent(qt::EnterEvent* enter_event) {
|
||||||
|
pimpl->enter_event(enter_event);
|
||||||
|
QComboBox::enterEvent(enter_event);
|
||||||
|
}
|
||||||
|
|
||||||
|
void DropdownMenu::leaveEvent(QEvent* event) {
|
||||||
|
pimpl->leave_event(event);
|
||||||
|
QComboBox::leaveEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
void DropdownMenu::focusInEvent(QFocusEvent* focus_event) {
|
||||||
|
pimpl->focus_in(focus_event);
|
||||||
|
QComboBox::focusInEvent(focus_event);
|
||||||
|
}
|
||||||
|
|
||||||
|
void DropdownMenu::focusOutEvent(QFocusEvent* event) {
|
||||||
|
pimpl->focus_out(event);
|
||||||
|
QComboBox::focusOutEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
void DropdownMenu::changeEvent(QEvent* event) { QComboBox::changeEvent(event); }
|
||||||
|
|
||||||
|
void DropdownMenu::showPopup() { pimpl->show_popup(); }
|
||||||
|
|
||||||
|
void DropdownMenu::hidePopup() { pimpl->hide_popup(); }
|
||||||
|
|
||||||
|
auto DropdownMenu::set_measurements(const Measurements& measurements) noexcept -> void {
|
||||||
|
pimpl->set_measurements(measurements);
|
||||||
|
}
|
||||||
|
|
||||||
|
void DropdownMenu::setTextMargins(const QMargins& margins) { this->margins = margins; }
|
||||||
|
|
||||||
|
QMargins DropdownMenu::textMargins() const { return margins; }
|
||||||
|
|
||||||
|
using namespace creeper;
|
||||||
|
|
||||||
|
void FilledDropdownMenu::paintEvent(QPaintEvent* event) {
|
||||||
|
pimpl->paint_filled(event);
|
||||||
|
// QComboBox::paintEvent(event);
|
||||||
|
}
|
||||||
@@ -1,26 +1,23 @@
|
|||||||
//
|
#pragma once
|
||||||
// Created by Lenn on 2025/10/14.
|
|
||||||
//
|
|
||||||
|
|
||||||
#ifndef TOUCHSENSOR_COMBO_BOX_HH
|
#include "creeper-qt/utility/qt_wrapper/enter-event.hh"
|
||||||
#define TOUCHSENSOR_COMBO_BOX_HH
|
#include "creeper-qt/utility/theme/theme.hh"
|
||||||
|
#include "creeper-qt/utility/wrapper/common.hh"
|
||||||
#include "modern-qt/utility/qt_wrapper/enter_event.hh"
|
#include "creeper-qt/utility/wrapper/pimpl.hh"
|
||||||
#include "modern-qt/utility/theme/theme.hh"
|
#include "creeper-qt/utility/wrapper/property.hh"
|
||||||
#include "modern-qt/utility/wrapper/common.hh"
|
#include "creeper-qt/utility/wrapper/widget.hh"
|
||||||
#include "modern-qt/utility/wrapper/widget.hh"
|
|
||||||
#include "modern-qt/utility/wrapper/mutable-value.hh"
|
|
||||||
|
|
||||||
#include <qcombobox.h>
|
#include <qcombobox.h>
|
||||||
|
|
||||||
namespace creeper {
|
namespace creeper {
|
||||||
class MatSelect;
|
|
||||||
|
|
||||||
namespace select_widget::internal {
|
class FilledDropdownMenu;
|
||||||
class BasicSelect : public QComboBox {
|
|
||||||
CREEPER_PIMPL_DEFINITION(BasicSelect);
|
|
||||||
|
|
||||||
friend MatSelect;
|
namespace dropdown_menu::internal {
|
||||||
|
|
||||||
|
class DropdownMenu : public QComboBox {
|
||||||
|
CREEPER_PIMPL_DEFINITION(DropdownMenu);
|
||||||
|
friend FilledDropdownMenu;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
struct ColorSpace {
|
struct ColorSpace {
|
||||||
@@ -35,7 +32,6 @@ namespace select_widget::internal {
|
|||||||
QColor supporting_text;
|
QColor supporting_text;
|
||||||
|
|
||||||
QColor leading_icon;
|
QColor leading_icon;
|
||||||
|
|
||||||
QColor outline;
|
QColor outline;
|
||||||
|
|
||||||
QColor itemlist_bg;
|
QColor itemlist_bg;
|
||||||
@@ -58,12 +54,12 @@ namespace select_widget::internal {
|
|||||||
|
|
||||||
int icon_rect_size = 24;
|
int icon_rect_size = 24;
|
||||||
int input_rect_size = 24;
|
int input_rect_size = 24;
|
||||||
int label_rect_size = 16;
|
int label_rect_size = 24;
|
||||||
|
|
||||||
int standard_font_height = 18;
|
int standard_font_height = 18;
|
||||||
|
|
||||||
int col_padding = 8;
|
int col_padding = 8;
|
||||||
int row_padding_without_icons = 16;
|
int row_padding_widthout_icons = 16;
|
||||||
int row_padding_with_icons = 12;
|
int row_padding_with_icons = 12;
|
||||||
int row_padding_populated_label_text = 4;
|
int row_padding_populated_label_text = 4;
|
||||||
|
|
||||||
@@ -72,10 +68,9 @@ namespace select_widget::internal {
|
|||||||
int supporting_text_and_character_counter_top_padding = 4;
|
int supporting_text_and_character_counter_top_padding = 4;
|
||||||
int supporting_text_and_character_counter_row_padding = 16;
|
int supporting_text_and_character_counter_row_padding = 16;
|
||||||
|
|
||||||
auto icon_size() const { return QSize { icon_rect_size, icon_rect_size }; }
|
auto icon_size() const -> QSize { return QSize { icon_rect_size, icon_rect_size }; };
|
||||||
};
|
};
|
||||||
|
auto set_color_scheme(const ColorScheme&) -> void;
|
||||||
void set_color_scheme(const ColorScheme&);
|
|
||||||
|
|
||||||
void load_theme_manager(ThemeManager&);
|
void load_theme_manager(ThemeManager&);
|
||||||
|
|
||||||
@@ -90,13 +85,13 @@ namespace select_widget::internal {
|
|||||||
protected:
|
protected:
|
||||||
void resizeEvent(QResizeEvent* event) override;
|
void resizeEvent(QResizeEvent* event) override;
|
||||||
|
|
||||||
void enterEvent(qt::EnterEvent*) override;
|
void enterEvent(qt::EnterEvent* event) override;
|
||||||
void leaveEvent(QEvent* event) override;
|
void leaveEvent(QEvent* event) override;
|
||||||
|
|
||||||
void focusInEvent(QFocusEvent*) override;
|
void focusInEvent(QFocusEvent*) override;
|
||||||
void focusOutEvent(QFocusEvent* event) override;
|
void focusOutEvent(QFocusEvent* event) override;
|
||||||
|
|
||||||
void changeEvent(QEvent *) override;
|
void changeEvent(QEvent* event) override;
|
||||||
|
|
||||||
void showPopup() override;
|
void showPopup() override;
|
||||||
void hidePopup() override;
|
void hidePopup() override;
|
||||||
@@ -105,57 +100,61 @@ namespace select_widget::internal {
|
|||||||
friend struct Impl;
|
friend struct Impl;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
void setTextMargins(int left, int top, int right, int bottom);
|
|
||||||
void setTextMargins(const QMargins& margins);
|
void setTextMargins(const QMargins& margins);
|
||||||
QMargins textMargins() const;
|
QMargins textMargins() const;
|
||||||
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QMargins margins { 13, 24, 13, 0 };
|
QMargins margins { 13, 24, 13, 0 };
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace select_widget::pro {
|
namespace dropdown_menu::pro {
|
||||||
using Token = common::Token<internal::BasicSelect>;
|
|
||||||
|
using Token = common::Token<internal::DropdownMenu>;
|
||||||
|
|
||||||
using LabelText = common::pro::String<Token,
|
using LabelText = common::pro::String<Token,
|
||||||
[](auto& self, const auto& string) { self.set_label_text(string); }>;
|
[](auto& self, const auto& string) { self.set_label_text(string); }>;
|
||||||
|
|
||||||
struct LeadingIcon : Token {
|
struct LeadingIcon : Token {
|
||||||
QString code;
|
QString code;
|
||||||
QString font;
|
QString font;
|
||||||
|
|
||||||
explicit LeadingIcon(const QString& code, const QString& font)
|
explicit LeadingIcon(const QString& code, const QString& font)
|
||||||
: code {code}, font {font} {}
|
: code { code }
|
||||||
|
, font { font } { }
|
||||||
void apply(auto& self) const { self.set_leading_icon(code, font); }
|
void apply(auto& self) const { self.set_leading_icon(code, font); }
|
||||||
};
|
};
|
||||||
|
|
||||||
struct LeadingText : Token {
|
/// @note: currentIndexChanged(int index)
|
||||||
QString text;
|
template <typename F>
|
||||||
explicit LeadingText(const QString& text) : text {text} {}
|
using IndexChanged =
|
||||||
void apply(auto& self) const {self.set_label_text(text);};
|
common::pro::SignalInjection<F, Token, &internal::DropdownMenu::currentIndexChanged>;
|
||||||
};
|
|
||||||
|
|
||||||
template<class Select>
|
template <typename F>
|
||||||
concept trait = std::derived_from<Select, Token>;
|
using TextChanged = common::pro::SignalInjection<F, Token, &internal::DropdownMenu::currentTextChanged>;
|
||||||
|
|
||||||
template<class Callback>
|
using Items = DerivedProp<Token, QVector<QString>, //
|
||||||
using IndexChanged = common::pro::IndexChanged<Callback, Token>;
|
[](auto& self, const auto& vec) {
|
||||||
|
self.clear();
|
||||||
using SelectItems = common::pro::Vector<Token, QString,
|
|
||||||
[](auto& self, const auto& vec) {self.clear();
|
|
||||||
self.addItems(vec);
|
self.addItems(vec);
|
||||||
self.setCurrentIndex(-1);
|
self.setCurrentIndex(-1);
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
template <class Select>
|
||||||
|
concept trait = std::derived_from<Select, Token>;
|
||||||
|
|
||||||
CREEPER_DEFINE_CHECKER(trait);
|
CREEPER_DEFINE_CHECKER(trait);
|
||||||
using namespace widget::pro;
|
using namespace widget::pro;
|
||||||
using namespace theme::pro;
|
using namespace theme::pro;
|
||||||
}
|
}
|
||||||
struct MatSelect
|
|
||||||
: public Declarative<select_widget::internal::BasicSelect,
|
struct FilledDropdownMenu
|
||||||
CheckerOr<select_widget::pro::checker, widget::pro::checker, theme::pro::checker>> {
|
: public Declarative<dropdown_menu::internal::DropdownMenu,
|
||||||
|
CheckerOr<dropdown_menu::pro::checker, widget::pro::checker, theme::pro::checker>> {
|
||||||
using Declarative::Declarative;
|
using Declarative::Declarative;
|
||||||
void paintEvent(QPaintEvent* event) override;
|
void paintEvent(QPaintEvent* event) override;
|
||||||
};
|
};
|
||||||
|
namespace filled_dropdown_menu::pro {
|
||||||
|
using namespace dropdown_menu::pro;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
#endif //TOUCHSENSOR_COMBO_BOX_HH
|
|
||||||
386
creeper-qt/widget/dropdown-menu.impl.hh
Normal file
386
creeper-qt/widget/dropdown-menu.impl.hh
Normal file
@@ -0,0 +1,386 @@
|
|||||||
|
/// 显然的,原生 QComboBox 的下拉列表样式并不符合 Material Design
|
||||||
|
/// 规范,未来必须切换成自定义的组件,相关参考:
|
||||||
|
/// - https://m3.material.io/components/menus/guidelines
|
||||||
|
/// - https://api.flutter.dev/flutter/material/DropdownMenu-class.html
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include "dropdown-menu.hh"
|
||||||
|
|
||||||
|
#include "creeper-qt/utility/animation/animatable.hh"
|
||||||
|
#include "creeper-qt/utility/animation/state/pid.hh"
|
||||||
|
#include "creeper-qt/utility/animation/transition.hh"
|
||||||
|
#include "creeper-qt/utility/material-icon.hh"
|
||||||
|
#include "creeper-qt/utility/painter/helper.hh"
|
||||||
|
|
||||||
|
#include <qabstractitemview.h>
|
||||||
|
#include <qfontmetrics.h>
|
||||||
|
|
||||||
|
using namespace creeper::dropdown_menu::internal;
|
||||||
|
|
||||||
|
struct DropdownMenu::Impl {
|
||||||
|
public:
|
||||||
|
explicit Impl(DropdownMenu& self) noexcept
|
||||||
|
: animatable(self)
|
||||||
|
, self { self } {
|
||||||
|
{
|
||||||
|
auto state = std::make_shared<PidState<double>>();
|
||||||
|
state->config.kp = 20.0;
|
||||||
|
state->config.ki = 00.0;
|
||||||
|
state->config.kd = 00.0;
|
||||||
|
state->config.epsilon = 1e-2;
|
||||||
|
label_position = make_transition(animatable, std::move(state));
|
||||||
|
}
|
||||||
|
|
||||||
|
set_measurements(Measurements {});
|
||||||
|
}
|
||||||
|
|
||||||
|
auto set_color_scheme(const ColorScheme& scheme) -> void {
|
||||||
|
color_space.enabled.container = scheme.surface_container_highest;
|
||||||
|
color_space.enabled.label_text = scheme.on_surface_variant;
|
||||||
|
color_space.enabled.selected_text = scheme.on_surface;
|
||||||
|
color_space.enabled.leading_icon = scheme.on_surface_variant;
|
||||||
|
color_space.enabled.active_indicator = scheme.on_surface_variant;
|
||||||
|
color_space.enabled.supporting_text = scheme.on_surface_variant;
|
||||||
|
color_space.enabled.input_text = scheme.on_surface;
|
||||||
|
color_space.enabled.caret = scheme.primary;
|
||||||
|
color_space.enabled.outline = scheme.outline;
|
||||||
|
|
||||||
|
color_space.disabled.container = scheme.on_surface;
|
||||||
|
color_space.disabled.container.setAlphaF(0.04);
|
||||||
|
color_space.disabled.label_text = scheme.on_surface;
|
||||||
|
color_space.disabled.label_text.setAlphaF(0.38);
|
||||||
|
color_space.disabled.selected_text = scheme.on_surface;
|
||||||
|
color_space.disabled.selected_text.setAlphaF(0.38);
|
||||||
|
color_space.disabled.leading_icon = scheme.on_surface;
|
||||||
|
color_space.disabled.leading_icon.setAlphaF(0.38);
|
||||||
|
color_space.disabled.supporting_text = scheme.on_surface;
|
||||||
|
color_space.disabled.supporting_text.setAlphaF(0.38);
|
||||||
|
color_space.disabled.input_text = scheme.on_surface;
|
||||||
|
color_space.disabled.input_text.setAlphaF(0.38);
|
||||||
|
color_space.disabled.active_indicator = scheme.on_surface;
|
||||||
|
color_space.disabled.active_indicator.setAlphaF(0.38);
|
||||||
|
color_space.disabled.outline = scheme.outline;
|
||||||
|
color_space.disabled.outline.setAlphaF(0.38);
|
||||||
|
|
||||||
|
color_space.focused.container = scheme.surface_container_highest;
|
||||||
|
color_space.focused.label_text = scheme.primary;
|
||||||
|
color_space.focused.selected_text = scheme.on_surface;
|
||||||
|
color_space.focused.leading_icon = scheme.on_surface_variant;
|
||||||
|
color_space.focused.input_text = scheme.on_surface;
|
||||||
|
color_space.focused.supporting_text = scheme.on_surface_variant;
|
||||||
|
color_space.focused.active_indicator = scheme.primary;
|
||||||
|
color_space.focused.outline = scheme.primary;
|
||||||
|
|
||||||
|
color_space.error.container = scheme.surface_container_highest;
|
||||||
|
color_space.error.active_indicator = scheme.error;
|
||||||
|
color_space.error.label_text = scheme.error;
|
||||||
|
color_space.error.selected_text = scheme.on_surface;
|
||||||
|
color_space.error.input_text = scheme.on_surface;
|
||||||
|
color_space.error.supporting_text = scheme.error;
|
||||||
|
color_space.error.leading_icon = scheme.on_surface_variant;
|
||||||
|
color_space.error.caret = scheme.error;
|
||||||
|
color_space.error.outline = scheme.error;
|
||||||
|
|
||||||
|
color_space.state_layer = scheme.on_surface;
|
||||||
|
color_space.state_layer.setAlphaF(0.08);
|
||||||
|
|
||||||
|
const auto& color = get_color_tokens();
|
||||||
|
sync_basic_text_style(color.input_text, scheme.surface_container_highest, color.input_text,
|
||||||
|
color_space.state_layer);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto load_theme_manager(ThemeManager& manager) {
|
||||||
|
manager.append_handler(&self,
|
||||||
|
[this](const ThemeManager& manager) { set_color_scheme(manager.color_scheme()); });
|
||||||
|
}
|
||||||
|
|
||||||
|
auto set_label_text(const QString& text) { label_text = text; }
|
||||||
|
|
||||||
|
auto set_leading_icon(const QString& code, const QString& font) {
|
||||||
|
leading_icon_code = code;
|
||||||
|
leading_icon_font = font;
|
||||||
|
is_update_component_status = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto set_measurements(const Measurements& measurements) -> void {
|
||||||
|
this->measurements = measurements;
|
||||||
|
self.setFixedHeight(measurements.container_height + measurements.standard_font_height);
|
||||||
|
is_update_component_status = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto paint_filled(QPaintEvent*) -> void {
|
||||||
|
const auto widget_rect = self.rect();
|
||||||
|
const auto color = get_color_tokens();
|
||||||
|
|
||||||
|
constexpr auto container_radius = 5;
|
||||||
|
update_component_status();
|
||||||
|
|
||||||
|
auto painter = QPainter { &self };
|
||||||
|
|
||||||
|
// Draw container with fixed measurements height and vertically centered
|
||||||
|
const auto container_rect = QRect { widget_rect.left(),
|
||||||
|
widget_rect.top() + (widget_rect.height() - measurements.container_height) / 2,
|
||||||
|
widget_rect.width(), measurements.container_height };
|
||||||
|
|
||||||
|
{
|
||||||
|
util::PainterHelper { painter }
|
||||||
|
.set_render_hint(QPainter::Antialiasing)
|
||||||
|
.rounded_rectangle(color.container, Qt::transparent, 0, container_rect,
|
||||||
|
container_radius, container_radius, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Active indicator at container bottom
|
||||||
|
{
|
||||||
|
const auto p0 = container_rect.bottomLeft();
|
||||||
|
const auto p1 = container_rect.bottomRight();
|
||||||
|
painter.setBrush(Qt::NoBrush);
|
||||||
|
painter.setPen({ color.active_indicator, filled_line_width() });
|
||||||
|
painter.drawLine(p0, p1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Icon positioned relative to container_rect
|
||||||
|
const auto rect_icon = QRectF {
|
||||||
|
container_rect.right() - self.textMargins().right() - measurements.icon_rect_size * 1.,
|
||||||
|
container_rect.top() + (container_rect.height() - measurements.icon_rect_size) * 0.5,
|
||||||
|
1. * measurements.icon_rect_size,
|
||||||
|
1. * measurements.icon_rect_size,
|
||||||
|
};
|
||||||
|
const auto icon_center = rect_icon.center();
|
||||||
|
const bool is_active = (self.view() && self.view()->isVisible());
|
||||||
|
|
||||||
|
painter.save();
|
||||||
|
painter.setBrush(Qt::NoBrush);
|
||||||
|
painter.setPen(QPen { color.leading_icon });
|
||||||
|
painter.setFont(leading_icon_font);
|
||||||
|
painter.translate(icon_center);
|
||||||
|
painter.rotate(is_active ? 180.0 : 0.0);
|
||||||
|
painter.translate(-icon_center);
|
||||||
|
painter.drawText(rect_icon, leading_icon_code, { Qt::AlignCenter });
|
||||||
|
painter.restore();
|
||||||
|
|
||||||
|
if (!label_text.isEmpty()) {
|
||||||
|
const auto margins = self.textMargins();
|
||||||
|
|
||||||
|
const auto center_label_y = container_rect.top()
|
||||||
|
+ (measurements.container_height - measurements.label_rect_size) / 2.0;
|
||||||
|
|
||||||
|
const auto rect_center = QRectF {
|
||||||
|
QPointF { static_cast<double>(margins.left()), center_label_y },
|
||||||
|
QPointF(container_rect.right() - margins.right(),
|
||||||
|
center_label_y + measurements.label_rect_size),
|
||||||
|
};
|
||||||
|
|
||||||
|
const auto rect_top = QRectF {
|
||||||
|
QPointF(margins.left(), container_rect.top() + measurements.col_padding),
|
||||||
|
QPointF(container_rect.right() - margins.right(),
|
||||||
|
container_rect.top() + measurements.col_padding + measurements.label_rect_size),
|
||||||
|
};
|
||||||
|
|
||||||
|
const auto position = self.currentText().isEmpty() ? *label_position : 1.;
|
||||||
|
const auto label_rect = animate::interpolate(rect_center, rect_top, position);
|
||||||
|
const auto scale = 1. - position * 0.25;
|
||||||
|
const auto label_anchor = QPointF { label_rect.left(), label_rect.center().y() };
|
||||||
|
|
||||||
|
painter.save();
|
||||||
|
painter.translate(label_anchor);
|
||||||
|
painter.scale(scale, scale);
|
||||||
|
painter.translate(-label_anchor);
|
||||||
|
painter.setBrush(Qt::NoBrush);
|
||||||
|
painter.setPen(QPen { color.label_text });
|
||||||
|
painter.setFont(standard_text_font);
|
||||||
|
painter.setRenderHint(QPainter::Antialiasing);
|
||||||
|
painter.drawText(label_rect, label_text, { Qt::AlignVCenter | Qt::AlignLeading });
|
||||||
|
painter.restore();
|
||||||
|
|
||||||
|
if (self.currentIndex() != -1) {
|
||||||
|
painter.save();
|
||||||
|
// Place selected text in the input area (below the floating label)
|
||||||
|
const auto input_top =
|
||||||
|
container_rect.top() + measurements.col_padding + measurements.label_rect_size;
|
||||||
|
const auto input_bottom = container_rect.bottom() - measurements.col_padding;
|
||||||
|
const auto rect_center_selected = QRectF {
|
||||||
|
QPointF { static_cast<double>(margins.left()), static_cast<double>(input_top) },
|
||||||
|
QPointF(container_rect.right() - margins.right(),
|
||||||
|
static_cast<double>(input_bottom)),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Draw selected text with input text color
|
||||||
|
painter.setBrush(Qt::NoBrush);
|
||||||
|
painter.setPen(QPen { color.selected_text });
|
||||||
|
painter.setFont(standard_text_font);
|
||||||
|
painter.setRenderHint(QPainter::Antialiasing);
|
||||||
|
painter.drawText(
|
||||||
|
rect_center_selected, self.currentText(), Qt::AlignVCenter | Qt::AlignLeading);
|
||||||
|
|
||||||
|
painter.restore();
|
||||||
|
}
|
||||||
|
} else if (label_text.isEmpty() && self.currentIndex() != -1) {
|
||||||
|
const auto margins = self.textMargins();
|
||||||
|
const auto input_top = container_rect.top()
|
||||||
|
+ (container_rect.height() - measurements.input_rect_size) / 2.0;
|
||||||
|
const auto input_bottom = input_top + measurements.input_rect_size;
|
||||||
|
const auto rect_selected = QRectF {
|
||||||
|
QPointF(margins.left(), input_top),
|
||||||
|
QPointF(container_rect.right() - margins.right(), input_bottom),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Draw selected text
|
||||||
|
painter.save();
|
||||||
|
painter.setBrush(Qt::NoBrush);
|
||||||
|
painter.setPen(QPen { color.selected_text });
|
||||||
|
painter.setFont(standard_text_font);
|
||||||
|
painter.setRenderHint(QPainter::Antialiasing);
|
||||||
|
painter.drawText(
|
||||||
|
rect_selected, self.currentText(), Qt::AlignVCenter | Qt::AlignLeading);
|
||||||
|
painter.restore();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_hovered) {
|
||||||
|
util::PainterHelper { painter }
|
||||||
|
.set_render_hint(QPainter::Antialiasing)
|
||||||
|
.rounded_rectangle(color_space.state_layer, Qt::transparent, 0, container_rect,
|
||||||
|
container_radius, container_radius, 0, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto enter_event(qt::EnterEvent*) {
|
||||||
|
is_hovered = true;
|
||||||
|
self.update();
|
||||||
|
}
|
||||||
|
|
||||||
|
auto leave_event(QEvent*) {
|
||||||
|
is_hovered = false;
|
||||||
|
self.update();
|
||||||
|
}
|
||||||
|
|
||||||
|
auto focus_in(QFocusEvent*) {
|
||||||
|
is_focused = true;
|
||||||
|
update_label_position();
|
||||||
|
self.update();
|
||||||
|
}
|
||||||
|
|
||||||
|
auto focus_out(QFocusEvent*) {
|
||||||
|
is_focused = false;
|
||||||
|
update_label_position();
|
||||||
|
self.update();
|
||||||
|
}
|
||||||
|
|
||||||
|
auto show_popup() {
|
||||||
|
if (self.count() == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
is_active = true;
|
||||||
|
self.QComboBox::showPopup();
|
||||||
|
update_label_position();
|
||||||
|
self.update();
|
||||||
|
}
|
||||||
|
|
||||||
|
auto hide_popup() -> void {
|
||||||
|
is_active = false;
|
||||||
|
self.QComboBox::hidePopup();
|
||||||
|
update_label_position();
|
||||||
|
self.update();
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
auto update_component_status() -> void {
|
||||||
|
if (is_update_component_status) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto font = self.font();
|
||||||
|
font.setPixelSize(measurements.standard_font_height);
|
||||||
|
self.setFont(font);
|
||||||
|
standard_text_font = self.font();
|
||||||
|
standard_text_font.setPixelSize(measurements.standard_font_height);
|
||||||
|
|
||||||
|
is_update_component_status = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto update_label_position() -> void {
|
||||||
|
if ((is_focused || is_active) && self.currentIndex() != -1) {
|
||||||
|
label_position->transition_to(1.0);
|
||||||
|
} else if (is_focused || is_active) {
|
||||||
|
label_position->transition_to(1.0);
|
||||||
|
} else {
|
||||||
|
label_position->transition_to(0.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto sync_basic_text_style(const QColor& text, const QColor& background,
|
||||||
|
const QColor& selection_text, const QColor& selection_background) -> void {
|
||||||
|
|
||||||
|
constexpr auto to_rgba = [](const QColor& color) {
|
||||||
|
return QStringLiteral("rgba(%1, %2, %3, %4)")
|
||||||
|
.arg(color.red())
|
||||||
|
.arg(color.green())
|
||||||
|
.arg(color.blue())
|
||||||
|
.arg(color.alpha());
|
||||||
|
};
|
||||||
|
|
||||||
|
constexpr auto kQComboBoxStyle = R"(
|
||||||
|
QComboBox {
|
||||||
|
border: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
selection-color: %3;
|
||||||
|
selection-background-color: %4;
|
||||||
|
}
|
||||||
|
QComboBox QAbstractItemView {
|
||||||
|
border: none;
|
||||||
|
border-radius: 3px;
|
||||||
|
padding-top: 8px;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
color: %1;
|
||||||
|
background-color: %2;
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
self.setStyleSheet(QString { kQComboBoxStyle }
|
||||||
|
.arg(to_rgba(text))
|
||||||
|
.arg(to_rgba(background))
|
||||||
|
.arg(to_rgba(selection_text))
|
||||||
|
.arg(to_rgba(selection_background)));
|
||||||
|
}
|
||||||
|
|
||||||
|
auto get_color_tokens() const -> ColorSpace::Tokens const& {
|
||||||
|
return is_disable ? color_space.disabled
|
||||||
|
: is_error ? color_space.error
|
||||||
|
: is_active ? color_space.focused
|
||||||
|
: is_focused ? color_space.focused
|
||||||
|
: color_space.enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto filled_line_width() const -> double { return 1.5; }
|
||||||
|
|
||||||
|
static constexpr auto measure_text(
|
||||||
|
const QFont& font, const QString& text, const QTextOption& options) {
|
||||||
|
const auto fm = QFontMetricsF(font);
|
||||||
|
const auto size = fm.size(Qt::TextSingleLine, text);
|
||||||
|
return size.width();
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
Measurements measurements;
|
||||||
|
ColorSpace color_space;
|
||||||
|
|
||||||
|
bool is_disable = false;
|
||||||
|
bool is_hovered = false;
|
||||||
|
bool is_focused = false;
|
||||||
|
bool is_error = false;
|
||||||
|
bool is_active = false;
|
||||||
|
|
||||||
|
bool is_update_component_status = false;
|
||||||
|
|
||||||
|
QString label_text;
|
||||||
|
QIcon leading_icon;
|
||||||
|
QString leading_icon_code = material::icon::kArrowDropDown;
|
||||||
|
QFont leading_icon_font = material::round::font_1;
|
||||||
|
|
||||||
|
QFont standard_text_font;
|
||||||
|
|
||||||
|
Animatable animatable;
|
||||||
|
std::unique_ptr<TransitionValue<PidState<double>>> label_position;
|
||||||
|
|
||||||
|
DropdownMenu& self;
|
||||||
|
};
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include "modern-qt/utility/content-scale.hh"
|
#include "creeper-qt/utility/content-scale.hh"
|
||||||
#include "modern-qt/utility/painter-resource.hh"
|
#include "creeper-qt/utility/painter-resource.hh"
|
||||||
#include "modern-qt/utility/wrapper/common.hh"
|
#include "creeper-qt/utility/wrapper/common.hh"
|
||||||
#include "modern-qt/utility/wrapper/pimpl.hh"
|
#include "creeper-qt/utility/wrapper/pimpl.hh"
|
||||||
#include "modern-qt/utility/wrapper/widget.hh"
|
#include "creeper-qt/utility/wrapper/widget.hh"
|
||||||
|
|
||||||
namespace creeper {
|
namespace creeper {
|
||||||
namespace image::internal {
|
namespace image::internal {
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
#include "modern-qt/utility/painter/helper.hh"
|
#include "creeper-qt/utility/painter/helper.hh"
|
||||||
#include "image.hh"
|
#include "image.hh"
|
||||||
|
|
||||||
#include <qevent.h>
|
#include <qevent.h>
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include "modern-qt/utility/wrapper/common.hh"
|
#include "creeper-qt/utility/wrapper/common.hh"
|
||||||
#include "modern-qt/utility/wrapper/pimpl.hh"
|
#include "creeper-qt/utility/wrapper/pimpl.hh"
|
||||||
#include "modern-qt/widget/widget.hh"
|
#include "creeper-qt/widget/widget.hh"
|
||||||
|
|
||||||
namespace creeper::circular_progress_indicator::internal {
|
namespace creeper::circular_progress_indicator::internal {
|
||||||
|
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include <qmainwindow.h>
|
#include <qmainwindow.h>
|
||||||
|
|
||||||
#include "modern-qt/utility/trait/widget.hh"
|
#include "creeper-qt/utility/trait/widget.hh"
|
||||||
#include "modern-qt/utility/wrapper/common.hh"
|
#include "creeper-qt/utility/wrapper/common.hh"
|
||||||
#include "modern-qt/utility/wrapper/pimpl.hh"
|
#include "creeper-qt/utility/wrapper/pimpl.hh"
|
||||||
#include "modern-qt/utility/wrapper/property.hh"
|
#include "creeper-qt/utility/wrapper/property.hh"
|
||||||
#include "modern-qt/utility/wrapper/widget.hh"
|
#include "creeper-qt/utility/wrapper/widget.hh"
|
||||||
|
|
||||||
namespace creeper::main_window::internal {
|
namespace creeper::main_window::internal {
|
||||||
|
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "modern-qt/utility/painter/helper.hh"
|
#include "creeper-qt/utility/painter/helper.hh"
|
||||||
#include "modern-qt/utility/wrapper/property.hh"
|
#include "creeper-qt/utility/wrapper/property.hh"
|
||||||
#include "modern-qt/widget/shape/shape.hh"
|
#include "creeper-qt/widget/shape/shape.hh"
|
||||||
#include "modern-qt/widget/widget.hh"
|
#include "creeper-qt/widget/widget.hh"
|
||||||
|
|
||||||
namespace creeper {
|
namespace creeper {
|
||||||
|
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "modern-qt/utility/painter/helper.hh"
|
#include "creeper-qt/utility/painter/helper.hh"
|
||||||
#include "modern-qt/utility/wrapper/property.hh"
|
#include "creeper-qt/utility/wrapper/property.hh"
|
||||||
#include "modern-qt/utility/wrapper/widget.hh"
|
#include "creeper-qt/utility/wrapper/widget.hh"
|
||||||
#include "modern-qt/widget/shape/shape.hh"
|
#include "creeper-qt/widget/shape/shape.hh"
|
||||||
|
|
||||||
namespace creeper::rounded_rect::internal {
|
namespace creeper::rounded_rect::internal {
|
||||||
|
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "modern-qt/utility/solution/round-angle.hh"
|
#include "creeper-qt/utility/solution/round-angle.hh"
|
||||||
#include "modern-qt/utility/wrapper/common.hh"
|
#include "creeper-qt/utility/wrapper/common.hh"
|
||||||
#include "modern-qt/utility/wrapper/property.hh"
|
#include "creeper-qt/utility/wrapper/property.hh"
|
||||||
#include "modern-qt/widget/shape/shape.hh"
|
#include "creeper-qt/widget/shape/shape.hh"
|
||||||
#include "modern-qt/widget/widget.hh"
|
#include "creeper-qt/widget/widget.hh"
|
||||||
|
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <qpainterpath.h>
|
#include <qpainterpath.h>
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user