UI 菜单初步完成
This commit is contained in:
@@ -85,8 +85,10 @@ def main():
|
||||
chars |= collect(open(f, encoding='utf-8-sig', errors='ignore').read())
|
||||
s = ''.join(sorted(chars))
|
||||
out_path = os.path.join(ROOT, 'ui_cn_chars.txt')
|
||||
with open(out_path, 'w', encoding='utf-8') as fp:
|
||||
fp.write(s)
|
||||
# newline='' 关闭行尾转换:恒输出 LF(与 gen_menu.py / make_font.py 一致),
|
||||
# 避免同一脚本在 Windows 出 CRLF、Linux 出 LF 导致提交产物跨平台 diff
|
||||
with open(out_path, 'w', encoding='utf-8', newline='') as fp:
|
||||
fp.write(s + '\n')
|
||||
print('scanned %d files; unique non-ASCII chars: %d' % (len(SCAN), len(s)))
|
||||
print('->', out_path)
|
||||
|
||||
|
||||
@@ -110,7 +110,30 @@ def merge_arrow_glyph(font_path):
|
||||
print('已合并箭头 U+25B6:位图+%d 字节,glyph_id=%d' % (len(ARROW_BITMAP), glyph_count))
|
||||
|
||||
|
||||
def fix_trailing_newline(path):
|
||||
"""规范化文件末尾为恰好一个 \n(pre-commit end-of-file-fixer 要求)。
|
||||
|
||||
lv_font_conv 产物末尾多一个空行(`#endif` 之后 `\n\n`),统一在此收敛为单个换行,
|
||||
避免每次重生成字体都触发 end-of-file-fixer 拦截提交。
|
||||
"""
|
||||
with open(path, 'rb') as fp:
|
||||
data = fp.read()
|
||||
body = data
|
||||
while body.endswith(b'\r\n') or body.endswith(b'\n'):
|
||||
body = body[:-2] if body.endswith(b'\r\n') else body[:-1]
|
||||
fixed = body + b'\n'
|
||||
if fixed != data:
|
||||
with open(path, 'wb') as fp:
|
||||
fp.write(fixed)
|
||||
|
||||
|
||||
def main():
|
||||
# Windows GBK 控制台无法打印 ▶ 等符号:降级为替换字符,避免 UnicodeEncodeError 崩溃
|
||||
try:
|
||||
sys.stdout.reconfigure(errors='replace')
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument('--bpp', type=int, default=1, help='字体位深 1/2/3(默认 1)')
|
||||
ap.add_argument('--size', type=int, default=12, help='字号(默认 12)')
|
||||
@@ -138,6 +161,7 @@ def main():
|
||||
if r.returncode != 0:
|
||||
sys.exit('FAILED rc=%d' % r.returncode)
|
||||
merge_arrow_glyph(OUT) # 补回宋体缺失的 ▶ 字形(幂等)
|
||||
fix_trailing_newline(OUT) # 规范化末尾换行(pre-commit end-of-file-fixer)
|
||||
print('OK %d bytes -> %s' % (os.path.getsize(OUT), OUT))
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,222 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""生成菜单节点表 ui_menu_tab.c / ui_menu_tab.h(扁平"父子索引表",输出到 Drivers/BSP/UI/)。
|
||||
|
||||
把"菜单长什么样"声明在缩进文本源 ui_menu_tree.txt(缩进即层级,每级 2 空格,
|
||||
`#` 开头为注释)。本脚本读源、推导每个节点的 parent 下标、产出 C 表,供 ui_menu.c 消费。
|
||||
改菜单只需编辑 ui_menu_tree.txt 后重跑本脚本(与字体 ui_cn_chars.txt -> make_font.py 同模式)。
|
||||
|
||||
用法(仓库根目录或任意目录均可,脚本自动定位仓库根):
|
||||
python tools/menu_gen/gen_menu.py
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
SRC = os.path.join(ROOT, 'ui_menu_tree.txt')
|
||||
OUT_H = os.path.join(ROOT, 'Drivers', 'BSP', 'UI', 'ui_menu_tab.h')
|
||||
OUT_C = os.path.join(ROOT, 'Drivers', 'BSP', 'UI', 'ui_menu_tab.c')
|
||||
ROOT_ID = 0xFF # 内部根标记(= MENU_ROOT=0xFF);与 8 位索引一致,映射到 C 的 MENU_ROOT 宏
|
||||
MAX_DEPTH = 8
|
||||
MAX_NODES = 255 # uint8_t 索引 + 哨兵 0xFF → 最多 255 个节点(下标 0..254)
|
||||
|
||||
|
||||
def parse_tree(text):
|
||||
"""把缩进文本解析为 [(parent, name, depth), ...]。
|
||||
|
||||
parent 为父节点下标,根节点为 ROOT_ID;子项 = parent 等于本下标的所有行(按表序)。
|
||||
缩进单元取"最小非零缩进"(每级一个单元),据此把缩进换算成层级。
|
||||
"""
|
||||
lines = []
|
||||
for raw in text.splitlines():
|
||||
line = raw.rstrip()
|
||||
stripped = line.strip()
|
||||
if not stripped or stripped.startswith('#'):
|
||||
continue
|
||||
if '\t' in line:
|
||||
sys.exit('ERROR: 请用空格缩进(勿用 Tab): ' + repr(raw))
|
||||
indent = len(line) - len(line.lstrip(' '))
|
||||
lines.append((indent, stripped))
|
||||
|
||||
if not lines:
|
||||
sys.exit('ERROR: ui_menu_tree.txt 无有效节点')
|
||||
|
||||
indents = sorted(set(i for i, _ in lines if i > 0))
|
||||
unit = indents[0] if indents else 1
|
||||
for i, name in lines:
|
||||
if i % unit != 0:
|
||||
sys.exit('ERROR: 缩进不是 %d 的整数倍: %r (indent=%d)' % (unit, name, i))
|
||||
|
||||
nodes = []
|
||||
stack = [] # [(depth, index), ...] 当前各层的父节点下标
|
||||
for indent, name in lines:
|
||||
depth = indent // unit
|
||||
if depth > MAX_DEPTH:
|
||||
sys.exit('ERROR: 层级过深(>%d): %r' % (MAX_DEPTH, name))
|
||||
while stack and stack[-1][0] >= depth:
|
||||
stack.pop()
|
||||
parent = stack[-1][1] if stack else ROOT_ID
|
||||
idx = len(nodes)
|
||||
nodes.append((parent, name, depth))
|
||||
stack.append((depth, idx))
|
||||
|
||||
roots = [i for i, (_, _, d) in enumerate(nodes) if d == 0]
|
||||
if len(roots) != 1:
|
||||
sys.exit('ERROR: 必须有且仅有一个根节点(depth 0),当前 %d 个: %s'
|
||||
% (len(roots), [nodes[i][1] for i in roots]))
|
||||
return nodes
|
||||
|
||||
|
||||
def emit(nodes):
|
||||
n = len(nodes)
|
||||
menus = sum(1 for i in range(n) if any(nodes[j][0] == i for j in range(n)))
|
||||
max_depth = max(d for _, _, d in nodes)
|
||||
|
||||
h = [
|
||||
'/* 本文件由 tools/menu_gen/gen_menu.py 从 ui_menu_tree.txt 自动生成,请勿手改。',
|
||||
' * 修改菜单:编辑 ui_menu_tree.txt 后重跑 `python tools/menu_gen/gen_menu.py`。',
|
||||
' */',
|
||||
'#ifndef UI_MENU_TAB_H',
|
||||
'#define UI_MENU_TAB_H',
|
||||
'',
|
||||
'#include <stdint.h>',
|
||||
'#include <stdbool.h>',
|
||||
'',
|
||||
'#define MENU_ROOT 0xFFu /* 根哨兵:仅根节点(主界面)的 parent 取此值;8 位,上限 255 项 */',
|
||||
'',
|
||||
'/* 菜单节点:子项 = parent 等于本下标的所有行(按表序);有子项=菜单,无=叶子 */',
|
||||
'typedef struct {',
|
||||
' uint8_t parent; /* 父节点下标(0..254);根为 MENU_ROOT(0xFF) */',
|
||||
' const char *name; /* 显示文本(字面量,存 Flash) */',
|
||||
'} ui_menu_node_t;',
|
||||
'',
|
||||
'#define UI_MENU_TAB_COUNT %d' % n,
|
||||
'',
|
||||
'/* 护栏:8 位索引上限 255 项(哨兵 0xFF 占用 255);超界则编译失败(C99 写法,兼容 ARMCC v5) */',
|
||||
'typedef char menu_tab_count_check[(UI_MENU_TAB_COUNT <= 255) ? 1 : -1];',
|
||||
'',
|
||||
'extern const ui_menu_node_t g_menu_tab[];',
|
||||
'',
|
||||
'/*',
|
||||
' * 表查询 API(导航逻辑层 ui_menu.c 与显示层 ui_menu_view.c 共用):',
|
||||
' * 实现随表生成,改菜单(ui_menu_tree.txt)调用方零代码改动。',
|
||||
' */',
|
||||
'uint8_t ui_menu_tab_children(uint8_t node, uint8_t out[], uint8_t max); /* 取 node 直接子项(按表序),最多 max 个;返回实际个数 */',
|
||||
'bool ui_menu_tab_has_children(uint8_t node); /* 有子项=菜单(带▶);无=叶子 */',
|
||||
'const char *ui_menu_tab_name(uint8_t node); /* 显示名 */',
|
||||
'bool ui_menu_tab_valid(void); /* 启动自检:非根 parent 下标均有效 */',
|
||||
'',
|
||||
'#endif',
|
||||
'',
|
||||
]
|
||||
|
||||
c = [
|
||||
'/* 本文件由 tools/menu_gen/gen_menu.py 从 ui_menu_tree.txt 自动生成,请勿手改。',
|
||||
' * 修改菜单:编辑 ui_menu_tree.txt 后重跑 `python tools/menu_gen/gen_menu.py`。',
|
||||
' */',
|
||||
'#include "ui_menu_tab.h"',
|
||||
'',
|
||||
'const ui_menu_node_t g_menu_tab[UI_MENU_TAB_COUNT] = {',
|
||||
]
|
||||
for i, (parent, name, depth) in enumerate(nodes):
|
||||
pstr = 'MENU_ROOT' if parent == ROOT_ID else '%d' % parent
|
||||
c.append(' { %8s, "%s" }, /* %2d L%d */' % (pstr, name, i, depth))
|
||||
c.append('};')
|
||||
c.append('')
|
||||
c += [
|
||||
'/*',
|
||||
' * 函数功能:取 node 的直接子项(按表序),最多 max 个',
|
||||
' * 入口参数:node - 节点下标 uint8_t',
|
||||
' * out - 子节点下标输出缓冲 uint8_t *',
|
||||
' * max - 缓冲容量 uint8_t',
|
||||
' * 返回值:实际子项个数 uint8_t',
|
||||
' * 限定条件:node < UI_MENU_TAB_COUNT',
|
||||
' * 函数说明:子项 = 表中所有 parent == 本下标的行(表序即显示序);建页核心查询',
|
||||
' */',
|
||||
'uint8_t ui_menu_tab_children(uint8_t node, uint8_t out[], uint8_t max)',
|
||||
'{',
|
||||
' uint8_t n = 0u;',
|
||||
' for (uint8_t i = 0u; i < UI_MENU_TAB_COUNT && n < max; i++) {',
|
||||
' if (g_menu_tab[i].parent == node) {',
|
||||
' out[n++] = i;',
|
||||
' }',
|
||||
' }',
|
||||
' return n;',
|
||||
'}',
|
||||
'',
|
||||
'/*',
|
||||
' * 函数功能:判断 node 有无子项',
|
||||
' * 入口参数:node - 节点下标 uint8_t',
|
||||
' * 返回值:有子项返回 true(=菜单、行带▶);无子项返回 false(=叶子)',
|
||||
' * 限定条件:node < UI_MENU_TAB_COUNT',
|
||||
' * 函数说明:菜单/叶子由"是否有子项"自动推导,无单独类型字段',
|
||||
' */',
|
||||
'bool ui_menu_tab_has_children(uint8_t node)',
|
||||
'{',
|
||||
' for (uint8_t i = 0u; i < UI_MENU_TAB_COUNT; i++) {',
|
||||
' if (g_menu_tab[i].parent == node) {',
|
||||
' return true;',
|
||||
' }',
|
||||
' }',
|
||||
' return false;',
|
||||
'}',
|
||||
'',
|
||||
'/*',
|
||||
' * 函数功能:取节点显示名',
|
||||
' * 入口参数:node - 节点下标 uint8_t',
|
||||
' * 返回值:显示文本(Flash 字面量) const char *',
|
||||
' * 限定条件:node < UI_MENU_TAB_COUNT',
|
||||
' * 函数说明:行文本一律经此取静态字符串,不 strdup',
|
||||
' */',
|
||||
'const char *ui_menu_tab_name(uint8_t node)',
|
||||
'{',
|
||||
' return g_menu_tab[node].name;',
|
||||
'}',
|
||||
'',
|
||||
'/*',
|
||||
' * 函数功能:菜单表启动自检',
|
||||
' * 入口参数:无',
|
||||
' * 返回值:true - 表有效;false - 存在越界 parent(表内笔误)',
|
||||
' * 限定条件:无',
|
||||
' * 函数说明:每个非根节点的 parent 必须为 MENU_ROOT 或有效下标;在 ui_menu_init 调用',
|
||||
' */',
|
||||
'bool ui_menu_tab_valid(void)',
|
||||
'{',
|
||||
' for (uint8_t i = 0u; i < UI_MENU_TAB_COUNT; i++) {',
|
||||
' if (i == 0u) {',
|
||||
' continue; /* 根 */',
|
||||
' }',
|
||||
' uint8_t p = g_menu_tab[i].parent;',
|
||||
' if (p != MENU_ROOT && p >= UI_MENU_TAB_COUNT) {',
|
||||
' return false;',
|
||||
' }',
|
||||
' }',
|
||||
' return true;',
|
||||
'}',
|
||||
'',
|
||||
]
|
||||
|
||||
return '\n'.join(h), '\n'.join(c), menus, max_depth
|
||||
|
||||
|
||||
def main():
|
||||
if not os.path.exists(SRC):
|
||||
sys.exit('ERROR: 未找到 ui_menu_tree.txt(应在仓库根目录)')
|
||||
text = open(SRC, encoding='utf-8-sig').read()
|
||||
nodes = parse_tree(text)
|
||||
if len(nodes) > MAX_NODES:
|
||||
sys.exit('ERROR: 节点数 %d 超过 uint8_t 索引上限 %d(哨兵 MENU_ROOT=0xFF 占用 255);请精简菜单或改回 16 位索引' % (len(nodes), MAX_NODES))
|
||||
header, body, menus, max_depth = emit(nodes)
|
||||
# Drivers/BSP/UI/* 约定为 UTF-8 带 BOM(同 ui_menu.c);字符串字面量含中文,
|
||||
# 无 BOM 时 cl.exe(代码页 936) 会按 GBK 误读 UTF-8 字节导致字符串解析错乱。
|
||||
with open(OUT_H, 'w', encoding='utf-8-sig', newline='') as fp:
|
||||
fp.write(header)
|
||||
with open(OUT_C, 'w', encoding='utf-8-sig', newline='') as fp:
|
||||
fp.write(body)
|
||||
print('nodes=%d menu=%d leaf=%d max_depth=%d' % (len(nodes), menus, len(nodes) - menus, max_depth))
|
||||
print('->', os.path.relpath(OUT_H, ROOT))
|
||||
print('->', os.path.relpath(OUT_C, ROOT))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user