493fa6ba0e
STATUS(命令 4)一帧回答"现在在哪个画面":kind(MENU/PAGE/STANDBY) + 菜单路径/选中 + 页 id,布局契约与字节序见 ui_net_sink.h。由 uiTask 轮询驱动,同上下文读静态导航状态:零画面副作用、免锁、不发日志。 新增只读接口 ui_menu_get_nav_state / ui_content_page_current_desc。 GOTO(命令 5)按页 id 查生成表直开内容页,以 VIEW 权限进入(与 "已过查看密码"同档,不代输任何密码)。编译期门控 DTU_BENCH || UI_REMO_ALLOW_GOTO:产品构建恒拒(status=4),模拟器 构建默认开启便于无板验证;执行类页(clear/reset)固件侧同样拒绝 (status=2,与主机 EXEC_PAGE_IDS 同一事实源);待机中拒绝(status=5), 由主机按键唤醒后再来。成功应答带回目标叶节点下标便于对账。 ui_sink 主机单测补导航侧链接假件 + 最小 lvgl 桩(仅本测试目录)。 产品构建 Keil 0 Error 0 Warning;ui_sink 单测 17/17。
616 lines
27 KiB
C
616 lines
27 KiB
C
/*
|
||
* 模块名称:UI Content Page (内容页框架)
|
||
* 模块功能:菜单叶子节点进入的内容页(独立全屏页面)框架——全屏容器
|
||
* 自绘全部内容(浅 V 装饰 + 居中标题 + 行数据 + 页脚页码),无子对象;
|
||
* 只持机制:屏/容器按需建毁、焦点、按键分发、描述符驱动通用绘制;
|
||
* 不持任何页面数据——各页数据/几何/钩子由 ui_page_xxx 模块经
|
||
* content_page_def_t 提供,框架按**描述符指针**开页;"哪个叶子进哪一页"
|
||
* 的绑定在生成物 ui_page_registry.c(唯一真源 = ui_menu_tree.txt 的
|
||
* #page= 注记;架构见 内容页架构.md);
|
||
* 进入时切屏并获焦、ESC 返回菜单屏并还原焦点;
|
||
* 不持有导航状态——由逻辑层 ui_menu.c 在 ENTER 叶子节点时驱动
|
||
* 适用平台:STM32F407ZGTx / PC 模拟器(两端共用)
|
||
* 作者:王建锋
|
||
* 创建日期:2026-09-09
|
||
* 修改记录:
|
||
* 2026-09-09 王建锋 创建:装置信息/交流量/直流量三页(参考图 10/20/21 实测几何)
|
||
* 2026-09-09 王建锋 "浅 V"装饰几何提取至共享 ui_deco 模块(不再本地重复定义)
|
||
* 2026-09-09 王建锋 拆分为"框架 + 页面插件"模型(内容页架构.md):三页数据移入
|
||
* ui_page_xxx 模块,本文件只留机制 + 描述符表;绘制/按键改
|
||
* 按描述符驱动(静态表/refresh + 4 钩子),行为零变化
|
||
* 2026-09-09 王建锋 密码页(用户密钥):绘制链支持每页 title_y/"浅 V"/页脚开关;
|
||
* 按键分发支持每页 ← 拦截(ESC 恒不可拦截);新 API
|
||
* ui_content_page_invalidate(表单页触发整页重绘)
|
||
* 2026-09-09 王建锋 权限模型:open(id, perm) 记录授予权限(页面经
|
||
* ui_content_page_is_editable 查询:edit_capable 且修改权限)
|
||
* 2026-09-10 王建锋 网口设置页(node 30):新增 CONTENT_PAGE_NET_CFG 入描述符表;
|
||
* 新 API ui_content_page_selected_row(页面钩子感知选中行)+
|
||
* 描述符 init_cb(开页后调一次,页面模块重置运行态)
|
||
* 2026-09-13 王建锋 cp_draw_cb 按区块拆为四个绘制子函数(cp_draw_deco/header/rows/footer),
|
||
* 主函数仅编排(行数取一次供行窗口/页脚共用),行为零变化
|
||
* 2026-10-15 王建锋 UI 设计令牌层:屏幕/骨架/行距/缓冲/1bpp 语义色字面量收进
|
||
* ui_tokens.h(纯替换,像素逐位一致)
|
||
* 2026-10-15 王建锋 UI 页面原型化:删 content_page_id_t 枚举与 s_defs[],open() 改收
|
||
* 描述符指针、s_open 变指针(NULL = 未打开);7 个页面头包含移交
|
||
* ui_page_registry.c;日志页身份改打描述符 id(ASCII 页名,比原
|
||
* id=%u 更可定位);绘制/按键/几何一律未动
|
||
* 2026-11-02 DTU 项目组 新增 ui_content_page_current_desc(#69 板端导航身份查询:
|
||
* 页 id 从"只有串口开/关沿可见"升级为可按帧查询)
|
||
*/
|
||
|
||
/* 头文件包含区 */
|
||
#include <string.h>
|
||
|
||
#include "ui_content_page.h"
|
||
|
||
#include "ui_standby.h"
|
||
|
||
#include "ui_pwd_def.h"
|
||
|
||
#include "ui_menu_view.h"
|
||
|
||
#include "ui_deco.h"
|
||
|
||
#include "ui_style.h"
|
||
|
||
#include "ui_tokens.h"
|
||
|
||
#include "lvgl.h"
|
||
|
||
#define DBG_TAG "[UI_CONTENT_PAGE]"
|
||
#include "dbg_log.h"
|
||
|
||
/* 私有宏定义区 */
|
||
|
||
/*
|
||
* 布局取值全部来自 ui_tokens.h(L0 设计令牌层,本文件不再自带同值字面量):
|
||
* 屏幕 UI_SCREEN_W/H、标题 UI_TITLE_Y、页脚 UI_PAGE_LINE_Y、文本条带
|
||
* UI_TEXT_BAND_H、行缓冲 UI_BUF_STRLEN、选中带内缩 UI_SEL_BAND_INSET_L/R、
|
||
* 页脚左起 UI_FOOTER_X、语义色 UI_COL_FG/BG/SEL_FG/SEL_BG。
|
||
* 几何实测来源仍是参考图 10/20/21(3x 放大图缩回 160x160 测量):标题 y=2
|
||
* (CJK 字形顶距 +1 → 墨迹自 y=3 起,与参考图一致)、页脚 y=143;字形顶距
|
||
* (CJK +1 / ASCII +2)已计入放置 y,墨迹与参考图对齐
|
||
*/
|
||
|
||
/* 静态变量定义区 */
|
||
|
||
/*
|
||
* 当前打开的页:直接持页面描述符指针(NULL = 未打开)。
|
||
* 页身份由调用方给出(逻辑层经 ui_page_registry_for / _password 取描述符),
|
||
* 框架不再维护"页 ID 枚举 ↔ s_defs 下标 ↔ 菜单节点"三处必须手工对齐的映射
|
||
* ——那正是旧 menu_content_page_for() 手写 case 的错源(表序一变即错位)。
|
||
*/
|
||
static const content_page_def_t *s_open = NULL;
|
||
static uint16_t s_sel; /* 列表页选中行(全局下标,跨页连续) */
|
||
static uint8_t s_perm; /* 本次授予权限(ui_perm_t;开页时装载,关页重置) */
|
||
static lv_obj_t *s_scr; /* 页面屏幕(进入时建、返回时毁) */
|
||
static lv_obj_t *s_cont; /* 页面全屏容器(焦点/键入口 + 自绘载体) */
|
||
|
||
/* 容器样式:flat(透明 + 无边框/圆角/内边距)取自共享样式表 ui_style.c——
|
||
* 与 ui_menu_view / ui_standby 共用同一 const 对象(原先三处各定义一份逐字相同
|
||
* 的 9 项属性数组);常量样式零 LV_MEM 池分配的纪律见 ui_style.h */
|
||
|
||
/* 函数定义区 */
|
||
|
||
/*
|
||
* 函数功能:判断两区域是否相交(等价 lv_area_intersect 非空判断;避免依赖 LVGL 私有头)
|
||
* 入口参数:a - 区域 1 const lv_area_t *
|
||
* b - 区域 2 const lv_area_t *
|
||
* 返回值:true - 相交;false - 不相交
|
||
* 限定条件:无
|
||
* 函数说明:四边取 min/max 成交集,任一边为空即不相交(与 ui_menu_view.c 同实现)
|
||
*/
|
||
static bool cp_areas_intersect(const lv_area_t *a, const lv_area_t *b) {
|
||
int32_t x1 = (a->x1 > b->x1) ? a->x1 : b->x1;
|
||
int32_t y1 = (a->y1 > b->y1) ? a->y1 : b->y1;
|
||
int32_t x2 = (a->x2 < b->x2) ? a->x2 : b->x2;
|
||
int32_t y2 = (a->y2 < b->y2) ? a->y2 : b->y2;
|
||
|
||
return (x1 <= x2 && y1 <= y2);
|
||
}
|
||
|
||
/*
|
||
* 函数功能:取页面总行数(静态页 = 描述符 row_count,动态页 = count_cb)
|
||
* 入口参数:d - 页面描述符 const content_page_def_t *
|
||
* 返回值:总行数 uint16_t
|
||
* 限定条件:d 非空
|
||
* 函数说明:行数唯一入口;绘制(窗口/页脚)与按键(移选取模)均经此取
|
||
*/
|
||
static uint16_t cp_row_total(const content_page_def_t *d) {
|
||
if (d->count_cb != NULL) {
|
||
return d->count_cb();
|
||
}
|
||
return d->row_count;
|
||
}
|
||
|
||
/*
|
||
* 函数功能:失效当前打开的页面容器(触发整页重绘)
|
||
* 入口参数:无
|
||
* 返回值:无
|
||
* 限定条件:页面打开时(s_cont 非空)
|
||
* 函数说明:供页面模块(表单/操作页)自身状态变化后调用;无页打开时空操作
|
||
*/
|
||
void ui_content_page_invalidate(void) {
|
||
if (s_cont != NULL) {
|
||
lv_obj_invalidate(s_cont);
|
||
}
|
||
}
|
||
|
||
/*
|
||
* 函数功能:绘制居中单行文本(标题/副标题)
|
||
* 入口参数:layer - 绘制层 lv_layer_t *
|
||
* text - 文本(Flash) const char *
|
||
* y - 放置 y int32_t
|
||
* 返回值:无
|
||
* 限定条件:text 非空;重绘区已含该行区域(调用方 culling)
|
||
* 函数说明:x = (屏宽 - 文本实际步进)/2 居中;白色、静态字符串不拷内存
|
||
*/
|
||
static void cp_draw_centered_text(lv_layer_t *layer, const char *text, int32_t y) {
|
||
lv_area_t area;
|
||
lv_draw_label_dsc_t ld;
|
||
|
||
area.x1 = 0;
|
||
area.x2 = UI_SCREEN_W - 1;
|
||
area.y1 = y;
|
||
area.y2 = y + UI_TEXT_BAND_H; /* 覆盖字体行高(13)+ 余量 */
|
||
|
||
lv_draw_label_dsc_init(&ld);
|
||
ld.base.layer = layer;
|
||
ld.text = text;
|
||
ld.text_static = 1; /* Flash 字符串:不拷内存 */
|
||
ld.color = UI_COL_FG;
|
||
/* 本 LVGL 版 lv_text_get_width:length=0 表示"不测量"(返回 0);
|
||
* length≠0 时循环无 NUL 检查,传 -1 会越串尾死循环——必须传 strlen */
|
||
ld.ofs_x = (UI_SCREEN_W - lv_text_get_width(text, (uint32_t)strlen(text), LV_FONT_DEFAULT, 0)) / 2;
|
||
ld.ofs_y = 0;
|
||
lv_draw_label(layer, &ld, &area);
|
||
}
|
||
|
||
/*
|
||
* 函数功能:绘制"浅 V"装饰(共享 ui_deco 模块,与菜单根框同几何)
|
||
* 入口参数:layer - 绘制层 lv_layer_t *
|
||
* d - 页面描述符 const content_page_def_t *
|
||
* 返回值:无
|
||
* 限定条件:页面已打开
|
||
* 函数说明:每页可关(hide_deco;密码页跟随实机:无装饰)
|
||
*/
|
||
static void cp_draw_deco(lv_layer_t *layer, const content_page_def_t *d) {
|
||
if (d->hide_deco == 0u) {
|
||
ui_deco_draw(layer, &layer->_clip_area);
|
||
}
|
||
}
|
||
|
||
/*
|
||
* 函数功能:绘制标题/副标题(居中;整宽带 + culling)
|
||
* 入口参数:layer - 绘制层 lv_layer_t *
|
||
* d - 页面描述符 const content_page_def_t *
|
||
* 返回值:无
|
||
* 限定条件:页面已打开
|
||
* 函数说明:标题放置 y 每页可配(title_y,0 = 框架默认 UI_TITLE_Y);
|
||
* 副标题仅信息页(subtitle 非空);重绘区不相交则跳过区块
|
||
*/
|
||
static void cp_draw_header(lv_layer_t *layer, const content_page_def_t *d) {
|
||
lv_area_t t;
|
||
|
||
/* 标题(居中;放置 y 每页可配,0 = 框架默认) */
|
||
{
|
||
int32_t ty = (d->title_y != 0) ? d->title_y : UI_TITLE_Y;
|
||
|
||
t.x1 = 0;
|
||
t.x2 = UI_SCREEN_W - 1;
|
||
t.y1 = ty;
|
||
t.y2 = ty + UI_TEXT_BAND_H;
|
||
if (cp_areas_intersect(&t, &layer->_clip_area)) {
|
||
cp_draw_centered_text(layer, d->title, ty);
|
||
}
|
||
}
|
||
|
||
/* 副标题(仅信息页) */
|
||
if (d->subtitle != NULL) {
|
||
t.x1 = 0;
|
||
t.x2 = UI_SCREEN_W - 1;
|
||
t.y1 = d->subtitle_y;
|
||
t.y2 = d->subtitle_y + UI_TEXT_BAND_H;
|
||
if (cp_areas_intersect(&t, &layer->_clip_area)) {
|
||
cp_draw_centered_text(layer, d->subtitle, d->subtitle_y);
|
||
}
|
||
}
|
||
}
|
||
|
||
/*
|
||
* 函数功能:绘制行数据(列表页窗口跟随选中行所在页,信息页全量;逐行 culling)
|
||
* 入口参数:layer - 绘制层 lv_layer_t *
|
||
* d - 页面描述符 const content_page_def_t *
|
||
* nrows - 总行数 int32_t(调用方取一次,与页脚同帧共用)
|
||
* 返回值:无
|
||
* 限定条件:页面已打开
|
||
* 函数说明: 1. 列表页窗口顶行 = (sel/每页行数)*每页行数,末页不足整页截断
|
||
* 2. 行条带(整宽)不在重绘区则跳过(culling)
|
||
* 3. 选中行:整行白带(x=2..156,高 band_h)+ 黑字,非选中白字
|
||
* 4. 行文本:静态 = Flash 表(不拷内存);动态 = refresh() 逐行取
|
||
* 入栈缓冲(同帧同步光栅化,安全)
|
||
*/
|
||
static void cp_draw_rows(lv_layer_t *layer, const content_page_def_t *d, int32_t nrows) {
|
||
int32_t first = 0;
|
||
int32_t cnt = nrows;
|
||
|
||
if (d->selectable != 0u && d->rows_per_page > 0u) {
|
||
first = (int32_t)(s_sel / d->rows_per_page) * d->rows_per_page; /* 窗口顶行 */
|
||
cnt = nrows - first;
|
||
if (cnt > (int32_t)d->rows_per_page) {
|
||
cnt = (int32_t)d->rows_per_page; /* 末页不足整页时截断 */
|
||
}
|
||
}
|
||
|
||
bool dynamic = (d->refresh != NULL);
|
||
char dyn_name[UI_BUF_STRLEN]; /* 动态行栈缓冲(同帧同步光栅化) */
|
||
char dyn_value[UI_BUF_STRLEN];
|
||
|
||
for (int32_t i = 0; i < cnt; i++) {
|
||
int32_t row_idx = first + i; /* 数据下标(跨页连续) */
|
||
int32_t slot_y = d->top_y + i * d->pitch; /* 行槽顶(窗口内位置固定) */
|
||
bool is_sel = (d->selectable != 0u) && (row_idx == (int32_t)s_sel);
|
||
lv_color_t color = is_sel ? UI_COL_SEL_FG : UI_COL_FG;
|
||
lv_area_t strip;
|
||
|
||
/* 行条带(整宽)不在重绘区:跳过(culling) */
|
||
strip.x1 = 0;
|
||
strip.x2 = UI_SCREEN_W - 1;
|
||
strip.y1 = slot_y;
|
||
strip.y2 = slot_y + d->pitch - 1;
|
||
if (!cp_areas_intersect(&strip, &layer->_clip_area)) {
|
||
continue;
|
||
}
|
||
|
||
/* 选中行:整行白带(x=2..156,高 band_h) */
|
||
if (is_sel && d->band_h > 0) {
|
||
lv_draw_rect_dsc_t fd;
|
||
lv_area_t band;
|
||
|
||
band.x1 = UI_SEL_BAND_INSET_L;
|
||
band.x2 = UI_SCREEN_W - UI_SEL_BAND_INSET_R; /* x=156(与参考图实测一致) */
|
||
band.y1 = slot_y;
|
||
band.y2 = slot_y + d->band_h - 1;
|
||
lv_draw_rect_dsc_init(&fd);
|
||
fd.base.layer = layer;
|
||
fd.bg_color = UI_COL_SEL_BG;
|
||
fd.bg_opa = LV_OPA_COVER;
|
||
lv_draw_rect(layer, &fd, &band);
|
||
}
|
||
|
||
/* 行文本:静态 = Flash 表(不拷内存);动态 = refresh 逐行取(栈缓冲) */
|
||
const char *name;
|
||
const char *value;
|
||
|
||
if (dynamic) {
|
||
d->refresh((uint16_t)row_idx, dyn_name, dyn_value);
|
||
name = dyn_name;
|
||
value = dyn_value;
|
||
}
|
||
else {
|
||
name = d->rows[row_idx].name;
|
||
value = d->rows[row_idx].value;
|
||
}
|
||
|
||
{
|
||
lv_draw_label_dsc_t ld;
|
||
|
||
/* ofs_x/ofs_y 相对 coords 原点(strip.y1=slot_y),勿再叠加 slot_y */
|
||
lv_draw_label_dsc_init(&ld);
|
||
ld.base.layer = layer;
|
||
ld.text = name;
|
||
ld.text_static = (uint32_t)(!dynamic); /* 静态不拷;动态缓冲同帧内有效 */
|
||
ld.color = color; /* 选中黑 / 非选中白 */
|
||
ld.ofs_x = d->left_x;
|
||
ld.ofs_y = d->text_dy;
|
||
lv_draw_label(layer, &ld, &strip);
|
||
|
||
lv_draw_label_dsc_init(&ld);
|
||
ld.base.layer = layer;
|
||
ld.text = value;
|
||
ld.text_static = (uint32_t)(!dynamic);
|
||
ld.color = color;
|
||
ld.ofs_x = d->right_x;
|
||
ld.ofs_y = d->text_dy;
|
||
lv_draw_label(layer, &ld, &strip);
|
||
}
|
||
}
|
||
}
|
||
|
||
/*
|
||
* 函数功能:绘制页脚"第 X 页,共 Y 页"
|
||
* 入口参数:layer - 绘制层 lv_layer_t *
|
||
* d - 页面描述符 const content_page_def_t *
|
||
* nrows - 总行数 int32_t(调用方取一次,与行窗口同帧同值)
|
||
* 返回值:无
|
||
* 限定条件:页面已打开
|
||
* 函数说明:列表页随选中行现算,信息页按参考图固定值;每页可关
|
||
* (hide_footer;密码页跟随实机)
|
||
*/
|
||
static void cp_draw_footer(lv_layer_t *layer, const content_page_def_t *d, int32_t nrows) {
|
||
char buf[UI_BUF_STRLEN]; /* 页行文本缓冲(栈):"第 %u 页,共 %u 页" 最长远不足 32B */
|
||
uint8_t cur; /* 当前页码(1 基) */
|
||
uint8_t pages; /* 总页数 */
|
||
lv_area_t t; /* 页脚条带区域(整宽,放置于 UI_PAGE_LINE_Y) */
|
||
|
||
/* 页码计算,两种来源:
|
||
* - 列表页(selectable):随选中行现算——当前页 = 选中行/每页行数 + 1
|
||
* (选中行所在页);总页数 = ceil(nrows/每页行数),
|
||
* 写成经典向上取整 (n + p - 1) / p(nrows 恰为整页倍数时不多算一页);
|
||
* nrows 由调用方取一次、与行窗口共用,保证同帧页码与窗口一致
|
||
* - 信息/表单页(非 selectable):取参考图固定值(fixed_cur_page / fixed_total_pages) */
|
||
if (d->selectable != 0u) {
|
||
cur = (uint8_t)(s_sel / d->rows_per_page + 1u);
|
||
pages = (uint8_t)((nrows + (int32_t)d->rows_per_page - 1) / d->rows_per_page);
|
||
}
|
||
else {
|
||
cur = d->fixed_cur_page;
|
||
pages = d->fixed_total_pages;
|
||
}
|
||
|
||
/* 页脚条带:整宽,放置于 UI_PAGE_LINE_Y(143,字形顶距已计入放置 y,
|
||
* 墨迹与参考图对齐);高 15 = 字体行高 13 + 余量,整行文字一次覆盖 */
|
||
t.x1 = 0;
|
||
t.x2 = UI_SCREEN_W - 1;
|
||
t.y1 = UI_PAGE_LINE_Y;
|
||
t.y2 = UI_PAGE_LINE_Y + UI_TEXT_BAND_H;
|
||
|
||
/* 双重门控:① 本页开启页脚(hide_footer;密码页跟随实机:无页码)
|
||
* ② 页脚条带与重绘区相交(culling)——两者皆成立才格式化并绘制 */
|
||
if (d->hide_footer == 0u && cp_areas_intersect(&t, &layer->_clip_area)) {
|
||
/* lv_snprintf 而非 snprintf:MSVC 对 printf 族做格式串扫描,中文字面量会误报 C4819 */
|
||
lv_snprintf(buf, sizeof(buf), "第 %u 页,共 %u 页", (unsigned)cur, (unsigned)pages);
|
||
|
||
lv_draw_label_dsc_t ld;
|
||
|
||
/* ofs 相对 coords 原点(t.x1=0,t.y1=UI_PAGE_LINE_Y):
|
||
* 水平 6 = 左对齐、左留白 6px(与实机一致);垂直 0(顶距已计入放置 y) */
|
||
lv_draw_label_dsc_init(&ld);
|
||
ld.base.layer = layer;
|
||
ld.text = buf; /* 栈缓冲:本页码每次现算,同帧内有效(同步光栅化) */
|
||
ld.color = UI_COL_FG; /* 页码恒白字(不像选中行那样反色) */
|
||
ld.ofs_x = UI_FOOTER_X;
|
||
ld.ofs_y = 0;
|
||
lv_draw_label(layer, &ld, &t);
|
||
}
|
||
}
|
||
|
||
/*
|
||
* 函数功能:页面自绘(编排:浅 V 装饰 + 标题/副标题 + 行数据 + 页脚页码 + 页面钩子)
|
||
* 入口参数:e - 事件指针 lv_event_t *
|
||
* 返回值:无
|
||
* 限定条件:事件为 LV_EVENT_DRAW_MAIN,页面已打开
|
||
* 函数说明:行数取一次(cp_row_total)供行窗口与页脚页码共用(同帧同值);
|
||
* 各区块由子函数绘制(deco/header/rows/footer),均按 culling 区
|
||
* 跳过不在重绘区的部分;末尾调 draw_cb(页自定义内容,在内置区块之后)
|
||
*/
|
||
static void cp_draw_cb(lv_event_t *e) {
|
||
const content_page_def_t *d = s_open;
|
||
lv_layer_t *layer = lv_event_get_layer(e);
|
||
int32_t nrows = (int32_t)cp_row_total(d); /* 行数取一次:供行窗口与页脚页码共用 */
|
||
|
||
cp_draw_deco(layer, d);
|
||
cp_draw_header(layer, d);
|
||
cp_draw_rows(layer, d, nrows);
|
||
cp_draw_footer(layer, d, nrows);
|
||
|
||
/* 页面钩子:自定义内容(表单/操作页;在内置区块之后绘制) */
|
||
if (d->draw_cb != NULL) {
|
||
d->draw_cb(layer, &layer->_clip_area);
|
||
}
|
||
}
|
||
|
||
/*
|
||
* 函数功能:页面按键事件入口(↑↓移选 / ESC·←返回 / 页面钩子)
|
||
* 入口参数:e - 事件指针 lv_event_t *
|
||
* 返回值:无
|
||
* 限定条件:容器已加入默认输入组并获焦
|
||
* 函数说明: 1. ESC:恒关页返回菜单(框架保证,页面钩子不可拦截)
|
||
* 2. ←:页面置拦截标志(key_intercept)时先交 key_cb(true = 页已处理不关页),
|
||
* 否则关页返回(密码页光标 ←/→ 即由此处理)
|
||
* 3. 其余键:key_cb 钩子先调(返回 true = 页已处理,框架跳过内置)
|
||
* 4. 内置:↑/↓ 列表页移选(首尾环绕,跨页连续),窗口随页切换后重绘;
|
||
* 信息页不响应;动态行数可能缩小,选中行越界时重置回首行
|
||
*/
|
||
static void cp_key_cb(lv_event_t *e) {
|
||
const content_page_def_t *d = s_open;
|
||
lv_key_t key = (lv_key_t)lv_event_get_key(e);
|
||
|
||
ui_standby_notify_activity(); /* 用户活动:重置待机空闲计时 */
|
||
|
||
/* ESC:关闭页面返回菜单屏(焦点/导航状态还原;恒不可拦截) */
|
||
if (key == LV_KEY_ESC) {
|
||
ui_content_page_close();
|
||
return;
|
||
}
|
||
|
||
/* ←:页面置拦截标志且钩子已处理则不关页(密码页光标 ←/→);否则照旧返回 */
|
||
if (key == LV_KEY_LEFT) {
|
||
if (d->key_intercept != 0u && d->key_cb != NULL && d->key_cb(key)) {
|
||
return;
|
||
}
|
||
ui_content_page_close();
|
||
return;
|
||
}
|
||
|
||
/* 其余键:页面钩子先调:true = 页已处理,框架跳过内置语义 */
|
||
if (d->key_cb != NULL && d->key_cb(key)) {
|
||
return;
|
||
}
|
||
|
||
switch (key) {
|
||
case LV_KEY_UP: {
|
||
uint16_t total = cp_row_total(d);
|
||
|
||
if (d->selectable != 0u && total > 0u) {
|
||
if (s_sel >= total) {
|
||
s_sel = 0u; /* 动态行数可能缩小:选中行越界重置回首行 */
|
||
}
|
||
/* 上移、首尾环绕:先加 total 再减 1 保证无符号下不为负 */
|
||
s_sel = (uint16_t)((s_sel + total - 1u) % total);
|
||
lv_obj_invalidate(s_cont); /* 重绘(选中带/窗口/页码可能变化) */
|
||
}
|
||
break;
|
||
}
|
||
case LV_KEY_DOWN: {
|
||
uint16_t total = cp_row_total(d);
|
||
|
||
if (d->selectable != 0u && total > 0u) {
|
||
if (s_sel >= total) {
|
||
s_sel = 0u; /* 动态行数可能缩小:选中行越界重置回首行 */
|
||
}
|
||
s_sel = (uint16_t)((s_sel + 1u) % total); /* 下移、越尾绕回首行 */
|
||
lv_obj_invalidate(s_cont);
|
||
}
|
||
break;
|
||
}
|
||
default:
|
||
break; /* 其余键值忽略 */
|
||
}
|
||
}
|
||
|
||
/*
|
||
* 函数功能:打开指定内容页(建屏 + 切屏 + 焦点置页面容器)
|
||
* 入口参数:desc - 页面描述符(取自 ui_page_registry_for / ui_page_registry_password)
|
||
* const content_page_def_t *
|
||
* perm - 本次进入授予的权限 ui_perm_t(UI_PERM_VIEW/MODIFY)
|
||
* 返回值:true - 成功;false - 失败(desc 为 NULL / 已有页面打开 / 对象创建失败)
|
||
* 限定条件:ui_menu_init() 已完成(默认输入组与菜单屏存在)
|
||
* 函数说明:1. lv_obj_create(NULL) 新建独立屏幕 + 全屏容器(自绘载体,无子对象)
|
||
* 2. 容器入默认输入组并获焦后再切屏——按键立即由本模块接管
|
||
* 3. 导航状态不压层:菜单 s_depth/s_sel 原样保留,ESC 返回即还原
|
||
* 4. 记录授予权限(页面经 ui_content_page_is_editable 查询)
|
||
*/
|
||
bool ui_content_page_open(const content_page_def_t *desc, uint8_t perm) {
|
||
if (s_open != NULL) {
|
||
return false; /* 已有页面打开(防御:页面按需建毁,同一时刻至多一页) */
|
||
}
|
||
if (desc == NULL) {
|
||
DBG_ERROR("page open: NULL descriptor (node has no bound page)");
|
||
return false; /* 调用方把未绑定内容页的叶子当页面开了:拒开并留痕,不崩溃 */
|
||
}
|
||
|
||
s_perm = perm; /* 本次授予权限(页面可编辑性 = edit_capable 且修改权限) */
|
||
|
||
s_scr = lv_obj_create(NULL); /* parent=NULL → 新建屏幕 */
|
||
if (s_scr == NULL) {
|
||
return false; /* LVGL 池耗尽 */
|
||
}
|
||
|
||
s_cont = lv_obj_create(s_scr);
|
||
if (s_cont == NULL) {
|
||
lv_obj_delete(s_scr); /* 回滚:屏已建、容器失败 */
|
||
s_scr = NULL;
|
||
return false;
|
||
}
|
||
lv_obj_add_flag(s_cont, LV_OBJ_FLAG_IGNORE_LAYOUT); /* 绝对定位,不参与父级布局 */
|
||
lv_obj_set_size(s_cont, UI_SCREEN_W, UI_SCREEN_H); /* 全屏覆盖 */
|
||
lv_obj_add_style(s_cont, &ui_style_flat, 0); /* 透明、无边框:剥离主题 card */
|
||
lv_obj_add_flag(s_cont, LV_OBJ_FLAG_CLICK_FOCUSABLE); /* 可获焦:键事件入口 */
|
||
lv_obj_add_event_cb(s_cont, cp_key_cb, LV_EVENT_KEY, NULL);
|
||
lv_obj_add_event_cb(s_cont, cp_draw_cb, LV_EVENT_DRAW_MAIN, NULL);
|
||
|
||
s_open = desc;
|
||
s_sel = 0u; /* 每次进入从首行开始(页面按需建毁,不跨次保留) */
|
||
|
||
if (desc->init_cb != NULL) {
|
||
desc->init_cb(); /* 页面模块重置运行态(编辑模式/光标/保存标志等) */
|
||
}
|
||
|
||
/* 焦点/键入口:入默认输入组并获焦(先于切屏,保证按键立即由本模块接管) */
|
||
lv_group_t *g = lv_group_get_default();
|
||
if (g != NULL) {
|
||
lv_group_add_obj(g, s_cont);
|
||
lv_group_focus_obj(s_cont);
|
||
}
|
||
|
||
lv_screen_load(s_scr); /* 切到内容页(菜单屏对象保留、不销毁) */
|
||
DBG_INFO("page open: id=%s rows=%u", desc->id, (unsigned)cp_row_total(desc));
|
||
return true;
|
||
}
|
||
|
||
/*
|
||
* 函数功能:关闭当前内容页(焦点回菜单容器 + 切回菜单屏 + 销毁页面)
|
||
* 入口参数:无
|
||
* 返回值:无
|
||
* 限定条件:ui_content_page_is_open() 为 true
|
||
* 函数说明:1. 先移焦到菜单容器再删屏——防输入组焦点悬空(删除自动移出组)
|
||
* 2. 切回菜单屏后销毁页面屏幕,LVGL 池内存随即回落
|
||
*/
|
||
void ui_content_page_close(void) {
|
||
if (s_open == NULL || s_cont == NULL) {
|
||
return; /* 未打开(防御) */
|
||
}
|
||
|
||
lv_obj_t *menu_cont = ui_menu_view_container();
|
||
if (menu_cont != NULL) {
|
||
lv_group_focus_obj(menu_cont); /* 焦点先回菜单容器 */
|
||
lv_screen_load(lv_obj_get_screen(menu_cont)); /* 切回菜单屏(对象未动,原样还原) */
|
||
}
|
||
|
||
DBG_INFO("page close: id=%s", s_open->id);
|
||
|
||
lv_obj_delete(s_cont); /* 先删容器 */
|
||
lv_obj_delete(s_scr); /* 再删屏 */
|
||
|
||
s_scr = NULL;
|
||
s_cont = NULL;
|
||
s_open = NULL;
|
||
s_sel = 0u;
|
||
s_perm = (uint8_t)UI_PERM_VIEW; /* 权限重置(卫生:不留上次进入的授权) */
|
||
}
|
||
|
||
/*
|
||
* 函数功能:查询是否有内容页打开
|
||
* 入口参数:无
|
||
* 返回值:true - 有页面打开;false - 无
|
||
* 限定条件:无
|
||
* 函数说明:供逻辑层/调试判断当前处于菜单屏还是内容页
|
||
*/
|
||
bool ui_content_page_is_open(void) {
|
||
return (s_open != NULL);
|
||
}
|
||
|
||
/*
|
||
* 函数功能:查询当前打开的页面是否可编辑
|
||
* 入口参数:无
|
||
* 返回值:true - 页面支持编辑模式(edit_capable)且被授予修改权限;false - 其余
|
||
* 限定条件:无
|
||
* 函数说明:供页面钩子(key_cb/draw_cb)决定编辑行为;权限由 open 的 perm 实参授予
|
||
*/
|
||
bool ui_content_page_is_editable(void) {
|
||
if (s_open == NULL) {
|
||
return false; /* 无页打开或无描述符(防御) */
|
||
}
|
||
return (s_open->edit_capable != 0u && s_perm == (uint8_t)UI_PERM_MODIFY);
|
||
}
|
||
|
||
/*
|
||
* 函数功能:查询当前列表页的选中行(全局下标,跨页连续)
|
||
* 入口参数:无
|
||
* 返回值:选中行下标;无页打开/非列表页 = 0 uint16_t
|
||
* 限定条件:无
|
||
* 函数说明:供页面钩子(key_cb/draw_cb)感知当前选中位置
|
||
*/
|
||
uint16_t ui_content_page_selected_row(void) {
|
||
if (s_open == NULL) {
|
||
return 0u;
|
||
}
|
||
if (s_open->selectable == 0u) {
|
||
return 0u; /* 信息页/表单页无行选中概念 */
|
||
}
|
||
return s_sel;
|
||
}
|
||
|
||
/*
|
||
* 函数功能:查询当前打开页面的描述符(契约见 ui_content_page.h,Issue #69)
|
||
* 入口参数:无
|
||
* 返回值:当前页面描述符指针(Flash 常量,只读);无页打开 = NULL
|
||
* 限定条件:同上下文(uiTask)读取才有一致性语义;本函数零副作用、不打日志
|
||
* 函数说明:与 ui_content_page_is_open 的区别只在于顺带交出身份(desc->id),
|
||
* 推流 STATUS 用它把"开着哪一页"随帧带回,不再只有串口开/关沿可见
|
||
*/
|
||
const content_page_def_t *ui_content_page_current_desc(void) {
|
||
return s_open;
|
||
}
|