Merge branch 'dev'

# Conflicts:
#	components/ffmsep/cpstream_core.hh
#	components/view.cc
This commit is contained in:
2025-11-25 16:20:04 +08:00
112 changed files with 10627 additions and 8992 deletions

View File

@@ -1,14 +1,147 @@
# 基础样式LLVMGoogleChromiumMozillaWebKit
BasedOnStyle: LLVM BasedOnStyle: LLVM
# 类似于下面这行还没有实现未来希望clang提供分号;后不会强制换行的功能
# BreakAfterSemicolon: false
---
# 要使用的预处理器指令缩进样式
IndentPPDirectives: AfterHash
# 缩进宽度
IndentWidth: 4 IndentWidth: 4
# 标准: Cpp03, Cpp11, Auto
Language: Cpp Standard: Latest
# tab宽度
DerivePointerAlignment: false 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
# 使用反斜杠换行对齐 RightDontAlign
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
# 指针的*的位置
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

View File

@@ -24,22 +24,20 @@ 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
Eigen3::Eigen Eigen3::Eigen
) )
file( file(
@@ -52,9 +50,9 @@ add_library(qcustomplot SHARED ${QCUSTOMPLOT_SOURCES})
target_include_directories(qcustomplot PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) target_include_directories(qcustomplot PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(qcustomplot target_link_libraries(qcustomplot
PUBLIC PUBLIC
${QT_VERSION}::Core ${QT_VERSION}::Core
${QT_VERSION}::Gui ${QT_VERSION}::Gui
${QT_VERSION}::PrintSupport ${QT_VERSION}::PrintSupport
) )
file( file(
@@ -68,17 +66,26 @@ 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
@@ -86,6 +93,7 @@ set(TOUCHSENSOR_HEADERS
components/charts/heatmap.impl.hh components/charts/heatmap.impl.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)
@@ -94,23 +102,25 @@ add_executable(${PROJECT_NAME}
${COMPONENT_SOURCES} ${COMPONENT_SOURCES}
${UTILITY_SOURCES} ${UTILITY_SOURCES}
${TOUCHSENSOR_HEADERS} ${TOUCHSENSOR_HEADERS}
${BASE_SOURCES}
main.cc 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
spdlog spdlog
) )
if(BUILD_EXAMPLE) if(BUILD_EXAMPLE)
@@ -120,8 +130,8 @@ if(BUILD_EXAMPLE)
) )
target_include_directories(cpstream_demo target_include_directories(cpstream_demo
PRIVATE PRIVATE
${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_SOURCE_DIR}
${FFMSEP_INCLUDE_DIR} ${FFMSEP_INCLUDE_DIR}
) )
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)

159
base/globalhelper.cc Normal file
View 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
View 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;
};

View File

@@ -1,6 +1,6 @@
#pragma once #pragma once
#include <modern-qt/utility/theme/theme.hh> #include <creeper-qt/utility/theme/theme.hh>
#include <qwidget.h> #include <qwidget.h>
#include <string_view> #include <string_view>
@@ -10,8 +10,8 @@ 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;
}; };
auto NavComponent(NavComponentState&) noexcept -> raw_pointer<QWidget>; auto NavComponent(NavComponentState&) noexcept -> raw_pointer<QWidget>;
@@ -20,3 +20,16 @@ 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();

View File

@@ -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>
@@ -82,10 +82,13 @@ 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); // self.set_data(data);
}>; // }>;
using Data = DerivedProp<Token, QVector<QString>, [](auto& self, const auto& data) {
self.set_data(data);
}>;
struct ColorRange : Token { struct ColorRange : Token {
double min; double min;
@@ -107,11 +110,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]);
}>; }>;

View File

@@ -6,17 +6,23 @@
#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 : self{self}, initialized(false), matrix_size(QSize{3, 4}) {}
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) {
@@ -45,6 +51,8 @@ public:
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();
} }
@@ -52,9 +60,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;
@@ -63,7 +70,7 @@ 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();
} }
} }
@@ -71,7 +78,8 @@ public:
auto initialize_plot() -> void { auto initialize_plot() -> void {
if (initialized) return; if (initialized) return;
QCPColorMap* cpmp = new QCPColorMap(self.xAxis, self.yAxis); color_map = 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));
@@ -103,14 +111,26 @@ QCPColorMap* cpmp = new QCPColorMap(self.xAxis, self.yAxis);
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);
QCPColorScale* color_scale = new QCPColorScale(&self); if (!color_scale) {
color_scale->setType(QCPAxis::atBottom); color_scale = new QCPColorScale(&self);
self.plotLayout()->addElement(1, 0, color_scale); 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);
}
}
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(240, 246, 255)); // 低值淡色
gradient.setColorStopAt(1.0, QColor(191, 68, 76)); // BF444C gradient.setColorStopAt(0.35, QColor(142, 197, 252));
gradient.setColorStopAt(0.7, QColor(56, 128, 199));
gradient.setColorStopAt(1.0, QColor(8, 36, 95)); // 高值深色
cpmp->setGradient(gradient); cpmp->setGradient(gradient);
cpmp->setDataRange(QCPRange(color_min, color_max)); cpmp->setDataRange(QCPRange(color_min, color_max));
@@ -121,6 +141,7 @@ QCPColorMap* cpmp = new QCPColorMap(self.xAxis, self.yAxis);
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();
@@ -133,24 +154,37 @@ QCPColorMap* cpmp = new QCPColorMap(self.xAxis, self.yAxis);
self.clearGraphs(); self.clearGraphs();
self.clearItems(); self.clearItems();
self.clearFocus(); self.clearFocus();
color_map = nullptr;
cell_labels.clear();
// 重新初始化 // 重新初始化
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;
}
} }
} }
update_label_values(values);
// 重绘 // 重绘
self.replot(); self.replot();
} }
@@ -172,6 +206,110 @@ private:
double color_max = 800.0; double color_max = 800.0;
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 = QColor(0, 0, 0); // 固定黑色
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 double range = std::max(color_max - color_min, 1.0);
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);
}
}
}; };
#endif // TOUCHSENSOR_HEATMAP_IMPL_HH #endif // TOUCHSENSOR_HEATMAP_IMPL_HH

View File

@@ -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 <algorithm> #include <algorithm>
#include <atomic> #include <atomic>
#include <chrono> #include <chrono>
@@ -7,20 +10,22 @@
#include <cstddef> #include <cstddef>
#include <cstdint> #include <cstdint>
#include <deque> #include <deque>
#include <future>
#include <memory> #include <memory>
#include <mutex> #include <mutex>
#include <optional> #include <optional>
#include <thread> #include <thread>
#include <utility> #include <utility>
#include <vector> #include <vector>
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()) {
@@ -49,6 +54,7 @@ struct CPStreamCore::Impl {
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;
@@ -64,7 +70,7 @@ struct CPStreamCore::Impl {
config_.frame_queue_capacity = 1U; config_.frame_queue_capacity = 1U;
} }
if (config_.slave_request_interval.count() < 0) { if (config_.slave_request_interval.count() < 0) {
config_.slave_request_interval = std::chrono::milliseconds{0}; config_.slave_request_interval = 0ms;
} }
frame_queue_capacity_ = config_.frame_queue_capacity; frame_queue_capacity_ = config_.frame_queue_capacity;
} }
@@ -116,7 +122,7 @@ struct CPStreamCore::Impl {
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) {
@@ -140,11 +146,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);
@@ -165,7 +171,7 @@ 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()) {
@@ -185,12 +191,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();
} }
} }
@@ -201,7 +208,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()) {
@@ -250,7 +257,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();
} }
@@ -260,7 +267,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();
} }
@@ -279,7 +286,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_;
} }
@@ -301,17 +308,17 @@ struct CPStreamCore::Impl {
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();
@@ -324,7 +331,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();
} }
@@ -333,7 +340,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_) {
@@ -342,8 +349,46 @@ 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);
} }
@@ -352,7 +397,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_;
} }
@@ -366,7 +411,7 @@ struct CPStreamCore::Impl {
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()) {
@@ -401,7 +446,7 @@ struct CPStreamCore::Impl {
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();
} }
@@ -415,7 +460,7 @@ struct CPStreamCore::Impl {
const auto command = config_.slave_request_command; const auto command = config_.slave_request_command;
auto interval = config_.slave_request_interval; auto interval = config_.slave_request_interval;
if (interval.count() < 0) { if (interval.count() < 0) {
interval = std::chrono::milliseconds{0}; interval = 0ms;
} }
const bool repeat = interval.count() > 0; const bool repeat = interval.count() > 0;
@@ -485,23 +530,24 @@ 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);
if (codec_descriptor_ && codec_descriptor_->id == CPCodecID::Tactile) { decoded->id = codec_descriptor_ ? codec_descriptor_->id : CPCodecID::Unknow;
if (auto parsed = tactile::parse_frame(decoded.frame)) { if (decoded->id == CPCodecID::Tactile) {
decoded.tactile = parsed; if (auto parsed = tactile::parse_frame(decoded->frame)) {
decoded.tactile_pressures = tactile::parse_pressure_values(*parsed); decoded->tactile = parsed;
decoded->tactile_pressures = tactile::parse_pressure_values(*parsed);
if (auto matrix = tactile::parse_matrix_size_payload(*parsed)) { if (auto matrix = tactile::parse_matrix_size_payload(*parsed)) {
decoded.tactile_matrix_size = matrix; decoded->tactile_matrix_size = matrix;
} }
} }
} }
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) {
@@ -509,11 +555,14 @@ 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) {
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) {
@@ -537,7 +586,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();
@@ -555,7 +604,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);
} }
@@ -575,9 +624,12 @@ struct CPStreamCore::Impl {
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_;
@@ -589,6 +641,8 @@ 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)
@@ -641,11 +695,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);
} }
@@ -657,6 +711,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));
} }

