feat: 发布4×4候选与Android手表应用内键盘组件
这个提交包含在:
1 父节点
82aeeba816
当前提交
87aa966a51
86 个文件已更改
+2697
-2975
没有与搜索条件相匹配的文件
@@ -20,3 +20,7 @@
|
||||
- 使用实际 Inspector 树检查按钮和文字是否超出任一父容器;另检查屏幕边界、边缘黑色缺口、四候选与翻页/底栏的整体显示。几何与截图同时通过才可用于最终预览。
|
||||
|
||||
浏览器1.1.0同步使用浅色主界面和同一个键盘维护源。14键使用独立编码索引,候选仍来自同一完整词库;账号、私密输入和草稿持久化规则保持有效。
|
||||
|
||||
## 1.2.0候选网格
|
||||
|
||||
完整候选默认4列4行、单字16个一页,隐藏注音及该页拼音栏。2~4字词组占两格,长词占整行并按长度增加行高,单页最多四行单位。不得省略或截断文字,不丢候选;每页只将当前可见项放入响应式数据。圆屏网格保持中段安全宽度,底部返回、退格和完成使用原有矢量来源图标。
|
||||
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 herry
|
||||
Copyright (c) 2026 sydxky
|
||||
Copyright (c) 2026 kqakqakqa
|
||||
Copyright (c) 2026 crabKun
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# 蟹蟹输入 RTOS适配改版
|
||||
|
||||
本项目为基于[CrabKeyboard / 蟹蟹输入](https://github.com/crabPangXie/crabKeyboard)的独立RTOS适配改版,由herry进行Glyphix页面、离线词库索引、九宫/14键消歧、标准QWERTY、候选分页及生命周期适配。不是上游原版,也不表示上游作者、华为、OPPO或Glyphix对本改版作出背书。
|
||||
本项目为基于[CrabKeyboard / 蟹蟹输入](https://github.com/crabPangXie/crabKeyboard)的独立RTOS适配改版,由sydxky进行Glyphix页面、离线词库索引、九宫/14键消歧、标准QWERTY、候选分页及生命周期适配。不是上游原版,也不表示上游作者、华为、OPPO或Glyphix对本改版作出背书。
|
||||
|
||||
上游固定提交:`93d36b5002dee80a3d253b1ec7ec9e29800a104c`。保留原作者kqakqakqa、crabKun的版权声明、MIT许可证及原始词库/读取器。RTOS自研适配部分以根目录[MIT许可证](./LICENSE)发布,**允许商用、修改、再分发和闭源集成**,须保留适用版权及许可全文。
|
||||
|
||||
@@ -14,4 +14,4 @@
|
||||
|
||||
14键参考公开演示中的相邻字母分组,自行实现离线消歧;不包含华为内部模型或固件代码。Glyphix/厂商SDK与运行时不包含在本项目MIT授权中,也不随源码分发,使用者需自行取得适用的厂商授权。
|
||||
|
||||
公开仓库:<https://git2.sydxky.cn/herry/crab-input-rtos>。匿名问题与建议:<https://git2.sydxky.cn/feedback/herry/crab-input-rtos>。
|
||||
公开仓库:<https://git2.sydxky.cn/sydxky/crab-input-rtos>。匿名问题与建议:<https://git2.sydxky.cn/feedback/sydxky/crab-input-rtos>。
|
||||
@@ -1,67 +1,69 @@
|
||||
# 蟹蟹输入 / Glyphix RTOS
|
||||
|
||||
**蟹蟹输入 RTOS适配改版**:基于CrabKeyboard的独立改版,RTOS适配代码采用[MIT协议](./LICENSE),允许商用、修改及再分发,须保留版权与许可。上游与第三方署名见[NOTICE.md](./NOTICE.md)。公开源码:<https://git2.sydxky.cn/herry/crab-input-rtos>;免注册[匿名反馈](https://git2.sydxky.cn/feedback/herry/crab-input-rtos)。
|
||||
|
||||
独立应用 `com.sydxky.rtos.inputmethod`,版本 `1.1.0 / 10100`。基于 [CrabKeyboard](https://github.com/crabPangXie/crabKeyboard) 的 MIT 全量词库和拼音前缀消费方式,重写为 Glyphix `.ux` 页面;不依赖 HarmonyOS HML、Android 输入法服务、DOM 或网络。
|
||||
|
||||
1.0.1 按用户选择重做为浅色手机输入法风格:浅灰底、白色大按键、深色候选和蓝色主操作。候选栏与模式栏减少色块,英语和数字模式隐藏无用候选区;首页只突出一个输入主按钮。设计规范见 [DESIGN.md](./DESIGN.md)。
|
||||
|
||||
1.1.0 将全拼和英文改为标准 **QWERTY 10/9/7 三排**,新增公开华为风格的 **14键双字母拼音**,布局及独立离线消歧算法见 [14键说明](./KEYBOARD_14.md)。修复键盘容器被压缩导致末排裁切、四候选列表间距超出父容器、背景留黑的问题,圆屏下方逐行收窄。取消、退格、完成、编辑、键盘返回和翻页使用同一套 iconfont / Remix Icon 线性图标,保留“空格”和输入模式文字。SVG 原稿、来源和 Apache-2.0 许可见 [图标说明](./third_party/remix-icon/README.md);运行包采用本地透明 PNG,不增加网络请求。
|
||||
|
||||
上游固定提交 `93d36b5002dee80a3d253b1ec7ec9e29800a104c`(v2.0.0-patch01)。41 个原始词库文件均按 Git blob SHA-1 验证后保存于 `third_party/crabKeyboard/`,许可证和原始读取器一并保留。全量数据包含 **104,837 组拼音/缩写索引、163,238 个候选项**;这是索引项计数,不表示不同词语数。按原词库内容逐字/逐词组合输入长句。
|
||||
|
||||
## 使用
|
||||
|
||||
- 点击首页文字区或“开始输入”即打开满屏键盘。完成回填;取消保留原文。草稿需主动保存,仅保存在本应用。
|
||||
- 默认九宫大按键。“26键”切到标准 QWERTY 全拼,`v` 表示 `ü`;“九宫”切回九宫。“14键”直接进入双字母模式。连续点击对应字母组,选择候选后只消费匹配部分,剩余编码继续查词。存在未选词的九宫或14键编码时禁止切换模式/完成,避免丢字或把编码当文字提交。
|
||||
- 第一行显示三个候选。点击右侧下箭头按每页四项查看全部匹配候选,左右箭头翻页,不使用原项目扩展列表的 100 项截断。拼音候选保留每组原始顺序;九宫和14键歧义通过本地 jieba 0.42.1 词频排序,学习偏好可优先排序。
|
||||
- 支持英文大小写、数字、完整可打印 ASCII 标点与常用中文标点。符号分两组大按键,通过“下一组符号”切换。网址额外提供 `https://`、`.com`。
|
||||
- 点击文本预览或铅笔图标移动光标、换行、上屏全拼、清空全文。Unicode 字符按码点计数和删除,避免拆坏 emoji。清空全文需第二次点击确认;长按退格图标连续删除,松开/隐藏/销毁停止。勾号完成,叉号取消,键盘图标返回输入。
|
||||
- 学习默认关闭,可开启或清除,仅保存最多 100 组、每组三个已选择的真实候选,不记录整段正文。私密输入始终遮盖、不学习、不保存;切到其他应用时清空私密编辑状态。
|
||||
|
||||
## 自动弹出的实际范围
|
||||
|
||||
Glyphix 官方 [text-field 文档](https://docs.xfaith-tech.com/glyphix/docs/components/text-field.html)明确说明普通输入框不会弹出输入法。当前厂商工具没有提供系统级输入法注册/全局输入焦点注入接口。本项目交付独立文本应用和可接入页面,**不能接管未接入的第三方应用或固件界面**。
|
||||
|
||||
腕上浏览 `1.1.0` 同步使用此键盘:点击地址、配对码及兼容网页输入项后直接打开此页。输入法本身全离线;浏览器原有统一账号验证和兼容模式数据告知继续有效。其他应用可按下述方式接入。
|
||||
|
||||
```powershell
|
||||
# 先构建本项目生成资源,再复制页面/引擎/资源并注册路由。
|
||||
& '.\build.ps1'
|
||||
node .\scripts\install.mjs '<目标 Glyphix 工程根目录>'
|
||||
```
|
||||
|
||||
在业务输入区的 `on:click` 中调用;页面上保留单一 `opening` 标志防止重复弹出:
|
||||
|
||||
```javascript
|
||||
const result = await router.push({uri: 'keyboard', params: {
|
||||
initial: this.value, title: '输入内容', inputType: 'text',
|
||||
maxLength: 2048, allowNewline: true, secret: false
|
||||
}})
|
||||
if (result && result.accepted) this.value = result.text
|
||||
```
|
||||
|
||||
`inputType` 支持 `text`、`url`、`number`、`pin`、`password`。`pin` 仅接受数字;`number` 允许小数点和负号;`maxLength` 上限 4096 个 Unicode 码点。业务仍需校验数值、网址或配对码的实际格式。不要在异步生命周期中等待页面结果。在宿主 `app.js` 的 `onHide` 中调用 `clearPrivateInputs(router)`(`common/ime-lifecycle.js`),覆盖厂商页面生命周期差异。宿主销毁后不得继续处理异步结果。
|
||||
|
||||
## 构建和复现
|
||||
|
||||
使用非 MSIX PowerShell 7,从本目录执行 `build.ps1`。脚本先无损转换词库、运行真实引擎和 UX 脚本测试,再用 RTOS 父目录的 OPPO 附件 `gx.exe` 生成圆/方普通镜像及圆/方 QuickApp 完整目录。SDK 不随源码分发。新机器另需从厂商附件准备工具,先构建/启动 `com.sydxky.rtos.snakeov` 一次取得 launcher/global 资源;通用 Inspector 支持位于 `../tests/emulator-validate.py`。
|
||||
|
||||
```powershell
|
||||
python .\tests\native.py --work '<独立临时目录>' --output '<原生证据目录>'
|
||||
python .\tests\visual.py --work '<另一个临时目录>' --output '<原生证据目录>/visual'
|
||||
```
|
||||
|
||||
原生验收覆盖四配置,每配置 21 项,共 **84/84**;真实引擎及 UX 脚本分别通过 **40 + 29** 项断言。实际结果与运行包 SHA-256 见生成的 `results.json`。测试脚本使用既有离线参考应用执行真实前后台切换;不会接管被其他进程占用的 Inspector 14201。
|
||||
|
||||
1.1.0 的原生检查同时逐层核对可见 Button、图标、Label 与父容器边界;圆屏按钮四角限制在半径 230px 内。独立视觉回归覆盖四配置各11个页面状态,共 **44/44**,并统计靠近屏幕边缘的黑色缺口,避免只检查屏幕边界而漏掉内部裁切。截图保留实际原生画面,未绘制替代界面。详见 [本版验收记录](./VALIDATION_1.1.0.md)。
|
||||
|
||||
1.0.0 历史版本的 **72/72** 原生用例及 **26 + 22** 项逻辑断言见 [此前验收记录](./VALIDATION_2026-11-13.md)。1.0.1 浅色版重新完成四配置 **72/72** 原生验证,详见 [本版验收记录](./VALIDATION_1.0.1.md)。
|
||||
|
||||
## 内存与词库
|
||||
|
||||
`convert-dictionary.py` 将原始 V5 trie 无损转换为 **6779** 个编码分片(九宫/全拼4194个、14键2585个),最大分片 **116,686** 字节。九宫/全拼使用四位前缀,14键使用三位前缀;短编码单独索引。解码缓存限制为四项且 JSON 文本合计不超过 90,000 字符,大分片不缓存。仅当前页的四个候选进入展开列表,所有候选对象不放进响应式状态。转换器分别回读两种索引,验证与原始索引完全相同;不改写原始字典。
|
||||
|
||||
初期两位编码分片曾触发原生模拟器内存回收,已改为四位编码分片。验收保持既有 `--memory-profile 8192`,没有通过增大模拟器内存规避问题。模拟器耗时不能替代真机帧率、延迟或功耗结论。
|
||||
|
||||
MIT 许可副本也随运行包保留。jieba 词频只用于歧义候选排序,不联网、不分词上传。官方公开 API 不足以实现全局输入法安装;本轮仍按用户确认的模拟器与提交包范围交付,真机安装与平台审核单独处理。
|
||||
# 蟹蟹输入 / Glyphix RTOS
|
||||
|
||||
**蟹蟹输入 RTOS适配改版**:基于CrabKeyboard的独立改版,RTOS适配代码采用[MIT协议](./LICENSE),允许商用、修改及再分发,须保留版权与许可。上游与第三方署名见[NOTICE.md](./NOTICE.md)。公开源码:<https://git2.sydxky.cn/sydxky/crab-input-rtos>;免注册[匿名反馈](https://git2.sydxky.cn/feedback/sydxky/crab-input-rtos)。
|
||||
|
||||
独立应用 `com.sydxky.rtos.inputmethod`,版本 `1.2.0 / 10200`。基于 [CrabKeyboard](https://github.com/crabPangXie/crabKeyboard) 的 MIT 全量词库和拼音前缀消费方式,重写为 Glyphix `.ux` 页面;不依赖 HarmonyOS HML、Android 输入法服务、DOM 或网络。
|
||||
|
||||
1.2.0 完整候选重做为 **4×4 网格**:单字每页16个,2~4字词组占两格,长词占整行并可增加行高;保持原有顺序,最后一页可达,不显示候选拼音。圆/方普通及QuickApp四配置已通过84项原生操作、84项布局及48个视觉状态,见[验收记录](./VALIDATION_1.2.0.md)。同源新增[Android应用内键盘组件](../android/README.md),独立包名、无需联网,接入应用的输入框自动弹出,保持系统默认输入法。
|
||||
|
||||
1.0.1 按用户选择重做为浅色手机输入法风格:浅灰底、白色大按键、深色候选和蓝色主操作。候选栏与模式栏减少色块,英语和数字模式隐藏无用候选区;首页只突出一个输入主按钮。设计规范见 [DESIGN.md](./DESIGN.md)。
|
||||
|
||||
1.1.0 将全拼和英文改为标准 **QWERTY 10/9/7 三排**,新增公开华为风格的 **14键双字母拼音**,布局及独立离线消歧算法见 [14键说明](./KEYBOARD_14.md)。修复键盘容器被压缩导致末排裁切、四候选列表间距超出父容器、背景留黑的问题,圆屏下方逐行收窄。取消、退格、完成、编辑、键盘返回和翻页使用同一套 iconfont / Remix Icon 线性图标,保留“空格”和输入模式文字。SVG 原稿、来源和 Apache-2.0 许可见 [图标说明](./third_party/remix-icon/README.md);运行包采用本地透明 PNG,不增加网络请求。
|
||||
|
||||
上游固定提交 `93d36b5002dee80a3d253b1ec7ec9e29800a104c`(v2.0.0-patch01)。41 个原始词库文件均按 Git blob SHA-1 验证后保存于 `third_party/crabKeyboard/`,许可证和原始读取器一并保留。全量数据包含 **104,837 组拼音/缩写索引、163,238 个候选项**;这是索引项计数,不表示不同词语数。按原词库内容逐字/逐词组合输入长句。
|
||||
|
||||
## 使用
|
||||
|
||||
- 点击首页文字区或“开始输入”即打开满屏键盘。完成回填;取消保留原文。草稿需主动保存,仅保存在本应用。
|
||||
- 默认九宫大按键。“26键”切到标准 QWERTY 全拼,`v` 表示 `ü`;“九宫”切回九宫。“14键”直接进入双字母模式。连续点击对应字母组,选择候选后只消费匹配部分,剩余编码继续查词。存在未选词的九宫或14键编码时禁止切换模式/完成,避免丢字或把编码当文字提交。
|
||||
- 第一行显示三个候选。点击右侧下箭头按每页四项查看全部匹配候选,左右箭头翻页,不使用原项目扩展列表的 100 项截断。拼音候选保留每组原始顺序;九宫和14键歧义通过本地 jieba 0.42.1 词频排序,学习偏好可优先排序。
|
||||
- 支持英文大小写、数字、完整可打印 ASCII 标点与常用中文标点。符号分两组大按键,通过“下一组符号”切换。网址额外提供 `https://`、`.com`。
|
||||
- 点击文本预览或铅笔图标移动光标、换行、上屏全拼、清空全文。Unicode 字符按码点计数和删除,避免拆坏 emoji。清空全文需第二次点击确认;长按退格图标连续删除,松开/隐藏/销毁停止。勾号完成,叉号取消,键盘图标返回输入。
|
||||
- 学习默认关闭,可开启或清除,仅保存最多 100 组、每组三个已选择的真实候选,不记录整段正文。私密输入始终遮盖、不学习、不保存;切到其他应用时清空私密编辑状态。
|
||||
|
||||
## 自动弹出的实际范围
|
||||
|
||||
Glyphix 官方 [text-field 文档](https://docs.xfaith-tech.com/glyphix/docs/components/text-field.html)明确说明普通输入框不会弹出输入法。当前厂商工具没有提供系统级输入法注册/全局输入焦点注入接口。本项目交付独立文本应用和可接入页面,**不能接管未接入的第三方应用或固件界面**。
|
||||
|
||||
腕上浏览 `1.1.0` 同步使用此键盘:点击地址、配对码及兼容网页输入项后直接打开此页。输入法本身全离线;浏览器原有统一账号验证和兼容模式数据告知继续有效。其他应用可按下述方式接入。
|
||||
|
||||
```powershell
|
||||
# 先构建本项目生成资源,再复制页面/引擎/资源并注册路由。
|
||||
& '.\build.ps1'
|
||||
node .\scripts\install.mjs '<目标 Glyphix 工程根目录>'
|
||||
```
|
||||
|
||||
在业务输入区的 `on:click` 中调用;页面上保留单一 `opening` 标志防止重复弹出:
|
||||
|
||||
```javascript
|
||||
const result = await router.push({uri: 'keyboard', params: {
|
||||
initial: this.value, title: '输入内容', inputType: 'text',
|
||||
maxLength: 2048, allowNewline: true, secret: false
|
||||
}})
|
||||
if (result && result.accepted) this.value = result.text
|
||||
```
|
||||
|
||||
`inputType` 支持 `text`、`url`、`number`、`pin`、`password`。`pin` 仅接受数字;`number` 允许小数点和负号;`maxLength` 上限 4096 个 Unicode 码点。业务仍需校验数值、网址或配对码的实际格式。不要在异步生命周期中等待页面结果。在宿主 `app.js` 的 `onHide` 中调用 `clearPrivateInputs(router)`(`common/ime-lifecycle.js`),覆盖厂商页面生命周期差异。宿主销毁后不得继续处理异步结果。
|
||||
|
||||
## 构建和复现
|
||||
|
||||
使用非 MSIX PowerShell 7,从本目录执行 `build.ps1`。脚本先无损转换词库、运行真实引擎和 UX 脚本测试,再用 RTOS 父目录的 OPPO 附件 `gx.exe` 生成圆/方普通镜像及圆/方 QuickApp 完整目录。SDK 不随源码分发。新机器另需从厂商附件准备工具,先构建/启动 `com.sydxky.rtos.snakeov` 一次取得 launcher/global 资源;通用 Inspector 支持位于 `../tests/emulator-validate.py`。
|
||||
|
||||
```powershell
|
||||
python .\tests\native.py --work '<独立临时目录>' --output '<原生证据目录>'
|
||||
python .\tests\visual.py --work '<另一个临时目录>' --output '<原生证据目录>/visual'
|
||||
```
|
||||
|
||||
原生验收覆盖四配置,每配置 21 项,共 **84/84**;真实引擎及 UX 脚本分别通过 **40 + 29** 项断言。实际结果与运行包 SHA-256 见生成的 `results.json`。测试脚本使用既有离线参考应用执行真实前后台切换;不会接管被其他进程占用的 Inspector 14201。
|
||||
|
||||
1.1.0 的原生检查同时逐层核对可见 Button、图标、Label 与父容器边界;圆屏按钮四角限制在半径 230px 内。独立视觉回归覆盖四配置各11个页面状态,共 **44/44**,并统计靠近屏幕边缘的黑色缺口,避免只检查屏幕边界而漏掉内部裁切。截图保留实际原生画面,未绘制替代界面。详见 [本版验收记录](./VALIDATION_1.1.0.md)。
|
||||
|
||||
1.0.0 历史版本的 **72/72** 原生用例及 **26 + 22** 项逻辑断言见 [此前验收记录](./VALIDATION_2026-11-13.md)。1.0.1 浅色版重新完成四配置 **72/72** 原生验证,详见 [本版验收记录](./VALIDATION_1.0.1.md)。
|
||||
|
||||
## 内存与词库
|
||||
|
||||
`convert-dictionary.py` 将原始 V5 trie 无损转换为 **6779** 个编码分片(九宫/全拼4194个、14键2585个),最大分片 **116,686** 字节。九宫/全拼使用四位前缀,14键使用三位前缀;短编码单独索引。解码缓存限制为四项且 JSON 文本合计不超过 90,000 字符,大分片不缓存。仅当前页最多16个候选进入展开网格,所有候选对象不放进响应式状态。转换器分别回读两种索引,验证与原始索引完全相同;不改写原始字典。
|
||||
|
||||
初期两位编码分片曾触发原生模拟器内存回收,已改为四位编码分片。验收保持既有 `--memory-profile 8192`,没有通过增大模拟器内存规避问题。模拟器耗时不能替代真机帧率、延迟或功耗结论。
|
||||
|
||||
MIT 许可副本也随运行包保留。jieba 词频只用于歧义候选排序,不联网、不分词上传。官方公开 API 不足以实现全局输入法安装;本轮仍按用户确认的模拟器与提交包范围交付,真机安装与平台审核单独处理。
|
||||
@@ -1,56 +1,56 @@
|
||||
# 蟹蟹输入1.1.0验收
|
||||
|
||||
版本`1.1.0 / 10100`,包名`com.sydxky.rtos.inputmethod`。本轮沿用厂商`gx v0.10.1 / 3ed38835`,普通/QuickApp圆屏466×466、方屏390×450四配置,内存配置保持8192 KiB。视觉记录时间:`2026-11-14T09:48:39.828292+08:00`。
|
||||
|
||||
## 本版修正
|
||||
|
||||
- 全拼和英文均为标准QWERTY:`qwertyuiop`、`asdfghjkl`、`zxcvbnm`,10/9/7三排。
|
||||
- 新增14键:`QW ER TY UI OP / AS DF GH JK L / ZX CV BN M`。参考公开布局,独立离线消歧;完整词库、连续前缀消费、候选分页和模式保护见[算法说明](./KEYBOARD_14.md)。
|
||||
- 键盘容器禁止纵向压缩,背景外层铺满屏幕,圆屏按行收窄;完整候选上下间距减少,页码禁止收缩,修复末排及四候选在父容器内被截断。
|
||||
- iconfont / Remix Icon统一操作图标,SVG原稿及Apache-2.0许可保留,运行用本地透明PNG。
|
||||
|
||||
## 验证结果
|
||||
|
||||
| 验证 | 结果 |
|
||||
| --- | --- |
|
||||
| 真实引擎逻辑 | 40项断言通过 |
|
||||
| 真实UX脚本 | 29项断言通过 |
|
||||
| 全量词库转换 | 两套索引分别回读与全部104,837组原始索引、163,238候选项一致 |
|
||||
| 四配置构建 | 4/4,每个QuickApp目录含bundle.pkg、icon.png、manifest.json |
|
||||
| 原生操作回归 | 84/84,四配置各21项 |
|
||||
| 原生操作后布局 | 84/84 |
|
||||
| 独立视觉/布局回归 | 44/44,四配置各11个状态 |
|
||||
|
||||
原生回归保留原有18项,新增14键词组歧义与模式保护、连续输入、完整候选分页三项。实际点击原生按钮,检查“你好/不好”共存、未确认组合禁止完成/切模式、只消费选词匹配编码和候选前后翻页。全拼与英文额外检查实际三排顺序。
|
||||
|
||||
视觉状态覆盖首页、九宫、QWERTY全拼、四候选、编辑、14键、14键完整候选、英文、数字及两组符号。检查可见Button/ImageBox/Label是否超出任一父容器,容忍1px原生舍入;圆屏按钮/图标四角限制半径230px,实测按钮最大半径227.86px,小于屏幕233px。全部44个状态的指定边缘采样带黑色缺口像素为0。此结论针对验收状态,不代替未来新增页面的检查。
|
||||
|
||||
Inspector空闲时可能将新树关联到旧back buffer。视觉脚本通过给提示文本暂加空格后恢复,触发真实原生重绘,再重复snapshot保留新帧。没有绘制替代界面。`UI_PREVIEW.png`排除模拟器外壳周边像素,所有按钮完整保留;未裁剪原图及控件树在证据ZIP中。
|
||||
|
||||
## 查询与内存
|
||||
|
||||
八个全拼样本依次为nihao、zhongguo、shijie、shurufa、beijing、tianqi、shanghai、gongzuo,原生记录如下,单位ms:
|
||||
|
||||
| 配置 | 八个全拼样本耗时 |
|
||||
| --- | --- |
|
||||
| round | 80, 45, 42, 41, 44, 45, 46, 43 |
|
||||
| rect | 78, 45, 44, 22, 44, 45, 45, 45 |
|
||||
| round-quickapp | 79, 45, 44, 22, 44, 46, 64, 44 |
|
||||
| rect-quickapp | 77, 44, 43, 23, 44, 45, 47, 43 |
|
||||
|
||||
上述为模拟器全拼查词样本;不是专门的14键基准,也不表示真机端到端延迟、帧率或功耗。14键原生用例等待词库稳定上限5秒并验证实际结果,未另行声明毫秒级指标。
|
||||
|
||||
共6779个分片(4194九宫/全拼、2585十四键),最大116,686字节;缓存最多四项、解码JSON文本合计90,000字符,大分片不驻留。未扩大模拟器内存。
|
||||
|
||||
## 运行包与交付
|
||||
|
||||
| 配置 | 实际运行 bundle.pkg SHA-256 |
|
||||
| --- | --- |
|
||||
| round | `D0D0BBCBACA78A3300F9CE7538BD39CC0810BCF9872B3B42F0E4ED040EDB0A51` |
|
||||
| rect | `767D7A034D943D4B2D75CBE1EA623BEAAEF8CCBF0033B7944A3763513E74B291` |
|
||||
| round-quickapp | `C82E78F47B4CDCBD33B6E39D2C08005CCFFD617608336A17CF5C4F808370E31F` |
|
||||
| rect-quickapp | `A1CA39663B85D36FF464BE5D62949C7ADEFD2EFA7939C8E65ABD90DF41F192E8` |
|
||||
|
||||
提交目录包含两屏QuickApp完整6文件,`crab-keyboard-1.1.0-quickapp.zip`为打包副本,源码和`keyboard-emulator-evidence.zip`另行提供。`results.json`与`visual-results.json`分别记录操作和视觉结果,交付脚本核对两份证据使用同一构建包;`archive-hashes.json`及`DELIVERY_INTEGRITY.json`记录归档哈希、CRC及敏感扫描。
|
||||
|
||||
自动弹出仅覆盖本应用与已接入宿主,厂商公开API没有已验证的系统级IME注册能力。输入法全离线,无账号或联网请求;浏览器联网维持统一账号验证。按用户确认仅做模拟器和提交包验收;RTOS真机安装、目标固件行为、性能、续航及平台审核未验收。
|
||||
# 蟹蟹输入1.1.0验收
|
||||
|
||||
版本`1.1.0 / 10100`,包名`com.sydxky.rtos.inputmethod`。本轮沿用厂商`gx v0.10.1 / 3ed38835`,普通/QuickApp圆屏466×466、方屏390×450四配置,内存配置保持8192 KiB。视觉记录时间:`2026-11-14T09:48:39.828292+08:00`。
|
||||
|
||||
## 本版修正
|
||||
|
||||
- 全拼和英文均为标准QWERTY:`qwertyuiop`、`asdfghjkl`、`zxcvbnm`,10/9/7三排。
|
||||
- 新增14键:`QW ER TY UI OP / AS DF GH JK L / ZX CV BN M`。参考公开布局,独立离线消歧;完整词库、连续前缀消费、候选分页和模式保护见[算法说明](./KEYBOARD_14.md)。
|
||||
- 键盘容器禁止纵向压缩,背景外层铺满屏幕,圆屏按行收窄;完整候选上下间距减少,页码禁止收缩,修复末排及四候选在父容器内被截断。
|
||||
- iconfont / Remix Icon统一操作图标,SVG原稿及Apache-2.0许可保留,运行用本地透明PNG。
|
||||
|
||||
## 验证结果
|
||||
|
||||
| 验证 | 结果 |
|
||||
| --- | --- |
|
||||
| 真实引擎逻辑 | 40项断言通过 |
|
||||
| 真实UX脚本 | 29项断言通过 |
|
||||
| 全量词库转换 | 两套索引分别回读与全部104,837组原始索引、163,238候选项一致 |
|
||||
| 四配置构建 | 4/4,每个QuickApp目录含bundle.pkg、icon.png、manifest.json |
|
||||
| 原生操作回归 | 84/84,四配置各21项 |
|
||||
| 原生操作后布局 | 84/84 |
|
||||
| 独立视觉/布局回归 | 44/44,四配置各11个状态 |
|
||||
|
||||
原生回归保留原有18项,新增14键词组歧义与模式保护、连续输入、完整候选分页三项。实际点击原生按钮,检查“你好/不好”共存、未确认组合禁止完成/切模式、只消费选词匹配编码和候选前后翻页。全拼与英文额外检查实际三排顺序。
|
||||
|
||||
视觉状态覆盖首页、九宫、QWERTY全拼、四候选、编辑、14键、14键完整候选、英文、数字及两组符号。检查可见Button/ImageBox/Label是否超出任一父容器,容忍1px原生舍入;圆屏按钮/图标四角限制半径230px,实测按钮最大半径227.86px,小于屏幕233px。全部44个状态的指定边缘采样带黑色缺口像素为0。此结论针对验收状态,不代替未来新增页面的检查。
|
||||
|
||||
Inspector空闲时可能将新树关联到旧back buffer。视觉脚本通过给提示文本暂加空格后恢复,触发真实原生重绘,再重复snapshot保留新帧。没有绘制替代界面。`UI_PREVIEW.png`排除模拟器外壳周边像素,所有按钮完整保留;未裁剪原图及控件树在证据ZIP中。
|
||||
|
||||
## 查询与内存
|
||||
|
||||
八个全拼样本依次为nihao、zhongguo、shijie、shurufa、beijing、tianqi、shanghai、gongzuo,原生记录如下,单位ms:
|
||||
|
||||
| 配置 | 八个全拼样本耗时 |
|
||||
| --- | --- |
|
||||
| round | 80, 45, 42, 41, 44, 45, 46, 43 |
|
||||
| rect | 78, 45, 44, 22, 44, 45, 45, 45 |
|
||||
| round-quickapp | 79, 45, 44, 22, 44, 46, 64, 44 |
|
||||
| rect-quickapp | 77, 44, 43, 23, 44, 45, 47, 43 |
|
||||
|
||||
上述为模拟器全拼查词样本;不是专门的14键基准,也不表示真机端到端延迟、帧率或功耗。14键原生用例等待词库稳定上限5秒并验证实际结果,未另行声明毫秒级指标。
|
||||
|
||||
共6779个分片(4194九宫/全拼、2585十四键),最大116,686字节;缓存最多四项、解码JSON文本合计90,000字符,大分片不驻留。未扩大模拟器内存。
|
||||
|
||||
## 运行包与交付
|
||||
|
||||
| 配置 | 实际运行 bundle.pkg SHA-256 |
|
||||
| --- | --- |
|
||||
| round | `D0D0BBCBACA78A3300F9CE7538BD39CC0810BCF9872B3B42F0E4ED040EDB0A51` |
|
||||
| rect | `767D7A034D943D4B2D75CBE1EA623BEAAEF8CCBF0033B7944A3763513E74B291` |
|
||||
| round-quickapp | `C82E78F47B4CDCBD33B6E39D2C08005CCFFD617608336A17CF5C4F808370E31F` |
|
||||
| rect-quickapp | `A1CA39663B85D36FF464BE5D62949C7ADEFD2EFA7939C8E65ABD90DF41F192E8` |
|
||||
|
||||
提交目录包含两屏QuickApp完整6文件,`crab-keyboard-1.1.0-quickapp.zip`为打包副本,源码和`keyboard-emulator-evidence.zip`另行提供。`results.json`与`visual-results.json`分别记录操作和视觉结果,交付脚本核对两份证据使用同一构建包;`archive-hashes.json`及`DELIVERY_INTEGRITY.json`记录归档哈希、CRC及敏感扫描。
|
||||
|
||||
自动弹出仅覆盖本应用与已接入宿主,厂商公开API没有已验证的系统级IME注册能力。输入法全离线,无账号或联网请求;浏览器联网维持统一账号验证。按用户确认仅做模拟器和提交包验收;RTOS真机安装、目标固件行为、性能、续航及平台审核未验收。
|
||||
@@ -0,0 +1,21 @@
|
||||
# 蟹蟹输入 RTOS 1.2.0 验收
|
||||
|
||||
日期:2026-11-14,版本 1.2.0 / 10200,包名 `com.sydxky.rtos.inputmethod`。
|
||||
|
||||
完整候选改为 4×4 网格。单字占一列,每页最多 16 项;词组按长度扩宽或增加行高,保留原顺序与全部候选,展开页不显示拼音。查询“shi”时实际 90 项、自适应分为 7 页。QWERTY 和 14 键布局维持原有正确顺序。
|
||||
|
||||
| 验证 | 结果 |
|
||||
| --- | --- |
|
||||
| 引擎逻辑 | 44 项断言通过,包含单字网格、混合词组、长词、分页完整性 |
|
||||
| UX 脚本 | 32 项断言通过 |
|
||||
| 原始词库 | 104,837 组索引、163,238 候选,无损转换 |
|
||||
| 构建 | 普通/QuickApp 圆屏 466×466、方屏 390×450 四配置通过 |
|
||||
| 真实原生操作 | 84/84 通过 |
|
||||
| 操作后布局 | 84/84 通过 |
|
||||
| 视觉状态 | 48/48 通过,包括候选网格与词组候选 |
|
||||
|
||||
厂商工具 `gx v0.10.1 / 3ed38835`,内存配置保持 8192 KiB。使用真实 Inspector、原生控件树及截图,不用浏览器绘制页面代替运行时验收。当前页至多 16 项进入响应式状态,分页元数据不进入深度响应式遍历,页面销毁时清理候选及定时任务。
|
||||
|
||||
QuickApp 两屏交付各包含 `bundle.pkg`、`icon.png`、`manifest.json`。实际截图、汇总报告和产物 SHA-256 另附发行版。Android 应用内组件验证见同仓库 Android 文档。
|
||||
|
||||
按用户确认,本平台仅完成模拟器及提交包验收;RTOS 真机安装、目标固件长期运行、性能/功耗与商店审核尚未验收。
|
||||
@@ -26,6 +26,23 @@ export function digitsOf(text) {
|
||||
return result
|
||||
}
|
||||
export function chars(text){return Array.from(String(text||''))}
|
||||
// Four columns and four row units. Phrases take more width/height, never ellipsis.
|
||||
export function candidatePages(candidates) {
|
||||
const pages=[];let rows=[],usedRows=0,row=null
|
||||
function flushRow(){if(row){rows.push(row);usedRows+=row.units;row=null}}
|
||||
function flushPage(){flushRow();if(rows.length)pages.push(rows);rows=[];usedRows=0}
|
||||
for(const item of candidates){
|
||||
const length=chars(item.word).length
|
||||
const span=length===1?1:length<=4?2:4
|
||||
const units=span===4?Math.max(1,Math.ceil(length/12)):1
|
||||
if(row&&(row.slots+span>4||row.units!==units))flushRow()
|
||||
if(usedRows+units>4)flushPage()
|
||||
if(!row)row={items:[],slots:0,units}
|
||||
row.items.push({item,span});row.slots+=span
|
||||
if(row.slots===4)flushRow()
|
||||
}
|
||||
flushPage();return pages
|
||||
}
|
||||
export function edit(text,cursor,insertion,maxLength) {
|
||||
const value=chars(text), at=Math.max(0,Math.min(value.length,cursor))
|
||||
const room=Math.max(0,maxLength-value.length), added=chars(insertion).slice(0,room)
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<div class="ime">
|
||||
<div class="top"><button id="ime-cancel" class="cancel icon-button" on:click="cancel"><image class="icon" src="/assets/ime-icons/close-line.png" /></button><p class="title">{{title}}</p><p class="count">{{count}} 字</p></div>
|
||||
<button class="preview" on:click="showTools">{{count ? display : '输入文字…'}}</button>
|
||||
<div class="composition"><p class="spelling">{{compositionDisplay || modeLabel}}</p><p class="hint">{{hint}}</p></div>
|
||||
<div if="panel !== 'candidates'" class="composition"><p class="spelling">{{compositionDisplay || modeLabel}}</p><p class="hint">{{hint}}</p></div>
|
||||
|
||||
<div if="panel === 'keys'" class="body">
|
||||
<div if="mode === 't9' || mode === 'py' || mode === '14' || mode === 'sym' || inputType === 'url'" class="candidates"><button for="item in bar" class="candidate" on:click="choose(item)">{{item.word}}</button><button if="!composition && inputType === 'url'" class="candidate" on:click="insert('https://')">https://</button><button if="!composition && inputType === 'url'" class="candidate" on:click="insert('.com')">.com</button><button if="mode === 'sym'" class="candidate" on:click="symbols">下一组符号 {{symbolPage+1}}/2</button><p if="!total && !busy && mode !== 'sym' && inputType !== 'url'" class="candidate-placeholder">{{secret ? '私密输入' : '选择候选词'}}</p><button if="mode !== 'sym' && total" id="ime-expand" class="more icon-button" on:click="expand"><image class="icon" src="/assets/ime-icons/arrow-down-s-line.png" /></button></div>
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
<div if="panel === 'candidates'" class="body expanded">
|
||||
<p class="page-info">完整候选 · {{total}} 项 · {{candidatePage + 1}}/{{pageCount}}</p>
|
||||
<div class="full-list"><button for="item in visibleCandidates" class="full-candidate" on:click="choose(item)"><p class="word">{{item.word}}</p><p class="reading">{{item.pinyin}}</p></button><p if="total === 0" class="empty">{{busy ? '正在查词…' : '继续输入拼音后查看候选'}}</p></div>
|
||||
<div class="full-list"><div for="row in candidateRows" class="candidate-grid-row" :style="row.style"><button for="entry in row.items" class="full-candidate" :style="entry.style" on:click="choose(entry.item)"><p class="word" :style="entry.labelStyle">{{entry.item.word}}</p></button></div><p if="total === 0" class="empty">{{busy ? '正在查词…' : '继续输入后查看候选'}}</p></div>
|
||||
<div class="pager"><button id="ime-page-prev" class="icon-button" on:click="previousPage"><image class="icon" src="/assets/ime-icons/arrow-left-s-line.png" /></button><button id="ime-page-next" class="icon-button" on:click="nextPage"><image class="icon" src="/assets/ime-icons/arrow-right-s-line.png" /></button></div>
|
||||
<div class="bottom"><button id="ime-back" class="icon-button" on:click="closePanel"><image class="icon" src="/assets/ime-icons/keyboard-box-line.png" /></button><button id="ime-delete" class="delete icon-button" on:click="backspace"><image class="icon" src="/assets/ime-icons/delete-back-2-line.png" /></button><button id="ime-finish" class="done icon-button" on:click="finish"><image class="icon" src="/assets/ime-icons/check-line.png" /></button></div>
|
||||
</div>
|
||||
@@ -47,22 +47,22 @@
|
||||
.keyrow{align-items:stretch;align-self:center;width:100%}.key{display:flex;flex-direction:column;align-items:center;justify-content:center;flex:1;height:52px;min-width:0;background-color:#FFFFFF;border-radius:12px}.key-label{font-size:.88rem;color:#22252E}.key-sub{font-size:.35rem;color:#9DA2AD;margin-top:1px}
|
||||
.modes{height:36px;margin-top:3px}.modes button{flex:1;height:31px;background-color:transparent;font-size:.53rem;color:#636A77}
|
||||
.bottom{height:41px;margin-top:2px}.bottom button{flex:1;height:37px;font-size:.6rem;background-color:#FFFFFF}.bottom .done{background-color:#2867D9;color:#FFFFFF}.bottom .delete{background-color:#D9DDE4;color:#515866}
|
||||
.page-info{text-align:center;color:#8B919D;font-size:.45rem;margin:5px 0;flex-shrink:0}.full-list{flex:1;justify-content:center}.full-candidate{display:flex;flex-direction:row;align-items:center;justify-content:space-between;height:41px;margin:4px 5px;padding:0 16px;background-color:#FFFFFF}.word{font-size:.8rem;color:#22252E}.reading{font-size:.4rem;color:#A1A7B1;margin-left:7px}.empty{margin:15px 8px;color:#9299A5;text-align:center}
|
||||
.page-info{text-align:center;color:#8B919D;font-size:.45rem;margin:5px 0;flex-shrink:0}.full-list{flex:1;justify-content:center}.candidate-grid-row{display:flex;flex-direction:row;align-self:flex-start;align-items:stretch;flex-shrink:0}.full-candidate{display:flex;align-items:center;justify-content:center;flex:1;min-width:0;margin:2px;padding:2px 4px;background-color:#FFFFFF;border-radius:10px}.word{width:100%;text-align:center;font-size:25px;color:#22252E}.empty{margin:15px 8px;color:#9299A5;text-align:center}
|
||||
.pager{height:38px}.pager button{flex:1;height:34px;background-color:transparent;color:#2867D9;font-size:.55rem}.toolrow button{flex:1;height:40px;font-size:.57rem}.tools{justify-content:center}.tools .danger{color:#C85656}.tools .bottom{margin-top:8px}
|
||||
@media (shape:circle){.ime{padding:30px 8px}.top{align-self:center;width:186px;height:24px}.cancel{height:24px}.preview{align-self:center;width:280px;height:32px;margin-top:5px}.composition{align-self:center;width:300px;height:19px}.candidates{align-self:center;width:352px;height:38px}.candidate{height:34px}.more{height:34px}.keypad{align-self:center;width:376px}.key{height:49px}.modes{align-self:center;width:286px;height:33px}.modes button{height:29px}.bottom{align-self:center;width:200px;height:38px}.bottom button{height:34px}.full-list{align-self:center;width:320px}.full-candidate{height:37px;margin:2px 5px}.pager{align-self:center;width:276px}.toolrow{align-self:center;width:300px}.toolrow button{height:35px}.tools .page-info{align-self:center;width:280px}.tools .bottom{margin-top:4px}.full-candidate .word{font-size:.7rem}}
|
||||
@media (shape:circle){.ime{padding:30px 8px}.top{align-self:center;width:186px;height:24px}.cancel{height:24px}.preview{align-self:center;width:280px;height:32px;margin-top:5px}.composition{align-self:center;width:300px;height:19px}.candidates{align-self:center;width:352px;height:38px}.candidate{height:34px}.more{height:34px}.keypad{align-self:center;width:376px}.key{height:49px}.modes{align-self:center;width:286px;height:33px}.modes button{height:29px}.bottom{align-self:center;width:200px;height:38px}.bottom button{height:34px}.full-list{align-self:center;width:340px}.pager{align-self:center;width:276px}.toolrow{align-self:center;width:300px}.toolrow button{height:35px}.tools .page-info{align-self:center;width:280px}.tools .bottom{margin-top:4px}}
|
||||
</style>
|
||||
<script>
|
||||
import file from '@system.file'
|
||||
import storage from '@system.storage'
|
||||
import router from '@system.router'
|
||||
import {createDictionary,chars,edit,erase,preview,learn,sanitizePreferences,KEY14_GROUPS,display14} from '../common/ime-engine.js'
|
||||
import {createDictionary,chars,candidatePages,edit,erase,preview,learn,sanitizePreferences,KEY14_GROUPS,display14} from '../common/ime-engine.js'
|
||||
|
||||
const LETTERS=['qwertyuiop','asdfghjkl','zxcvbnm']
|
||||
const SYMBOLS=['!"#$%&',"'()*+,",'-./:;<','=>?@[\\',']^_`{|','}~,。?!']
|
||||
function keys(rows){return rows.map(row=>Array.from(row).map(value=>({label:value,value:value,sub:''})))}
|
||||
export default {
|
||||
data:{initial:'',title:'蟹蟹输入',inputType:'text',maxLength:2048,allowNewline:true,secret:false,text:'',cursor:0,composition:'',compositionDisplay:'',display:'│',count:0,limit:2048,mode:'t9',modeLabel:'九宫拼音',upper:false,symbolPage:0,panel:'keys',keyRows:[],layoutRows:[],keyStyle:'',labelStyle:'',bar:[],visibleCandidates:[],total:0,candidatePage:0,pageCount:1,busy:false,hint:'离线全量词库',learning:false,clearArmed:false},
|
||||
dictionary:null,preferences:{},allCandidates:[],active:true,life:null,requestId:0,lookupTimer:null,repeatTimer:null,repeatDelay:null,result:null,original:'',
|
||||
data:{initial:'',title:'蟹蟹输入',inputType:'text',maxLength:2048,allowNewline:true,secret:false,text:'',cursor:0,composition:'',compositionDisplay:'',display:'│',count:0,limit:2048,mode:'t9',modeLabel:'九宫拼音',upper:false,symbolPage:0,panel:'keys',keyRows:[],layoutRows:[],keyStyle:'',labelStyle:'',bar:[],visibleCandidates:[],candidateRows:[],total:0,candidatePage:0,pageCount:1,busy:false,hint:'离线全量词库',learning:false,clearArmed:false},
|
||||
dictionary:null,preferences:{},allCandidates:[],pages:[],active:true,life:null,requestId:0,lookupTimer:null,repeatTimer:null,repeatDelay:null,result:null,original:'',
|
||||
onInit(){
|
||||
this.life={active:true,id:0};this.secret=!!this.secret||this.inputType==='password'
|
||||
this.limit=Math.floor(Math.max(1,Math.min(4096,Number(this.maxLength)||2048)))
|
||||
@@ -74,7 +74,7 @@
|
||||
},
|
||||
onShow(){this.active=true;this.life.active=true;if(this.composition)this.schedule()},
|
||||
onHide(){this.active=false;this.life.active=false;this.invalidate();this.stopRepeat();if(this.secret)this.clearPrivate()},
|
||||
clearPrivate(){this.active=false;this.life.active=false;this.invalidate();this.stopRepeat();this.text='';this.initial='';this.original='';this.composition='';this.allCandidates=[];this.bar=[];this.visibleCandidates=[];this.total=0;this.updateDisplay()},
|
||||
clearPrivate(){this.active=false;this.life.active=false;this.invalidate();this.stopRepeat();this.text='';this.initial='';this.original='';this.composition='';this.allCandidates=[];this.bar=[];this.visibleCandidates=[];this.candidateRows=[];this.pages=[];this.total=0;this.updateDisplay()},
|
||||
onDestroy(){this.active=false;this.life.active=false;this.invalidate();this.stopRepeat();if(this.dictionary)this.dictionary.clear();this.allCandidates=[];this.preferences={};this.text='';this.initial='';this.original='';router.close(this.$page,this.result||{accepted:false});this.result=null},
|
||||
invalidate(){this.requestId++;this.life.id=this.requestId;if(this.lookupTimer!==null){clearTimeout(this.lookupTimer);this.lookupTimer=null}this.busy=false},
|
||||
updateDisplay(){this.count=chars(this.text).length;this.display=preview(this.text,this.cursor,this.secret);this.compositionDisplay=this.mode==='14'?display14(this.composition):this.composition},
|
||||
@@ -105,7 +105,7 @@
|
||||
},
|
||||
schedule(){
|
||||
this.compositionDisplay=this.mode==='14'?display14(this.composition):this.composition
|
||||
this.invalidate();this.allCandidates=[];this.bar=[];this.visibleCandidates=[];this.total=0;this.pageCount=1;this.candidatePage=0
|
||||
this.invalidate();this.allCandidates=[];this.bar=[];this.visibleCandidates=[];this.candidateRows=[];this.pages=[];this.total=0;this.pageCount=1;this.candidatePage=0
|
||||
if(!this.composition||!this.active)return
|
||||
const id=this.requestId;this.busy=true;this.hint='正在查词…'
|
||||
this.lookupTimer=setTimeout(()=>{this.lookupTimer=null;this.query(id)},45)
|
||||
@@ -114,11 +114,11 @@
|
||||
const raw=this.composition,life=this.life
|
||||
try{const result=await this.dictionary.lookup(raw,this.mode==='14'?'14':this.mode==='t9',this.learning?this.preferences:{})
|
||||
if(!life.active||id!==life.id)return
|
||||
this.allCandidates=result;this.total=result.length;this.bar=result.slice(0,3);this.pageCount=Math.max(1,Math.ceil(result.length/4));this.pageCandidates();this.hint=result.length?result.length+' 个候选':'暂无匹配 · 可退格修改'
|
||||
this.allCandidates=result;this.total=result.length;this.bar=result.slice(0,3);this.pages=candidatePages(result);this.pageCount=Math.max(1,this.pages.length);this.pageCandidates();this.hint=result.length?result.length+' 个候选':'暂无匹配 · 可退格修改'
|
||||
}catch(error){if(life.active&&id===life.id)this.hint='词库暂不可用,可切换英文输入'}
|
||||
finally{if(life.active&&id===life.id)this.busy=false}
|
||||
},
|
||||
pageCandidates(){this.visibleCandidates=this.allCandidates.slice(this.candidatePage*4,this.candidatePage*4+4)},
|
||||
pageCandidates(){const rows=this.pages[this.candidatePage]||[];this.visibleCandidates=[];this.candidateRows=rows.map(row=>({style:'height:'+(row.units*43)+'px;width:'+(row.slots*25)+'%',items:row.items.map(entry=>{this.visibleCandidates.push(entry.item);return {item:entry.item,style:'flex:'+entry.span+';height:'+(row.units*43-4)+'px',labelStyle:'font-size:'+(entry.span===1?25:entry.span===2?23:20)+'px'}})}))},
|
||||
choose(item){
|
||||
if(this.busy||!this.composition||!this.allCandidates.some(x=>x.word===item.word&&x.pinyin===item.pinyin&&x.consume===item.consume))return
|
||||
if(chars(this.text).length+chars(item.word).length>this.limit){this.hint='剩余空间不足,未截断候选词';return}
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
"package": "com.sydxky.rtos.inputmethod",
|
||||
"name": "蟹蟹输入",
|
||||
"icon": "/assets/icon.png",
|
||||
"versionName": "1.1.0",
|
||||
"versionCode": 10100,
|
||||
"versionName": "1.2.0",
|
||||
"versionCode": 10200,
|
||||
"features": [],
|
||||
"router": {"entry": "main", "pages": {"main": {"component": "index"}, "keyboard": {"component": "index"}}},
|
||||
"config": {"designWidth": 410}
|
||||
|
||||
@@ -32,6 +32,7 @@ p=page({inputType:'password',initial:'😀secret'});p.toggleLearning();check(!p.
|
||||
p=page({initial:'你😀好'});p.cursor=2;p.backspace();check(p.text==='你好','UI Unicode deletion')
|
||||
p.startDelete();check(p.repeatDelay!==null,'delete hold delay starts');p.onHide();check(p.repeatDelay===null&&p.repeatTimer===null,'hide stops repeat timers')
|
||||
p=page();let resolve;p.dictionary={lookup:()=>new Promise(yes=>resolve=yes)};p.composition='64';const pending=p.query(p.requestId);p.onHide();resolve([{word:'你',consume:2,pinyin:'ni'}]);await pending;check(p.total===0,'late dictionary response cannot update hidden page')
|
||||
p=page();p.mode='py';p.composition='shi';await p.query(p.requestId);p.expand();check(p.visibleCandidates.length===16&&p.candidateRows.length===4,'expanded single characters use 16 real candidate cells');p.nextPage();check(p.candidatePage===1&&p.visibleCandidates[0]===p.allCandidates[16],'next grid page continues without omissions');check(!ux.includes('item.pinyin}}')&&!ux.includes('class="reading"'),'expanded cells do not display pronunciation');
|
||||
p=page({maxLength:2,initial:'你'});p.composition='nihao';p.allCandidates=[{word:'你好',consume:5,pinyin:'nihao'}];p.choose(p.allCandidates[0]);check(p.text==='你'&&p.composition==='nihao','candidate is not silently truncated')
|
||||
p.finish();check(p.composition==='nihao','finish does not silently drop uncommitted composition')
|
||||
p=page({inputType:'number'});p.insert('12x😀.3');check(p.text==='12.3','number field filters nonnumeric input')
|
||||
|
||||
@@ -2,7 +2,7 @@ import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import assert from 'node:assert/strict'
|
||||
import {fileURLToPath} from 'node:url'
|
||||
import {createDictionary,digitsOf,code14Of,KEY14_GROUPS,display14,edit,erase,preview,learn,sanitizePreferences,chars} from '../src/common/ime-engine.js'
|
||||
import {createDictionary,digitsOf,code14Of,KEY14_GROUPS,display14,candidatePages,edit,erase,preview,learn,sanitizePreferences,chars} from '../src/common/ime-engine.js'
|
||||
const root=path.resolve(path.dirname(fileURLToPath(import.meta.url)),'../src')
|
||||
let count=0
|
||||
function check(ok,label){assert.ok(ok,label);count++}
|
||||
@@ -34,6 +34,13 @@ check((await dictionary.lookup(pairInput,'14',{[pairInput]:[{word:pairLast.word,
|
||||
check((await dictionary.lookup('hello!','14',{})).length===0,'14-key rejects invalid token stream')
|
||||
check(display14('c3754')==='BN UI GH AS OP'&&display14('c3754'.repeat(6)).length<24,'14-key preview displays bounded paired-letter groups')
|
||||
const many=await dictionary.lookup('shi',false,{})
|
||||
const grid=candidatePages(many),flatten=pages=>pages.flatMap(rows=>rows.flatMap(row=>row.items.map(entry=>entry.item)))
|
||||
check(grid[0].length===4&&flatten([grid[0]]).length===16,'single characters form a four by four page')
|
||||
check(flatten(grid).every((item,index)=>item===many[index]),'grid retains all candidates in their exact original order')
|
||||
const phrases=[{word:'你好'},{word:'完整词组'},{word:'很长的词组必须能够完整显示而不能被截断'},{word:'😀'}]
|
||||
const phraseGrid=candidatePages(phrases)
|
||||
check(flatten(phraseGrid).length===phrases.length&&phraseGrid[0][0].items.length===2,'phrases use wider cells without dropping results')
|
||||
check(phraseGrid.every(rows=>rows.reduce((sum,row)=>sum+row.units,0)<=4),'long phrases use row units within the page budget')
|
||||
const original=JSON.parse(fs.readFileSync(root+'/assets/ime-dict/s'+digitsOf('shi')+'.json','utf8')).entries.shi
|
||||
check(original.every(word=>many.some(x=>x.word===word&&x.consume===3)),'every original shi candidate remains accessible')
|
||||
check(dictionary.cacheSize<=4,'bounded shard cache')
|
||||
|
||||
@@ -1,76 +1,76 @@
|
||||
"""Real Glyphix input acceptance on four ordinary/QuickApp profiles."""
|
||||
import argparse,datetime,hashlib,importlib.util,json,os,pathlib,time
|
||||
APP=pathlib.Path(__file__).resolve().parents[1]
|
||||
spec=importlib.util.spec_from_file_location('emu',APP.parent/'tests/emulator-validate.py')
|
||||
emu=importlib.util.module_from_spec(spec);spec.loader.exec_module(emu)
|
||||
emu.APPS=['inputmethod','imefixture']
|
||||
layout_spec=importlib.util.spec_from_file_location('layout',APP/'tests/layout.py')
|
||||
layout=importlib.util.module_from_spec(layout_spec);layout_spec.loader.exec_module(layout)
|
||||
|
||||
def save(path,data):
|
||||
temporary=path.with_suffix('.tmp')
|
||||
with temporary.open('w',encoding='utf-8') as handle:
|
||||
json.dump(data,handle,ensure_ascii=False,indent=2);handle.flush();os.fsync(handle.fileno())
|
||||
os.replace(temporary,path)
|
||||
|
||||
def main():
|
||||
parser=argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('--profiles',nargs='+',default=emu.PROFILES)
|
||||
parser.add_argument('--work',type=pathlib.Path,required=True)
|
||||
parser.add_argument('--output',type=pathlib.Path,required=True)
|
||||
args=parser.parse_args();args.output.mkdir(parents=True,exist_ok=True)
|
||||
report={'recorded_at':datetime.datetime.now(datetime.timezone(datetime.timedelta(hours=8))).isoformat(),'method':'real native @system.test, no networking or real credentials','artifacts':{},'cases':[],'layouts':[]}
|
||||
for profile in args.profiles:
|
||||
process,artifacts=emu.start(profile,args.work,'inputmethod');report['artifacts'][profile]=artifacts
|
||||
def run(name,body,timeout=45):
|
||||
case=profile+'-'+name
|
||||
emu.evaluate('var caseName='+json.dumps(case)+';')
|
||||
emu.evaluate('var caseBody=async function(){'+body+'};')
|
||||
emu.evaluate("tc.testcase(caseName,async function(){await caseBody();console.log('IME_PASS '+caseName)});tc.run(caseName)")
|
||||
logs=[];passed=finished=False;deadline=time.monotonic()+timeout
|
||||
while time.monotonic()<deadline:
|
||||
time.sleep(.1);fresh=[x for x in emu.request('/logs') if x.get('tag')!='eval'];logs.extend(fresh)
|
||||
passed=passed or any('IME_PASS '+case in x.get('body','') for x in fresh)
|
||||
finished=finished or any(x.get('tag')=='tester' and 'testcase '+case+' finished' in x.get('body','') for x in fresh)
|
||||
if passed and finished:break
|
||||
if any('terminated:' in x.get('body','') or 'Uncaught' in x.get('body','') or 'Exception:' in x.get('body','') for x in fresh):break
|
||||
entry={'profile':profile,'case':name,'passed':passed and finished,'checks':[x['body'].strip() for x in logs if 'IME_CHECK ' in x.get('body','')]}
|
||||
report['cases'].append(entry);save(args.output/'results.json',report);save(args.output/(case+'.logs.json'),logs)
|
||||
time.sleep(.35) # Settle native route/layout animations before capturing.
|
||||
emu.snapshot(case,args.output)
|
||||
time.sleep(.25);emu.snapshot(case,args.output) # Read a freshly composited frame.
|
||||
geometry=layout.inspect(args.output/(case+'.tree.json'),profile)
|
||||
report['layouts'].append({'profile':profile,'case':name,**geometry});save(args.output/'results.json',report)
|
||||
print(json.dumps(entry,ensure_ascii=False),flush=True)
|
||||
if not entry['passed']:raise RuntimeError('Native failure '+case+': '+str(logs[-4:]))
|
||||
if not geometry['passed']:raise RuntimeError('Clipped native layout '+case+': '+str(geometry['failures'][:3]))
|
||||
try:
|
||||
emu.evaluate("var router=require('@system.router').default,tc=require('@system.test').default,launch=require('@system.launch').default,root=router.getPage(0),p=null;function check(ok,m){if(!ok)throw new Error(m);console.log('IME_CHECK '+m)};async function click(t){var ids={'取消':'ime-cancel','空格':'ime-space','删除':'ime-delete','完成':'ime-finish','更多':'ime-expand','编辑':'ime-tools','上一页':'ime-page-prev','下一页':'ime-page-next','键盘':'ime-back','返回键盘':'ime-back'};if(ids[t]){await tc.getById(ids[t]).click();return}var pattern=t.split('').map(function(c){return String.fromCharCode(92)+'u'+('0000'+c.charCodeAt(0).toString(16)).slice(-4)}).join('');await tc.getHasText(new RegExp('^'+pattern+'$')).click()}")
|
||||
emu.evaluate("async function ready(){var end=Date.now()+5000;while(p.busy&&Date.now()<end)await tc.wait(.05);check(!p.busy,'dictionary settled')};function reset(){p.invalidate();p.text='';p.cursor=0;p.composition='';p.allCandidates=[];p.bar=[];p.total=0;p.panel='keys';p.mode='t9';p.limit=2048;p.renderKeys();p.updateDisplay()}")
|
||||
run('auto-open',"await tc.getByClass('document').click();await tc.wait(.4);p=router.getPage(1);check(router.getLength()===2&&p.mode==='t9','field opens full screen T9');root.editText();check(router.getLength()===2,'duplicate opener guarded');")
|
||||
run('t9-phrase',"for(var label of ['MNO','GHI','GHI','ABC','MNO'])await click(label);await ready();check(p.bar[0].word==='你好','frequency ranks common phrase first');await click('你好');check(p.text==='你好'&&!p.composition,'native candidate commits phrase');")
|
||||
run('continuous',"reset();p.mode='py';p.renderKeys();p.composition='nihaoshijie';p.schedule();await ready();check(p.allCandidates.some(x=>x.word==='你好'&&x.consume===5),'prefix phrase retained');await click('你好');await ready();check(p.composition==='shijie','only matched pinyin consumed');await click('世界');check(p.text==='你好世界'&&!p.composition,'remaining syllables compose');")
|
||||
run('complete-candidates',"reset();p.mode='py';p.renderKeys();p.composition='shi';p.schedule();await ready();var total=p.total;await click('更多');check(p.visibleCandidates.length===4&&p.pageCount>5,'full list paginated');for(var i=1;i<p.pageCount;i++)await click('下一页');check(p.candidatePage===p.pageCount-1,'last candidate page accessible');")
|
||||
run('candidate-select',"var last=p.visibleCandidates[p.visibleCandidates.length-1];await click(last.word);check(p.text===last.word,'last original candidate can commit');")
|
||||
run('full-pinyin',"reset();await click('26键');check(p.keyRows.map(row=>row.map(key=>key.value).join('')).join('/')==='qwertyuiop/asdfghjkl/zxcvbnm','standard 10-9-7 QWERTY order');for(var letter of ['s','h','u','r','u','f','a'])await click(letter);await ready();check(p.bar.some(x=>x.word==='输入法'),'long phrase in full dictionary');await click('输入法');check(p.text==='输入法','native full pinyin commit');")
|
||||
run('14-key-phrase',"reset();await click('14键');check(p.mode==='14'&&p.keyRows.flat().length===14,'14-key mode has fourteen real buttons');for(var label of ['BN','UI','GH','AS','OP'])await click(label);await ready();check(p.bar.some(x=>x.word==='你好')&&p.allCandidates.some(x=>x.word==='不好'),'paired ambiguity retains both valid phrases');await click('完成');check(router.getLength()===2,'14-key uncommitted input blocks finish');await click('123');check(p.mode==='14'&&p.composition==='c3754','14-key mode switch preserves composition');await click('你好');check(p.text==='你好'&&!p.composition,'native 14-key phrase commit');")
|
||||
run('14-key-continuous',"reset();await click('14键');for(var label of ['BN','UI','GH','AS','OP','AS','GH','UI','JK','UI','ER'])await click(label);await ready();await click('你好');await ready();check(p.composition.length===6,'14-key consumes five matched keys only');await click('世界');check(p.text==='你好世界'&&!p.composition,'14-key continuous phrase commit');")
|
||||
run('14-key-candidates',"reset();await click('14键');for(var label of ['AS','GH','UI'])await click(label);await ready();check(p.total>20,'14-key full dictionary candidates');await click('更多');check(p.visibleCandidates.length===4,'14-key first full candidate page');await click('下一页');check(p.candidatePage===1,'14-key native next candidate page');await click('上一页');check(p.candidatePage===0,'14-key native previous candidate page');var selected=p.visibleCandidates[1];await click(selected.word);check(p.text===selected.word,'14-key expanded candidate commit');")
|
||||
run('english',"reset();await click('abc');check(p.keyRows.map(row=>row.map(key=>key.value).join('')).join('/')==='qwertyuiop/asdfghjkl/zxcvbnm','English follows standard QWERTY');await click('abc');await click('Z');await click('ABC');await click('a');check(p.text==='Za','uppercase and lowercase native keys');")
|
||||
run('symbols',"reset();await click('符号');for(var symbol of ['@','%','&','?',',','。']){if(!p.keyRows.some(row=>row.some(key=>key.value===symbol)))await click('符号');await click(symbol)}check(p.text==='@%&?,。','ASCII and Chinese punctuation native keys');")
|
||||
run('cursor-unicode',"reset();p.text='你😀好';p.cursor=2;p.updateDisplay();await click('编辑');await click('删除一字');check(p.text==='你好','Unicode-safe deletion');await click('到开头');await click('返回键盘');await click('abc');await click('a');check(p.text==='a你好','insertion at moved cursor');")
|
||||
run('hold-delete',"reset();p.mode='en';p.renderKeys();p.text='abcdefghijklmnop';p.cursor=16;p.updateDisplay();await tc.wait(.25);await tc.getById('ime-delete').scroll(0,0,.95);await tc.wait(.1);check(p.text.length<15,'hold repeats delete');check(p.repeatTimer===null&&p.repeatDelay===null,'release stops repeats');")
|
||||
run('length-and-mode',"reset();p.limit=2;p.text='你';p.cursor=1;p.mode='py';p.renderKeys();p.composition='nihao';p.schedule();await ready();await click('你好');check(p.text==='你'&&p.composition==='nihao','overlong candidate not truncated');await click('完成');check(router.getLength()===2,'uncommitted composition not dropped');p.mode='t9';p.composition='64';p.renderKeys();p.english();check(p.mode==='t9'&&p.composition==='64','mode switch preserves uncommitted composition');")
|
||||
run('learning',"reset();await click('编辑');check(!p.learning,'learning defaults off');await click('学习:关');check(p.learning,'learning opt-in');await click('返回键盘');p.composition='64';p.schedule();await ready();await click(p.bar[1].word);check(Object.keys(p.preferences).length>0,'only chosen candidate learned');await click('编辑');await click('清除学习');check(Object.keys(p.preferences).length===0,'learning cleared');await click('学习:开');await click('返回键盘');")
|
||||
run('cancel',"p.text='discard';p.composition='';await click('取消');await tc.wait(.4);check(root.text===''&&router.getLength()===1,'cancel keeps original document');")
|
||||
run('numeric',"await click('数字输入');await tc.wait(.3);p=router.getPage(1);check(p.mode==='num','numeric field auto keyboard');for(var digit of ['1','2','3','0'])await click(digit);await click('完成');await tc.wait(.3);check(root.status.indexOf('1230')>=0,'numeric result returned');")
|
||||
run('password-privacy',"await click('私密输入');await tc.wait(.3);p=router.getPage(1);await click('s');await click('e');check(p.secret&&p.display.indexOf('s')<0&&!p.learning,'secret preview masked');await click('编辑');await click('学习:关');check(!p.learning,'secret cannot enable learning');await launch.launch('com.sydxky.rtos.imefixture');await tc.wait(.9);check(!p.active&&!p.text&&!p.initial,'secret erased on actual app switch');await tc.wait(2);await click('取消');")
|
||||
run('long-text-clear',"await click('开始输入');await tc.wait(.3);p=router.getPage(1);p.text='长😀'.repeat(1000);p.cursor=2000;p.updateDisplay();check(p.count===2000&&p.display.length<60,'long text preview bounded');await click('编辑');await click('清空全文');check(p.count===2000,'clear needs confirmation');await click('确认清空');check(p.count===0,'confirmed clear');await click('返回键盘');")
|
||||
run('lookup-performance',"reset();var times=[];for(var word of ['nihao','zhongguo','shijie','shurufa','beijing','tianqi','shanghai','gongzuo']){var now=Date.now();var candidates=await p.dictionary.lookup(word,false,{});times.push(Date.now()-now);check(candidates.length>0,'lookup '+word)}check(p.dictionary.cacheSize<=4,'bounded decoded shard cache');check(Math.max.apply(null,times)<1000,'emulator lookup under one second');console.log('IME_TIMING '+JSON.stringify(times));")
|
||||
run('finish-save',"reset();p.insert('你好,RTOS!');await click('完成');await tc.wait(.4);check(root.text==='你好,RTOS!'&&router.getLength()===1,'finished text returned to opener');await click('保存草稿');check(root.saved,'explicit local draft save');")
|
||||
finally:
|
||||
if process.poll() is None:process.terminate()
|
||||
process.wait(timeout=10);process.stdin.close()
|
||||
report['passed']=all(x['passed'] for x in report['cases']+report['layouts']);save(args.output/'results.json',report)
|
||||
|
||||
if __name__=='__main__':main()
|
||||
"""Real Glyphix input acceptance on four ordinary/QuickApp profiles."""
|
||||
import argparse,datetime,hashlib,importlib.util,json,os,pathlib,time
|
||||
APP=pathlib.Path(__file__).resolve().parents[1]
|
||||
spec=importlib.util.spec_from_file_location('emu',APP.parent/'tests/emulator-validate.py')
|
||||
emu=importlib.util.module_from_spec(spec);spec.loader.exec_module(emu)
|
||||
emu.APPS=['inputmethod','imefixture']
|
||||
layout_spec=importlib.util.spec_from_file_location('layout',APP/'tests/layout.py')
|
||||
layout=importlib.util.module_from_spec(layout_spec);layout_spec.loader.exec_module(layout)
|
||||
|
||||
def save(path,data):
|
||||
temporary=path.with_suffix('.tmp')
|
||||
with temporary.open('w',encoding='utf-8') as handle:
|
||||
json.dump(data,handle,ensure_ascii=False,indent=2);handle.flush();os.fsync(handle.fileno())
|
||||
os.replace(temporary,path)
|
||||
|
||||
def main():
|
||||
parser=argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('--profiles',nargs='+',default=emu.PROFILES)
|
||||
parser.add_argument('--work',type=pathlib.Path,required=True)
|
||||
parser.add_argument('--output',type=pathlib.Path,required=True)
|
||||
args=parser.parse_args();args.output.mkdir(parents=True,exist_ok=True)
|
||||
report={'recorded_at':datetime.datetime.now(datetime.timezone(datetime.timedelta(hours=8))).isoformat(),'method':'real native @system.test, no networking or real credentials','artifacts':{},'cases':[],'layouts':[]}
|
||||
for profile in args.profiles:
|
||||
process,artifacts=emu.start(profile,args.work,'inputmethod');report['artifacts'][profile]=artifacts
|
||||
def run(name,body,timeout=45):
|
||||
case=profile+'-'+name
|
||||
emu.evaluate('var caseName='+json.dumps(case)+';')
|
||||
emu.evaluate('var caseBody=async function(){'+body+'};')
|
||||
emu.evaluate("tc.testcase(caseName,async function(){await caseBody();console.log('IME_PASS '+caseName)});tc.run(caseName)")
|
||||
logs=[];passed=finished=False;deadline=time.monotonic()+timeout
|
||||
while time.monotonic()<deadline:
|
||||
time.sleep(.1);fresh=[x for x in emu.request('/logs') if x.get('tag')!='eval'];logs.extend(fresh)
|
||||
passed=passed or any('IME_PASS '+case in x.get('body','') for x in fresh)
|
||||
finished=finished or any(x.get('tag')=='tester' and 'testcase '+case+' finished' in x.get('body','') for x in fresh)
|
||||
if passed and finished:break
|
||||
if any('terminated:' in x.get('body','') or 'Uncaught' in x.get('body','') or 'Exception:' in x.get('body','') for x in fresh):break
|
||||
entry={'profile':profile,'case':name,'passed':passed and finished,'checks':[x['body'].strip() for x in logs if 'IME_CHECK ' in x.get('body','')]}
|
||||
report['cases'].append(entry);save(args.output/'results.json',report);save(args.output/(case+'.logs.json'),logs)
|
||||
time.sleep(.35) # Settle native route/layout animations before capturing.
|
||||
emu.snapshot(case,args.output)
|
||||
time.sleep(.25);emu.snapshot(case,args.output) # Read a freshly composited frame.
|
||||
geometry=layout.inspect(args.output/(case+'.tree.json'),profile)
|
||||
report['layouts'].append({'profile':profile,'case':name,**geometry});save(args.output/'results.json',report)
|
||||
print(json.dumps(entry,ensure_ascii=False),flush=True)
|
||||
if not entry['passed']:raise RuntimeError('Native failure '+case+': '+str(logs[-4:]))
|
||||
if not geometry['passed']:raise RuntimeError('Clipped native layout '+case+': '+str(geometry['failures'][:3]))
|
||||
try:
|
||||
emu.evaluate("var router=require('@system.router').default,tc=require('@system.test').default,launch=require('@system.launch').default,root=router.getPage(0),p=null;function check(ok,m){if(!ok)throw new Error(m);console.log('IME_CHECK '+m)};async function click(t){var ids={'取消':'ime-cancel','空格':'ime-space','删除':'ime-delete','完成':'ime-finish','更多':'ime-expand','编辑':'ime-tools','上一页':'ime-page-prev','下一页':'ime-page-next','键盘':'ime-back','返回键盘':'ime-back'};if(ids[t]){await tc.getById(ids[t]).click();return}var pattern=t.split('').map(function(c){return String.fromCharCode(92)+'u'+('0000'+c.charCodeAt(0).toString(16)).slice(-4)}).join('');await tc.getHasText(new RegExp('^'+pattern+'$')).click()}")
|
||||
emu.evaluate("async function ready(){var end=Date.now()+5000;while(p.busy&&Date.now()<end)await tc.wait(.05);check(!p.busy,'dictionary settled')};function reset(){p.invalidate();p.text='';p.cursor=0;p.composition='';p.allCandidates=[];p.bar=[];p.total=0;p.panel='keys';p.mode='t9';p.limit=2048;p.renderKeys();p.updateDisplay()}")
|
||||
run('auto-open',"await tc.getByClass('document').click();await tc.wait(.4);p=router.getPage(1);check(router.getLength()===2&&p.mode==='t9','field opens full screen T9');root.editText();check(router.getLength()===2,'duplicate opener guarded');")
|
||||
run('t9-phrase',"for(var label of ['MNO','GHI','GHI','ABC','MNO'])await click(label);await ready();check(p.bar[0].word==='你好','frequency ranks common phrase first');await click('你好');check(p.text==='你好'&&!p.composition,'native candidate commits phrase');")
|
||||
run('continuous',"reset();p.mode='py';p.renderKeys();p.composition='nihaoshijie';p.schedule();await ready();check(p.allCandidates.some(x=>x.word==='你好'&&x.consume===5),'prefix phrase retained');await click('你好');await ready();check(p.composition==='shijie','only matched pinyin consumed');await click('世界');check(p.text==='你好世界'&&!p.composition,'remaining syllables compose');")
|
||||
run('complete-candidates',"reset();p.mode='py';p.renderKeys();p.composition='shi';p.schedule();await ready();var total=p.total;await click('更多');check(p.visibleCandidates.length===16&&p.candidateRows.length===4&&p.pageCount>1,'full list paginated');for(var i=1;i<p.pageCount;i++)await click('下一页');check(p.candidatePage===p.pageCount-1,'last candidate page accessible');")
|
||||
run('candidate-select',"var last=p.visibleCandidates[p.visibleCandidates.length-1];await click(last.word);check(p.text===last.word,'last original candidate can commit');")
|
||||
run('full-pinyin',"reset();await click('26键');check(p.keyRows.map(row=>row.map(key=>key.value).join('')).join('/')==='qwertyuiop/asdfghjkl/zxcvbnm','standard 10-9-7 QWERTY order');for(var letter of ['s','h','u','r','u','f','a'])await click(letter);await ready();check(p.bar.some(x=>x.word==='输入法'),'long phrase in full dictionary');await click('输入法');check(p.text==='输入法','native full pinyin commit');")
|
||||
run('14-key-phrase',"reset();await click('14键');check(p.mode==='14'&&p.keyRows.flat().length===14,'14-key mode has fourteen real buttons');for(var label of ['BN','UI','GH','AS','OP'])await click(label);await ready();check(p.bar.some(x=>x.word==='你好')&&p.allCandidates.some(x=>x.word==='不好'),'paired ambiguity retains both valid phrases');await click('完成');check(router.getLength()===2,'14-key uncommitted input blocks finish');await click('123');check(p.mode==='14'&&p.composition==='c3754','14-key mode switch preserves composition');await click('你好');check(p.text==='你好'&&!p.composition,'native 14-key phrase commit');")
|
||||
run('14-key-continuous',"reset();await click('14键');for(var label of ['BN','UI','GH','AS','OP','AS','GH','UI','JK','UI','ER'])await click(label);await ready();await click('你好');await ready();check(p.composition.length===6,'14-key consumes five matched keys only');await click('世界');check(p.text==='你好世界'&&!p.composition,'14-key continuous phrase commit');")
|
||||
run('14-key-candidates',"reset();await click('14键');for(var label of ['AS','GH','UI'])await click(label);await ready();check(p.total>20,'14-key full dictionary candidates');await click('更多');check(p.visibleCandidates.length<=16&&p.visibleCandidates.length>4,'14-key adaptive candidate grid');await click('下一页');check(p.candidatePage===1,'14-key native next candidate page');await click('上一页');check(p.candidatePage===0,'14-key native previous candidate page');var selected=p.visibleCandidates[1];await click(selected.word);check(p.text===selected.word,'14-key expanded candidate commit');")
|
||||
run('english',"reset();await click('abc');check(p.keyRows.map(row=>row.map(key=>key.value).join('')).join('/')==='qwertyuiop/asdfghjkl/zxcvbnm','English follows standard QWERTY');await click('abc');await click('Z');await click('ABC');await click('a');check(p.text==='Za','uppercase and lowercase native keys');")
|
||||
run('symbols',"reset();await click('符号');for(var symbol of ['@','%','&','?',',','。']){if(!p.keyRows.some(row=>row.some(key=>key.value===symbol)))await click('符号');await click(symbol)}check(p.text==='@%&?,。','ASCII and Chinese punctuation native keys');")
|
||||
run('cursor-unicode',"reset();p.text='你😀好';p.cursor=2;p.updateDisplay();await click('编辑');await click('删除一字');check(p.text==='你好','Unicode-safe deletion');await click('到开头');await click('返回键盘');await click('abc');await click('a');check(p.text==='a你好','insertion at moved cursor');")
|
||||
run('hold-delete',"reset();p.mode='en';p.renderKeys();p.text='abcdefghijklmnop';p.cursor=16;p.updateDisplay();await tc.wait(.25);await tc.getById('ime-delete').scroll(0,0,.95);await tc.wait(.1);check(p.text.length<15,'hold repeats delete');check(p.repeatTimer===null&&p.repeatDelay===null,'release stops repeats');")
|
||||
run('length-and-mode',"reset();p.limit=2;p.text='你';p.cursor=1;p.mode='py';p.renderKeys();p.composition='nihao';p.schedule();await ready();await click('你好');check(p.text==='你'&&p.composition==='nihao','overlong candidate not truncated');await click('完成');check(router.getLength()===2,'uncommitted composition not dropped');p.mode='t9';p.composition='64';p.renderKeys();p.english();check(p.mode==='t9'&&p.composition==='64','mode switch preserves uncommitted composition');")
|
||||
run('learning',"reset();await click('编辑');check(!p.learning,'learning defaults off');await click('学习:关');check(p.learning,'learning opt-in');await click('返回键盘');p.composition='64';p.schedule();await ready();await click(p.bar[1].word);check(Object.keys(p.preferences).length>0,'only chosen candidate learned');await click('编辑');await click('清除学习');check(Object.keys(p.preferences).length===0,'learning cleared');await click('学习:开');await click('返回键盘');")
|
||||
run('cancel',"p.text='discard';p.composition='';await click('取消');await tc.wait(.4);check(root.text===''&&router.getLength()===1,'cancel keeps original document');")
|
||||
run('numeric',"await click('数字输入');await tc.wait(.3);p=router.getPage(1);check(p.mode==='num','numeric field auto keyboard');for(var digit of ['1','2','3','0'])await click(digit);await click('完成');await tc.wait(.3);check(root.status.indexOf('1230')>=0,'numeric result returned');")
|
||||
run('password-privacy',"await click('私密输入');await tc.wait(.3);p=router.getPage(1);await click('s');await click('e');check(p.secret&&p.display.indexOf('s')<0&&!p.learning,'secret preview masked');await click('编辑');await click('学习:关');check(!p.learning,'secret cannot enable learning');await launch.launch('com.sydxky.rtos.imefixture');await tc.wait(.9);check(!p.active&&!p.text&&!p.initial,'secret erased on actual app switch');await tc.wait(2);await click('取消');")
|
||||
run('long-text-clear',"await click('开始输入');await tc.wait(.3);p=router.getPage(1);p.text='长😀'.repeat(1000);p.cursor=2000;p.updateDisplay();check(p.count===2000&&p.display.length<60,'long text preview bounded');await click('编辑');await click('清空全文');check(p.count===2000,'clear needs confirmation');await click('确认清空');check(p.count===0,'confirmed clear');await click('返回键盘');")
|
||||
run('lookup-performance',"reset();var times=[];for(var word of ['nihao','zhongguo','shijie','shurufa','beijing','tianqi','shanghai','gongzuo']){var now=Date.now();var candidates=await p.dictionary.lookup(word,false,{});times.push(Date.now()-now);check(candidates.length>0,'lookup '+word)}check(p.dictionary.cacheSize<=4,'bounded decoded shard cache');check(Math.max.apply(null,times)<1000,'emulator lookup under one second');console.log('IME_TIMING '+JSON.stringify(times));")
|
||||
run('finish-save',"reset();p.insert('你好,RTOS!');await click('完成');await tc.wait(.4);check(root.text==='你好,RTOS!'&&router.getLength()===1,'finished text returned to opener');await click('保存草稿');check(root.saved,'explicit local draft save');")
|
||||
finally:
|
||||
if process.poll() is None:process.terminate()
|
||||
process.wait(timeout=10);process.stdin.close()
|
||||
report['passed']=all(x['passed'] for x in report['cases']+report['layouts']);save(args.output/'results.json',report)
|
||||
|
||||
if __name__=='__main__':main()
|
||||
@@ -55,6 +55,8 @@ def main():
|
||||
emu.evaluate("p.composition='shi';p.mode='py';p.renderKeys();p.schedule()")
|
||||
time.sleep(.5);capture(profile,'pinyin')
|
||||
emu.evaluate("p.expand()");capture(profile,'candidates')
|
||||
emu.evaluate("p.closePanel();p.composition='nihao';p.schedule()")
|
||||
time.sleep(.5);emu.evaluate("p.expand()");capture(profile,'phrase-candidates')
|
||||
emu.evaluate("p.showTools()");capture(profile,'tools')
|
||||
emu.evaluate("p.composition='';p.setMode('14');p.composition='c3754';p.schedule()")
|
||||
time.sleep(.5);capture(profile,'14-key')
|
||||
|
||||
@@ -1,95 +1,95 @@
|
||||
{
|
||||
"upstream_version": "v4.6.0",
|
||||
"license": "Apache-2.0",
|
||||
"source": "iconfont.cn anonymous public icon search",
|
||||
"icons": [
|
||||
{
|
||||
"name": "delete-back-2-line",
|
||||
"iconfont_id": 39249781,
|
||||
"collection_id": 651928,
|
||||
"user_id": 12586164,
|
||||
"iconfont_url": "https://www.iconfont.cn/collections/detail?cid=651928",
|
||||
"iconfont_svg_sha256": "A6279D35F9FD1578F813BCFA2077FAC98EC8E3A4D7BC8F90A5B3F3F2744C3A59",
|
||||
"upstream": "https://raw.githubusercontent.com/Remix-Design/RemixIcon/v4.6.0/icons/System/delete-back-2-line.svg",
|
||||
"upstream_svg_sha256": "D6FC7F41C0B84FE48A06685DC081780B6CD103AC9B3DB96DC44E624AA0A113E8",
|
||||
"normalized_alpha_difference_512": 4.517798330269608e-06
|
||||
},
|
||||
{
|
||||
"name": "check-line",
|
||||
"iconfont_id": 39249772,
|
||||
"collection_id": 651928,
|
||||
"user_id": 12586164,
|
||||
"iconfont_url": "https://www.iconfont.cn/collections/detail?cid=651928",
|
||||
"iconfont_svg_sha256": "1B194F3B6AC39E6199FB003AC3140CC2ED2D1213201C0BDBF69ABFFABA67F6CE",
|
||||
"upstream": "https://raw.githubusercontent.com/Remix-Design/RemixIcon/v4.6.0/icons/System/check-line.svg",
|
||||
"upstream_svg_sha256": "B83A64BF881169D5EDEAA6134A7BA37192F0DDC80C2E2A890578AE0F2F8F9169",
|
||||
"normalized_alpha_difference_512": 0.0
|
||||
},
|
||||
{
|
||||
"name": "close-line",
|
||||
"iconfont_id": 39249776,
|
||||
"collection_id": 651928,
|
||||
"user_id": 12586164,
|
||||
"iconfont_url": "https://www.iconfont.cn/collections/detail?cid=651928",
|
||||
"iconfont_svg_sha256": "748EA3E8A4FE22671FF3347E998D51679EAF3FF3950F6348F286F6DD36F19A92",
|
||||
"upstream": "https://raw.githubusercontent.com/Remix-Design/RemixIcon/v4.6.0/icons/System/close-line.svg",
|
||||
"upstream_svg_sha256": "DB3B4DCEBB2D962A0231A191538BF6939620798B18F0BA52D4B91C5A4649CE54",
|
||||
"normalized_alpha_difference_512": 0.0
|
||||
},
|
||||
{
|
||||
"name": "keyboard-box-line",
|
||||
"iconfont_id": 42197986,
|
||||
"collection_id": 20073,
|
||||
"user_id": 89260,
|
||||
"iconfont_url": "https://www.iconfont.cn/collections/detail?cid=20073",
|
||||
"iconfont_svg_sha256": "813D79347C357A919317EDF84252A505DBDC441C1144076B4E8389A8CE217DE0",
|
||||
"upstream": "https://raw.githubusercontent.com/Remix-Design/RemixIcon/v4.6.0/icons/Device/keyboard-box-line.svg",
|
||||
"upstream_svg_sha256": "E39F28CBF418269405F517126A64CBF4B3332B3295CC8A3AF670F77344A669C4",
|
||||
"normalized_alpha_difference_512": 0.0
|
||||
},
|
||||
{
|
||||
"name": "edit-2-line",
|
||||
"iconfont_id": 42194542,
|
||||
"collection_id": 20073,
|
||||
"user_id": 89260,
|
||||
"iconfont_url": "https://www.iconfont.cn/collections/detail?cid=20073",
|
||||
"iconfont_svg_sha256": "74D6590A22053C253C0D00D9AC5E35FB1F8C02E2243D0E3643A959EEF233476F",
|
||||
"upstream": "https://raw.githubusercontent.com/Remix-Design/RemixIcon/v4.6.0/icons/Design/edit-2-line.svg",
|
||||
"upstream_svg_sha256": "D03A3A7B874377A18488B7B24B596B8078AFBD9BCED93AD791B6FB2F4866613D",
|
||||
"normalized_alpha_difference_512": 1.914828431372549e-06
|
||||
},
|
||||
{
|
||||
"name": "arrow-left-s-line",
|
||||
"iconfont_id": 42191242,
|
||||
"collection_id": 20073,
|
||||
"user_id": 89260,
|
||||
"iconfont_url": "https://www.iconfont.cn/collections/detail?cid=20073",
|
||||
"iconfont_svg_sha256": "CCF02486D37383BBDC3319D9B348C5DAEF43693526D41AFF6AD409D04E0664EF",
|
||||
"upstream": "https://raw.githubusercontent.com/Remix-Design/RemixIcon/v4.6.0/icons/Arrows/arrow-left-s-line.svg",
|
||||
"upstream_svg_sha256": "7D0407D47B59E7D69446EF81B79E95C5D5A4085E7FA7BA30770556B67197407E",
|
||||
"normalized_alpha_difference_512": 0.00010100719975490195
|
||||
},
|
||||
{
|
||||
"name": "arrow-right-s-line",
|
||||
"iconfont_id": 42191240,
|
||||
"collection_id": 20073,
|
||||
"user_id": 89260,
|
||||
"iconfont_url": "https://www.iconfont.cn/collections/detail?cid=20073",
|
||||
"iconfont_svg_sha256": "B6678427EC45E7B00257B6C2D8EEB533E6B92C6D09864CE7844178E38FADE8AF",
|
||||
"upstream": "https://raw.githubusercontent.com/Remix-Design/RemixIcon/v4.6.0/icons/Arrows/arrow-right-s-line.svg",
|
||||
"upstream_svg_sha256": "2C579A4DBAF25E19A128D71E04F4CF43B19AD2487A0076FF7E7E5A94C9B3E013",
|
||||
"normalized_alpha_difference_512": 9.967579561121323e-05
|
||||
},
|
||||
{
|
||||
"name": "arrow-down-s-line",
|
||||
"iconfont_id": 42191207,
|
||||
"collection_id": 20073,
|
||||
"user_id": 89260,
|
||||
"iconfont_url": "https://www.iconfont.cn/collections/detail?cid=20073",
|
||||
"iconfont_svg_sha256": "1EFA1C713B710C0E9EF5EB47BEC762A4B5CE21FFB454D44A3BBB118A8BEC3E83",
|
||||
"upstream": "https://raw.githubusercontent.com/Remix-Design/RemixIcon/v4.6.0/icons/Arrows/arrow-down-s-line.svg",
|
||||
"upstream_svg_sha256": "4B0BF8A45D554BA97D8115EF2713010F196AD49D57EEECB8A42D4839A5BA8F9E",
|
||||
"normalized_alpha_difference_512": 9.942148246017157e-05
|
||||
}
|
||||
]
|
||||
}
|
||||
{
|
||||
"upstream_version": "v4.6.0",
|
||||
"license": "Apache-2.0",
|
||||
"source": "iconfont.cn anonymous public icon search",
|
||||
"icons": [
|
||||
{
|
||||
"name": "delete-back-2-line",
|
||||
"iconfont_id": 39249781,
|
||||
"collection_id": 651928,
|
||||
"user_id": 12586164,
|
||||
"iconfont_url": "https://www.iconfont.cn/collections/detail?cid=651928",
|
||||
"iconfont_svg_sha256": "A6279D35F9FD1578F813BCFA2077FAC98EC8E3A4D7BC8F90A5B3F3F2744C3A59",
|
||||
"upstream": "https://raw.githubusercontent.com/Remix-Design/RemixIcon/v4.6.0/icons/System/delete-back-2-line.svg",
|
||||
"upstream_svg_sha256": "D6FC7F41C0B84FE48A06685DC081780B6CD103AC9B3DB96DC44E624AA0A113E8",
|
||||
"normalized_alpha_difference_512": 4.517798330269608e-06
|
||||
},
|
||||
{
|
||||
"name": "check-line",
|
||||
"iconfont_id": 39249772,
|
||||
"collection_id": 651928,
|
||||
"user_id": 12586164,
|
||||
"iconfont_url": "https://www.iconfont.cn/collections/detail?cid=651928",
|
||||
"iconfont_svg_sha256": "1B194F3B6AC39E6199FB003AC3140CC2ED2D1213201C0BDBF69ABFFABA67F6CE",
|
||||
"upstream": "https://raw.githubusercontent.com/Remix-Design/RemixIcon/v4.6.0/icons/System/check-line.svg",
|
||||
"upstream_svg_sha256": "B83A64BF881169D5EDEAA6134A7BA37192F0DDC80C2E2A890578AE0F2F8F9169",
|
||||
"normalized_alpha_difference_512": 0.0
|
||||
},
|
||||
{
|
||||
"name": "close-line",
|
||||
"iconfont_id": 39249776,
|
||||
"collection_id": 651928,
|
||||
"user_id": 12586164,
|
||||
"iconfont_url": "https://www.iconfont.cn/collections/detail?cid=651928",
|
||||
"iconfont_svg_sha256": "748EA3E8A4FE22671FF3347E998D51679EAF3FF3950F6348F286F6DD36F19A92",
|
||||
"upstream": "https://raw.githubusercontent.com/Remix-Design/RemixIcon/v4.6.0/icons/System/close-line.svg",
|
||||
"upstream_svg_sha256": "DB3B4DCEBB2D962A0231A191538BF6939620798B18F0BA52D4B91C5A4649CE54",
|
||||
"normalized_alpha_difference_512": 0.0
|
||||
},
|
||||
{
|
||||
"name": "keyboard-box-line",
|
||||
"iconfont_id": 42197986,
|
||||
"collection_id": 20073,
|
||||
"user_id": 89260,
|
||||
"iconfont_url": "https://www.iconfont.cn/collections/detail?cid=20073",
|
||||
"iconfont_svg_sha256": "813D79347C357A919317EDF84252A505DBDC441C1144076B4E8389A8CE217DE0",
|
||||
"upstream": "https://raw.githubusercontent.com/Remix-Design/RemixIcon/v4.6.0/icons/Device/keyboard-box-line.svg",
|
||||
"upstream_svg_sha256": "E39F28CBF418269405F517126A64CBF4B3332B3295CC8A3AF670F77344A669C4",
|
||||
"normalized_alpha_difference_512": 0.0
|
||||
},
|
||||
{
|
||||
"name": "edit-2-line",
|
||||
"iconfont_id": 42194542,
|
||||
"collection_id": 20073,
|
||||
"user_id": 89260,
|
||||
"iconfont_url": "https://www.iconfont.cn/collections/detail?cid=20073",
|
||||
"iconfont_svg_sha256": "74D6590A22053C253C0D00D9AC5E35FB1F8C02E2243D0E3643A959EEF233476F",
|
||||
"upstream": "https://raw.githubusercontent.com/Remix-Design/RemixIcon/v4.6.0/icons/Design/edit-2-line.svg",
|
||||
"upstream_svg_sha256": "D03A3A7B874377A18488B7B24B596B8078AFBD9BCED93AD791B6FB2F4866613D",
|
||||
"normalized_alpha_difference_512": 1.914828431372549e-06
|
||||
},
|
||||
{
|
||||
"name": "arrow-left-s-line",
|
||||
"iconfont_id": 42191242,
|
||||
"collection_id": 20073,
|
||||
"user_id": 89260,
|
||||
"iconfont_url": "https://www.iconfont.cn/collections/detail?cid=20073",
|
||||
"iconfont_svg_sha256": "CCF02486D37383BBDC3319D9B348C5DAEF43693526D41AFF6AD409D04E0664EF",
|
||||
"upstream": "https://raw.githubusercontent.com/Remix-Design/RemixIcon/v4.6.0/icons/Arrows/arrow-left-s-line.svg",
|
||||
"upstream_svg_sha256": "7D0407D47B59E7D69446EF81B79E95C5D5A4085E7FA7BA30770556B67197407E",
|
||||
"normalized_alpha_difference_512": 0.00010100719975490195
|
||||
},
|
||||
{
|
||||
"name": "arrow-right-s-line",
|
||||
"iconfont_id": 42191240,
|
||||
"collection_id": 20073,
|
||||
"user_id": 89260,
|
||||
"iconfont_url": "https://www.iconfont.cn/collections/detail?cid=20073",
|
||||
"iconfont_svg_sha256": "B6678427EC45E7B00257B6C2D8EEB533E6B92C6D09864CE7844178E38FADE8AF",
|
||||
"upstream": "https://raw.githubusercontent.com/Remix-Design/RemixIcon/v4.6.0/icons/Arrows/arrow-right-s-line.svg",
|
||||
"upstream_svg_sha256": "2C579A4DBAF25E19A128D71E04F4CF43B19AD2487A0076FF7E7E5A94C9B3E013",
|
||||
"normalized_alpha_difference_512": 9.967579561121323e-05
|
||||
},
|
||||
{
|
||||
"name": "arrow-down-s-line",
|
||||
"iconfont_id": 42191207,
|
||||
"collection_id": 20073,
|
||||
"user_id": 89260,
|
||||
"iconfont_url": "https://www.iconfont.cn/collections/detail?cid=20073",
|
||||
"iconfont_svg_sha256": "1EFA1C713B710C0E9EF5EB47BEC762A4B5CE21FFB454D44A3BBB118A8BEC3E83",
|
||||
"upstream": "https://raw.githubusercontent.com/Remix-Design/RemixIcon/v4.6.0/icons/Arrows/arrow-down-s-line.svg",
|
||||
"upstream_svg_sha256": "4B0BF8A45D554BA97D8115EF2713010F196AD49D57EEECB8A42D4839A5BA8F9E",
|
||||
"normalized_alpha_difference_512": 9.942148246017157e-05
|
||||
}
|
||||
]
|
||||
}
|
||||
在新工单中引用
屏蔽一个用户