/* 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 }, { "亮度", "display.brightness", SC_SLIDER, 1,100,1, NULL,NULL,0 }, { "息屏模式", "display.screen_off_mode", SC_ENUM, 0,0,0, NULL,NULL,0 }, { "息屏时间", "display.screen_off_timeout", SC_SLIDER, 0,(S_TIME_TIERS_N-1),1, NULL,NULL,0 }, { "刷新率", "display.refresh_rate", SC_ENUM, 0,0,0, NULL,NULL,0 }, { "最高刷新率", "display.refresh_always_max", SC_SWITCH, 0,0,0, NULL,NULL,0 }, /* B2b 主题三方式:标题行「主题」改选 display.theme_mode(手动/时段自动), * 手动深浅由下方「主题深浅」(display.theme) 控制;时段两键为分钟滑条(STEP=15)。 */ { "主题", "display.theme_mode", SC_ENUM, 0,0,0, NULL,NULL,0 }, { "主题深浅", "display.theme", SC_ENUM, 0,0,0, NULL,NULL,0 }, { "深色时段开始", "display.theme_dark_start", SC_SLIDER, 0,1439,15, NULL,NULL,0 }, { "深色时段结束", "display.theme_dark_end", SC_SLIDER, 0,1439,15, NULL,NULL,0 }, { "背景", "display.background", SC_PRESET, 0,0,0, bg_presets, bg_preset_labels, BG_PRESETS_N }, /* 网络 */ { "网络", NULL, SC_HEADER, 0,0,0, NULL,NULL,0 }, { "WiFi", "network.wifi_enabled", SC_SWITCH, 0,0,0, NULL,NULL,0 }, { "WiFi连接", NULL, SC_LIST, 0,0,0, NULL,NULL,0, NET_LIST_WIFI_SSID }, { "已保存网络", NULL, SC_LIST, 0,0,0, NULL,NULL,0, NET_LIST_WIFI_SAVED }, { "附近WiFi", NULL, SC_LIST, 0,0,0, NULL,NULL,0, NET_LIST_WIFI_SCAN }, { "蓝牙", "network.bluetooth_enabled", SC_SWITCH, 0,0,0, NULL,NULL,0 }, { "蓝牙模式", NULL, SC_INFO, 0,0,0, NULL,NULL,0, NET_LIST_BT_MODE }, { "蓝牙设备", NULL, SC_LIST, 0,0,0, NULL,NULL,0, NET_LIST_BT_NEARBY }, /* 电源 */ { "电源", NULL, SC_HEADER, 0,0,0, NULL,NULL,0 }, { "长按弹菜单", "power.longpress_menu_ms", SC_SLIDER, 500,5000,500, NULL,NULL,0 }, { "长按强关", "power.forceoff_ms", SC_SLIDER, 5500,15000,500, NULL,NULL,0 }, { "退出热键", "power.defeat_hotkey", SC_PRESET, 0,0,0, hk_presets, hk_preset_labels, HK_PRESETS_N }, /* 开机 */ { "开机", NULL, SC_HEADER, 0,0,0, NULL,NULL,0 }, { "默认系统", "boot.default", SC_ENUM, 0,0,0, NULL,NULL,0 }, { "显示选单", "boot.menu_enabled", SC_SWITCH, 0,0,0, NULL,NULL,0 }, { "选单超时", "boot.timeout_ms", SC_SLIDER, 0,30000,500, NULL,NULL,0 }, /* 输入 */ { "输入", NULL, SC_HEADER, 0,0,0, NULL,NULL,0 }, { "热键序列", "input.hotkey_seq", SC_PRESET, 0,0,0, seq_presets, seq_preset_labels, SEQ_PRESETS_N }, { "热键窗口", "input.hotkey_window_ms", SC_SLIDER, 0,5000,100, NULL,NULL,0 }, { "触摸布防", "input.touch_arm_delay_ms", SC_SLIDER, 0,2000,50, NULL,NULL,0 }, { "滑动方向", "input.swipe_invert", SC_SWITCH, 0,0,0, NULL,NULL,0 }, /* 字体主题 */ { "字体主题", NULL, SC_HEADER, 0,0,0, NULL,NULL,0 }, { "字体文件", "font.file", SC_PRESET, 0,0,0, font_presets, font_preset_labels, 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 }, /* M2.5x 多字体:开关 + 6 个字符类别字体(preset=可用 MeowOS/fonts/// 列表; * 值空 = 跟随全局 font.file)。仅开关开启时 6 行可见(s_row_visible)。 */ { "多字体", "display.multi_font", SC_SWITCH, 0,0,0, NULL,NULL,0 }, { "中文(CJK)", "font.class_cjk", SC_PRESET, 0,0,0, font_presets, font_preset_labels, 0 }, { "中文标点", "font.class_cjk_sym", SC_PRESET, 0,0,0, font_presets, font_preset_labels, 0 }, { "数字", "font.class_num", SC_PRESET, 0,0,0, font_presets, font_preset_labels, 0 }, { "拉丁字母", "font.class_latin", SC_PRESET, 0,0,0, font_presets, font_preset_labels, 0 }, { "半角标点", "font.class_latin_sym", SC_PRESET, 0,0,0, font_presets, font_preset_labels, 0 }, { "其他字符", "font.class_other", SC_PRESET, 0,0,0, font_presets, font_preset_labels, 0 }, { "标题字号", "font.size.title", SC_SLIDER, 16,40,1, NULL,NULL,0 }, { "正文字号", "font.size.body", SC_SLIDER, 12,28,1, NULL,NULL,0 }, { "UI风格", "ui.style", SC_ENUM, 0,0,0, NULL,NULL,0 }, /* 通知 */ { "通知", NULL, SC_HEADER, 0,0,0, NULL,NULL,0 }, { "通知中心", "notify.center_enabled", SC_SWITCH, 0,0,0, NULL,NULL,0 }, { "应用配额", "notify.app_quota", SC_SLIDER, 1,64,1, NULL,NULL,0 }, /* 日志 */ { "日志", NULL, SC_HEADER, 0,0,0, NULL,NULL,0 }, { "日志级别", "log.level", SC_ENUM, 0,0,0, NULL,NULL,0 }, { "写入TF卡", "log.to_card", SC_SWITCH, 0,0,0, NULL,NULL,0 }, /* 音频 */ { "音频", NULL, SC_HEADER, 0,0,0, NULL,NULL,0 }, { "音量", "audio.volume", SC_SLIDER, 0,100,1, NULL,NULL,0 }, { "音量上限", "audio.volume_max", SC_SLIDER, 0,100,1, NULL,NULL,0 }, { "音量跳步", "audio.volume_step", SC_SLIDER, 1,5,1, NULL,NULL,0 }, { "输出设备", "audio.output", SC_PRESET, 0,0,0, out_presets, out_preset_labels, OUT_PRESETS_N }, /* 关于(设置列表最底;「关于本机」点开 s_about_open 弹只读设备信息框) */ { "关于", NULL, SC_HEADER, 0,0,0, NULL,NULL,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。 */ /* 当前选中行若是滑条:记录其 track 纵向位置,供触摸拖动命中判(x/w 为布局常量, * 所有滑条行相同,可直接算;仅 y 随行变,故独立记录)。 */ static int s_sel_track_y = -1; /* ---- 行高/位置/导航辅助 ---- */ /* 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/// 扫描结果,见字体区 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; } /* 滚动:纯范围钳制到 [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); } /* ---- 值格式化/展示 ---- */ 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); 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); if (!strcmp(bg, "blue")) menu_bg = 0x0A1428; /* 深蓝 */ else if (!strcmp(bg, "green")) menu_bg = 0x0A2814; /* 深绿 */ else menu_bg = 0x000000; /* black(默认) */ /* 状态栏(menu_bg)与主界面背景都会用到 menu_bg;退出设置页回主界面时重绘即可。 */ } /* 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(); /* 状态栏文字/底色随主题刷新(整条标脏) */ } /* 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; } /* ---- 绘制设置页(整页画进 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); } } /* 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; return s_dd_modal(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 ? 0xFFFFFF : 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→字重」两级子菜单(M2.6 字重结构)---- * 一级:列出扫描到的去重 family(class 行首项「跟随全局」=空值);选 family 后自动进入 * 二级:该 family 的可用字重(regular/bold/italic…)。选中写 out_val = 卡根相对路径 * MeowOS/fonts///font.png(或 class 行「跟随全局」=空串)。 * 复用 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 的字重列表(值=完整卡根相对路径,显示=label)---- */ 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 && wn < S_NET_MAX; i++) { if (!font_preset_fams[i] || strcmp(font_preset_fams[i], fam)) continue; snprintf(wt_val[wn], sizeof wt_val[0], "%s", font_presets[i]); snprintf(wt_text[wn], sizeof wt_text[0], "%s", font_preset_labels[i]); wn++; } 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]; int cur_wt = -1; for (int i = 0; i < wn; i++) if (cur[0] && !strcmp(wt_val[i], cur)) cur_wt = i; 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; } /* ---- 运行设置页(独立子循环;run_menu 触摸命中设置行后调用)---- */ void run_settings_page(void) { int sel = s_first_setting(); int scroll_off = 0; int H = fb_yres; /* M2.6:进设置页扫描一次 TF 卡 MeowOS/fonts///,刷新「字体文件」下拉。 * 进设置时卡通常在位;此步只枚举/校验,不加载(加载由 meow_font_ensure 按 font.file 决定)。 */ meow_font_scan_presets(); 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 覆盖过状态栏,需整帧重绘还原 */ /* 触摸拖动滑条状态 */ 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 scroll_base = 0; /* 进入滚动时对应的 scroll_off 基准(连续跟手) */ 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 同款) */ /* 首次全帧绘制 */ s_net_sync_from_sys(); /* M2.39:WiFi/蓝牙开关行同步真实硬件态 + 刷新网络信息缓存 */ scroll_off = s_clamp_scroll(sel, scroll_off); draw_settings_page(sel, scroll_off); meow_dirty_full(); meow_ui_flush(); 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"); scroll_off = s_clamp_scroll_range(scroll_off); draw_settings_page(sel, scroll_off); meow_dirty_full(); meow_ui_flush(); } usleep(POLL_MS * 1000); continue; } /* ---- 按键 ---- */ 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 >= 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); sys_do_reboot(LINUX_REBOOT_CMD_POWER_OFF); } /* 关机一般不复返;若 reboot 失败返回仍按按住处理(m_prev 由下方统一复写) */ } else if (now - pwr_hold_at >= 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 < 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; scroll_y0 = tdy; /* 列表拖动基准:按下原点(触发时才锁 scroll_base) */ /* 按下原点落在选中滑条 track 带内 → 开始拖动 */ if (SMENU[sel].ctrl == SC_SLIDER && s_sel_track_y >= 0 && tlx >= S_SLIDER_X - 12 && tlx <= S_SLIDER_X + S_SLIDER_W + 12 && tly >= s_sel_track_y - 14 && tly <= s_sel_track_y + 20) { drag = sel; const srow_t *r = &SMENU[sel]; int v = s_slider_val_from_x(r, tlx); s_slider_set_from_track(r, v, 0); need_paint = 1; } } /* M2.37 列表拖动滚动:非滑条行按下后纵向位移超阈值 → 进入列表滚动。 * scroll_base 在触发瞬间反推,保证进入滚动那一刻不跳变(连续跟手)。 * M2.38 Bug4:默认标准滑动(手指上滑 → scroll_off 增大 = 内容往下翻看后面项); * 方向可经 input.swipe_invert 切换(1=反向:手指上滑 → 内容上移)。 */ 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); scroll_base = scroll_off + (scroll_invert ? -ddy : ddy); } } if (scroll_drag && td_ok) { int dydt = tly - scroll_y0; scroll_off = s_clamp_scroll_range(scroll_base + (scroll_invert ? dydt : -dydt)); need_paint = 1; } 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; 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 */ scroll_base = 0; 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; } int cy = ty - SET_CONTENT_Y + scroll_off; int idx = s_row_at_content_y(cy); 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 分支)。 */ after_modal = 1; 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_base = 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_base = 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; } } } } } /* ---- 滚动 + 重绘 ---- */ if (need_paint) { last_act = now; /* 仅选中行变化时强制其可见;列表拖动滚动时保留用户滚到的位置,不把选中拉回视野 */ if (sel_changed) { scroll_off = s_clamp_scroll(sel, scroll_off); sel_changed = 0; } draw_settings_page(sel, scroll_off); /* 局部标脏:标题+卡片+滚动条+底部(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(); } usleep(POLL_MS * 1000); } done: if (tfd >= 0) close(tfd); kp_close(&kp_power); kp_close(&kp_keys); logmsg("settings page closed"); }