View File

@@ -5,16 +5,23 @@
#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::optional<tactile::TactileFrame> tactile;
@@ -22,7 +29,6 @@ struct DecodedFrame {
std::optional<tactile::MatrixSize> tactile_matrix_size; std::optional<tactile::MatrixSize> tactile_matrix_size;
}; };
using namespace std::chrono_literals;
struct CPStreamConfig { struct CPStreamConfig {
std::string port; std::string port;
std::uint32_t baudrate = 115200; std::uint32_t baudrate = 115200;
@@ -37,12 +43,12 @@ struct CPStreamConfig {
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::vector<std::uint8_t> slave_request_command{};
std::chrono::milliseconds slave_request_interval = 200ms; 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();
@@ -64,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);
@@ -76,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_;

View File

@@ -0,0 +1,255 @@
//
// 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 <system_error>
#include <nlohmann/json.hpp>
namespace ffmsep::persist {
namespace {
using nlohmann::json;
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};
}
json tactile_frames = json::array();
for (const auto& frame : frames) {
if (!frame) {
continue;
}
if (frame->id != CPCodecID::Tactile || !frame->tactile) {
continue;
}
tactile_frames.push_back(serialize_tactile_frame(*frame));
}
if (tactile_frames.empty()) {
return {false, "no tactile frames available for export", path};
}
json root;
root["codec"] = "tactile";
root["frames"] = std::move(tactile_frames);
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};
}
dump_compact_json(stream, root);
stream << '\n';
stream.flush();
if (!stream.good()) {
return {false, "failed to write export file", path};
}
return {true, {}, path};
}
void JsonWritter::stop() {
if (!stopped_.exchange(true)) {
write_queue_.stop();
if (write_thread_.joinable()) {
write_thread_.join();
}
}
}
} // namespace ffmsep::persist

View 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

View File

@@ -10,36 +10,37 @@ inline constexpr std::uint8_t kStartByteFirst = 0xAA;
inline constexpr std::uint8_t kStartByteSecond = 0x55; inline constexpr std::uint8_t kStartByteSecond = 0x55;
enum class FunctionCode : std::uint8_t { enum class FunctionCode : std::uint8_t {
Unknown = 0x00, Unknown = 0x00,
ReadMatrix = 0x01, ReadMatrix = 0x01,
ReadSingle = 0x02, ReadSingle = 0x02,
ReadTemperature = 0x03, ReadTemperature = 0x03,
SetDeviceId = 0x51, SetDeviceId = 0x51,
SetMatrixSize = 0x52, SetMatrixSize = 0x52,
CalibrationMode = 0x53, CalibrationMode = 0x53,
}; };
struct MatrixSize { struct MatrixSize {
std::uint8_t long_edge = 0; std::uint8_t long_edge = 0;
std::uint8_t short_edge = 0; std::uint8_t short_edge = 0;
}; };
struct TactileFrame { struct TactileFrame {
std::uint8_t device_address = 0; std::uint8_t device_address = 0;
std::uint8_t reserved = 0; std::uint8_t reserved = 0;
std::uint8_t response_function = 0; std::uint8_t response_function = 0;
FunctionCode function = FunctionCode::Unknown; FunctionCode function = FunctionCode::Unknown;
std::uint32_t start_address = 0; std::uint32_t start_address = 0;
std::uint16_t return_byte_count = 0; std::uint16_t return_byte_count = 0;
std::uint8_t status = 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);
const CPCodec* tactile_codec(); const CPCodec *tactile_codec();
void register_tactile_codec(); void register_tactile_codec();
} } // namespace ffmsep::tactile

44
components/hand-view.cc Normal file
View 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},
};
}

View File

@@ -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,

437
components/setting.cc Normal file
View File

@@ -0,0 +1,437 @@
//
// 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 <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 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 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{
AddProfileLongItem(state.manager)
},
col::pro::Item<ScrollArea>{
scroll::pro::ThemeManager { state.manager },
scroll::pro::HorizontalScrollBarPolicy { Qt::ScrollBarAlwaysOff },
scroll::pro::Item {
profiles_flow
},
},
},
};
}

View File

