Files
MeowOS-For-R3P2/patch_v2/meow_launcherd/settings_page.c
T
AskaEth c3abc3fee4 feat(launcher,lock): 锁屏补充三定案—面板一次性解锁恢复口 / 设置页 PIN 管理 / 电源菜单主动锁强化
- 面板一次性解锁恢复口:/tmp/meow_pin_unlock_once 存在即被锁屏页消费删除并放行一次
  (不校验 PIN、不清 system.pin、不重置失败计数;重启标志自然消失;lck_unlock_once_consume,
  launcher 侧只做检查/消费,panel 端点由主会话另实现)。
- 系统设置「安全 → 锁屏 PIN」页(lock_settings.c,subpage.c 脚手架):状态(未设置/已设置/
  锁定中+剩余)/ 设置(4 位双输确认)/ 修改(需旧 PIN 一次)/ 清除(**免旧 PIN**,仅二次确认弹窗)
  / system.pin_lock_on_wake 开关;数字输入用 SDK 按钮键盘(lck_input_pin_modal)。
- 电源菜单「锁定」主动锁强化:立即熄屏 + 写 /tmp/meow_pin_locked(主动锁标志,重启消失,
  唤醒时无论 pin_lock_on_wake 是否开都必锁屏)+ rm /tmp/meow_pin_unlock_once 作废挂起绕过;
  lck_wake_should_lock() 作为唤醒门取代 lck_lock_required(1)(主菜单 2 处 + App 会话 2 处)。
- 测试:test_lock_screen 扩 51 ok——bypass 存在即消费/不存在不影响/消费后删除;清除免旧 PIN;
  主动锁标志唤醒必锁/清除后回退;全回归 settings_brightness/notif_panel/notif_tf/vol_ramp_jack 无碍;
  交叉 -Wall -Wextra 0 warning。meow_launcherd md5 bdf884756ef8a8353aa8e5dfb6e10a0a。
- SYSTEM_SETTINGS.md 同步锁屏行为段。
2026-09-02 00:57:17 +08:00

1401 lines
80 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。 */
/* 当前选中行若是滑条:记录其 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/<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;
}
/* 滚动:纯范围钳制到 [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, "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;
}
/* ---- 绘制设置页(整页画进 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,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;
}
/* ---- 运行设置页(独立子循环;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/<family>/<weight>/,刷新「字体文件」下拉。
* 进设置时卡通常在位;此步只枚举/校验,不加载(加载由 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 >= (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;
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 分支)。
* 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(); }
else if (r->net_list == NET_LIST_BT_PAGE) { run_bt_page(); s_net_refresh(); }
else if (r->net_list == NET_LIST_PIN_PAGE) { run_lock_settings_page(); s_net_refresh(); }
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_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");
}