Files
MeowOS-For-R3P2/patch_v2/meow_launcherd/settings_page.c
T
AskaEth 3d70a3dd2c feat: M6.10–M6.19 批量收编 + M6.19 推卡验收
- M7 launcher 重写 5 期(虚拟化宫格/meow_theme 单一权威/横幅动画/自定义 PIN 键盘)
- OOM 通用化(cachegov 上收 SDK)、自动切歌根治、meowpanel jv_free 泄漏修复、Hello 滚动卡顿修复
- ncm 歌词 UI / 媒体通知封面 / 封面分辨率 640
- 5 bug 修复:ncm 后台化 UI 叠层+点击穿透、再进 ncm 桌面叠层、播放页新旧 UI 叠层抢刷脏区(meow_ui_gfx 恢复边沿整帧重画)、PIN 亮屏亮度跳(keys.c meow_panel_on 按 settings_brightness_clamped 恢复)、通知中心封面显歌单封面(cover.c save_cover_raw 加 is_song + 命中块守卫)
- 四 App 统一重链新 meow_ui.a 51caee4a;M6.19=0.8.19 固件 2d2331f2 推卡自刷验收全绿
2026-09-03 12:16:01 +08:00

1674 lines
96 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* settings_page.c — MeowOS launcher 拆分模块(LAUNCHER_SPLIT_PLAN 拆分)。*/
/* 本文件由拆分工具从 meow_launcherd.c 按职责搬移;逻辑与原文 0-diff(仅 static 可见性按跨模块需求调整)。*/
#include "meow_launcher_internal.h"
/* SC_PRESET 预设表(路径/枚举字符串的"预设项,自定义文件选择留后期") */
static const char *const bg_presets[] = { "black", "blue", "green" };
static const char *const bg_preset_labels[] = { "黑色", "深蓝", "深绿" };
static const char *const hk_presets[] = { "power_voldown", "seq" };
static const char *const hk_preset_labels[] = { "电源+音量减", "连按序列" };
static const char *const seq_presets[] = { "volup,volup,volup", "voldown,voldown,voldown" };
static const char *const seq_preset_labels[] = { "音量上×3", "音量下×3" };
/* font.file 行(字体文件)不使用编译期静态表:其 presets/preset_labels 指向字体区
* (font_presets/font_preset_labels, 见本文件字体区) 的运行时动态数组,计数由
* s_row_presets() 动态返回(默认 1,进设置页扫描 TF 卡后更新)。见 SMENU 行。 */
static const char *const out_presets[] = { "hw:0,0" };
static const char *const out_preset_labels[] = { "CS43198 (hw:0,0)" };
/* M2.37:息屏时间滑条改为「离散档位」而非线性 0~86400 秒(线性步进 1 根本调不到 30s,
* 上限 24h 过于抽象)。档位 0=常亮;从 5s 起步的细粒度段可精调「30s 左右」,随后按
* 45s/分/刻/时 跳档,最高 2 时。滑条值 = 档位索引(0..S_TIME_TIERS_N-1,步进 1),
* 落盘仍写「秒」(display.screen_off_timeout);索引<->秒双向映射见 s_time_tier_*。 */
static const int s_time_tiers[] = {
0, /* 常亮 */
5, 10, 15, 20, /* 细粒度段:可精确到 30s 附近 */
30, 45, /* 半分钟/三刻钟 */
60, 90, 120, 180, /* 1 分 / 1.5 分 / 2 分 / 3 分 */
300, 600, 900, /* 5 分 / 10 分 / 15 分 */
1800, /* 30 分 */
3600, 7200 /* 1 时 / 2 时 */
};
/* 设置页扁平化菜单(分组=组标题行,其余=设置项)。顺序即显示顺序。 */
/* 分组/键/类型/默认与 SYSTEM_SETTINGS.md §1 一致(新建项四端同步)。 */
static const srow_t SMENU[] = {
/* 显示 */
{ "显示", NULL, SC_HEADER, 0,0,0, NULL,NULL,0,0 },
{ "亮度", "display.brightness", SC_SLIDER, 1,100,1, NULL,NULL,0,0 },
{ "息屏模式", "display.screen_off_mode", SC_ENUM, 0,0,0, NULL,NULL,0,0 },
{ "息屏时间", "display.screen_off_timeout", SC_SLIDER, 0,(S_TIME_TIERS_N-1),1, NULL,NULL,0,0 },
{ "刷新率", "display.refresh_rate", SC_ENUM, 0,0,0, NULL,NULL,0,0 },
{ "最高刷新率", "display.refresh_always_max", SC_SWITCH, 0,0,0, NULL,NULL,0,0 },
/* B2b 主题三方式:标题行「主题」改选 display.theme_mode(手动/时段自动),
* 手动深浅由下方「主题深浅」(display.theme) 控制;时段两键为分钟滑条(STEP=15)。 */
{ "主题", "display.theme_mode", SC_ENUM, 0,0,0, NULL,NULL,0,0 },
{ "主题深浅", "display.theme", SC_ENUM, 0,0,0, NULL,NULL,0,0 },
{ "深色时段开始", "display.theme_dark_start", SC_SLIDER, 0,1439,15, NULL,NULL,0,0 },
{ "深色时段结束", "display.theme_dark_end", SC_SLIDER, 0,1439,15, NULL,NULL,0,0 },
{ "背景", "display.background", SC_PRESET, 0,0,0, bg_presets, bg_preset_labels, BG_PRESETS_N,0 },
/* M2.79 主界面背景图片叠加层强度(图片背景上叠 黑/白 半透明遮罩,防文字/控件被图片淹没) */
{ "背景叠加强度", "display.bg_overlay_alpha", SC_SLIDER, 0,255,1, NULL,NULL,0,0 },
/* M2.79 主界面布局:卡片宫格 / 列表(写 ui.home_layout,回主界面即生效) */
{ "主界面布局", "ui.home_layout", SC_ENUM, 0,0,0, NULL,NULL,0,0 },
/* 网络:M2.76 WiFi/蓝牙升级为独立子页面(安卓风)。入口行为「列表行」+右侧 chevron,
* 摘要显示当前状态(s_net_get);点开进入对应子页(顶栏+返回+分组列表),详见
* wifi_page.c / bt_page.c。原来散落的开关/列表/信息行收敛进子页面。 */
{ "网络", NULL, SC_HEADER, 0,0,0, NULL,NULL,0,0 },
{ "WiFi", NULL, SC_LIST, 0,0,0, NULL,NULL,0, NET_LIST_WIFI_PAGE },
{ "蓝牙", NULL, SC_LIST, 0,0,0, NULL,NULL,0, NET_LIST_BT_PAGE },
/* 电源 */
{ "电源", NULL, SC_HEADER, 0,0,0, NULL,NULL,0,0 },
{ "长按弹菜单", "power.longpress_menu_ms", SC_SLIDER, 500,5000,500, NULL,NULL,0,0 },
{ "长按强关", "power.forceoff_ms", SC_SLIDER, 5500,15000,500, NULL,NULL,0,0 },
{ "退出热键", "power.defeat_hotkey", SC_PRESET, 0,0,0, hk_presets, hk_preset_labels, HK_PRESETS_N,0 },
/* 开机 */
{ "开机", NULL, SC_HEADER, 0,0,0, NULL,NULL,0,0 },
{ "默认系统", "boot.default", SC_ENUM, 0,0,0, NULL,NULL,0,0 },
{ "显示选单", "boot.menu_enabled", SC_SWITCH, 0,0,0, NULL,NULL,0,0 },
{ "选单超时", "boot.timeout_ms", SC_SLIDER, 0,30000,500, NULL,NULL,0,0 },
/* 输入 */
{ "输入", NULL, SC_HEADER, 0,0,0, NULL,NULL,0,0 },
{ "热键序列", "input.hotkey_seq", SC_PRESET, 0,0,0, seq_presets, seq_preset_labels, SEQ_PRESETS_N,0 },
{ "热键窗口", "input.hotkey_window_ms", SC_SLIDER, 0,5000,100, NULL,NULL,0,0 },
{ "触摸布防", "input.touch_arm_delay_ms", SC_SLIDER, 0,2000,50, NULL,NULL,0,0 },
{ "滑动方向", "input.swipe_invert", SC_SWITCH, 0,0,0, NULL,NULL,0,0 },
/* 字体主题 */
{ "字体主题", NULL, SC_HEADER, 0,0,0, NULL,NULL,0,0 },
{ "字体文件", "font.file", SC_PRESET, 0,0,0, font_presets, font_preset_labels, 0,0 },
/* M2.5x 字体档位分层/按需大字库:「加载大字库」= font.load_full(int 0/1 默认 0)。
* 开 → 强制全系统用大字库 gbk≈20k 档(覆盖 app 未声明/覆盖默认 font.file);
* 关(默认)→ 小字库 common≈4k,仅 app 声明 fonts:"full" 才大字库。 */
{ "加载大字库", "font.load_full", SC_SWITCH, 0,0,0, NULL,NULL,0,0 },
/* M2.5x 多字体:开关 + 6 个字符类别字体(preset=可用 MeowOS/fonts/<family>/<weight>/ 列表;
* 值空 = 跟随全局 font.file)。仅开关开启时 6 行可见(s_row_visible)。 */
{ "多字体", "display.multi_font", SC_SWITCH, 0,0,0, NULL,NULL,0,0 },
{ "中文(CJK)", "font.class_cjk", SC_PRESET, 0,0,0, font_presets, font_preset_labels, 0,0 },
{ "中文标点", "font.class_cjk_sym", SC_PRESET, 0,0,0, font_presets, font_preset_labels, 0,0 },
{ "数字", "font.class_num", SC_PRESET, 0,0,0, font_presets, font_preset_labels, 0,0 },
{ "拉丁字母", "font.class_latin", SC_PRESET, 0,0,0, font_presets, font_preset_labels, 0,0 },
{ "半角标点", "font.class_latin_sym", SC_PRESET, 0,0,0, font_presets, font_preset_labels, 0,0 },
{ "其他字符", "font.class_other", SC_PRESET, 0,0,0, font_presets, font_preset_labels, 0,0 },
{ "标题字号", "font.size.title", SC_SLIDER, 16,40,1, NULL,NULL,0,0 },
{ "正文字号", "font.size.body", SC_SLIDER, 12,28,1, NULL,NULL,0,0 },
{ "UI风格", "ui.style", SC_ENUM, 0,0,0, NULL,NULL,0,0 },
/* 通知 */
{ "通知", NULL, SC_HEADER, 0,0,0, NULL,NULL,0,0 },
{ "通知中心", "notify.center_enabled", SC_SWITCH, 0,0,0, NULL,NULL,0,0 },
{ "应用配额", "notify.app_quota", SC_SLIDER, 1,64,1, NULL,NULL,0,0 },
/* 日志 */
{ "日志", NULL, SC_HEADER, 0,0,0, NULL,NULL,0,0 },
{ "日志级别", "log.level", SC_ENUM, 0,0,0, NULL,NULL,0,0 },
{ "写入TF卡", "log.to_card", SC_SWITCH, 0,0,0, NULL,NULL,0,0 },
/* 音频 */
{ "音频", NULL, SC_HEADER, 0,0,0, NULL,NULL,0,0 },
{ "音量", "audio.volume", SC_SLIDER, 0,100,1, NULL,NULL,0,0 },
{ "音量上限", "audio.volume_max", SC_SLIDER, 0,100,1, NULL,NULL,0,0 },
{ "音量跳步", "audio.volume_step", SC_SLIDER, 1,5,1, NULL,NULL,0,0 },
{ "输出设备", "audio.output", SC_PRESET, 0,0,0, out_presets, out_preset_labels, OUT_PRESETS_N,0 },
/* M6.x 锁屏:安全组「锁屏 PIN」独立子页面(状态/设置/修改/清除/唤醒锁开关,见 lock_settings.c) */
{ "安全", NULL, SC_HEADER, 0,0,0, NULL,NULL,0,0 },
{ "锁屏 PIN", NULL, SC_LIST, 0,0,0, NULL,NULL,0, NET_LIST_PIN_PAGE },
/* 关于(设置列表最底;「关于本机」点开 s_about_open 弹只读设备信息框) */
{ "关于", NULL, SC_HEADER, 0,0,0, NULL,NULL,0,0 },
{ "关于本机", NULL, SC_LIST, 0,0,0, NULL,NULL,0, NET_LIST_ABOUT },
};
/* M2.39 左缘返回手势已抽为共享函数 meow_edge_back()(meow_touch.h MEOW_EDGE_W/SWIPE 由
* 其定义),设置页运行期调用;此处不再定义独立 S_EDGE_W/S_SWIPE_THRESH。 */
#if 0 /* M7.2 回退位:旧「当前选中滑条行 track y」随旧手绘 draw_settings_page 退役
* (新路径在触摸按下时由 meow_listview hit 直接算 track 中心,无需全局记录)。 */
/* 当前选中行若是滑条:记录其 track 纵向位置,供触摸拖动命中判(x/w 为布局常量,
* 所有滑条行相同,可直接算;仅 y 随行变,故独立记录)。 */
static int s_sel_track_y = -1;
#endif
/* ---- 行高/位置/导航辅助 ---- */
/* M2.5x 多字体:6 个「字体文件」行仅在 display.multi_font=1 时可见(开时显示)。
* 可见性与分组的标题行无关(标题恒显示)。 */
static int s_row_visible(int i)
{
const srow_t *r = &SMENU[i];
if (r->key && !strncmp(r->key, "font.class_", 11)) {
int on = 0; meow_settings_get_switch("display.multi_font", &on);
return on;
}
return 1;
}
/* 可见行数(= 压缩后占用的行数;标题行计入,隐藏的 class 行不计) */
static int s_vis_count(void)
{
int n = 0;
for (int i = 0; i < SMENU_N; i++) if (s_row_visible(i)) n++;
return n;
}
/* SMENU 下标 → 压缩位置(第几个可见行);不可见返回 -1 */
static int s_vis_of(int i)
{
if (i < 0 || i >= SMENU_N || !s_row_visible(i)) return -1;
int p = 0;
for (int k = 0; k < i; k++) if (s_row_visible(k)) p++;
return p;
}
/* 压缩位置 → SMENU 下标 */
static int s_vis_at(int p)
{
int c = 0;
for (int i = 0; i < SMENU_N; i++) if (s_row_visible(i)) { if (c == p) return i; c++; }
return SMENU_N - 1;
}
static int s_setting_idx(int i) { return i >= 0 && i < SMENU_N && SMENU[i].ctrl != SC_HEADER && s_row_visible(i); }
static int s_first_setting(void)
{
for (int i = 0; i < SMENU_N; i++) if (s_setting_idx(i)) return i;
return 0;
}
static int s_next_setting(int i)
{
for (int k = i + 1; k < SMENU_N; k++) if (s_setting_idx(k)) return k;
return s_first_setting(); /* 回绕 */
}
/* 字体行(font.file + font.class_*)走「family→字重」两级子菜单(M2.6 字重结构) */
static int s_row_is_font(const srow_t *r)
{
return r && r->key && (!strcmp(r->key, "font.file") || !strncmp(r->key, "font.class_", 11));
}
/* SC_PRESET 行取「选项表 + 数量」:字体行(font.file + font.class_*)用运行时动态数组
* (TF 卡 MeowOS/fonts/<family>/<weight>/ 扫描结果,见字体区 font_presets/font_presets_n),
* 其余行用编译期静态表(r->presets / r->preset_n)。 */
static const char *const *s_row_presets(const srow_t *r, int *n)
{
if (r && r->key && (!strcmp(r->key, "font.file") || !strncmp(r->key, "font.class_", 11))) {
if (n) *n = font_presets_n;
return font_presets;
}
if (n) *n = r->preset_n;
return r->presets;
}
#if 0 /* M7.2 回退位:旧「纯范围/选中可见滚动钳制 + 内容 y→行号」随旧手绘 draw_settings_page 退役
* (新路径滚动走 meow_scroll 包裹 meow_listview,钳制/命中/可见性由 SDK 接管)。
* 删除 #if 0 行并切回旧 draw_settings_page 路径即可恢复。 */
/* 滚动:纯范围钳制到 [0, 内容高度-视口高](列表拖动滚动用;不强制选中行可见) */
static int s_clamp_scroll_range(int scroll)
{
int total = s_vis_count() * SROW_H;
if (total <= SET_CONTENT_H) return 0;
int max_scroll = total - SET_CONTENT_H;
if (scroll < 0) scroll = 0;
if (scroll > max_scroll) scroll = max_scroll;
return scroll;
}
/* 滚动:保证当前选中行在 [SET_CONTENT_Y, SET_CONTENT_BOT] 内 */
static int s_clamp_scroll(int sel, int scroll)
{
scroll = s_clamp_scroll_range(scroll);
int pos = s_vis_of(sel);
if (pos < 0) pos = s_vis_of(s_first_setting());
int top = pos * SROW_H;
int bot = top + SROW_H;
if (top < scroll) scroll = top; /* 选中行滑出顶部 → 上滚 */
if (bot > scroll + SET_CONTENT_H) scroll = bot - SET_CONTENT_H; /* 滑出底部 → 下滚 */
return s_clamp_scroll_range(scroll);
}
/* 行 y 落在 content 坐标 cy 时对应的行号(压缩位置 → SMENU 下标);越界返回 -1 */
static int s_row_at_content_y(int cy)
{
if (cy < 0) return -1;
int pos = cy / SROW_H;
if (pos >= s_vis_count()) return -1;
return s_vis_at(pos);
}
#endif /* M7.2 回退位:旧滚动钳制 + 内容 y→行号 */
/* ---- 值格式化/展示 ---- */
static void s_fmt_time(int sec, char *buf, int len)
{
if (sec <= 0) { snprintf(buf, len, "常亮"); return; }
if (sec < 60) { snprintf(buf, len, "%d 秒", sec); return; }
if (sec < 3600) { snprintf(buf, len, "%d 分", sec / 60); return; }
snprintf(buf, len, "%d 时", sec / 3600);
}
/* B2b 主题时段:分钟(0-1439) → "HH:MM"(如 1140→"19:00")。注意与 s_fmt_time 的秒语义区分。 */
static void s_fmt_minutes(int mins, char *buf, int len)
{
if (mins < 0) mins = 0;
if (mins > 1439) mins = 1439;
snprintf(buf, len, "%02d:%02d", mins / 60, mins % 60);
}
/* 息屏时间:秒 → 最近档位索引(显示滑动位置用;等值优先,跨档取较近档,平手取高档) */
static int s_time_tier_idx(int sec)
{
if (sec <= s_time_tiers[0]) return 0;
for (int i = 1; i < S_TIME_TIERS_N; i++) {
if (sec == s_time_tiers[i]) return i;
if (sec < s_time_tiers[i]) {
int lo = s_time_tiers[i - 1], hi = s_time_tiers[i];
return (sec - lo < hi - sec) ? i - 1 : i;
}
}
return S_TIME_TIERS_N - 1;
}
/* 息屏时间:档位索引 → 秒(落盘写给 display.screen_off_timeout 的实值) */
static int s_time_tier_val(int idx)
{
if (idx < 0) idx = 0;
if (idx >= S_TIME_TIERS_N) idx = S_TIME_TIERS_N - 1;
return s_time_tiers[idx];
}
static void s_fmt_value(const srow_t *r, char *buf, int len)
{
int v = 0;
if (meow_settings_get_int(r->key, &v) != 0) { snprintf(buf, len, "-"); return; }
if (!strcmp(r->key, "display.screen_off_timeout")) { s_fmt_time(v, buf, len); return; }
/* B2b 深色时段(分钟 0-1439 → "HH:MM");start==end 视为未配置,显示「未设置」 */
if (!strcmp(r->key, "display.theme_dark_start") || !strcmp(r->key, "display.theme_dark_end")) {
int s2 = 0, e2 = 0;
meow_settings_get_int("display.theme_dark_start", &s2);
meow_settings_get_int("display.theme_dark_end", &e2);
if (s2 == e2) { snprintf(buf, len, "未设置"); return; }
s_fmt_minutes(v, buf, len);
return;
}
/* 长按弹菜单/强关:毫秒换算成秒(0.5 步进,如 "2.5 秒") */
if (!strcmp(r->key, "power.longpress_menu_ms") || !strcmp(r->key, "power.forceoff_ms")) {
if (v % 1000 == 0) snprintf(buf, len, "%d 秒", v / 1000);
else snprintf(buf, len, "%.1f 秒", v / 1000.0);
return;
}
snprintf(buf, len, "%d", v);
}
static void s_display_value(const srow_t *r, char *buf, int len)
{
if (r->ctrl == SC_ENUM) {
int idx = 0;
if (meow_settings_get_enum(r->key, &idx) != 0) idx = 0;
if (meow_settings_enum_label(r->key, idx, buf, len) != 0) snprintf(buf, len, "-");
return;
}
/* SC_PRESET */
char cur[MEOW_SET_MAX_LEN];
if (meow_settings_get_str(r->key, cur, sizeof cur) != 0) { snprintf(buf, len, "-"); return; }
/* M2.5x 多字体:class 行空值 = 跟随全局 font.file */
if (r->key && !strncmp(r->key, "font.class_", 11) && !cur[0]) {
snprintf(buf, len, "跟随全局");
return;
}
int pn = 0;
const char *const *pps = s_row_presets(r, &pn);
for (int i = 0; i < pn; i++) {
if (!strcmp(pps[i], cur)) {
snprintf(buf, len, "%s", (r->preset_labels && r->preset_labels[i][0]) ? r->preset_labels[i] : cur);
return;
}
}
snprintf(buf, len, "%s", cur);
}
/* ---- 控件绘制(MD3 形态;全部用主题角色色,无散落硬编码)---- */
void s_draw_switch(int x, int y, int on, const meow_ui_theme_t *th)
{
int w = 48, h = 28;
meow_gfx_fill_round(x, y, w, h, h / 2, on ? th->primary : th->surface_container);
int knob_r = h / 2 - 3;
int kx = on ? x + w - knob_r * 2 - 3 : x + 3;
meow_gfx_fill_round(kx, y + 3, knob_r * 2, knob_r * 2, knob_r,
on ? 0xFFFFFF : th->on_surface_variant);
}
void s_draw_slider(int x, int y, int w, int val, int mn, int mx,
const meow_ui_theme_t *th)
{
int h = 6, ty = y;
fill_rect(x, ty, w, h, th->surface_container); /* 未达段 */
int range = mx - mn;
int frac = range ? (val - mn) * w / range : 0;
if (frac > 0) fill_rect(x, ty, frac, h, th->primary); /* 已达段 */
int tx = x + frac, cy = ty + h / 2;
meow_gfx_fill_round(tx - 12, cy - 12, 24, 24, 12, th->primary); /* thumb */
meow_gfx_fill_round(tx - 5, cy - 5, 10, 10, 5, 0xFFFFFF); /* thumb 中点 */
}
/* ---- set 后统一侧效应(DAC/背光/刷新率)+ 可选落盘 ---- */
static void s_commit(const srow_t *r, int save_now)
{
if (!r || !r->key) return;
if (!strcmp(r->key, "audio.volume")) {
/* M2.38 Bug1/2:音量变更——拖动中节流写 DAC(防 system() 高频卡死),抬手才无条件直写;
* 同时把 g_sys.vol 同步为当前值并刷整条状态栏(顶层 VOL 数字随之更新,不整屏重绘)。 */
g_sys.vol = settings_volume_get();
paint_sb_right();
if (save_now) meow_dac_vol_flush(); /* 抬手/点击落盘=无条件直写真实 DAC */
else meow_dac_vol_write_throttled();/* 拖动中=墙钟节流写 DAC */
}
else if (!strcmp(r->key, "display.brightness")) {
/* M2.46-2 ①:亮度滑条拖动时状态栏百分比数字实时跟。根因:旧代码只 write_brightness(v)
* 写背光,未同步 g_sys.brightness(状态栏亮度数字取它)也未重绘 → 数字卡住。
* 修法:写背光后同步 g_sys.brightness 并 paint_sb_right()(≡sb_redraw 整条标脏)。
* 一致性:其后 meow_sys_refresh 从 sysfs 读回的也是刚写入的 v,二者一致不打架。 */
int v; if (meow_settings_get_int("display.brightness", &v) == 0) {
write_brightness(v, "settings_page bright_slider");
g_sys.brightness = v;
paint_sb_right();
}
}
else if (!strcmp(r->key, "display.refresh_rate"))
meow_ui_set_refresh_rate(refresh_rate_cfg);
else if (!strcmp(r->key, "display.refresh_always_max"))
meow_ui_set_refresh_always_max(refresh_always_max);
else if (!strcmp(r->key, "network.wifi_enabled")) {
int on = 0; meow_settings_get_bool(r->key, &on);
s_net_toggle_wifi(on);
s_net_refresh();
}
else if (!strcmp(r->key, "network.bluetooth_enabled")) {
int on = 0; meow_settings_get_bool(r->key, &on);
s_net_toggle_bt(on);
s_net_refresh();
}
/* M2.44 背景选项真正生效:display.background(black/blue/green) → menu_bg(主界面背景)。
* 此前 SC_PRESET 只写 meow_settings,未应用 menu_bg → 黑/绿点了没用。 */
else if (!strcmp(r->key, "display.background")) {
char bg[16]; meow_settings_get_str(r->key, bg, sizeof bg);
int mstyle = 0; if (meow_settings_get_enum("ui.style", &mstyle) != 0) mstyle = 0;
if (mstyle == MEOW_STYLE_MIUIX) { /* MIUIx 暖色族(与 main_menu menu_bg_prepare 同色) */
if (!strcmp(bg, "blue")) menu_bg = 0x101A2E;
else if (!strcmp(bg, "green")) menu_bg = 0x14251A;
else menu_bg = 0x181310;
} else { /* MD3(既有) */
if (!strcmp(bg, "blue")) menu_bg = 0x0A1428; /* 深蓝 */
else if (!strcmp(bg, "green")) menu_bg = 0x0A2814; /* 深绿 */
else menu_bg = 0x000000; /* black(默认) */
}
/* menu_bg 仅主界面背景用(状态栏走 SDK meow_theme bg);退出设置页回主界面时经
* menu_bg_sig(含 ui.style)重build 缓存。 */
}
/* M2.44 主题/mode 变更:状态栏文字/底色已接 s_ui_theme(),此处仅确保回主界面重绘。 */
else if (!strcmp(r->key, "display.theme") || !strcmp(r->key, "display.theme_mode") ||
!strcmp(r->key, "display.theme_dark_start") || !strcmp(r->key, "display.theme_dark_end")) {
paint_sb_right(); /* 状态栏文字/底色随主题刷新(整条标脏) */
}
/* §8.1.2 一键切换闭环:ui.style(md3/miuix)顶级风格族切换 → 全系统即时换肤,无需重启。
* meow_ui_set_style() 设 SDK g_style(launcher 同一进程内立即生效)+ 广播(App/状态栏下一帧换肤);
* paint_sb_right() → sb_sdk_refresh() 逼 SDK 状态栏重读 settings(含 display.background 派生)跟手换肤;
* meow_dirty_full() 让 launcher 主界面/设置页整帧重绘(s_ui_theme() 已接 ui.style → 换族配色)。
* 主界面背景缓存经 menu_bg_sig(含 ui.style,见 main_menu.c)在回主界面时自动重建。 */
else if (!strcmp(r->key, "ui.style")) {
int st = 0; meow_settings_get_enum("ui.style", &st);
meow_ui_set_style(st);
paint_sb_right();
meow_dirty_full();
}
/* M2.5x:字体文件(font.file)变更 → 立即 unload + reload(若已加载)。
* 置 mfont_forced 跳过 meow_font_ensure 的 SDK 复用捷径,强制走
* font.file → 卡默认 → 固件 链;全失败则回退 5x7 点阵(系统不死)。 */
else if (!strcmp(r->key, "font.file")) {
mfont_ready = 0;
mfont_forced = 1;
if (!meow_font_ensure())
logmsg("font.file: reload failed -> dotmatrix fallback");
else
logmsg("font.file: reloaded");
meow_font_reload_multifont(); /* M2.5x:全局字体变更 → 重配多字体(各类跟随全局者随之更新) */
meow_dirty_full(); /* 字体变化可能影响所有文本渲染,整帧重绘 */
}
/* M2.5x「加载大字库」开关(font.load_full)变更 → 强制重载 launcher 自身字体(小/大字库切换),
* 并重配多字体;大字库 atlas 大,meow_font_ensure 先 unload 旧再 load 新(防双驻留)。 */
else if (!strcmp(r->key, "font.load_full")) {
mfont_ready = 0;
mfont_forced = 1; /* 跳过 SDK 复用捷径,强制按生效档(gbk/common)真实 reload */
if (!meow_font_ensure())
logmsg("font.load_full: reload failed -> dotmatrix fallback");
else {
int lf = 0; meow_settings_get_switch("font.load_full", &lf);
logmsg("font.load_full: reloaded (load_full=%d)", lf);
}
meow_font_reload_multifont();
meow_dirty_full();
}
/* M2.5x 多字体:开关或任一类别字体变更 → 重新去重装载类别字库并重绘(多字体可能影响文本渲染)。 */
else if (!strcmp(r->key, "display.multi_font") || !strncmp(r->key, "font.class_", 11)) {
meow_font_reload_multifont();
meow_dirty_full();
}
if (save_now) meow_settings_save();
}
static void s_set_int_value(const srow_t *r, int v, int save_now)
{
if (!strcmp(r->key, "audio.volume")) v = vol_clamp(v); /* 尊重 audio.volume_max */
meow_settings_set_int(r->key, v);
s_commit(r, save_now);
}
/* 滑条拖动/点击落值:对「档位型」息屏时间做 索引→秒 换算,其余行原样写盘。
* sl_v 为滑条轨迹值(s_slider_val_from_x 返回值;息屏时间=档位索引)。 */
static void s_slider_set_from_track(const srow_t *r, int sl_v, int save_now)
{
if (!strcmp(r->key, "display.screen_off_timeout")) sl_v = s_time_tier_val(sl_v);
/* B2b 深色时段滑条:分钟按 15 分钟步进吸附(0-1439,整 15 分钟),避免拖出任意分钟 */
if (!strcmp(r->key, "display.theme_dark_start") || !strcmp(r->key, "display.theme_dark_end")) {
sl_v = ((sl_v + 7) / 15) * 15;
if (sl_v < 0) sl_v = 0;
if (sl_v > 1439) sl_v = 1439;
}
s_set_int_value(r, sl_v, save_now);
}
/* 滑条拖动值域映射:x(屏幕坐标)→ value,基于布局常量 S_SLIDER_X/W。
* 所有滑条行共用同一 track 几何,故任意滑条行都能正确映射。 */
static int s_slider_val_from_x(const srow_t *r, int px)
{
int range = r->max - r->min;
int v = r->min + (range ? (px - S_SLIDER_X) * range / S_SLIDER_W : 0);
if (v < r->min) v = r->min;
if (v > r->max) v = r->max;
return v;
}
#if 0 /* M7.2 回退位:旧手绘设置页主列表(手绘分组列表 + s_draw_switch/s_draw_slider + 自算滚动/命中)。
* 新路径走 meow_listview + meow_switch + meow_scroll + meow_select_show;删除 #if 0 行并把
* 下方「M7.2 SDK 路径」的 set_paint_page 调用切回 draw_settings_page 即可一键回退。 */
/* ---- 绘制设置页(整页画进 SDK back_buf;调用方再 meow_dirty_add 局部 + flush)---- */
static void draw_settings_page(int sel, int scroll_off)
{
const meow_ui_theme_t *th = s_ui_theme();
int W = fb_xres, H = fb_yres;
fill_rect(0, 0, W, H, th->surface);
draw_statusbar(); /* Stage-One:驱动 SDK 全局状态栏(喂数据+标脏顶条) */
if (mfont_ready) ui_draw(24, 50, "系统设置", th->on_surface, 28);
else draw_text(24, 30, "SETTINGS", 3, th->on_surface);
fill_rect(0, 50 + (mfont_ready ? meow_font_line_height(28) : 24) + 2, W, 2, th->outline);
/* M2.38 Bug5:整屏背景替代带圆角的单张卡片(用户反馈圆角非全屏底很怪)。
* 内容区从 SET_CARD_Y 起整屏 fill surface_variant、去圆角、满宽直到屏幕底;
* 状态栏(0-28)与标题区(28-92)仍为 surface,标题/分隔线/分组 outline/选中高亮/滚动条布局不变。 */
fill_rect(0, SET_CARD_Y, W, H - SET_CARD_Y, th->surface_variant);
int clip_top = SET_CONTENT_Y, clip_bot = SET_CONTENT_BOT;
s_sel_track_y = -1;
for (int i = 0; i < SMENU_N; i++) {
int pos = s_vis_of(i);
if (pos < 0) continue; /* 隐藏行(多字体关闭时 6 个 class 行) */
int sy = SET_CONTENT_Y + pos * SROW_H - scroll_off;
if (sy + SROW_H < clip_top || sy >= clip_bot) continue; /* 视野外行跳过(配滚动) */
const srow_t *r = &SMENU[i];
int selected = (i == sel);
if (selected)
meow_ui_fill_rect_alpha(SET_CARD_X + 4, sy, SET_CARD_W - 8, SROW_H, th->primary, 34);
meow_ui_fill_rect(SET_CARD_X + 12, sy + SROW_H - 1, SET_CONTENT_W, 1, th->outline);
if (r->ctrl == SC_HEADER) {
ui_draw(SET_CARD_X + 16, sy + 14, r->label, th->on_surface_variant, 18);
} else if (r->ctrl == SC_SWITCH) {
ui_draw(SET_CARD_X + 16, sy + (SROW_H - meow_font_line_height(22)) / 2, r->label,
th->on_surface, 22);
int on = 0; meow_settings_get_switch(r->key, &on);
s_draw_switch(SET_CARD_X + SET_CARD_W - 16 - 48, sy + (SROW_H - 28) / 2, on, th);
} else if (r->ctrl == SC_SLIDER) {
int ly = sy + (SROW_H - meow_font_line_height(22)) / 2 - 6; /* 标题略上移,给下方滑条留位 */
ui_draw(SET_CARD_X + 16, ly, r->label, th->on_surface, 22);
char vb[32]; s_fmt_value(r, vb, sizeof vb);
ui_draw(SET_CARD_X + SET_CARD_W - 16 - ui_width(vb, 22), ly, vb, th->on_surface_variant, 22);
int v = 0; meow_settings_get_int(r->key, &v);
/* 息屏时间为档位索引型:把秒换算成档位索引再画(各档等距,便于触摸对位);
* 其余滑条防御性 clamp 到 [min,max],防止后端暂存值越界(如 brightness 旧值 101
* 在画时被 clamp 到 100,thumb 不冲出 track)。 */
if (!strcmp(r->key, "display.screen_off_timeout")) v = s_time_tier_idx(v);
else {
if (v < r->min) v = r->min;
if (v > r->max) v = r->max;
}
int tyy = sy + 40;
s_draw_slider(S_SLIDER_X, tyy, S_SLIDER_W, v, r->min, r->max, th);
if (selected) s_sel_track_y = tyy;
} else if (r->ctrl == SC_INFO) { /* M2.39 只读动态信息(如 WiFi 连接 SSID) */
int ly = sy + (SROW_H - meow_font_line_height(22)) / 2;
ui_draw(SET_CARD_X + 16, ly, r->label, th->on_surface, 22);
char vb[48]; s_net_get(r, vb, sizeof vb);
int vx = SET_CARD_X + SET_CARD_W - 16 - ui_width(vb, 22);
ui_draw(vx, ly, vb, th->on_surface_variant, 22);
} else if (r->ctrl == SC_LIST) { /* M2.39 网络列表行:摘要 + 右向展开提示 */
int ly = sy + (SROW_H - meow_font_line_height(22)) / 2;
ui_draw(SET_CARD_X + 16, ly, r->label, th->on_surface, 22);
char vb[48]; s_net_get(r, vb, sizeof vb);
int vx = SET_CARD_X + SET_CARD_W - 16 - 12 - ui_width(vb, 22); /* 给右向 chevron 留位 */
ui_draw(vx, ly, vb, th->on_surface_variant, 22);
s_draw_chevron(SET_CARD_X + SET_CARD_W - 16, sy + SROW_H / 2, th->on_surface_variant);
} else { /* SC_ENUM / SC_PRESET */
int ly = sy + (SROW_H - meow_font_line_height(22)) / 2;
ui_draw(SET_CARD_X + 16, ly, r->label, th->on_surface, 22);
char vb[40]; s_display_value(r, vb, sizeof vb);
int vx = SET_CARD_X + SET_CARD_W - 16 - ui_width(vb, 22);
ui_draw(vx, ly, vb, th->on_surface_variant, 22);
s_draw_chevron(vx - 8, sy + SROW_H / 2, th->on_surface_variant);
}
}
/* 滚动条(内容超出视口时显示 thumb;on_surface_variant 弱化) */
int vis_total = s_vis_count() * SROW_H;
if (vis_total > SET_CONTENT_H) {
int total = vis_total;
int thumb_h = SET_CONTENT_H * SET_CONTENT_H / total;
if (thumb_h < 24) thumb_h = 24;
int th_y = SET_CONTENT_Y + (SET_CONTENT_H - thumb_h) * scroll_off / (total - SET_CONTENT_H);
meow_gfx_fill_round(fb_xres - 12, SET_CONTENT_Y, 5, SET_CONTENT_H, 3, th->surface_container);
meow_gfx_fill_round(fb_xres - 12, th_y, 5, thumb_h, 3, th->on_surface_variant);
}
/* 底部:返回提示(左)+ 版本号(右),复用应用列表底部对齐逻辑。
* M2.39:返回按钮改用像素左向箭头 + 「返回」——点阵回退字库缺少 '‹' 字形,
* 原 「‹ 返回」在 !mfont_ready 时 '‹' 渲染成方框。 */
{
char ver[32];
snprintf(ver, sizeof ver, "MeowOS v%s dev", g_meowos_version);
int foot_y = fb_yres - meow_font_line_height(20) - 8;
s_draw_chevron_left(SET_CARD_X + 12, foot_y + meow_font_line_height(20) / 2, th->on_surface_variant);
ui_draw(SET_CARD_X + 24, foot_y, "返回", th->on_surface_variant, 20);
ui_draw(fb_xres - 8 - ui_width(ver, 20), foot_y, ver, th->outline, 20);
}
}
#endif /* M7.2 回退位:旧手绘设置页主列表 */
/* M2.5x 多字体:class 行下拉的「跟随全局」选项(值=空串)在 write 路径经此映射回真实存储值。 */
static const char *s_preset_value(const srow_t *r, int pick)
{
int is_cls = r->key && !strncmp(r->key, "font.class_", 11);
if (is_cls) {
if (pick == 0) return ""; /* 跟随全局 font.file */
pick--;
}
int pn = 0;
const char *const *pps = s_row_presets(r, &pn);
if (pick < 0 || pick >= pn) return is_cls ? "" : (pps && pn > 0 ? pps[0] : "");
return pps[pick];
}
/* ---- 选择器子对话框(MD3 §3.3:居中圆角卡片,列选项;中文标签)----
* ENUM 用 meow_settings_enum_names/enum_label;PRESET 用本地预设表。
* 返回选中索引;取消返回 -1。阻塞(自开输入设备)。
* M2.39:modal 核心抽到 s_dd_modal(网络列表复用),本函数只负责组装选项数组。 */
static int s_dropdown(const srow_t *r)
{
int n = 0, cur = 0;
const char *names[8];
char labels[8][48];
const char *item[S_NET_MAX];
if (r->ctrl == SC_ENUM) {
int kn = 0;
meow_settings_enum_names(r->key, names, &kn);
n = kn;
if (n > S_NET_MAX) n = S_NET_MAX;
for (int i = 0; i < n; i++) {
meow_settings_enum_label(r->key, i, labels[i], sizeof labels[i]);
item[i] = labels[i];
}
int ci = 0; meow_settings_get_enum(r->key, &ci); cur = ci;
} else {
char cstr[MEOW_SET_MAX_LEN];
meow_settings_get_str(r->key, cstr, sizeof cstr);
int pn = 0;
const char *const *pps = s_row_presets(r, &pn);
int is_cls = r->key && !strncmp(r->key, "font.class_", 11);
int base = is_cls ? 1 : 0; /* class 行 0 号留给「跟随全局」 */
int cnt = pn; if (cnt > S_NET_MAX - base) cnt = S_NET_MAX - base;
n = base + cnt;
if (is_cls) {
names[0] = "";
snprintf(labels[0], sizeof labels[0], "跟随全局字体");
item[0] = labels[0];
if (!cstr[0]) cur = 0;
}
for (int i = 0; i < cnt; i++) {
names[base + i] = pps[i];
snprintf(labels[base + i], sizeof labels[base + i], "%s",
(r->preset_labels && r->preset_labels[i][0]) ? r->preset_labels[i] : pps[i]);
item[base + i] = labels[base + i];
if (!strcmp(pps[i], cstr)) cur = base + i;
}
}
if (n <= 0) return -1;
if (cur < 0 || cur >= n) cur = 0;
/* M7.2:枚举/预设选择器迁 SDK meow_select_show(BottomSheet+单选列表,批次2 新控件),
* 替换自绘 s_dd_modal(后者仍供 about/网络列表/字体两级子菜单用,勿删)。 */
return meow_select_show(r->label, item, n, cur);
}
/* ---- 通用选择器下拉模态(MD3 §3.3:居中圆角卡片,列选项)----
* title=标题;item[i]=第 i 项显示文本;n=项数(≤S_NET_MAX);cur=当前值下标(画勾选)。
* 返回选中下标;取消/点卡片外返回 -1。阻塞(自开输入设备)。
* s_dropdown(设置项 enum/preset)与 s_net_open(网络列表)共用。
* M2.40:触摸命中判定改在抬手用抬手时刻最新 abs 坐标(见循环内注释),修「点选项无反应」。
* 说明:模态内部仍保留音量键=移动选项高亮(这是「选值」不是「设置页列表光标」,属另一语义,
* 与 M2.39 仅移除 run_settings_page 列表光标移动的约束不冲突)。 */
/* 绘制下拉/关于本机模态:全屏压暗 + 居中圆角卡片 + 标题 + 可见行(按 dd_scroll 裁剪,视口外整行跳过)。 */
static void dd_modal_paint(const meow_ui_theme_t *th, int W, int H, int d_x, int d_y,
int d_w, int d_h, int title_h, int opt_row, const char *title,
const char **item, int n, int sel, int cur,
int content_top, int content_bot, int dd_scroll)
{
meow_ui_fill_rect_alpha(0, 0, W, H, 0x000000, 150);
meow_gfx_fill_round(d_x, d_y, d_w, d_h, 16, th->surface_container);
meow_ui_draw_rect_round(d_x, d_y, d_w, d_h, 16, th->outline);
if (mfont_ready) ui_draw(d_x + 18, d_y + 8, title, th->on_surface, 20);
else draw_text(d_x + 18, d_y + 8, title, 2, th->on_surface);
for (int i = 0; i < n; i++) {
int ry = d_y + title_h + i * opt_row - dd_scroll;
if (ry + opt_row < content_top || ry >= content_bot) continue; /* 视口外整行跳过(配滚动) */
int cur_sel = (i == sel);
if (cur_sel) meow_ui_fill_rect_alpha(d_x + 4, ry, d_w - 8, opt_row, th->primary, 36);
ui_draw(d_x + 18, ry + (opt_row - meow_font_line_height(20)) / 2, item[i] ? item[i] : "",
cur_sel ? meow_theme_get()->onPrimary : th->on_surface, 20);
if (i == cur) meow_ui_fill_rect(d_x + d_w - 28, ry + (opt_row - 12) / 2, 12, 12, th->primary);
}
}
int s_dd_modal(const char *title, const char *item[S_NET_MAX], int n, int cur)
{
int sel = cur;
const meow_ui_theme_t *th = s_ui_theme();
int W = fb_xres, H = fb_yres;
int opt_row = 48, pad = 20, title_h = 40;
/* M2.5x 关于本机:内容超高(13 行)时把卡片高钳到屏内,内容区允许滚动(复用设置页列滚动语义)。 */
int raw_d_h = title_h + n * opt_row + pad;
int max_card_h = H - SB_BAR_H - 20; /* 卡片可视高上限(不压状态栏、留底边) */
int scrollable = (raw_d_h > max_card_h);
int d_h = scrollable ? max_card_h : raw_d_h;
int d_w = 348, d_x = (W - d_w) / 2, d_y = (H - d_h) / 2;
if (d_y < SB_BAR_H) d_y = SB_BAR_H; /* 下拉不压状态栏(栏高 40→28,随常量) */
if (H - d_y - d_h < 20) d_y = H - d_h - 20;
/* 内容可视区(卡片体内、标题下、底部内边距上);dd_scroll 仅在 scrollable 时非 0。 */
int content_top = d_y + title_h;
int content_bot = d_y + d_h - pad;
int dd_view_h = content_bot - content_top;
int dd_scroll_max = scrollable ? (n * opt_row - dd_view_h) : 0;
if (dd_scroll_max < 0) dd_scroll_max = 0;
int dd_scroll = 0;
int swipe_inv = 0;
if (scrollable) meow_settings_get_switch("input.swipe_invert", &swipe_inv);
struct keypoll kp_power, kp_keys;
kp_open(&kp_power, DEV_POWER); kp_open(&kp_keys, DEV_KEYS);
struct kd_state kd_vold, kd_volu, kd_pwr;
memset(&kd_vold, 0, sizeof kd_vold); memset(&kd_volu, 0, sizeof kd_volu); memset(&kd_pwr, 0, sizeof kd_pwr);
int m_prev_vold = 0, m_prev_volu = 0, m_prev_pwr = 0;
for (int w = 0; w < KEYBUF_WORDS; w++) {
if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEDOWN % 32))) m_prev_vold = 1;
if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEUP % 32))) m_prev_volu = 1;
if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) m_prev_pwr = 1;
}
int tfd = open(DEV_TOUCH, O_RDONLY | O_NONBLOCK);
if (tfd >= 0) {
/* M2.37:彻底清空「打开下拉」这次触摸的残留边沿。goodix 的抬起序列(ABS 坐标/
* BTN_TOUCH=0/MT_TRACKING=-1)在开 fd 后仍可能陆续送达,若只清空一次,第一轮
* 下拉循环会读到这些残边 → 误判抬起/选项错位/点卡片外穿透。故 排空→短延时→再排空。 */
unsigned char ev[16];
while (read(tfd, ev, sizeof ev) == 16) {}
usleep(40000); /* M2.38 Bug3:排空→40ms→再排空,吸收 goodix 抬起序列长尾 */
while (read(tfd, ev, sizeof ev) == 16) {}
}
int pwr_pending = 0;
int result = -1;
int dlx = -1, dly = -1, dtd = 0; /* 下拉触摸解析(dlx/dly=最新有效坐标;dtd=已按下) */
/* M2.5x 滚动状态(仅 scrollable 生效;非滚动路径与原文一致) */
int dd_oy = -1; /* 按下原点纵坐标(首个有效 ABS 后锁) */
int dd_scrolling = 0; /* 本次手势是否已判为纵向滚动 */
int dd_scroll_base = 0; /* 进入滚动时锁定的 dd_scroll 基准(连续跟手) */
int sel_moved = 0; /* 音量键改变了 sel(滚动需跟随选中行) */
/* M2.40:不再锁按下时刻的 ddx/ddy —— goodix 时序 BTN_TOUCH 先于 ABS,按下边沿时坐标可能
* 还是 -1/上一手势残留,锁原点会把点击判成「点卡片外=取消」。命中改在抬手时用抬手时刻的
* dlx/dly(抬起时必已更新到本次最新有效坐标),与 meow_touch mt_up 语义同源。 */
/* 初始绘制:全屏压暗覆盖层 + 居中圆角卡片 + 可见行(按 dd_scroll 裁剪) */
dd_modal_paint(th, W, H, d_x, d_y, d_w, d_h, title_h, opt_row, title, item, n,
sel, cur, content_top, content_bot, dd_scroll);
meow_dirty_full(); meow_ui_flush();
for (;;) {
if (sigterm_flag) { result = -1; break; }
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
int vold = 0, volu = 0, pwr = 0;
for (int w = 0; w < KEYBUF_WORDS; w++) {
if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEDOWN % 32))) vold = 1;
if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEUP % 32))) volu = 1;
if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) pwr = 1;
}
(void)kd_tick(&kd_vold, vold); (void)kd_tick(&kd_volu, volu); (void)kd_tick(&kd_pwr, pwr);
vold = kd_vold.stable; volu = kd_volu.stable; pwr = kd_pwr.stable;
int need = 0;
if (vold && !m_prev_vold && sel < n - 1) { sel++; need = 1; sel_moved = 1; }
if (volu && !m_prev_volu && sel > 0) { sel--; need = 1; sel_moved = 1; }
if (pwr && !m_prev_pwr) pwr_pending = 1; /* 按下:待抬起确认 */
if (!pwr && m_prev_pwr && pwr_pending) { pwr_pending = 0; result = sel; break; } /* 抬起=确认 */
m_prev_vold = vold; m_prev_volu = volu; m_prev_pwr = pwr;
/* 触摸:抬起命中选项=确认,点卡片外=取消 */
if (tfd >= 0) {
struct pollfd pfd = { tfd, POLLIN, 0 };
if (poll(&pfd, 1, 0) > 0) {
unsigned char ev[16];
while (read(tfd, ev, sizeof ev) == 16) {
uint16_t type = *(uint16_t *)(ev + 8), code = *(uint16_t *)(ev + 10);
int32_t val = *(int32_t *)(ev + 12);
if (type == EV_ABS) {
if (code == ABS_X || code == ABS_MT_POSITION_X) dlx = val;
else if (code == ABS_Y || code == ABS_MT_POSITION_Y) {
dly = val;
if (dtd) {
if (dd_oy < 0) dd_oy = val; /* 首个有效纵坐标 = 按下原点 */
if (scrollable) {
int ddy = val - dd_oy; /* 手指纵向位移(正=下移) */
if (!dd_scrolling && (ddy > S_SCROLL_SLOP || ddy < -S_SCROLL_SLOP)) {
dd_scrolling = 1;
dd_scroll_base = dd_scroll; /* 触发瞬间锁基准,不跳变 */
}
if (dd_scrolling) {
/* 标准方向:手指上移(ddy<0)→内容上移/看下方→scroll 增;swipe_invert 反向 */
int t = dd_scroll_base + (swipe_inv ? ddy : -ddy);
if (t < 0) t = 0;
if (t > dd_scroll_max) t = dd_scroll_max;
if (t != dd_scroll) { dd_scroll = t; need = 1; }
}
}
}
}
} else if (type == EV_KEY && code == BTN_TOUCH) {
if (val) {
/* 按下边沿:只标「已按下」,绝不锁原点(goodix 时序 BTN_TOUCH 先于
* ABS,此刻 dlx/dly 可能还是 -1 或上一手势残留)。 */
dtd = 1; dd_oy = -1; dd_scrolling = 0;
} else if (dtd) {
dtd = 0;
/* 抬手判定:此刻 dlx/dly 必为本次最新有效坐标(抬起时 ABS 已先到)。
* 卡片内命中 → 返回选项下标;卡片外 → 取消(-1);从未见有效坐标 → 丢弃。
* M2.5x:滚动手势抬手不选(不 launch);命中行换算叠加 dd_scroll。 */
if (dd_scrolling) {
dd_scrolling = 0;
} else if (dlx >= 0 && dly >= 0) {
if (dlx >= d_x && dlx < d_x + d_w && dly >= d_y && dly < d_y + d_h) {
int i = (dly - d_y - title_h + dd_scroll) / opt_row;
if (i >= 0 && i < n) { sel = i; result = sel; goto ddone; }
/* 卡片内但落在标题/内边距:保持弹出态,等待下一次点选 */
} else {
result = -1; goto ddone; /* 点卡片外=取消 */
}
}
/* dlx/dly 为 -1:ghost/残留抬手,丢弃不动作 */
}
}
}
}
}
if (need) {
/* 重绘覆盖层+选项(仅 sel 高亮/滚动变化);sel 经音量键移动时滚动跟随选中行。 */
if (sel_moved && scrollable) {
int ry0 = content_top + sel * opt_row - dd_scroll;
if (ry0 < content_top) dd_scroll = sel * opt_row;
else if (ry0 + opt_row > content_bot) dd_scroll = sel * opt_row - (dd_view_h - opt_row);
if (dd_scroll < 0) dd_scroll = 0;
if (dd_scroll > dd_scroll_max) dd_scroll = dd_scroll_max;
}
sel_moved = 0;
dd_modal_paint(th, W, H, d_x, d_y, d_w, d_h, title_h, opt_row, title, item, n,
sel, cur, content_top, content_bot, dd_scroll);
meow_dirty_full(); meow_ui_flush();
}
usleep(POLL_MS * 1000);
}
ddone:
if (tfd >= 0) close(tfd);
kp_close(&kp_power); kp_close(&kp_keys);
return result;
}
/* 三级 (family,weight) 下多 tier 的选档偏好分(越大越优先;设置页写 font.file/class 值用)。
* 关(font.load_full=0)→ common 优先(回退 gbk→full);开 → gbk 优先(回退 full→common)。 */
static int font_tier_pick_rank(const char *tier, int load_full)
{
int is_common = tier && !strcmp(tier, "common");
int is_gbk = tier && !strcmp(tier, "gbk");
int is_full = tier && !strcmp(tier, "full");
if (!load_full) {
if (is_common) return 3;
if (is_gbk) return 2;
if (is_full) return 1;
return 0;
}
if (is_gbk) return 3;
if (is_full) return 2;
if (is_common) return 1;
return 0;
}
/* ---- 字体行「family→字重」两级子菜单(M2.6 字重结构;M2.8 三级适配)----
* 一级:列出扫描到的去重 family(class 行首项「跟随全局」=空值);选 family 后自动进入
* 二级:该 family 的可用字重(regular/bold/italic…)。选中写 out_val = 卡根相对路径
* MeowOS/fonts/<family>/<weight>/font.png(两级)或 .../<family>/<weight>/<tier>/font.png(三级,
* 或 class 行「跟随全局」=空串)。
* M2.8 三级冲突:同 (family,weight) 出现多个 tier → 选择器仍为 family→weight 两级、不新增 tier
* 级菜单;每 weight 只显一行(代表 = 按「当前加载大字库」开关选档:关=common 优先、开=gbk 优先,
* 见 font_tier_pick_rank),写值 = 该代表 tier 的卡根相对路径。
* 复用 s_dd_modal(与 WiFi/关于 同款模态)。返回 1=已选中(out_val 已填),0=取消。 */
static int s_font_dropdown(const srow_t *r, char *out_val, int vlen)
{
if (!r) return 0;
int is_cls = r->key && !strncmp(r->key, "font.class_", 11);
char cur[MEOW_SET_MAX_LEN];
meow_settings_get_str(r->key, cur, sizeof cur);
char curfam[64] = "", curwt[32] = "";
if (cur[0])
font_family_weight_from_png(cur, curfam, sizeof curfam, curwt, sizeof curwt);
/* ---- 一级:去重 family 列表(class 行首项「跟随全局」)---- */
char fam_slug[S_NET_MAX][64], fam_disp[S_NET_MAX][64];
int famn = 0;
if (is_cls) {
if (famn < S_NET_MAX) { snprintf(fam_slug[famn], sizeof fam_slug[0], "");
snprintf(fam_disp[famn], sizeof fam_disp[0], "跟随全局字体"); famn++; }
}
for (int i = 0; i < font_presets_n && famn < S_NET_MAX; i++) {
const char *f = font_preset_fams[i] ? font_preset_fams[i] : "";
int dup = 0;
for (int k = 0; k < famn; k++) if (!strcmp(fam_slug[k], f)) { dup = 1; break; }
if (dup) continue;
snprintf(fam_slug[famn], sizeof fam_slug[0], "%s", f);
snprintf(fam_disp[famn], sizeof fam_disp[0], "%s",
(font_preset_names[i] && font_preset_names[i][0]) ? font_preset_names[i] : f);
famn++;
}
if (famn <= 0) return 0;
const char *fitem[S_NET_MAX];
char ftext[S_NET_MAX][64];
for (int i = 0; i < famn; i++) { snprintf(ftext[i], sizeof ftext[i], "%s", fam_disp[i]); fitem[i] = ftext[i]; }
int cur_fam = is_cls ? 0 : -1;
for (int i = is_cls ? 1 : 0; i < famn; i++)
if (curfam[0] && !strcmp(fam_slug[i], curfam)) cur_fam = i;
int pick = s_dd_modal(r->label, fitem, famn, cur_fam);
if (pick < 0) return 0;
if (is_cls && pick == 0) { snprintf(out_val, vlen, ""); return 1; } /* 跟随全局 */
const char *fam = fam_slug[pick];
/* ---- 二级:该 family 的字重列表(去重:同 (family,weight) 只显一行)----
* 三级结构下同 (family,weight) 出现多个 tier(common/gbk/full)→ 不新增 tier 级菜单,按
* 「当前加载大字库」开关定档:关=common 优先、开=gbk 优先(font_tier_pick_rank)。 */
int load_full = 0;
meow_settings_get_switch("font.load_full", &load_full);
char wt_pick[S_NET_MAX][32]; /* 该行代表的 weight slug */
int wt_rank[S_NET_MAX]; /* 该行当前档位偏好分(保留最高分 tier 为行代表) */
char wt_val[S_NET_MAX][MEOW_SET_MAX_LEN];
char wt_text[S_NET_MAX][96];
int wn = 0;
for (int i = 0; i < font_presets_n; i++) {
if (!font_preset_fams[i] || strcmp(font_preset_fams[i], fam)) continue;
const char *wt = font_preset_wts[i] ? font_preset_wts[i] : "regular";
const char *tier = font_preset_tiers[i] ? font_preset_tiers[i] : "common";
int rank = font_tier_pick_rank(tier, load_full);
int slot = -1;
for (int k = 0; k < wn; k++) if (!strcmp(wt_pick[k], wt)) { slot = k; break; }
if (slot < 0) {
if (wn >= S_NET_MAX) break;
snprintf(wt_pick[wn], sizeof wt_pick[0], "%s", wt);
wt_rank[wn] = -1;
slot = wn; wn++;
}
if (rank > wt_rank[slot]) { /* 保留档位偏好最高的一套作为该 weight 行代表 */
wt_rank[slot] = rank;
snprintf(wt_val[slot], sizeof wt_val[0], "%s", font_presets[i]);
snprintf(wt_text[slot], sizeof wt_text[0], "%s", font_preset_labels[i]);
}
}
if (wn <= 0) return 0; /* 该 family 无字重(理论不出现:能进 family 必已探到字重) */
const char *witem[S_NET_MAX];
for (int i = 0; i < wn; i++) witem[i] = wt_text[i];
/* 当前位置高亮:按当前 font.file 的 (family,weight) 命中所显示 weight 行(不依赖 tier 精确匹配,
* 因三级下 font.file 具体 tier 可能与该行代表 tier 不同)。 */
int cur_wt = -1;
{
char cfam2[64] = "", cwt2[32] = "";
if (cur[0]) font_family_weight_from_png(cur, cfam2, sizeof cfam2, cwt2, sizeof cwt2);
for (int i = 0; i < wn; i++)
if (cwt2[0] && !strcmp(wt_pick[i], cwt2)) { cur_wt = i; break; }
}
int wp = s_dd_modal(r->label, witem, wn, cur_wt);
if (wp < 0) return 0;
snprintf(out_val, vlen, "%s", wt_val[wp]);
return 1;
}
/* ==================================================================
* M7.2 设置页渲染层迁移(主列表迁 SDK 控件):
* - 分组列表 → meow_listview(虚拟化 set_vsource 固定 item 池 + section 段头 + set_item_draw 自绘);
* - 开关行 → 行内真 meow_switch(池化,SDK 绘制 + 按压态);
* - 滑条行 → 行内嵌 meow_slider(自绘 track/thumb,保留既有 s_draw_slider 语义色;
* 「行内嵌自绘」,M7 定案允许)——拖动实时写 settings.conf(读改写单键,
* 经 meow_settings_set_*,勿整文件覆盖——音量存盘教训);
* - 枚举/预设 → SDK meow_select_show(BottomSheet+单选列表,替换自绘 s_dd_modal);
* - 信息/列表行 → 行 right 值 + chevron(SDK 主题语义色)。
* 滚动 → meow_scroll(跟手 track + 松手 fling,同 subpage.c M7.1a 语义;刻意不 set_snap)。
* 控件 lazy 创建一次(owner=SET_SENTINEL_ACT 哨兵),跨多次进入复用;数据/动作(s_commit、
* s_set_int_value、s_net_*、s_font_dropdown、s_dropdown)逻辑层不动,仅换渲染层 + 命中;
* run_settings_page 会话骨架(触摸/滚动/返回/IPC/电源键语义)不改。旧手绘 draw_settings_page
* + s_clamp_scroll + s_row_at_content_y 保留 #if 0 作回退位(M7 红线:一键回退)。
* ================================================================== */
#define SET_SENTINEL_ACT 0x53 /* 设置页哨兵 activity(不与 launcher 其它页冲突) */
#define SET_LIST_Y SET_CONTENT_Y /* 100:列表顶缘(顶栏/标题下) */
#define SET_FOOT_H 40 /* 底部返回/版本带高 */
#define SET_ITEM_H 68 /* MD3 行高(滑条行给 track 留位) */
#define SET_SLIDER_YOFF 45 /* 滑条行内 track 顶缘相对行顶(÷s_draw_slider h=6,中心=顶+3) */
#define SET_TRACK_CY 48 /* 滑条 track 中心相对行顶 y(=SET_SLIDER_YOFF+3,与绘制对齐) */
#define SET_SLIDER_LY 12 /* 滑条行 label 顶相对行顶 y(上移给 track 留位) */
#define SET_SWITCH_POOL 12
static meow_widget *g_set_lv = NULL; /* meow_listview(虚拟化) */
static meow_scroll *g_set_sc = NULL; /* 滚动物理 */
static meow_widget *g_set_sw[SET_SWITCH_POOL]; /* 行内开关池(真 meow_switch) */
static int g_set_sw_n = 0, g_set_sw_use = 0; /* 池创建数 / 本轮已用(每次整绘复位) */
static int g_set_lv_h = 0; /* 列表视口高(选中可见性 clamp 用) */
static int g_set_init = 0;
static void set_lv_rect(int *x, int *y, int *w, int *h)
{
if (x) *x = 0;
if (y) *y = SET_LIST_Y;
if (w) *w = fb_xres;
if (h) { int t = fb_yres - SET_LIST_Y - SET_FOOT_H; if (t < 0) t = 0; *h = t; }
}
static int set_lv_count(void *ctx) { (void)ctx; return s_vis_count(); }
/* 行填充(虚拟化 pool bind;段头行 section=1、enabled=1 供 SDK 命中/段头绘制) */
static void set_lv_bind(int idx, meow_lv_item *out, void *ctx)
{
(void)ctx;
memset(out, 0, sizeof *out);
out->icon_kind = MEOW_LIST_ICON_NONE;
out->enabled = 1;
if (idx < 0 || idx >= s_vis_count()) { out->title = ""; return; }
const srow_t *r = &SMENU[s_vis_at(idx)];
out->title = r->label ? r->label : "";
out->section = (r->ctrl == SC_HEADER) ? 1 : 0;
if (r->ctrl == SC_SWITCH) {
int on = 0; meow_settings_get_switch(r->key, &on);
out->right = MEOW_LIST_RIGHT_SWITCH; out->on = on;
}
}
/* 行内开关池:取下一个可用槽(每帧 set_lv_draw 顺序取用,整绘前复位 g_set_sw_use) */
static meow_widget *set_sw_take(void)
{
if (g_set_sw_use < g_set_sw_n) return g_set_sw[g_set_sw_use++];
if (g_set_sw_n >= SET_SWITCH_POOL) return NULL;
meow_widget *w = meow_switch_create(0, 0, 0);
if (w) { g_set_sw[g_set_sw_n++] = w; return g_set_sw[g_set_sw_use++]; }
return NULL;
}
/* 整行自绘(set_item_draw 覆盖默认 renderer;x=列表左缘、y=行顶、w/h=行全宽高,
* state=SELECTED/PRESSED/DISABLED 组合;颜色全取 meow_theme 语义色,无散落硬编码) */
static void set_lv_draw(int idx, int x, int y, int w, int h, int state, void *ctx)
{
(void)ctx; (void)h;
const meow_theme *mt = meow_theme_get();
if (idx < 0) return;
int si = s_vis_at(idx);
const srow_t *r = &SMENU[si];
int sel = (state & MEOW_LISTVIEW_ST_SELECTED);
int press = (state & MEOW_LISTVIEW_ST_PRESSED);
int dis = (state & MEOW_LISTVIEW_ST_DISABLED);
int thh = meow_ui_text_height(mt->font_body);
uint32_t textcol = sel ? mt->onPrimaryContainer : mt->onSurface;
uint32_t subcol = sel ? mt->onPrimaryContainer : mt->onSurfaceVariant;
if (r->ctrl == SC_HEADER) {
meow_ui_fill_rect(x, y, w, SET_ITEM_H, mt->surfaceContainerHigh);
int ty = y + (SET_ITEM_H - thh) / 2;
if (ty >= 0) meow_ui_text(x + 16, ty, r->label, mt->font_body, mt->primary);
meow_ui_fill_rect(x, y + SET_ITEM_H - 1, w, 1, mt->outlineVariant);
return;
}
if (sel) meow_ui_fill_rect(x, y, w, SET_ITEM_H, mt->primaryContainer);
else if (press) meow_ui_fill_rect_alpha(x, y, w, SET_ITEM_H, mt->primary, 26);
int ly = y + (SET_ITEM_H - thh) / 2; /* label 单行垂直居中 */
switch (r->ctrl) {
case SC_SWITCH: {
meow_ui_text(x + 16, ly, r->label, mt->font_body, textcol);
meow_widget *sw = set_sw_take();
if (sw) {
int on = 0; meow_settings_get_switch(r->key, &on);
meow_widget_set_rect(sw, x + w - 16 - 56, y + (SET_ITEM_H - 32) / 2, 56, 32);
meow_switch_set(sw, on);
meow_widget_draw(sw);
}
break;
}
case SC_SLIDER: {
/* label 上移一行,track 放行下方(中心=SET_SLIDER_YOFF+3=48),与拖动命中带对齐 */
meow_ui_text(x + 16, y + SET_SLIDER_LY, r->label, mt->font_body, textcol);
char vb[32]; s_fmt_value(r, vb, sizeof vb);
meow_ui_text(x + w - 16 - meow_ui_text_width(vb, mt->font_body), y + SET_SLIDER_LY, vb, mt->font_body, subcol);
int v = 0; meow_settings_get_int(r->key, &v);
/* 息屏时间为档位索引型(秒→索引 等距画);其余 clamp 到 [min,max] */
if (!strcmp(r->key, "display.screen_off_timeout")) v = s_time_tier_idx(v);
else { if (v < r->min) v = r->min; if (v > r->max) v = r->max; }
s_draw_slider(S_SLIDER_X, y + SET_SLIDER_YOFF, S_SLIDER_W, v, r->min, r->max, s_ui_theme());
break;
}
case SC_INFO: {
meow_ui_text(x + 16, ly, r->label, mt->font_body, textcol);
char vb[48]; s_net_get(r, vb, sizeof vb);
meow_ui_text(x + w - 16 - meow_ui_text_width(vb, mt->font_body), ly, vb, mt->font_body, subcol);
break;
}
case SC_LIST: {
meow_ui_text(x + 16, ly, r->label, mt->font_body, textcol);
char vb[48]; s_net_get(r, vb, sizeof vb);
int vx = x + w - 16 - 12 - meow_ui_text_width(vb, mt->font_body);
meow_ui_text(vx, ly, vb, mt->font_body, subcol);
s_draw_chevron(x + w - 16, y + SET_ITEM_H / 2, subcol);
break;
}
default: { /* SC_ENUM / SC_PRESET */
meow_ui_text(x + 16, ly, r->label, mt->font_body, textcol);
char vb[40]; s_display_value(r, vb, sizeof vb);
int vx = x + w - 16 - meow_ui_text_width(vb, mt->font_body);
meow_ui_text(vx, ly, vb, mt->font_body, subcol);
s_draw_chevron(vx - 8, y + SET_ITEM_H / 2, subcol);
break;
}
}
meow_ui_fill_rect(x, y + SET_ITEM_H - 1, w, 1, mt->outlineVariant);
if (dis) meow_ui_fill_rect_alpha(x, y, w, SET_ITEM_H, mt->onSurface, 90);
}
/* ---- meow_scroll 回调(包 listview)---- */
static void set_sc_set(void *ctx, int off) { (void)ctx; if (g_set_lv) meow_listview_set_scroll(g_set_lv, off); }
static int set_sc_max(void *ctx) { (void)ctx; return g_set_lv ? meow_listview_max_scroll(g_set_lv) : 0; }
static int set_sc_get(void *ctx) { (void)ctx; return g_set_lv ? meow_listview_scroll(g_set_lv) : 0; }
/* 列表区重绘(含内容底 + listview),供 fling/滚动局部刷。 */
static void set_redraw_region(void *ctx)
{
(void)ctx;
int lvx, lvy, lvw, lvh; set_lv_rect(&lvx, &lvy, &lvw, &lvh);
if (lvh <= 0) return;
const meow_ui_theme_t *th = s_ui_theme();
meow_ui_fill_rect(lvx, lvy, lvw, lvh, th->surface_variant);
if (g_set_lv) { g_set_sw_use = 0; meow_widget_draw(g_set_lv); }
meow_dirty_add(lvx, lvy, lvw, lvh);
meow_ui_flush();
}
/* 选中行可见性:滚动到选中行不越界(仅靠 tap 触发,选中行即在视野内,防御性兜底)。 */
static void set_ensure_sel_visible(int sel)
{
if (!g_set_lv) return;
int p = s_vis_of(sel); if (p < 0) return;
int scroll = meow_listview_scroll(g_set_lv);
int top = p * SET_ITEM_H, bot = top + SET_ITEM_H;
if (top < scroll) meow_listview_set_scroll(g_set_lv, top);
else if (bot > scroll + g_set_lv_h) meow_listview_set_scroll(g_set_lv, bot - g_set_lv_h);
}
/* 创建设置页控件集(owner=SET_SENTINEL_ACT;仅首进创建一次,跨次进入复用)。 */
static void set_widgets_create(void)
{
meow_ui_activity_push(SET_SENTINEL_ACT);
int lvx, lvy, lvw, lvh; set_lv_rect(&lvx, &lvy, &lvw, &lvh);
g_set_lv_h = lvh;
g_set_lv = meow_listview_create(lvx, lvy, lvw, lvh, 0, NULL, NULL, MEOW_LIST_SEL_SINGLE);
if (g_set_lv) {
meow_listview_set_vsource(g_set_lv, set_lv_count, set_lv_bind, NULL);
meow_listview_set_item_draw(g_set_lv, set_lv_draw, NULL);
meow_listview_set_item_h(g_set_lv, SET_ITEM_H);
/* 注:刻意不 set_snap —— SDK meow_scroll 的 snap 以「fling 起点」为吸附目标会往回拉 */
}
g_set_sc = meow_scroll_create(set_sc_set, set_sc_max, set_sc_get, set_redraw_region, NULL);
meow_ui_activity_pop();
g_set_init = 1;
}
/* 整页绘制(进 back_buf;调用方 meow_dirty_full/meow_dirty_add + flush)。sel=当前选中(SMENU 下标)。 */
static void set_paint_page(int sel)
{
const meow_ui_theme_t *th = s_ui_theme();
int W = fb_xres, H = fb_yres;
fill_rect(0, 0, W, H, th->surface);
draw_statusbar();
if (mfont_ready) ui_draw(24, 50, "系统设置", th->on_surface, 28);
else draw_text(24, 30, "SETTINGS", 3, th->on_surface);
fill_rect(0, 50 + (mfont_ready ? meow_font_line_height(28) : 24) + 2, W, 2, th->outline);
fill_rect(0, SET_CARD_Y, W, H - SET_CARD_Y, th->surface_variant);
if (g_set_lv) {
g_set_sw_use = 0;
int p = s_vis_of(sel); if (p >= 0) meow_listview_set_sel(g_set_lv, p);
meow_widget_draw(g_set_lv);
}
/* 底部:返回(左)+ 版本(右),复用应用列表底部对齐逻辑 */
char ver[32];
snprintf(ver, sizeof ver, "MeowOS v%s dev", g_meowos_version);
int foot_y = fb_yres - meow_font_line_height(20) - 8;
s_draw_chevron_left(SET_CARD_X + 12, foot_y + meow_font_line_height(20) / 2, th->on_surface_variant);
ui_draw(SET_CARD_X + 24, foot_y, "返回", th->on_surface_variant, 20);
ui_draw(fb_xres - 8 - ui_width(ver, 20), foot_y, ver, th->outline, 20);
}
/* ---- 运行设置页(独立子循环;run_menu 触摸命中设置行后调用)---- */
void run_settings_page(void)
{
int sel = s_first_setting();
int H = fb_yres;
unsigned long long t_sets = now_ms(); /* M3 取证:进设置页耗时分段(真机核对用) */
/* M2.6:进设置页扫描一次 TF 卡 MeowOS/fonts/<family>/<weight>/,刷新「字体文件」下拉。
* 进设置时卡通常在位;此步只枚举/校验,不加载(加载由 meow_font_ensure 按 font.file 决定)。
* M3:verify=0 —— 跳过逐套 sha256 校验(gbk 套 6.8MB 级,逐套双 sha256 真机耗数秒),
* 只枚举+读 font.json+落 index,入口不再被 sha256sum 拖住;完整性校验留给启动全量扫描。 */
meow_font_scan_presets(0);
logmsg("settings entry: font rescan %llums", now_ms() - t_sets);
struct keypoll kp_power, kp_keys;
kp_open(&kp_power, DEV_POWER); kp_open(&kp_keys, DEV_KEYS);
struct kd_state kd_vold, kd_volu, kd_play, kd_pwr;
memset(&kd_vold, 0, sizeof kd_vold); memset(&kd_volu, 0, sizeof kd_volu);
memset(&kd_play, 0, sizeof kd_play); memset(&kd_pwr, 0, sizeof kd_pwr);
int m_prev_vold = 0, m_prev_volu = 0, m_prev_play = 0, m_prev_pwr = 0;
for (int w = 0; w < KEYBUF_WORDS; w++) {
if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEDOWN % 32))) m_prev_vold = 1;
if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEUP % 32))) m_prev_volu = 1;
if (kp_keys.cur[w] & (1UL << (KEY_PLAYPAUSE % 32))) m_prev_play = 1;
if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) m_prev_pwr = 1;
}
int tfd = open(DEV_TOUCH, O_RDONLY | O_NONBLOCK);
if (tfd >= 0) { unsigned char ev[16]; while (read(tfd, ev, sizeof ev) == 16) {} }
unsigned long long touch_cooldown_until = now_ms() + 300;
unsigned long long last_tick = now_ms();
unsigned long long last_act = now_ms(); /* 最近一次交互时间(自动息屏用) */
int after_modal = 0; /* 选择器子对话框刚关闭:其全屏 dim 覆盖过状态栏,需整帧重绘还原 */
unsigned long long net_refresh_pump_at = 0; /* M3:异步网络刷新下一推进时刻(0=立即) */
/* 触摸拖动滑条状态 */
int td = 0, td_ok = 0, tdx = -1, tdy = -1, tlx = -1, tly = -1;
int drag = -1; /* 被拖动的行索引;-1=无 */
/* M2.37 列表拖动滚动状态 */
int scroll_drag = 0; /* 是否处于列表拖动滚动手势 */
int drag_last_y = 0; /* M7.2:滚动跟手增量基准(meow_scroll_track 速度采样) */
int scroll_y0 = 0; /* 按下原点纵向坐标 */
int scroll_invert = 0; /* M2.38 Bug4:滑动方向切换(input.swipe_invert;0=标准方向) */
int sel_changed = 0; /* 本轮是否改变了选中行(仅此场景才强制选中行可见) */
int wake_suppress = 0; /* 自动息屏唤醒后:吸收唤醒键边沿,待全部抬起再接管 */
/* M2.x(KEY_CONTROL_DESIGN §2)电源键状态:设置页电源键 = 系统级语义(非返回),
* 对照主界面 run_menu 3130-3137 的同名变量。单击=息屏/亮屏、长按=电源菜单、8s=强关。 */
unsigned long long pwr_hold_at = 0; /* 本次电源按下计时起点 */
int pwr_menu_state = 0; /* 是否已因此次 hold 弹过电源菜单 */
int pwr_woke = 0; /* 本次按下=自动息屏唤醒(释放只唤醒不 toggle) */
int pwr_suppress_click = 0; /* 菜单刚关闭:松开不得误判单击熄屏(M2.34 同款) */
/* 首次全帧绘制 */
/* M2.39/M3:开关行同步真实硬件态(快)+ 异步 kick 网络刷新 —— 不再在入口同步跑
* wpa_cli/bluetoothctl 串行 popen(真机 ≥5s 卡顿根因);状态/列表由下方主循环逐步骤
* s_net_refresh_pump() 填充,draw 用缓存(g_net_*),数据变化即 need_paint。 */
s_net_sync_from_sys();
logmsg("settings entry: net toggle-sync+kick %llums", now_ms() - t_sets);
if (!g_set_init) set_widgets_create(); /* M7.2:lazy 建 SDK 控件集(一次,跨次进入复用) */
meow_ui_activity_push(SET_SENTINEL_ACT); /* 控件 owner 闸门打开(hit/按压态生效) */
set_paint_page(sel);
meow_dirty_full(); meow_ui_flush();
logmsg("settings entry: page shown in %llums", now_ms() - t_sets);
for (;;) {
unsigned long long now = now_ms();
if (sigterm_flag) break;
/* 重绘触发:值/选中/滚动变化 */
int need_paint = 0;
/* ---- M2.37 自动息屏态:留在设置页,等待任意输入唤醒,恢复后重绘(不丢上下文)----
* 对照 run_menu 主循环 meow_auto_slept 的唤醒重绘(M2.17/2.18)。休眠期间仅探测
* 输入(按键原始按下 / black 模式触摸按下);一旦唤醒:亮屏、清自动态并整帧重绘。 */
if (meow_auto_slept && !meow_screen_on) {
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
int wake = 0;
for (int w = 0; w < KEYBUF_WORDS; w++) {
if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) wake = 1;
if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEDOWN % 32))) wake = 1;
if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEUP % 32))) wake = 1;
if (kp_keys.cur[w] & (1UL << (KEY_PLAYPAUSE % 32))) wake = 1;
}
if (!wake && screen_off_mode == 1 && tfd >= 0) { /* black 模式:面板仍上电,触摸可唤醒 */
struct pollfd spfd = { tfd, POLLIN, 0 };
if (poll(&spfd, 1, 0) > 0) {
unsigned char ev[16];
while (read(tfd, ev, sizeof ev) == 16) {
uint16_t type = *(uint16_t *)(ev + 8), code = *(uint16_t *)(ev + 10);
int32_t val = *(int32_t *)(ev + 12);
if (type == EV_KEY && code == BTN_TOUCH && val) wake = 1;
}
}
}
if (wake) {
meow_panel_on();
meow_auto_slept = 0;
wake_suppress = 1;
last_act = now_ms();
/* 重置按键防抖与 m_prev_*(含唤醒键原始态对齐),并丢唤醒的这次触摸 */
memset(&kd_pwr, 0, sizeof kd_pwr); memset(&kd_vold, 0, sizeof kd_vold);
memset(&kd_volu, 0, sizeof kd_volu); memset(&kd_play, 0, sizeof kd_play);
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
m_prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN);
m_prev_volu = kp_is_down(&kp_keys, KEY_VOLUMEUP);
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
touch_cooldown_until = now_ms() + 300;
td = 0; td_ok = 0; drag = -1; scroll_drag = 0; tlx = tly = -1;
logmsg("settings: auto screen-off -> input wake, stayed in settings page");
set_paint_page(sel);
meow_dirty_full(); meow_ui_flush();
}
usleep(POLL_MS * 1000);
continue;
}
/* ---- M3:异步网络刷新(每 ~150ms 推进一步,每步一个有界 popen)----
* 入口已同步 draw(用 g_net_* 缓存),此处逐步填充 SSID/已保存/附近/蓝牙角色,
* 数据变化则 need_paint=1 让行值即时刷新,绝不在某一同步调用里连续跑数条 popen。 */
if (now >= net_refresh_pump_at) {
net_refresh_pump_at = now + 150;
if (s_net_refresh_pump()) need_paint = 1;
}
/* ---- 按键 ---- */
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
int vold = 0, volu = 0, play = 0, pwr = 0;
for (int w = 0; w < KEYBUF_WORDS; w++) {
if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEDOWN % 32))) vold = 1;
if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEUP % 32))) volu = 1;
if (kp_keys.cur[w] & (1UL << (KEY_PLAYPAUSE % 32))) play = 1;
if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) pwr = 1;
}
(void)kd_tick(&kd_vold, vold); (void)kd_tick(&kd_volu, volu);
(void)kd_tick(&kd_play, play); (void)kd_tick(&kd_pwr, pwr);
vold = kd_vold.stable; volu = kd_volu.stable;
play = kd_play.stable; pwr = kd_pwr.stable;
if (wake_suppress) {
/* 刚被唤醒:吸收唤醒键的边沿,等所有按键完全抬起后再接管动作,
* 避免「电源唤醒→抬起→被当成电源单击返回」或音量唤醒→误导航。 */
if (!pwr && !vold && !volu && !play) wake_suppress = 0;
} else {
/* M2.39:设置页音量键 = 调系统音量(写 DAC),不做光标移动(用户约束:
* 设置页仅触摸导航;音量键在电菜单/应用列表的系统级导航保持不动)。
* 按下边沿即起线性渐变(vol_ramp_begin),下方 vol_ramp_tick 逐 1% 爬 + 刷状态栏。 */
if (vold && !m_prev_vold) { settings_volume_adj(-settings_volume_step); need_paint = 1; }
if (volu && !m_prev_volu) { settings_volume_adj(+settings_volume_step); need_paint = 1; }
/* M2.x(KEY_CONTROL_DESIGN §2/§3)播放键在设置页同样被系统「抓住」:
* 捕获+日志+消费,不再映射为「返回/确认」(原 play 边沿=break 已删,播放键不再退出设置页)。
* 仍视为输入(重置自动息屏计时,与主界面 play 的媒体预留/any_input 一致)。 */
if (play && !m_prev_play) {
logmsg("settings: play key caught (media-reserved, no exit)");
last_act = now;
}
/* M2.x(KEY_CONTROL_DESIGN §2)电源键 = 系统级语义,对齐主界面 run_menu 3284-3366:
* 单击(按<长按阈值后松开)= 息屏/亮屏;长按>=longpress_menu_ms = 电源菜单;
* 按住>=forceoff_ms(且未开过菜单)= 强制关机;自动息屏按下只唤醒。
* ※ 电源键在设置页不再映射为「返回」——退出只能靠触摸(底部返回按钮 + 左缘左滑)。 */
if (pwr && !m_prev_pwr) {
pwr_hold_at = now; pwr_menu_state = 0;
pwr_suppress_click = 0; /* M2.34:新一次电源按下清除旧的抑制标志 */
last_act = now; /* 视为输入,防按住期间误触发自动息屏 */
/* M2.18 Bug 2 同款:按下正落在自动息屏态(auto_slept 且屏已关)→ 本次只做唤醒 */
if (meow_auto_slept && !meow_screen_on) pwr_woke = 1;
}
if (pwr && pwr_hold_at) {
/* M2.10/M2.11 B/M2.37 同款:8s 强关只在该「按住」从未开过菜单(!pwr_menu_state)
* 时触发;开过菜单则交给菜单内确认/取消,杜绝「开菜单->取消->仍按住->强关」残留。 */
if (now - pwr_hold_at >= (unsigned long long)power_forceoff_ms && !pwr_menu_state) {
if (sim_no_exec) { logmsg("settings pm: [sim-no-exec] POWER HOLD %dms -> would force poweroff", power_forceoff_ms); }
else { logmsg("settings pm: POWER HOLD %dms -> force poweroff", power_forceoff_ms); meow_power_action(POWER_SHUTDOWN); }
/* 关机一般不复返;若 reboot 失败返回仍按按住处理(m_prev 由下方统一复写) */
} else if (now - pwr_hold_at >= (unsigned long long)power_longpress_menu_ms && !pwr_menu_state) {
pwr_menu_state = 1;
logmsg("settings pm: POWER HOLD %dms -> show power menu", power_longpress_menu_ms);
show_power_menu(); /* 动作在函数内 system();取消则续循环 */
last_act = now_ms(); /* 菜单返回即视为「刚有输入」,重置自动息屏计时 */
/* M2.11 B 同款:菜单已关闭。清空按住计时与菜单态,绝不把已互动的 hold 再累计到 8s 强关。 */
pwr_hold_at = 0; pwr_menu_state = 0; pwr_woke = 0;
pwr_suppress_click = 1; /* M2.34:菜单返回后抑制本次电源松开被误判单击息屏 */
/* M2.25 同款:阻塞式 show_power_menu() 期间 kd_* 冻结,m_prev_* 与稳定值可能错配,
* 重播 kd_* 并以当前 raw 对齐 m_prev_*,杜绝「菜单内触摸点取消->返回后被误判单击息屏」。 */
memset(&kd_pwr, 0, sizeof kd_pwr);
memset(&kd_vold, 0, sizeof kd_vold);
memset(&kd_volu, 0, sizeof kd_volu);
memset(&kd_play, 0, sizeof kd_play);
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
m_prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN);
m_prev_volu = kp_is_down(&kp_keys, KEY_VOLUMEUP);
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
after_modal = 1; /* 全屏 dim 覆盖过状态栏,需整帧重绘还原 */
need_paint = 1;
/* 已按 raw 对齐 m_prev_*,跳过下方基线复写,防把 raw 值覆盖回旧稳定值 → 假单边沿 */
goto settings_keys_seeded;
}
}
if (!pwr && m_prev_pwr) {
/* 松开:短按(<长按阈值且菜单未开)→ 单击 = 息屏/亮屏切换 */
if (pwr_suppress_click) {
logmsg("settings pm: power release after power-menu -> no toggle (suppress)");
} else if (pwr_hold_at && (now - pwr_hold_at < (unsigned long long)power_longpress_menu_ms) && !pwr_menu_state && !pwr_woke) {
logmsg("settings pm: power single-click -> toggle panel (was %s)", meow_screen_on ? "on" : "off");
meow_toggle_panel();
meow_auto_slept = 0; /* M2.17:手动/自动息屏后电源单击唤醒均清自动态 */
last_act = now_ms();
need_paint = 1; /* 亮屏重绘 / 息屏留屏(面板已黑,下次亮屏重绘) */
} else if (pwr_woke) {
logmsg("settings pm: power release after auto screen-off wake -> no toggle (wake only)");
}
pwr_hold_at = 0; pwr_menu_state = 0; pwr_woke = 0;
pwr_suppress_click = 0; /* M2.34:电源已松开,抑制标志一次性用完即清 */
}
}
/* M2.39:驱动音量线性渐变(长按续爬/松开即止)+ 刷新状态栏 VOL;「音量」行随 need_paint 更新 */
if (vol_ramp_dir != 0) {
int held = (vol_ramp_dir > 0) ? volu : vold;
int going = vol_ramp_tick(held);
g_sys.vol = settings_volume_get();
paint_sb_right();
need_paint = 1;
if (!going) logmsg("settings vol ramp end -> %d", g_sys.vol);
}
m_prev_vold = vold; m_prev_volu = volu; m_prev_play = play; m_prev_pwr = pwr;
settings_keys_seeded:
;
/* ---- 触摸 ---- */
if (tfd >= 0 && meow_screen_on) { /* M2.x:手动电源息屏后不处理触摸(与主界面一致,且防屏黑时误退出/误改值) */
struct pollfd pfd = { tfd, POLLIN, 0 };
if (poll(&pfd, 1, 0) > 0) {
unsigned char ev[16];
while (read(tfd, ev, sizeof ev) == 16) {
uint16_t type = *(uint16_t *)(ev + 8), code = *(uint16_t *)(ev + 10);
int32_t val = *(int32_t *)(ev + 12);
if (type == EV_ABS) {
if (code == ABS_X || code == ABS_MT_POSITION_X) tlx = val;
else if (code == ABS_Y || code == ABS_MT_POSITION_Y) tly = val;
if (td && !td_ok && tlx >= 0 && tly >= 0) {
tdx = tlx; tdy = tly; td_ok = 1;
meow_input_touch_set(1, tdx, tdy, 0); /* 喂 SDK 输入态(行按压态渲染) */
scroll_y0 = tdy; /* 列表拖动基准:按下原点 */
/* 按下原点落在滑条行 track 带内 → 开始拖动(meow_listview hit 判行) */
int vp = g_set_lv ? meow_listview_hit(g_set_lv, tlx, tly) : -1;
if (vp >= 0 && drag < 0) {
const srow_t *rr = &SMENU[s_vis_at(vp)];
if (rr->ctrl == SC_SLIDER) {
int track_cy = SET_LIST_Y + vp * SET_ITEM_H -
meow_listview_scroll(g_set_lv) + SET_TRACK_CY;
if (tlx >= S_SLIDER_X - 12 && tlx <= S_SLIDER_X + S_SLIDER_W + 12 &&
tly >= track_cy - 14 && tly <= track_cy + 14) {
drag = s_vis_at(vp);
int v = s_slider_val_from_x(rr, tlx);
s_slider_set_from_track(rr, v, 0);
need_paint = 1;
}
}
}
}
/* M7.2 列表拖动滚动:非滑条行按下后纵向位移超阈值 → 进入列表滚动。
* 走 meow_scroll(跟手 track + 松手 fling),同 subpage.c M7.1a 语义;
* 方向经 input.swipe_invert 切换(默认手指上滑 → 内容下翻)。 */
if (td_ok && drag < 0 && !scroll_drag) {
int ddy = tly - scroll_y0;
if (ddy >= S_SCROLL_SLOP || ddy <= -S_SCROLL_SLOP) {
scroll_drag = 1;
meow_settings_get_bool("input.swipe_invert", &scroll_invert);
meow_input_touch_set(0, 0, 0, 0); /* 滚动中不压行(清按压态) */
drag_last_y = tly;
}
}
if (scroll_drag && td_ok) {
int dydt = tly - drag_last_y; /* 本次位移增量(跟手) */
drag_last_y = tly;
int scdy = scroll_invert ? dydt : -dydt;
if (g_set_sc && scdy) meow_scroll_track(g_set_sc, scdy, now);
/* 列表滚动不在此即时重绘:整批事件末尾由下方「if (scroll_drag && td_ok)
* set_redraw_region」合并为一次视口局部重绘。此处若 set_redraw_region 且再设
* need_paint=1,底部 set_paint_page 会再走一遍整页重绘 + dirty_add(0,42,...)
* 近整屏 flush —— 快滑每帧整页重绘 → 卡顿,违背「局部脏区不整屏重绘」铁律;
* 且一个 poll 多个移动事件各自 flush 会多次 FBIO_WAITFORVSYNC 阻塞放大延迟。 */
}
if (drag >= 0 && td_ok) {
const srow_t *r = &SMENU[drag];
int v = s_slider_val_from_x(r, tlx);
s_slider_set_from_track(r, v, 0); /* 拖动实时 set_*,抬起才落盘 */
need_paint = 1;
}
} else if (type == EV_KEY && code == BTN_TOUCH) {
if (val && !td) {
if (now_ms() < touch_cooldown_until) {
tlx = tly = -1; /* 冷却丢弃 */
} else {
td = 1; td_ok = 0; tlx = -1; tly = -1; drag = -1; scroll_drag = 0;
}
} else if (!val && td) {
td = 0;
meow_input_touch_set(0, 0, 0, 0); /* 抬手:清 SDK 按压态 */
if (drag >= 0) { /* 滑条拖动结束 → 落盘 */
const srow_t *r = &SMENU[drag];
s_commit(r, 1);
drag = -1;
need_paint = 1;
} else if (scroll_drag) {
scroll_drag = 0; /* 列表滚动结束(已实时重绘),不再当 tap */
if (g_set_sc) meow_scroll_fling(g_set_sc); /* 松手 fling(惯性 ease-out) */
scroll_y0 = 0;
need_paint = 1;
} else if (td_ok) {
int dx = tlx - tdx, dy = tly - tdy;
/* M2.39 左缘右滑返回:复用全局左缘返回判定 meow_edge_back()
* (与 SDK 全局左缘返回 A-0 同语义:左缘起点 + 右滑≥MEOW_EDGE_SWIPE
* 且横向主导),避免设置页自造一套重复判定。 */
if (meow_edge_back(tdx, dx, dy)) {
if (tfd >= 0) { unsigned char e[16]; while (read(tfd, e, sizeof e) == 16) {} }
goto done;
}
int adx = dx < 0 ? -dx : dx, ady = dy < 0 ? -dy : dy;
if (adx < 60 && ady < 60) { /* 视为 tap */
int tx = tdx, ty = tdy;
/* 底部返回按钮 */
if (ty >= H - 52 && tx >= SET_CARD_X && tx <= SET_CARD_X + 120) {
if (tfd >= 0) { unsigned char e[16]; while (read(tfd, e, sizeof e) == 16) {} }
goto done;
}
/* M7.2:行命中走 meow_listview(含滚动换算 + 禁用/越界行不响应);
* 段头行 enabled=1 故 hit 亦命中 → 下方按 ctrl==SC_HEADER 分流。 */
int vp = g_set_lv ? meow_listview_hit(g_set_lv, tx, ty) : -1;
int idx = (vp >= 0) ? s_vis_at(vp) : -1;
if (idx >= 0 && !s_setting_idx(idx)) {
/* 点分组标题行 → 仅选中下一设置行 */
int ns = s_next_setting(idx);
sel = ns; need_paint = 1; sel_changed = 1;
} else if (idx >= 0) {
const srow_t *r = &SMENU[idx];
sel = idx; need_paint = 1; sel_changed = 1;
if (r->ctrl == SC_SWITCH) {
int on = 0; meow_settings_get_switch(r->key, &on);
meow_settings_set_switch(r->key, !on);
s_commit(r, 1);
} else if (r->ctrl == SC_LIST) {
/* M2.39 网络列表行:开下拉(重新解析)+ 重绘 + 排空本页 tfd
* (复刻 s_dropdown 后的穿透防护,见下方 ENUM/PRESET 分支)。
* M2.76:WiFi/蓝牙升级为独立子页面 → 点开进 run_wifi_page/
* run_bt_page(阻塞,内部自开 fds;返回后仍须排空本页 tfd)。 */
after_modal = 1;
if (r->net_list == NET_LIST_WIFI_PAGE) { run_wifi_page(); s_net_refresh_kick(); }
else if (r->net_list == NET_LIST_BT_PAGE) { run_bt_page(); s_net_refresh_kick(); }
else if (r->net_list == NET_LIST_PIN_PAGE) { run_lock_settings_page(); s_net_refresh_kick(); }
else s_net_open(r);
if (tfd >= 0) {
unsigned char e[16];
while (read(tfd, e, sizeof e) == 16) {}
usleep(40000);
while (read(tfd, e, sizeof e) == 16) {}
}
td = 0; td_ok = 0; drag = -1; scroll_drag = 0;
scroll_y0 = 0;
tdx = tdy = tlx = tly = -1;
touch_cooldown_until = now_ms() + 300;
memset(&kd_pwr, 0, sizeof kd_pwr);
memset(&kd_vold, 0, sizeof kd_vold);
memset(&kd_volu, 0, sizeof kd_volu);
memset(&kd_play, 0, sizeof kd_play);
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
m_prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN);
m_prev_volu = kp_is_down(&kp_keys, KEY_VOLUMEUP);
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
need_paint = 1;
} else if (r->ctrl == SC_ENUM || r->ctrl == SC_PRESET) {
/* 单次点击即开选择器子对话框 */
after_modal = 1; /* 关闭后整帧重绘(还原被 dim 的状态栏) */
if (s_row_is_font(r)) {
/* M2.6 字重:字体行走「family→字重」两级子菜单 */
char fval[MEOW_SET_MAX_LEN];
if (s_font_dropdown(r, fval, sizeof fval)) {
meow_settings_set_str(r->key, fval);
s_commit(r, 1);
}
} else {
int pick = s_dropdown(r);
if (pick >= 0) {
if (r->ctrl == SC_ENUM) meow_settings_set_enum(r->key, pick);
else meow_settings_set_str(r->key, s_preset_value(r, pick));
s_commit(r, 1);
}
}
/* M2.38 Bug3:s_dropdown 返回后彻底排空本页 tfd +
* 重置触摸基线。根因:input 子系统把事件广播到所有
* 打开的 fd —— s_dropdown 用独立 fd 消费选项 tap,但
* 本页 tfd 同样收到了整套下拉会话事件;若不清空,
* 外层 while(read) 会把它们当新 tap → 穿透到下层设置项。
* 故 排空→40ms→再排空 + 重置触摸状态 + 冷却窗口吸收长尾。 */
if (tfd >= 0) {
unsigned char e[16];
while (read(tfd, e, sizeof e) == 16) {}
usleep(40000);
while (read(tfd, e, sizeof e) == 16) {}
}
td = 0; td_ok = 0; drag = -1; scroll_drag = 0;
scroll_y0 = 0;
tdx = tdy = tlx = tly = -1;
touch_cooldown_until = now_ms() + 300;
/* 重置设置页按键防抖基线,防残边 */
memset(&kd_pwr, 0, sizeof kd_pwr);
memset(&kd_vold, 0, sizeof kd_vold);
memset(&kd_volu, 0, sizeof kd_volu);
memset(&kd_play, 0, sizeof kd_play);
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
m_prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN);
m_prev_volu = kp_is_down(&kp_keys, KEY_VOLUMEUP);
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
need_paint = 1;
} else if (r->ctrl == SC_SLIDER && tlx >= 0) {
int v = s_slider_val_from_x(r, tlx);
s_slider_set_from_track(r, v, 1);
}
}
}
}
td_ok = 0;
}
}
}
}
/* 列表拖动滚动:本批连续触摸事件合并为一次视口局部重绘(只刷列表区,
* 最多一次 present),不再触发 need_paint 整页重绘。scroll_drag/td_ok 在
* 抬手处置 0(抬手那轮已走 meow_scroll_fling + need_paint 整页),故此处只兜活动滚动。 */
if (scroll_drag && td_ok) set_redraw_region(NULL);
}
/* ---- 滚动 + 重绘 ---- */
if (need_paint) {
last_act = now;
/* 仅选中行变化时强制其可见;列表拖动滚动时保留用户滚到的位置,不把选中拉回视野 */
if (sel_changed) { set_ensure_sel_visible(sel); sel_changed = 0; }
set_paint_page(sel);
/* 局部标脏:标题+卡片+滚动条+底部(y=42 以下全部,覆盖主题/选中/值变化),
* 状态栏(0-28)由 draw_statusbar/draw_clock 各自维护,不在此列。
* 选择器关闭后(after_modal)其全屏 dim 把状态栏也压暗了,须整帧重绘还原。 */
if (after_modal) { meow_dirty_full(); after_modal = 0; }
else meow_dirty_add(0, 42, fb_xres, fb_yres - 42);
meow_ui_flush();
}
/* M2.37 超时自动息屏(与主界面一致):空闲 > screen_off_timeout → 关屏,但留在设置页:
* 置 meow_auto_slept 进入休眠态,由顶部睡眠门等待任意输入唤醒并重绘设置页、不丢上下文
* (对照 run_menu 外层 meow_auto_slept 的唤醒重绘,M2.17/2.18 同款语义)。 */
if (screen_off_timeout > 0 && meow_screen_on && !meow_auto_slept &&
(unsigned long long)(now - last_act) >= (unsigned long long)screen_off_timeout * 1000) {
meow_panel_off();
meow_auto_slept = 1;
wake_suppress = 1; /* 唤醒输入边沿待完全抬起后再接管,避免误动作 */
logmsg("settings: idle %ds -> auto screen-off (stay in settings)", screen_off_timeout);
}
/* ~1s 状态栏时钟刷新 */
if (now - last_tick >= 1000) {
last_tick = now;
draw_clock();
fb_norm_buf0();
meow_ui_flush();
}
/* M7.2: 推进 fling/回弹动画(meow_scroll 惯性停;无动画零开销) */
meow_anim_tick();
usleep(POLL_MS * 1000);
}
done:
meow_input_touch_set(0, 0, 0, 0); /* M7.2 防残留按压态 */
meow_ui_activity_pop(); /* 关 owner 闸门(控件 owner 仍钉哨兵,复用) */
if (tfd >= 0) close(tfd);
kp_close(&kp_power); kp_close(&kp_keys);
logmsg("settings page closed");
}