@@ -6,13 +6,23 @@
#include <array> #include <array>
#include <cmath> #include <cmath>
#include <cstdint> #include <cstdint>
#include <exception>
#include <future>
#include <memory> #include <memory>
#include <mutex> #include <mutex>
#include <thread>
#include <QString> #include <QString>
#include <QObject> #include <QObject>
#include <QMetaObject> #include <QMetaObject>
#include <QStringList> #include <QStringList>
#include <QtCore/Qt> #include <QtCore/Qt>
#include <QCoreApplication>
#include <QDateTime>
#include <QDir>
#include <QFileDialog>
#include <QMessageBox>
#include <QStandardPaths>
#include <QPair>
#include <optional> #include <optional>
#include <string> #include <string>
#include <qsize.h> #include <qsize.h>
@@ -23,54 +33,71 @@
#include <sstream> #include <sstream>
#include "component.hh" #include "component.hh"
#include "cpstream_core.hh" #include "cpstream_core.hh"
#include "modern-qt/utility/theme/theme.hh" #include "base/globalhelper.hh"
#include "modern-qt/utility/wrapper/layout.hh" #include "creeper-qt/utility/theme/theme.hh"
#include "modern-qt/utility/wrapper/widget.hh" #include "creeper-qt/utility/wrapper/layout.hh"
#include "creeper-qt/utility/wrapper/widget.hh"
#include "components/charts/heatmap.hh" #include "components/charts/heatmap.hh"
#include <modern-qt/layout/flow.hh> #include <creeper-qt/layout/flow.hh>
#include <modern-qt/layout/linear.hh> #include <creeper-qt/layout/linear.hh>
#include <modern-qt/utility/material-icon.hh> #include <creeper-qt/utility/material-icon.hh>
#include <modern-qt/utility/wrapper/mutable-value.hh> #include <creeper-qt/utility/wrapper/mutable-value.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/buttons/filled-button.hh>
#include <modern-qt/widget/cards/outlined-card.hh> #include <creeper-qt/widget/cards/filled-card.hh>
#include <modern-qt/widget/image.hh> #include <creeper-qt/widget/cards/outlined-card.hh>
#include <modern-qt/widget/shape/wave-circle.hh> #include <creeper-qt/widget/image.hh>
#include <modern-qt/widget/sliders.hh> #include <creeper-qt/widget/shape/wave-circle.hh>
#include <modern-qt/widget/switch.hh> #include <creeper-qt/widget/sliders.hh>
#include <modern-qt/widget/text-fields.hh> #include <creeper-qt/widget/switch.hh>
#include <modern-qt/widget/text.hh> #include <creeper-qt/widget/text-fields.hh>
#include <modern-qt/widget/select.hh> #include <creeper-qt/widget/text.hh>
#include <creeper-qt/widget/dropdown-menu.hh>
#include "components/ffmsep/presist/presist.hh"
#include "components/ffmsep/tactile/tacdec.hh" #include "components/ffmsep/tactile/tacdec.hh"
using namespace std::chrono_literals; #include <qstringliteral.h>
#define DEBUG 0
using namespace creeper; using namespace creeper;
namespace capro = card::pro; namespace capro = card::pro;
namespace lnpro = linear::pro; namespace lnpro = linear::pro;
namespace impro = image::pro; namespace impro = image::pro;
namespace ibpro = icon_button::pro; namespace ibpro = icon_button::pro;
namespace slpro = select_widget::pro; namespace fbpro = filled_button::pro;
namespace dmpro = dropdown_menu::pro;
namespace pwpro = plot_widget::pro; namespace pwpro = plot_widget::pro;
namespace { namespace {
constexpr std::array<std::uint8_t, 14> kSlaveRequestCommand{ constexpr std::array<std::uint8_t, 14> kSlaveRequestCommand{
0x55, 0xAA, 0x09, 0x00, 0x34, 0x00, 0xFB, 0x55,
0x00, 0x1C, 0x00, 0x00, 0x18, 0x00, 0x7A 0xAA,
0x09,
0x00,
0x34,
0x00,
0xFB,
0x00,
0x1C,
0x00,
0x00,
0x18,
0x00,
0x7A
}; };
QVector<PointData> make_flat_points(const QSize& size, double value = 0.0) { QVector<PointData> make_flat_points(const QSize& size, double value = 0.0) {
const int width = std::max(size.width(), 1); const int width = std::max(size.width(), 1);
const int height = std::max(size.height(), 1); const int height = std::max(size.height(), 1);
QVector<PointData> points; QVector<PointData> points;
points.reserve(static_cast<int>(width * height)); points.reserve(static_cast<int>(width * height));
for (int y = 0; y < height; ++y) { for (int y = 0; y < height; ++y) {
for (int x = 0; x < width; ++x) { for (int x = 0; x < width; ++x) {
points.append(PointData{ points.append(PointData{
static_cast<double>(x), static_cast<double>(x),
static_cast<double>(y), static_cast<double>(y),
value value });
});
} }
} }
return points; return points;
@@ -81,14 +108,11 @@ std::once_flag& codec_registration_flag() {
return flag; return flag;
} }
class SensorStreamController : public QObject { class SensorStreamController: public QObject {
public: public:
SensorStreamController(std::shared_ptr<MutableValue<QVector<PointData>>> heatmap_data, SensorStreamController(std::shared_ptr<MutableValue<QVector<PointData>>> heatmap_data,
std::shared_ptr<MutableValue<QSize>> matrix_context, std::shared_ptr<MutableValue<QSize>> matrix_context,
QObject* parent = nullptr) QObject* parent = nullptr): QObject(parent), heatmap_data_(std::move(heatmap_data)), matrix_context_(std::move(matrix_context)) {
: QObject(parent)
, heatmap_data_(std::move(heatmap_data))
, matrix_context_(std::move(matrix_context)) {
std::call_once(codec_registration_flag(), [] { std::call_once(codec_registration_flag(), [] {
ffmsep::tactile::register_tactile_codec(); ffmsep::tactile::register_tactile_codec();
}); });
@@ -103,26 +127,33 @@ public:
return true; return true;
} }
const auto ports = ffmsep::CPStreamCore::list_available_ports(); const auto ports = ffmsep::CPStreamCore::list_available_ports();
std::string port_utf8; std::string port_utf8;
if (!requested_port.isEmpty()) { if (!requested_port.isEmpty()) {
port_utf8 = requested_port.toStdString(); port_utf8 = requested_port.toStdString();
const auto it = std::find_if( const auto it = std::find_if(
ports.begin(), ports.end(), ports.begin(), ports.end(), [&](const serial::PortInfo& info) { return info.port == port_utf8; });
[&](const serial::PortInfo& info) { return info.port == port_utf8; });
if (it == ports.end()) { if (it == ports.end()) {
if (ports.empty()) { if (ports.empty()) {
#if DEBUG
std::cerr << "SensorStreamController: requested port '" << port_utf8 << "' not available and no other ports detected.\n"; std::cerr << "SensorStreamController: requested port '" << port_utf8 << "' not available and no other ports detected.\n";
#endif
last_error_ = QString::fromUtf8("未检测到串口"); last_error_ = QString::fromUtf8("未检测到串口");
return false; return false;
} }
#if DEBUG
std::cerr << "SensorStreamController: requested port '" << port_utf8 << "' not available, falling back to first detected port.\n"; std::cerr << "SensorStreamController: requested port '" << port_utf8 << "' not available, falling back to first detected port.\n";
#endif
port_utf8 = ports.front().port; port_utf8 = ports.front().port;
} }
} else if (!ports.empty()) { }
else if (!ports.empty()) {
port_utf8 = ports.front().port; port_utf8 = ports.front().port;
} else { }
else {
#if DEBUG
std::cerr << "SensorStreamController: no serial ports available\n"; std::cerr << "SensorStreamController: no serial ports available\n";
#endif
last_error_ = QString::fromUtf8("未检测到串口"); last_error_ = QString::fromUtf8("未检测到串口");
return false; return false;
} }
@@ -130,12 +161,12 @@ public:
const std::uint32_t baud = baudrate == 0U ? 115200U : baudrate; const std::uint32_t baud = baudrate == 0U ? 115200U : baudrate;
ffmsep::CPStreamConfig cfg; ffmsep::CPStreamConfig cfg;
cfg.port = port_utf8; cfg.port = port_utf8;
cfg.baudrate = baud; cfg.baudrate = baud;
cfg.codec_id = ffmsep::CPCodecID::Tactile; cfg.codec_id = ffmsep::CPCodecID::Tactile;
cfg.read_chunk_size = 256; cfg.read_chunk_size = 256;
cfg.packet_queue_capacity = 128; cfg.packet_queue_capacity = 128;
cfg.frame_queue_capacity = 32; cfg.frame_queue_capacity = 32;
cfg.slave_request_command.assign(kSlaveRequestCommand.begin(), kSlaveRequestCommand.end()); cfg.slave_request_command.assign(kSlaveRequestCommand.begin(), kSlaveRequestCommand.end());
cfg.slave_request_interval = 3ms; cfg.slave_request_interval = 3ms;
@@ -149,7 +180,7 @@ public:
return false; return false;
} }
core_->set_frame_callback([this](const ffmsep::DecodedFrame& frame) { core_->set_frame_callback([this](std::shared_ptr<ffmsep::DecodedFrame> frame) {
handle_frame(frame); handle_frame(frame);
}); });
@@ -167,12 +198,27 @@ public:
} }
void stop() { void stop() {
reset_core(); if (!core_) {
active_port_.clear();
if (heatmap_data_ && matrix_context_) {
heatmap_data_->set(make_flat_points(matrix_context_->get()));
}
connected_ = false;
return;
}
core_->set_frame_callback({});
if (core_->is_running()) {
core_->stop();
}
core_->clear_frames();
connected_ = false;
active_port_.clear(); active_port_.clear();
if (heatmap_data_ && matrix_context_) { if (heatmap_data_ && matrix_context_) {
heatmap_data_->set(make_flat_points(matrix_context_->get())); heatmap_data_->set(make_flat_points(matrix_context_->get()));
} }
connected_ = false;
} }
[[nodiscard]] bool is_running() const noexcept { [[nodiscard]] bool is_running() const noexcept {
@@ -191,7 +237,22 @@ public:
return last_error_; return last_error_;
} }
private: std::future<ffmsep::persist::WriteResult> export_frames(const QString& path,
bool clear_after_export) {
if (path.isEmpty()) {
return make_failed_future(path, "export path is empty");
}
if (!core_) {
return make_failed_future(path, "stream is not active");
}
if (core_->recorded_frame_count() == 0U) {
return make_failed_future(path, "no tactile frames recorded");
}
const auto normalized = QDir::toNativeSeparators(path);
return core_->export_recorded_frames(normalized.toStdString(), clear_after_export);
}
private:
void reset_core() { void reset_core() {
connected_ = false; connected_ = false;
if (!core_) { if (!core_) {
@@ -214,95 +275,90 @@ private:
}; };
} }
void handle_frame(const ffmsep::DecodedFrame& frame) { void handle_frame(std::shared_ptr<ffmsep::DecodedFrame> frame) {
if (!frame.tactile || frame.tactile_pressures.empty()) { if (!frame->tactile || frame->tactile_pressures.empty()) {
return; return;
} }
auto pressures = frame.tactile_pressures; auto pressures = frame->tactile_pressures;
auto size_hint = frame.tactile_matrix_size; auto frame_bytes = frame->frame.data;
auto frame_bytes = frame.frame.data;
std::vector<std::uint8_t> raw_payload; std::vector<std::uint8_t> raw_payload;
if (frame.tactile) { if (frame->tactile) {
raw_payload = frame.tactile->payload; raw_payload = frame->tactile->payload;
} }
QMetaObject::invokeMethod( QMetaObject::invokeMethod(
this, this,
[this, [this,
pressures = std::move(pressures), pressures = std::move(pressures),
size_hint, frame_bytes = std::move(frame_bytes),
frame_bytes = std::move(frame_bytes), raw_payload = std::move(raw_payload)]() mutable {
raw_payload = std::move(raw_payload)]() { const auto format_raw = [](const std::vector<std::uint8_t>& data) -> std::string {
const auto format_raw = [](const std::vector<std::uint8_t>& data) -> std::string { if (data.empty()) {
if (data.empty()) { return "[]";
return "[]"; }
std::ostringstream oss;
oss << '[';
oss << std::uppercase << std::setfill('0');
for (std::size_t idx = 0; idx < data.size(); ++idx) {
if (idx != 0U) {
oss << ' ';
} }
std::ostringstream oss; oss << std::setw(2) << std::hex << static_cast<unsigned int>(data[idx]);
oss << '['; }
oss << std::uppercase << std::setfill('0'); oss << ']';
for (std::size_t idx = 0; idx < data.size(); ++idx) { return oss.str();
if (idx != 0U) { };
oss << ' ';
}
oss << std::setw(2) << std::hex << static_cast<unsigned int>(data[idx]);
}
oss << ']';
return oss.str();
};
std::cout << "[Sensor] frame=" << format_raw(frame_bytes); std::cout << "[Sensor] frame=" << format_raw(frame_bytes);
std::cout << " payload=" << format_raw(raw_payload); std::cout << " payload=" << format_raw(raw_payload);
std::cout << " received " << pressures.size() << " pressure values"; std::cout << " received " << pressures.size() << " pressure values";
if (size_hint) { const std::size_t preview = std::min<std::size_t>(pressures.size(), 12);
std::cout << " matrix=" << int(size_hint->long_edge) if (preview > 0) {
<< "x" << int(size_hint->short_edge); std::cout << " values=[";
} for (std::size_t idx = 0; idx < preview; ++idx) {
const std::size_t preview = std::min<std::size_t>(pressures.size(), 12); if (idx != 0U) {
if (preview > 0) { std::cout << ", ";
std::cout << " values=[";
for (std::size_t idx = 0; idx < preview; ++idx) {
if (idx != 0U) {
std::cout << ", ";
}
std::cout << pressures[idx];
} }
if (preview < pressures.size()) { std::cout << pressures[idx];
std::cout << ", ...";
}
std::cout << "]";
} }
std::cout << std::endl; if (preview < pressures.size()) {
std::cout << ", ...";
}
std::cout << "]";
}
std::cout << std::endl;
auto matrix = matrix_context_->get(); auto matrix = matrix_context_->get();
if (size_hint) { const auto cells_exp = static_cast<std::size_t>(std::max(1, matrix.width()) *
matrix = to_qsize(*size_hint); std::max(1, matrix.height()));
} if (cells_exp == 0) return;
matrix = normalize_matrix(matrix, pressures.size());
if (matrix.isEmpty()) {
return;
}
QVector<PointData> points; if (pressures.size() > cells_exp) {
points.reserve(matrix.width() * matrix.height()); pressures.resize(cells_exp);
for (int y = 0; y < matrix.height(); ++y) { } else if (pressures.size() < cells_exp) {
for (int x = 0; x < matrix.width(); ++x) { pressures.resize(cells_exp, 0);
const int idx = y * matrix.width() + x; }
if (idx >= static_cast<int>(pressures.size())) {
break; QVector<PointData> points;
} points.reserve(matrix.width() * matrix.height());
const auto value = static_cast<double>(pressures[static_cast<std::size_t>(idx)]); for (int y = 0; y < matrix.height(); ++y) {
points.append(PointData{ for (int x = 0; x < matrix.width(); ++x) {
static_cast<double>(x), const int idx = y * matrix.width() + x;
static_cast<double>(y), if (idx >= static_cast<int>(pressures.size())) {
value break;
});
} }
const auto value = static_cast<double>(pressures[static_cast<std::size_t>(idx)]);
points.append(PointData{
static_cast<double>(x),
static_cast<double>(y),
value });
} }
matrix_context_->set(matrix); }
heatmap_data_->set(std::move(points)); matrix_context_->set(matrix);
}, heatmap_data_->set(std::move(points));
Qt::QueuedConnection); },
Qt::QueuedConnection);
} }
[[nodiscard]] QSize normalize_matrix(QSize candidate, std::size_t value_count) const { [[nodiscard]] QSize normalize_matrix(QSize candidate, std::size_t value_count) const {
@@ -316,8 +372,7 @@ private:
} }
if (hint.width() > 0 && hint.height() > 0) { if (hint.width() > 0 && hint.height() > 0) {
const auto cells = static_cast<std::size_t>(hint.width()) * const auto cells = static_cast<std::size_t>(hint.width()) * static_cast<std::size_t>(hint.height());
static_cast<std::size_t>(hint.height());
if (cells == value_count) { if (cells == value_count) {
return hint; return hint;
} }
@@ -327,7 +382,7 @@ private:
const auto width = static_cast<std::size_t>(hint.width()); const auto width = static_cast<std::size_t>(hint.width());
if (width != 0U && (value_count % width) == 0U) { if (width != 0U && (value_count % width) == 0U) {
const auto height = static_cast<int>(value_count / width); const auto height = static_cast<int>(value_count / width);
return QSize{hint.width(), height}; return QSize{ hint.width(), height };
} }
} }
@@ -335,7 +390,7 @@ private:
const auto height = static_cast<std::size_t>(hint.height()); const auto height = static_cast<std::size_t>(hint.height());
if (height != 0U && (value_count % height) == 0U) { if (height != 0U && (value_count % height) == 0U) {
const auto width = static_cast<int>(value_count / height); const auto width = static_cast<int>(value_count / height);
return QSize{width, hint.height()}; return QSize{ width, hint.height() };
} }
} }
@@ -358,50 +413,92 @@ private:
} }
if ((value_count % divisor) == 0U) { if ((value_count % divisor) == 0U) {
const auto height = static_cast<int>(value_count / divisor); const auto height = static_cast<int>(value_count / divisor);
return QSize{width, height}; return QSize{ width, height };
} }
} }
return QSize{static_cast<int>(value_count), 1}; return QSize{ static_cast<int>(value_count), 1 };
} }
std::shared_ptr<MutableValue<QVector<PointData>>> heatmap_data_; std::shared_ptr<MutableValue<QVector<PointData>>> heatmap_data_;
std::shared_ptr<MutableValue<QSize>> matrix_context_; std::shared_ptr<MutableValue<QSize>> matrix_context_;
std::unique_ptr<ffmsep::CPStreamCore> core_; std::unique_ptr<ffmsep::CPStreamCore> core_;
QString active_port_; QString active_port_;
QString last_error_; QString last_error_;
bool connected_ = false; bool connected_ = false;
static std::future<ffmsep::persist::WriteResult> make_failed_future(
const QString& path,
std::string message) {
std::promise<ffmsep::persist::WriteResult> promise;
auto future = promise.get_future();
ffmsep::persist::WriteResult result{
false,
std::move(message),
path.toStdString()
};
promise.set_value(std::move(result));
return future;
}
}; };
struct SensorUiState { struct SensorUiState {
std::shared_ptr<MutableValue<QString>> link_icon = std::shared_ptr<MutableValue<QString>> link_icon =
std::make_shared<MutableValue<QString>>(QString::fromLatin1(material::icon::kAddLink)); std::make_shared<MutableValue<QString>>(QString::fromLatin1(material::icon::kAddLink));
std::shared_ptr<MutableValue<QVector<PointData>>> heatmap_data = std::shared_ptr<MutableValue<QVector<PointData>>> heatmap_data =
std::make_shared<MutableValue<QVector<PointData>>>(); std::make_shared<MutableValue<QVector<PointData>>>();
std::shared_ptr<MutableValue<QSize>> heatmap_matrix = std::shared_ptr<MutableValue<QSize>> heatmap_matrix =
std::make_shared<MutableValue<QSize>>(); std::make_shared<MutableValue<QSize>>();
std::shared_ptr<MutableValue<QPair<int, int>>> heatmap_range =
std::make_shared<MutableValue<QPair<int, int>>>(QPair<int, int>{0, 300});
std::shared_ptr<MutableValue<QStringList>> port_items = std::shared_ptr<MutableValue<QStringList>> port_items =
std::make_shared<MutableValue<QStringList>>(); std::make_shared<MutableValue<QStringList>>();
QString selected_port; std::shared_ptr<MutableValue<QStringList>> profile_items =
std::uint32_t selected_baud = 115200; std::make_shared<MutableValue<QStringList>>();
QString selected_port;
QString selected_profile;
std::uint32_t selected_baud = 115200;
std::unique_ptr<SensorStreamController> controller; std::unique_ptr<SensorStreamController> controller;
SensorUiState() { SensorUiState() {
const QSize size{3, 4}; const QSize size{ 3, 4 };
heatmap_matrix->set_silent(size); heatmap_matrix->set_silent(size);
heatmap_data->set_silent(make_flat_points(size)); heatmap_data->set_silent(make_flat_points(size));
// 初始化串口列表 // 初始化串口列表
QStringList ports_list; QStringList ports_list;
const auto ports = ffmsep::CPStreamCore::list_available_ports(); const auto ports = ffmsep::CPStreamCore::list_available_ports();
ports_list.reserve(static_cast<qsizetype>(ports.size())); ports_list.reserve(static_cast<qsizetype>(ports.size()));
for (const auto& info : ports) { for (const auto& info: ports) {
ports_list.emplace_back(QString::fromStdString(info.port)); ports_list.emplace_back(QString::fromStdString(info.port));
} }
port_items->set_silent(ports_list); port_items->set_silent(ports_list);
if (selected_port.isEmpty() && !ports_list.isEmpty()) { if (selected_port.isEmpty() && !ports_list.isEmpty()) {
selected_port = ports_list.front(); selected_port = ports_list.front();
} }
GlobalHelper::instance().reload_profiles();
QStringList profile_list;
const auto& profiles = GlobalHelper::instance().get_all_profile();
profile_list.reserve(static_cast<qsizetype>(profiles.size()));
for (const auto& p : profiles) {
profile_list << p.name;
}
profile_items->set_silent(profile_list);
if (selected_profile.isEmpty() && !profile_list.isEmpty()) {
selected_profile = profile_list.front();
if (!profiles.empty()) {
const auto size = QSize { std::max(1, profiles.front().matrix_width),
std::max(1, profiles.front().matrix_height) };
selected_baud = profiles.front().baud_rate == 0 ? 115200U : static_cast<std::uint32_t>(profiles.front().baud_rate);
heatmap_matrix->set_silent(size);
heatmap_data->set_silent(make_flat_points(size));
const int range_min = profiles.front().range_left;
const int range_max = (profiles.front().range_right == profiles.front().range_left)
? profiles.front().range_left + 1
: profiles.front().range_right;
heatmap_range->set_silent(QPair<int, int>{range_min, range_max});
}
}
controller = std::make_unique<SensorStreamController>(heatmap_data, heatmap_matrix); controller = std::make_unique<SensorStreamController>(heatmap_data, heatmap_matrix);
} }
@@ -414,24 +511,54 @@ SensorUiState& sensor_state() {
} // namespace } // namespace
static auto ComConfigComponent(ThemeManager& manager) { void RefreshProfilesForView() {
auto& sensor = sensor_state(); auto& sensor = sensor_state();
auto link_icon_context = sensor.link_icon;
// 串口下拉:改为绑定可变数据源,初始值由 SensorUiState 构造时填充 GlobalHelper::instance().reload_profiles();
QStringList profile_list;
const auto& profiles = GlobalHelper::instance().get_all_profile();
profile_list.reserve(static_cast<qsizetype>(profiles.size()));
for (const auto& p : profiles) {
profile_list << p.name;
}
if (!sensor.selected_profile.isEmpty()) {
const bool exists = profile_list.contains(sensor.selected_profile);
if (!exists) {
sensor.selected_profile = profile_list.isEmpty() ? QString{} : profile_list.front();
}
}
else if (!profile_list.isEmpty()) {
sensor.selected_profile = profile_list.front();
}
if (!sensor.selected_profile.isEmpty()) {
const auto it = std::find_if(profiles.begin(), profiles.end(),
[&](const ConfigProfile& p) { return p.name == sensor.selected_profile; });
if (it != profiles.end()) {
sensor.selected_baud = it->baud_rate == 0 ? 115200U : static_cast<std::uint32_t>(it->baud_rate);
const auto size = QSize { std::max(1, it->matrix_width), std::max(1, it->matrix_height) };
sensor.heatmap_matrix->set(size);
sensor.heatmap_data->set(make_flat_points(size));
const int range_min = it->range_left;
const int range_max = (it->range_right == it->range_left) ? it->range_left + 1 : it->range_right;
sensor.heatmap_range->set(QPair<int, int>{range_min, range_max});
}
}
sensor.profile_items->set(std::move(profile_list));
}
static auto ComConfigComponent(ThemeManager& manager) {
auto& sensor = sensor_state();
auto link_icon_context = sensor.link_icon;
if (sensor.selected_port.isEmpty() && !sensor.port_items->get().isEmpty()) { if (sensor.selected_port.isEmpty() && !sensor.port_items->get().isEmpty()) {
sensor.selected_port = sensor.port_items->get().front(); sensor.selected_port = sensor.port_items->get().front();
} }
const QStringList baud_items{
QString::fromLatin1("9600"),
QString::fromLatin1("115200")
};
if (sensor.selected_baud == 0U) { if (sensor.selected_baud == 0U) {
sensor.selected_baud = 115200U; sensor.selected_baud = 115200U;
} }
const auto row = new Row { const auto row = new Row{
// lnpro::Item<FilledTextField> { // lnpro::Item<FilledTextField> {
// text_field::pro::ThemeManager {manager}, // text_field::pro::ThemeManager {manager},
// text_field::pro::LeadingIcon {material::icon::kSearch, material::regular::font}, // text_field::pro::LeadingIcon {material::icon::kSearch, material::regular::font},
@@ -440,147 +567,232 @@ static auto ComConfigComponent(ThemeManager& manager) {
// slogen_context, // slogen_context,
// }, // },
// }, // },
lnpro::Item<MatSelect> { lnpro::Item<FilledDropdownMenu>{
slpro::ThemeManager {manager}, dmpro::ThemeManager{ manager },
slpro::LeadingIcon {material::icon::kArrowDropDown, material::regular::font}, dmpro::LeadingIcon{ material::icon::kArrowDropDown, material::regular::font },
slpro::IndexChanged {[sensor_ptr = &sensor](auto& self){ dmpro::TextChanged{ [sensor_ptr = &sensor](QString text) {
const auto text = self.currentText(); // const auto text = self.currentText();
if (!text.isEmpty()) { if (!text.isEmpty()) {
sensor_ptr->selected_port = text; sensor_ptr->selected_port = text;
} }
}}, } },
slpro::LeadingText {"COM"}, dmpro::LabelText{ "COM" },
MutableForward { MutableForward{
slpro::SelectItems {}, dmpro::Items{},
sensor.port_items, sensor.port_items,
},
}, },
lnpro::Item<MatSelect> {
slpro::ThemeManager {manager },
slpro::LeadingIcon { material::icon::kArrowDropDown, material::regular::font},
slpro::IndexChanged {[sensor_ptr = &sensor](auto& self){
bool ok = false;
const auto text = self.currentText();
const auto value = text.toUInt(&ok);
if (ok && value > 0U) {
sensor_ptr->selected_baud = static_cast<std::uint32_t>(value);
}
}},
slpro::LeadingText {"Baud"},
slpro::SelectItems {baud_items},
}, },
lnpro::SpacingItem {20}, lnpro::Item<FilledDropdownMenu>{
lnpro::Item<IconButton> { dmpro::ThemeManager{ manager },
ibpro::ThemeManager {manager}, dmpro::LeadingIcon{ material::icon::kArrowDropDown, material::regular::font },
ibpro::FixedSize {40, 40}, dmpro::TextChanged{ [sensor_ptr = &sensor](QString text) {
ibpro::Color { IconButton::Color::TONAL }, if (!text.isEmpty()) {
ibpro::Font { material::kRoundSmallFont }, sensor_ptr->selected_profile = text;
MutableForward { const auto& profiles = GlobalHelper::instance().get_all_profile();
icon_button::pro::FontIcon {}, const auto it = std::find_if(profiles.begin(), profiles.end(),
link_icon_context, [&text](const ConfigProfile& p) { return p.name == text; });
}, if (it != profiles.end()) {
ibpro::Clickable { [sensor_ptr = &sensor, link_icon_context]{ const auto baud = it->baud_rate == 0 ? 115200U : static_cast<std::uint32_t>(it->baud_rate);
auto& sensor = *sensor_ptr; sensor_ptr->selected_baud = baud;
if (!sensor.controller) { const auto size = QSize { std::max(1, it->matrix_width), std::max(1, it->matrix_height) };
return; sensor_ptr->heatmap_matrix->set(size);
sensor_ptr->heatmap_data->set(make_flat_points(size));
const int range_min = it->range_left;
const int range_max = (it->range_right == it->range_left) ? it->range_left + 1 : it->range_right;
sensor_ptr->heatmap_range->set(QPair<int, int>{range_min, range_max});
} }
if (sensor.controller->is_connected()) { }
sensor.controller->stop(); } },
link_icon_context->set(QString::fromLatin1(material::icon::kAddLink)); dmpro::LabelText{ "Profile" },
} else { MutableForward{
const auto port = sensor.selected_port; dmpro::Items{},
const auto baud = sensor.selected_baud == 0U ? 115200U : sensor.selected_baud; sensor.profile_items,
if (sensor.controller->start(port, baud)) {
sensor.selected_port = sensor.controller->active_port();
link_icon_context->set(QString::fromLatin1(material::icon::kLinkOff));
} else {
std::cerr << "Failed to start sensor stream: "
<< sensor.controller->last_error().toStdString()
<< "\n";
}
}
} }
}, },
lnpro::Item<IconButton> { },
ibpro::ThemeManager { manager }, lnpro::SpacingItem{ 20 },
ibpro::FixedSize { 40, 40 }, lnpro::Item<IconButton>{
ibpro::Color { IconButton::Color::TONAL }, ibpro::ThemeManager{ manager },
ibpro::Font { material::kRoundSmallFont }, ibpro::FixedSize{ 40, 40 },
ibpro::FontIcon { material::icon::kRefresh }, ibpro::Color{ IconButton::Color::TONAL },
ibpro::Clickable {[&sensor] { ibpro::Font{ material::kRegularExtraSmallFont },
// 刷新串口列表 MutableForward{
QStringList ports_list; icon_button::pro::FontIcon{},
const auto ports = ffmsep::CPStreamCore::list_available_ports(); link_icon_context,
ports_list.reserve(static_cast<qsizetype>(ports.size())); },
for (const auto& info : ports) { ibpro::Clickable{ [sensor_ptr = &sensor, link_icon_context] {
ports_list.emplace_back(QString::fromStdString(info.port)); auto& sensor = *sensor_ptr;
if (!sensor.controller) {
return;
}
if (sensor.controller->is_connected()) {
sensor.controller->stop();
link_icon_context->set(QString::fromLatin1(material::icon::kAddLink));
}
else {
const auto port = sensor.selected_port;
const auto baud = sensor.selected_baud == 0U ? 115200U : sensor.selected_baud;
if (sensor.controller->start(port, baud)) {
sensor.selected_port = sensor.controller->active_port();
link_icon_context->set(QString::fromLatin1(material::icon::kLinkOff));
}
else {
std::cerr << "Failed to start sensor stream: "
<< sensor.controller->last_error().toStdString()
<< "\n";
}
}
} } },
lnpro::Item<IconButton>{
ibpro::ThemeManager{ manager },
ibpro::FixedSize{ 40, 40 },
ibpro::Color{ IconButton::Color::TONAL },
ibpro::Font{ material::kRegularExtraSmallFont },
ibpro::FontIcon{ material::icon::kRefresh },
ibpro::Clickable{ [&sensor] {
QStringList ports_list;
const auto ports = ffmsep::CPStreamCore::list_available_ports();
ports_list.reserve(static_cast<qsizetype>(ports.size()));
for (const auto& info: ports) {
ports_list.emplace_back(QString::fromStdString(info.port));
}
if (!sensor.selected_port.isEmpty()) {
const bool exists = ports_list.contains(sensor.selected_port);
if (!exists) {
sensor.selected_port = ports_list.isEmpty() ? QString{} : ports_list.front();
}
}
else if (!ports_list.isEmpty()) {
sensor.selected_port = ports_list.front();
}
sensor.port_items->set(std::move(ports_list));
RefreshProfilesForView();
} },
},
lnpro::Item<FilledButton>{
fbpro::ThemeManager{ manager },
fbpro::FixedSize{ 40, 40 },
fbpro::Radius{ 8.0 },
// fbpro::Color { IconButton::Color::TONAL },
fbpro::Font{ material::kRegularExtraSmallFont },
fbpro::Text{ "drive_file_move" },
fbpro::Clickable{ [&sensor] {
auto* controller = sensor.controller.get();
if (!controller) {
QMessageBox::warning(nullptr,
QStringLiteral("导出失败"),
QStringLiteral("当前串流尚未初始化。"));
return;
}
const auto documents = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation);
const auto timestamp = QDateTime::currentDateTime().toString(QStringLiteral("yyyyMMdd_HHmmss"));
QString suggested_name = QStringLiteral("touchsensor_%1.json").arg(timestamp);
QString initial_path = documents.isEmpty() ? suggested_name : QDir(documents).filePath(suggested_name);
const QString chosen_path = QFileDialog::getSaveFileName(
nullptr,
QStringLiteral("导出触觉帧"),
initial_path,
QStringLiteral("JSON 文件 (*.json)"));
if (chosen_path.isEmpty()) {
return;
}
auto future = controller->export_frames(chosen_path, false);
std::thread([future = std::move(future)]() mutable {
ffmsep::persist::WriteResult result{};
try {
result = future.get();
}
catch (const std::exception& ex) {
result.ok = false;
result.error = ex.what();
}
catch (...) {
result.ok = false;
result.error = "unknown export failure";
} }
// 保持原选择(若仍然存在) if (auto* app = QCoreApplication::instance()) {
if (!sensor.selected_port.isEmpty()) { QMetaObject::invokeMethod(app, [res = std::move(result)]() {
const bool exists = ports_list.contains(sensor.selected_port); if (res.ok) {
if (!exists) { QMessageBox::information(
sensor.selected_port = ports_list.isEmpty() ? QString{} : ports_list.front(); nullptr,
} QStringLiteral("导出成功"),
} else if (!ports_list.isEmpty()) { QStringLiteral("触觉帧已导出至:\n%1")
sensor.selected_port = ports_list.front(); .arg(QString::fromStdString(res.path)));
} else {
const auto error = QString::fromStdString(
res.error.empty() ? std::string{"unknown error"} : res.error);
const auto target = QString::fromStdString(res.path);
const auto message = target.isEmpty()
? QStringLiteral("原因:%1").arg(error)
: QStringLiteral("原因:%1\n目标:%2").arg(error, target);
QMessageBox::warning(nullptr,
QStringLiteral("导出失败"),
message);
} }, Qt::QueuedConnection);
} }
})
sensor.port_items->set(std::move(ports_list)); .detach();
}}, } } }
},
}; };
return new Widget { return new Widget{
widget::pro::Layout {row}, widget::pro::Layout{ row },
}; };
} }
static auto DisplayComponent(ThemeManager& /*manager*/, int /*index*/ = 0) noexcept { static auto DisplayComponent(ThemeManager& manager, int /*index*/ = 0) noexcept {
auto& sensor = sensor_state(); auto& sensor = sensor_state();
const auto row = new Row{ const auto row = new Row{
lnpro::Item<HeatMapPlot> { lnpro::Item<HeatMapPlot>{
plot_widget::pro::SizePolicy { plot_widget::pro::SizePolicy{
QSizePolicy::Expanding, QSizePolicy::Expanding,
}, },
MutableForward { plot_widget::pro::ThemeManager{ manager },
plot_widget::pro::PlotData {}, MutableForward{
sensor.heatmap_data, plot_widget::pro::PlotData{},
}, sensor.heatmap_data,
pwpro::MatrixSize { },
sensor.heatmap_matrix->get() pwpro::MatrixSize{
}, sensor.heatmap_matrix->get() },
MutableTransform { MutableTransform{
[](auto& widget, const QSize& size) { [](auto& widget, const QSize& size) {
pwpro::MatrixSize{size}.apply(widget); pwpro::MatrixSize{ size }.apply(widget);
}, },
sensor.heatmap_matrix sensor.heatmap_matrix },
}, MutableTransform{
}, [](auto& widget, const QPair<int, int>& range) {
const double min = static_cast<double>(range.first);
const double max = static_cast<double>(range.second);
widget.set_color_gradient_range(min, max);
},
sensor.heatmap_range },
},
}; };
return new Widget { return new Widget{
widget::pro::Layout{row}, widget::pro::Layout{ row },
}; };
} }
auto ViewComponent(ViewComponentState& state) noexcept -> raw_pointer<QWidget> { auto ViewComponent(ViewComponentState& state) noexcept -> raw_pointer<QWidget> {
return new FilledCard { return new FilledCard{
capro::ThemeManager { state.manager }, capro::ThemeManager{ state.manager },
capro::SizePolicy {QSizePolicy::Expanding}, capro::SizePolicy{ QSizePolicy::Expanding },
capro::Layout<Col> { capro::Layout<Col>{
lnpro::Alignment {Qt::AlignTop}, lnpro::Alignment{ Qt::AlignTop },
lnpro::Margin {10}, // lnpro::Margin {10},
lnpro::Spacing {10}, // lnpro::Spacing {10},
lnpro::Item { lnpro::Item{
ComConfigComponent(state.manager), ComConfigComponent(state.manager),
}, },
lnpro::Item<Row>{
lnpro::Item<Row> { lnpro::Item{
lnpro::Item {
DisplayComponent(state.manager), DisplayComponent(state.manager),
}, },
lnpro::Item { lnpro::Item{
DisplayComponent(state.manager), DisplayComponent(state.manager),
}, },
}, },

View File

@@ -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>

View File

@@ -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 {

View File

@@ -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>

View File

@@ -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 {

View File

@@ -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 {
@@ -95,13 +96,21 @@ using BoxLayout = Declarative<T, CheckerOr<linear::pro::checker, layout::pro::ch
using Row = BoxLayout<QHBoxLayout>; using Row = BoxLayout<QHBoxLayout>;
using Col = BoxLayout<QVBoxLayout>; 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 {};
} }
} }

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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);
} }

View 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;
}

View File

@@ -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>

View File

@@ -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 {

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -1,5 +1,4 @@
#pragma once #pragma once
#include <qdebug.h>
#include <qpixmap.h> #include <qpixmap.h>
namespace creeper { namespace creeper {

View File

@@ -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";
@@ -143,6 +148,7 @@ namespace material {
constexpr auto kLogout = "logout"; constexpr auto kLogout = "logout";
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 +170,9 @@ namespace material {
// sensor icon // sensor icon
constexpr auto kTouchSensor = "touch_app"; constexpr auto kTouchSensor = "touch_app";
// setting page
constexpr auto kBorderColor = "border_color";
} }
} }
} }

View File

@@ -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 {

View File

@@ -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) // 存储主轴对齐

View File

@@ -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 {

View File

@@ -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>>;
} }
} }

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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;

View File

@@ -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;

View File

@@ -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*>()) };

View File

@@ -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;

View File

@@ -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 {

View File

@@ -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>

View File

@@ -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

View File

@@ -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>

View File

@@ -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 {

View File

@@ -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;

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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;

View File

@@ -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 =

View File

@@ -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 {

View 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);
}

View File

@@ -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&);
@@ -88,15 +83,15 @@ namespace select_widget::internal {
auto set_measurements(const Measurements&) noexcept -> void; auto set_measurements(const Measurements&) noexcept -> void;
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 }
void apply(auto& self) const {self.set_leading_icon(code, font);} , font { 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

View 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;
};

View File

@@ -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 {

View File

@@ -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>

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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>

View File

@@ -24,14 +24,14 @@ auto Slider::get_progress() const noexcept -> double {
auto Slider::mousePressEvent(QMouseEvent* event) -> void { auto Slider::mousePressEvent(QMouseEvent* event) -> void {
pimpl->mouse_press_event(event); pimpl->mouse_press_event(event);
Widget::mousePressEvent(event); QWidget::mousePressEvent(event);
} }
auto Slider::mouseReleaseEvent(QMouseEvent* event) -> void { auto Slider::mouseReleaseEvent(QMouseEvent* event) -> void {
pimpl->mouse_release_event(event); pimpl->mouse_release_event(event);
Widget::mouseReleaseEvent(event); QWidget::mouseReleaseEvent(event);
} }
auto Slider::mouseMoveEvent(QMouseEvent* event) -> void { auto Slider::mouseMoveEvent(QMouseEvent* event) -> void {
pimpl->mouse_move_event(event); pimpl->mouse_move_event(event);
Widget::mouseMoveEvent(event); QWidget::mouseMoveEvent(event);
} }
auto Slider::paintEvent(QPaintEvent* event) -> void { pimpl->paint_event(event); } auto Slider::paintEvent(QPaintEvent* event) -> void { pimpl->paint_event(event); }

View File

@@ -1,13 +1,13 @@
#pragma once #pragma once
#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/widget/widget.hh" #include "creeper-qt/utility/wrapper/widget.hh"
namespace creeper::slider::internal { namespace creeper::slider::internal {
class Slider : public Widget { class Slider : public QWidget {
Q_OBJECT Q_OBJECT
CREEPER_PIMPL_DEFINITION(Slider) CREEPER_PIMPL_DEFINITION(Slider)
@@ -95,6 +95,7 @@ public:
auto set_progress(double) noexcept -> void; auto set_progress(double) noexcept -> void;
auto get_progress() const noexcept -> double; auto get_progress() const noexcept -> double;
/// @bug Signals can not be exported on Windows
signals: signals:
auto signal_value_change(double) -> void; auto signal_value_change(double) -> void;
auto signal_value_change_finished(double) -> void; auto signal_value_change_finished(double) -> void;

View File

@@ -1,9 +1,9 @@
#include "sliders.hh" #include "sliders.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/transition.hh" #include "creeper-qt/utility/animation/transition.hh"
#include "modern-qt/utility/painter/helper.hh" #include "creeper-qt/utility/painter/helper.hh"
#include <qevent.h> #include <qevent.h>
#include <qnamespace.h> #include <qnamespace.h>

View File

@@ -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/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"
#include <qabstractbutton.h> #include <qabstractbutton.h>

View File

@@ -3,11 +3,11 @@
#include <qpainter.h> #include <qpainter.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/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/painter/helper.hh" #include "creeper-qt/utility/painter/helper.hh"
using namespace creeper::_switch::internal; using namespace creeper::_switch::internal;

View File

@@ -1,9 +1,9 @@
#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/common.hh" #include "creeper-qt/utility/wrapper/common.hh"
#include "modern-qt/utility/wrapper/widget.hh" #include "creeper-qt/utility/wrapper/widget.hh"
#include <qlineedit.h> #include <qlineedit.h>
@@ -114,6 +114,17 @@ namespace text_field::pro {
void apply(auto& self) const { self.set_leading_icon(code, font); } void apply(auto& self) const { self.set_leading_icon(code, font); }
}; };
template <typename F>
using OnTextChanged =
common::pro::SignalInjection<F, Token, &internal::BasicTextField::textChanged>;
template <typename F>
using OnEditingFinished =
common::pro::SignalInjection<F, Token, &internal::BasicTextField::editingFinished>;
template <typename F>
using OnChanged = OnTextChanged<F>;
template <class TextField> template <class TextField>
concept trait = std::derived_from<TextField, Token>; concept trait = std::derived_from<TextField, Token>;

View File

@@ -1,12 +1,12 @@
#include "text-fields.hh" #include "text-fields.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/transition.hh" #include "creeper-qt/utility/animation/transition.hh"
#include "modern-qt/utility/painter/common.hh" #include "creeper-qt/utility/painter/common.hh"
#include "modern-qt/utility/painter/container.hh" #include "creeper-qt/utility/painter/container.hh"
#include "modern-qt/utility/painter/helper.hh" #include "creeper-qt/utility/painter/helper.hh"
#include "modern-qt/utility/painter/shape.hh" #include "creeper-qt/utility/painter/shape.hh"
#include <qpainter.h> #include <qpainter.h>
#include <qpainterpath.h> #include <qpainterpath.h>
@@ -121,93 +121,97 @@ public:
} }
auto paint_filled(QPaintEvent*) -> void { auto paint_filled(QPaintEvent*) -> void {
const auto rect = self.rect(); const auto widget_rect = self.rect();
const auto color = get_color_tokens(); const auto color = get_color_tokens();
constexpr auto container_radius = 5; constexpr auto container_radius = 5;
update_component_status(FieldType::FILLED); update_component_status(FieldType::FILLED);
auto painter = QPainter { &self }; auto painter = QPainter { &self };
const auto container_rect = QRect { widget_rect.left(),
widget_rect.top() + (widget_rect.height() - measurements.container_height) / 2,
widget_rect.width(), measurements.container_height };
// Container // Container
{ {
util::PainterHelper { painter } util::PainterHelper { painter }
.set_render_hint(QPainter::Antialiasing) .set_render_hint(QPainter::Antialiasing)
.rounded_rectangle(color.container, Qt::transparent, 0, rect, container_radius, .rounded_rectangle(color.container, Qt::transparent, 0, container_rect,
container_radius, 0, 0); container_radius, container_radius, 0, 0);
} }
// Active Indicator // Active Indicator
{ {
const auto p0 = rect.bottomLeft(); const auto p0 = container_rect.bottomLeft();
const auto p1 = rect.bottomRight(); const auto p1 = container_rect.bottomRight();
painter.setBrush(Qt::NoBrush); painter.setBrush(Qt::NoBrush);
painter.setPen(QPen { color.active_indicator, filled_line_width() }); painter.setPen(QPen { color.active_indicator, filled_line_width() });
painter.drawLine(p0, p1); painter.drawLine(p0, p1);
} }
// Leading icon
// Leading icon
if (!leading_icon.isNull()) { if (!leading_icon.isNull()) {
// //
} else if (!leading_icon_code.isEmpty()) { } else if (!leading_icon_code.isEmpty()) {
const auto icon_rect = QRectF {
const auto rect = QRectF { 1.0 * container_rect.left() + measurements.row_padding_with_icons,
1.0 * measurements.row_padding_with_icons, 1.0 * container_rect.top()
0.5 * (measurements.container_height - measurements.icon_rect_size), + (measurements.container_height - measurements.icon_rect_size) / 2.0,
1.0 * measurements.icon_rect_size, 1.0 * measurements.icon_rect_size,
1.0 * measurements.icon_rect_size, 1.0 * measurements.icon_rect_size,
}; };
painter.save(); painter.save();
painter.setBrush(Qt::NoBrush); painter.setBrush(Qt::NoBrush);
painter.setPen(QPen { color.leading_icon }); painter.setPen(QPen { color.leading_icon });
painter.setFont(leading_icon_font); painter.setFont(leading_icon_font);
painter.drawText(rect, leading_icon_code, { Qt::AlignCenter }); painter.drawText(icon_rect, leading_icon_code, { Qt::AlignCenter });
painter.restore(); painter.restore();
} }
// Label Text // Label Text
if (!label_text.isEmpty()) { if (!label_text.isEmpty()) {
const auto position = self.text().isEmpty() ? *label_position : 1.; const auto position = self.text().isEmpty() ? *label_position : 1.;
const auto margins = self.textMargins(); const auto margins = self.textMargins();
// qDebug() << "margins = " << margins;
const auto center_label_padding = const auto center_label_y = container_rect.top()
0.5 * (measurements.container_height - measurements.icon_rect_size); + (measurements.container_height - measurements.label_rect_size) / 2.0;
const auto rect_center = QRectF { const auto rect_center = QRectF {
QPointF(margins.left(), center_label_padding), QPointF(margins.left(), center_label_y),
QPointF(self.width() - margins.right(), self.height() - center_label_padding), QPointF(container_rect.right() - margins.right(),
center_label_y + measurements.label_rect_size),
}; };
const auto rect_top = QRectF { const auto rect_top = QRectF {
QPointF(margins.left(), measurements.col_padding), QPointF(margins.left(), container_rect.top() + measurements.col_padding),
QPointF(self.width() - margins.right(), margins.top()), QPointF(container_rect.right() - margins.right(),
container_rect.top() + measurements.col_padding + measurements.label_rect_size),
}; };
const auto rect = animate::interpolate(rect_center, rect_top, position); const auto rect = animate::interpolate(rect_center, rect_top, position);
const auto scale = 1. - position * 0.25;
const auto scale = 1. - position * 0.25;
const auto anchor = QPointF { rect.left(), rect.center().y() }; const auto anchor = QPointF { rect.left(), rect.center().y() };
painter.save(); painter.save();
painter.translate(anchor); painter.translate(anchor);
painter.scale(scale, scale); painter.scale(scale, scale);
painter.translate(-anchor); painter.translate(-anchor);
painter.setBrush(Qt::NoBrush); painter.setBrush(Qt::NoBrush);
painter.setPen(QPen { color.label_text }); painter.setPen(QPen { color.label_text });
painter.setFont(standard_text_font); painter.setFont(standard_text_font);
painter.setRenderHint(QPainter::Antialiasing); painter.setRenderHint(QPainter::Antialiasing);
painter.drawText(rect, label_text, { Qt::AlignVCenter | Qt::AlignLeading }); painter.drawText(rect, label_text, { Qt::AlignVCenter | Qt::AlignLeading });
painter.restore(); painter.restore();
} }
// Hovered State Layer // Hovered State Layer
if (is_hovered) { if (is_hovered) {
util::PainterHelper { painter } util::PainterHelper { painter }
.set_render_hint(QPainter::Antialiasing) .set_render_hint(QPainter::Antialiasing)
.rounded_rectangle(color_specs.state_layer, Qt::transparent, 0, rect, .rounded_rectangle(color_specs.state_layer, Qt::transparent, 0, container_rect,
container_radius, container_radius, 0, 0); container_radius, container_radius, 0, 0);
} }
} }
@@ -308,12 +312,10 @@ public:
}, },
Paint::Box { Paint::Box {
BoxImpl { leading_box_size, Qt::AlignCenter, BoxImpl { leading_box_size, Qt::AlignCenter,
{ static_cast<qreal>(measurements.row_padding_with_icons), 0 } }, { 1. * measurements.row_padding_with_icons, 0 } },
Paint::Text { Paint::Icon {
TextOption { Qt::AlignCenter }, Icon { leading_font_name, leading_icon_code },
Size { leading_box_size }, Size { leading_box_size },
Font { leading_icon_font },
Text { leading_icon_code },
Color { color_tokens.leading_icon }, Color { color_tokens.leading_icon },
}, },
}, },
@@ -459,8 +461,9 @@ private:
QString trailing_code; QString trailing_code;
QString trailing_font; QString trailing_font;
QFont leading_icon_font; // State Cache
QFont standard_text_font; QFont leading_icon_font = {};
QFont standard_text_font = {};
Animatable animatable; Animatable animatable;
std::unique_ptr<TransitionValue<PidState<double>>> label_position; std::unique_ptr<TransitionValue<PidState<double>>> label_position;

View File

@@ -1,8 +1,8 @@
#pragma once #pragma once
#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/widget.hh" #include "creeper-qt/utility/wrapper/widget.hh"
#include <qlabel.h> #include <qlabel.h>
namespace creeper::text::internal { namespace creeper::text::internal {
@@ -42,6 +42,9 @@ using AdjustSize = ActionProp<Token, [](auto& self) { self.adjustSize(); }>;
using Alignment = using Alignment =
SetterProp<Token, Qt::Alignment, [](auto& self, const auto& v) { self.setAlignment(v); }>; SetterProp<Token, Qt::Alignment, [](auto& self, const auto& v) { self.setAlignment(v); }>;
using TextInteractionFlags = SetterProp<Token, Qt::TextInteractionFlags,
[](auto& self, const auto& v) { self.setTextInteractionFlags(v); }>;
template <class T> template <class T>
concept trait = std::derived_from<T, Token>; concept trait = std::derived_from<T, Token>;

Some files were not shown because too many files have changed in this diff Show More