refactor: 移除 WebView2,界面只保留 Direct2D (#1013)

* refactor(engine): 语音独立工具改用原生托盘菜单,移除 WebView2

托盘菜单改用 Win32 弹出菜单,设置项改为用关联编辑器打开 config.toml,关于页改为消息框。删除 window_webview2 与两份 HTML 资源,windows-app 特性不再依赖 webview2 端口。

* refactor(server): 移除 WebView2,候选窗、工具栏、托盘菜单与设置窗口只保留 Direct2D

删除 src/webview2 与 webview_utils,小窗口的置顶、皮肤重载和工具栏状态缓存移到 window/small_windows,presenter 成为唯一路径,D2D 下的置顶时序与 cloak 行为保持不变。设置进程去掉 WebView2 宿主、鼠标转发与启动图,只构建原生窗口;旧快捷方式传入的 --d2d / --webview2 被忽略。删除 appearance.ui_backend 与 appearance.settings_ui_backend(读取、写入、设置页下拉框与两份配置模板),升级时由模板合并丢弃旧值。设置窗口的图片资源移到 server/assets/settings,安装到数据目录的 assets\settings。不再链接 WebView2Loader,vcpkg 去掉 webview2 端口。

* build(installer): 打包与 CI 不再处理 WebView2 页面和运行时

Prepare-PackageFiles 去掉 -UiHtmlDirectory 与 html 暂存,改为暂存 server/assets/settings;安装器不再检查 WebView2 Runtime、不再创建 ProgramData 下的 WebView2 用户数据目录,升级和卸载时清理旧版留下的 webview2 / webview2-settings 与数据目录里的 html。CI 删除设置页构建、契约同步检查和候选窗 DOM 测试,release 的打包任务不再依赖网页产物;dependabot 去掉 npm。第三方声明去掉 WebView2Loader。

* refactor(ui-html): 删除 WebView2 页面与设置页前端工程

候选窗、工具栏、托盘菜单、设置页、主题和契约副本都已没有使用者;设置窗口用到的图片资源已移到 server/assets/settings。sync-contracts.py 与 pnpm/Vite/TypeScript 工程一并删除,契约仍以 engine/contracts/webview 为唯一来源。

* docs(server): 文档改为只描述 Direct2D 界面

仓库地图去掉 ui-html,组件边界、uiAccess 与置顶时序、设置窗口和配置读写的说明改写为 Direct2D 唯一界面;安装说明去掉 WebView2 Runtime 检查,打包命令去掉 -UiHtmlDirectory;engine/contracts/webview 的说明改为设置窗口是唯一使用者,名字是历史遗留。

* docs(skins): 皮肤说明不再区分 WebView2 与 D2D 渲染器

v2 字段去掉「仅 WebView2」,改为说明由 Direct2D 原生渲染、部分字段仍在补全;颜色必须带 # 改为清单格式约定。随包的 skins/niya-demo/skin.toml 不改,免得用户机器上的同名皮肤在升级时被当成已修改而改名备份。

* docs(ui): demo 说明文字不再提 WebView2
This commit is contained in:
Lu Fan(SonnyCalcr) authored and GitHub committed 2026-10-17 02:48:02 +08:00
1 parent d270395816
commit 1b875267db
342 files changed
+1359 -50942

No files matched your search

-14
View File
@@ -23,17 +23,3 @@ updates:
commit-message:
prefix: "chore(deps)"
open-pull-requests-limit: 5
# The settings page is the one part of this repository that is built rather than shipped as
# source, and it sits on the release path, so its dependencies need watching.
- package-ecosystem: "npm"
directory: "/ui-html/webview2/settings/ime-settings"
schedule:
interval: "monthly"
groups:
npm:
patterns:
- "*"
commit-message:
prefix: "chore(deps)"
open-pull-requests-limit: 5
+2 -55
View File
@@ -107,8 +107,8 @@ jobs:
# races and null dereferences that the Debug build did not catch. Linux acted on that; this
# repository had no sanitizer coverage at all.
#
# Sanitizing the whole server would mean a sanitized Boost, curl, sqlite and WebView2 on a Windows
# runner. Instead the host-independent parsing and policy code builds as its own tiny target with
# Sanitizing the whole server would mean a sanitized Boost, curl and sqlite on a Windows runner.
# Instead the host-independent parsing and policy code builds as its own tiny target with
# no dependencies, which runs under ASan and UBSan on Linux in seconds. It covers less than a full
# sanitized build would; it covers it on every pull request.
sanitizers:
@@ -295,59 +295,6 @@ jobs:
shell: pwsh
run: ctest --no-tests=error --test-dir ui/build -C Release --verbose --timeout 60
pages:
name: Settings page
runs-on: ubuntu-24.04
timeout-minutes: 20
steps:
- name: Check out source
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
# The pages ship a copy of the engine's web contract. Before the consolidation the product
# lock kept that copy honest by pinning UiHtml and the engine together; now that the engine is
# a directory of this repository, the copy is checked against engine/contracts/webview
# directly and a plain checkout is enough to do it.
- name: Check the bundled contract against the engine
run: python3 ui-html/scripts/sync-contracts.py --check
- name: Set up pnpm
uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0
with:
version: 10
- name: Set up Node
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 22
cache: pnpm
cache-dependency-path: ui-html/webview2/settings/ime-settings/pnpm-lock.yaml
- name: Install
working-directory: ui-html/webview2/settings/ime-settings
run: pnpm install --frozen-lockfile
- name: Test
working-directory: ui-html/webview2/settings/ime-settings
run: pnpm test
- name: Test candidate DOM rendering
run: node --test server/tests/test-candidate-dom-render.cjs
# Same command the release workflow runs. Until it was added here it was only ever exercised
# during a release, so a break surfaced while cutting a version.
- name: Build
working-directory: ui-html/webview2/settings/ime-settings
run: pnpm build
- name: Check the output the installer collects
working-directory: ui-html/webview2/settings/ime-settings
run: |
test -f dist/index.html
echo "dist contents:"
ls -la dist
# External skins are plain data, so the checks are the manifest contract the Server catalog
# enforces plus the example's installer; both run against the example and the skins/ collection.
skins:
+3 -55
View File
@@ -452,58 +452,12 @@ jobs:
path: server/build-release/bin/Release
if-no-files-found: error
ui:
name: Build settings page
needs: [prepare, dependencies]
if: ${{ needs.prepare.outputs.should_publish == 'true' }}
runs-on: ubuntu-24.04
timeout-minutes: 30
steps:
- name: Check out release commit
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ needs.prepare.outputs.target_sha }}
persist-credentials: false
- name: Verify the shipped pages carry the engine's contract
run: python3 ui-html/scripts/sync-contracts.py --check
- name: Set up pnpm
uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0
with:
version: 10
- name: Set up Node
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 22
cache: pnpm
cache-dependency-path: ui-html/webview2/settings/ime-settings/pnpm-lock.yaml
- name: Install and build the settings page
working-directory: ui-html/webview2/settings/ime-settings
run: |
pnpm install --frozen-lockfile
pnpm build
- name: Upload the web assets
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: webview2
path: |
ui-html/webview2/candwnd
ui-html/webview2/ftb
ui-html/webview2/menu
ui-html/webview2/shared
ui-html/webview2/settings/ime-settings/dist
if-no-files-found: error
package:
name: Package and publish installer
needs: [prepare, dependencies, tsf, server, ui]
needs: [prepare, dependencies, tsf, server]
# Stated explicitly even though skipped needs would already skip this job. This is the only job
# that spends a signature, so the condition that stops it belongs here rather than being a
# consequence of four other jobs' conditions that a later edit could quietly undo.
# consequence of the other jobs' conditions that a later edit could quietly undo.
if: ${{ needs.prepare.outputs.should_publish == 'true' }}
runs-on: ${{ needs.prepare.outputs.signing_provider == 'signpath' && 'windows-2025' || fromJSON('["self-hosted","Windows","X64","msime-windows"]') }}
# A SignPath signing request waits for an approver, twice per release, so that path gets room for both approvals on top of the build.
@@ -602,12 +556,6 @@ jobs:
name: server
path: server/build-release/bin/Release
- name: Download the web assets
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: webview2
path: ui-html/webview2
- name: Download the dictionaries
shell: bash
env:
@@ -656,7 +604,7 @@ jobs:
run: >
pwsh -File ./Prepare-PackageFiles.ps1 -TargetVersion $env:TARGET_VERSION
-RepoRoot (Resolve-Path '..').Path
-TsfDirectory windows -ServerDirectory server -UiHtmlDirectory ui-html -NoticesDirectory .
-TsfDirectory windows -ServerDirectory server -NoticesDirectory .
-HelpCodeDirectory engine/helpcode -IncludeSymbols
- name: Embed the exact product dependency manifest
+8 -10
View File
@@ -11,10 +11,9 @@ Windows 端的全部一方源码在本仓。**合仓改变的是仓库数量,
| 目录 | 职责 | 本目录的规则 |
|---|---|---|
| `windows/` | TSF 文本服务 DLL:按键预判、焦点、edit session、管道客户端 | [windows/AGENTS.md](windows/AGENTS.md) |
| `server/` | 常驻后端:候选状态、配置、词库、管道服务、窗口与 WebView2 宿主 | [server/AGENTS.md](server/AGENTS.md) |
| `server/` | 常驻后端:候选状态、配置、词库、管道服务,以及候选窗、悬浮工具栏、托盘菜单和设置窗口(均由 `msimeui` 以 Direct2D 绘制) | [server/AGENTS.md](server/AGENTS.md) |
| `engine/` | 输入引擎:输入会话、候选查询、辅助码、跨进程契约、语音模块 | [engine/AGENTS.md](engine/AGENTS.md) |
| `ui/` | 通用 Win32 / Direct2D / DirectWrite 控件库 `msimeui` | [ui/AGENTS.md](ui/AGENTS.md) |
| `ui-html/` | 候选窗、工具栏、菜单、设置页的 HTML / CSS / JS | [ui-html/README.md](ui-html/README.md) |
| `installer/` | 收集产物、自签名、Inno Setup 打包 | [installer/README.md](installer/README.md) |
| `log/` | 日志采集库 | — |
| `skins/` | 外部皮肤合集,每个子目录一个 `skin.toml` 皮肤包;除素材授权未核实的外随安装包装到数据目录,同名且改过的旧版改名 `.bak` 保留 | [skins/README.md](skins/README.md) |
@@ -27,11 +26,10 @@ Windows 端的全部一方源码在本仓。**合仓改变的是仓库数量,
## 组件之间的边界
- **协议的唯一来源是 `engine/contracts/`**。IPC 线格式、opcode、语音分帧和 WebView 消息定义都在那里,`windows/` 和 `server/` 各自引用同一份头文件。不要在任何一侧重新定义或复制一份。
- **协议的唯一来源是 `engine/contracts/`**。IPC 线格式、opcode、语音分帧和设置窗口的消息定义(`engine/contracts/webview/`,名字是历史遗留)都在那里,`windows/` 和 `server/` 各自引用同一份头文件。不要在任何一侧重新定义或复制一份。
- **`ui/` 不许反向依赖产品**。它不读 Server 配置、IPC、引擎、词库或全局输入状态;业务通过数据和回调接入。`ui/scripts/check-boundary.py` 在 CI 里检查已知的反向依赖,新增依赖会红。
- **窗口归 `server/`,页面归 `ui-html/`**。HWND、尺寸、位置、DPI、Z-order 和 WebView2 controller 的生命周期在 `server/src/window/` 与 `server/src/webview2/`;页面结构、样式和浏览器端交互在 `ui-html/webview2/`。改消息 `type`、JSON 字段或页面导出的 JS 函数时两侧要一起改。
- **候选与输入状态的权威在 Server 和引擎**,页面只负责展示和发出用户动作,不要在网页侧复制状态机。
- `ui-html/webview2/shared/` 是引擎 web 契约的副本,由 `ui-html/scripts/sync-contracts.py` 生成,CI 用 `--check` 验证它和 `engine/contracts/webview/` 一致。手改这个目录会被 CI 拦下来,改契约要改 `engine/contracts/`,然后在同一个提交里重新生成。
- **界面只有 Direct2D 一套**。候选窗、悬浮工具栏、托盘菜单的 HWND、尺寸、位置、DPI、Z-order 和绘制在 `server/src/window/`(presenter 用 `ui/` 的 `msimeui` 控件);设置窗口是独立进程 `server/src/settings/`,界面在 `server/src/settings/native/`,用到的图片资源在 `server/assets/settings/`,安装到数据目录的 `assets\settings\`。WebView2 与 `ui-html/` 已移除,不要再引入浏览器内核或网页前端。
- **候选与输入状态的权威在 Server 和引擎**,界面只负责展示和发出用户动作,不要在 presenter 或设置窗口里复制状态机。
- **新增配置项必须同时写进出厂模板 `installer/default_config/config.default.toml`。** 这个文件是安装包真正打进去的那一份,Server 升级时用 `MergeConfigIntoTemplate` 以它为骨架重建用户配置:**模板里没有的键会被丢弃**。只在 `ime_config.cpp` 里加读取和默认值,代码本身能跑,但用户在设置页改过的值会在下次升级时静默消失,而且没有任何编译期或运行期报错。同一个提交里要一并更新的还有 `server/assets/config/config.toml`(开发和 CI 测试用的工作配置),以及设置页的配置读写映射——服务端进程内已不再内嵌设置宿主,读写映射只剩独立设置进程 `server/src/settings/settings_app.cpp` 一处,其中 `BuildConfigMessage` 里 `configSnapshot` 的字段列表和 `ApplyConfigUpdate` 里 `path == "..."` 的分发两边都要加,漏掉任一边的表现是「设置页能点,重开就回退」。
- **`engine/` 是本仓的一等代码,不是 vendored 第三方。** 需要为 Windows 改引擎就直接改,和改 `server/` 一样评审和测试,不用绕上游。唯一例外是 `engine/` 下的 `ngram/kenlm/`、`ngram/octagram/darts.h`、`utfcpp/` 和 `voice/third_party/`,那几个是上游副本,保留原格式与许可;`darts.h` 按精确路径单独排除,同目录的 `octagram_gram.{h,cpp}` 是我们的代码,仍受格式门禁管。`googlepinyinime-rev/` 已与上游解绑,可以直接改,但保留它原有的格式与许可,本地改动记在 UPSTREAM.md。来源与裁剪清单见 [engine/UPSTREAM.md](engine/UPSTREAM.md)。
@@ -59,7 +57,7 @@ cmake -S ui -B ui/build -A x64 # GUI 框架
**格式化会卡 CI,提交前务必自查,注意两个坑:**
- **版本必须是 clang-format `18.1.8`**。`format.sh` 把版本钉死在这个值并从 PyPI 装,不同版本的默认排版不一致,用系统自带的 clang-format 本地过了、CI 仍可能红。`quality.yml` 的 `Check formatting` 步骤只跑 C++(`server`/`windows`/`ui`/`log`/`engine`),**不覆盖 `ui-html/` 的 TS/CSS**——那边只有 `pnpm build`(`tsc`)和 `pnpm test` 把关,没有 prettier/eslint 格式门禁。
- **版本必须是 clang-format `18.1.8`**。`format.sh` 把版本钉死在这个值并从 PyPI 装,不同版本的默认排版不一致,用系统自带的 clang-format 本地过了、CI 仍可能红。`quality.yml` 的 `Check formatting` 步骤只跑 C++(`server`/`windows`/`ui`/`log`/`engine`)。
- **`format.sh` 在 Windows 上跑不起来**:它假设 venv 是 Unix 布局(`bin/pip`、`bin/clang-format`),而 Windows 的 `python -m venv` 生成的是 `Scripts/`,脚本会以 `No such file or directory` 退出。在 Windows 上改了 C++ 就直接调 18.1.8 的 clang-format 自查,绕过这个脚本:
```powershell
@@ -74,7 +72,7 @@ cmake -S ui -B ui/build -A x64 # GUI 框架
## 产品输入清单
`product-lock.json` 只记录仍来自仓外的东西,现在只剩一项:词库 Release 的 tag、source commit 和每个产物的 SHA256。**引擎、Server、页面、GUI 框架和安装器都不在清单里**——它们是本仓的目录,本仓的一个 commit 就已经把它们钉住了。辅助码在 `engine/helpcode/`,同理。
`product-lock.json` 只记录仍来自仓外的东西,现在只剩一项:词库 Release 的 tag、source commit 和每个产物的 SHA256。**引擎、Server、GUI 框架和安装器都不在清单里**——它们是本仓的目录,本仓的一个 commit 就已经把它们钉住了。辅助码在 `engine/helpcode/`,同理。
- 引擎没有 `repositories` 条目,这是有意的。它不再有 gitlink,锁里写一个上游 commit 就等于声称这棵树仍等于上游——第一个 Windows 专门化的改动落地时这句话就假了,而且没有任何 CI 能发现。来源改记在 [engine/UPSTREAM.md](engine/UPSTREAM.md)。
- `refresh` 不解析任何浮动源码引用,词库取自指定 tag。词库清单里记录了构建它的 commit 和当时工作树是否干净,`verify-assets` 一并校验——摘要只能证明字节是评审过的字节,证明不了它来自一个能重建的源。
@@ -139,7 +137,7 @@ release workflow 里的每一段 shell 都抽在 `scripts/ci/` 下,workflow
| `revalidate-draft-release.sh` | 发布前复查 draft 仍指向被构建的那个 commit |
| `publish-release.sh` | 上传产物、追加说明、发布 |
合仓之前 CI 要把五个仓 checkout 到历史目录名下,`Prepare-PackageFiles.ps1` 才能不改一行地跑。现在源目录名由 workflow 显式传参(`-TsfDirectory windows -ServerDirectory server -UiHtmlDirectory ui-html -NoticesDirectory . -HelpCodeDirectory engine/helpcode`),只有词库还落在仓根的 `MetasequoiaImeDict/`,用的是那个参数的默认值。改动这些脚本里的产物路径时,要连同 release workflow 一起核对。
合仓之前 CI 要把五个仓 checkout 到历史目录名下,`Prepare-PackageFiles.ps1` 才能不改一行地跑。现在源目录名由 workflow 显式传参(`-TsfDirectory windows -ServerDirectory server -NoticesDirectory . -HelpCodeDirectory engine/helpcode`),只有词库还落在仓根的 `MetasequoiaImeDict/`,用的是那个参数的默认值。改动这些脚本里的产物路径时,要连同 release workflow 一起核对。
词库不在 CI 里现建,从产品锁指定仓库的 `dict-*` release 下载并校验 SHA256。**建库流水线不在本仓**——它随引擎的专门化一并裁掉了(见 [engine/UPSTREAM.md](engine/UPSTREAM.md)),现有 MSIME-Dict release 作为不可变旧产物保留;换发布源要在 `product_lock.py` 里明确评审,不是改个 tag 就能悄悄完成的事。词库改了要先在上游把新词库发布出来,再发 Windows 版本;通过 `scripts/product_lock.py refresh --dictionary-tag <tag>` 更新产品锁并评审摘要变更;发布构建不能临时覆盖词库版本。
@@ -149,4 +147,4 @@ release workflow 里的每一段 shell 都抽在 `scripts/ci/` 下,workflow
## 提交
提交信息用 `type(scope): 摘要`,scope 用目录名(`windows`、`server`、`engine`、`ui`、`ui-html`、`installer`)。不要添加 `Co-Authored-By`、`Generated with` 或其他 AI 生成标记。
提交信息用 `type(scope): 摘要`,scope 用目录名(`windows`、`server`、`engine`、`ui`、`installer`)。不要添加 `Co-Authored-By`、`Generated with` 或其他 AI 生成标记。
+4 -5
View File
@@ -70,7 +70,7 @@ Set-Location ..\server
.\scripts\lcompile-release.ps1
Set-Location ..\installer
.\Prepare-PackageFiles.ps1 -RepoRoot .. -TsfDirectory windows -ServerDirectory server -UiHtmlDirectory ui-html -NoticesDirectory .
.\Prepare-PackageFiles.ps1 -RepoRoot .. -TsfDirectory windows -ServerDirectory server -NoticesDirectory .
.\Sign-PackageBinaries-Local.ps1
.\Compile-Installer.ps1
```
@@ -79,12 +79,11 @@ Set-Location ..\installer
## 改动前要知道的几条边界
- **协议的唯一来源是 `engine/contracts/`。** IPC 线格式、opcode、语音分帧和 WebView 消息定义都在那里,两侧引用同一份头文件,不要在任何一侧另写一份。
- **协议的唯一来源是 `engine/contracts/`。** IPC 线格式、opcode、语音分帧和设置窗口的消息定义都在那里,两侧引用同一份头文件,不要在任何一侧另写一份。
- **`engine/` 是本仓的一等代码。** 要为 Windows 改引擎就直接改,和改 `server/` 一样评审和测试;引擎和调用它的代码可以、也应该在同一个 PR 里。例外是 `engine/` 下的 `googlepinyinime-rev/`、`ngram/kenlm/`、`utfcpp/`、`voice/third_party/`,那是上游副本。见 [engine/UPSTREAM.md](engine/UPSTREAM.md)。
- **`ui/` 不许反向依赖产品。** 它不读 Server 配置、IPC、引擎、词库或全局输入状态。`ui/scripts/check-boundary.py` 在 CI 里检查。
- **窗口归 `server/`,页面归 `ui-html/`。** 改消息 `type`、JSON 字段或页面导出的 JS 函数时两侧一起改。
- **`ui-html/webview2/shared/` 是生成物**,由 `ui-html/scripts/sync-contracts.py` 从 `engine/contracts/webview/` 同步,CI 用 `--check` 校验。要改契约就去改 `engine/contracts/`,然后在同一个提交里重新生成。
- **候选与输入状态的权威在 Server 和引擎**,页面只负责展示。
- **界面只有 Direct2D 一套,全部在 `server/`。** 候选窗、工具栏、托盘菜单在 `server/src/window/`,设置窗口在 `server/src/settings/native/`,都用 `ui/` 的 `msimeui` 绘制。改设置窗口的消息 `type` 或 JSON 字段时,`engine/contracts/webview/` 与 `settings_app.cpp` 一起改。
- **候选与输入状态的权威在 Server 和引擎**,界面只负责展示。
## 提交前
+1 -1
View File
@@ -18,7 +18,7 @@ Every claim below is checkable against this repository. Where a default matters,
## No telemetry
There is no analytics, telemetry, crash reporting, or usage measurement that leaves this machine. The input method does not record, store or transmit what you type: the only records it can keep are the optional local counters described below, and those cover character classes, activity time and candidate-choice counts, never text. No third-party SDK for those purposes is linked, and there is no endpoint that receives usage data. This is verifiable: searching the `server/`, `windows/`, `ui/`, `ui-html/` and `installer/` trees for `telemetry`, `analytics`, `sentry`, `matomo`, `posthog`, `amplitude`, `mixpanel`, `crashpad` and `breakpad` returns nothing.
There is no analytics, telemetry, crash reporting, or usage measurement that leaves this machine. The input method does not record, store or transmit what you type: the only records it can keep are the optional local counters described below, and those cover character classes, activity time and candidate-choice counts, never text. No third-party SDK for those purposes is linked, and there is no endpoint that receives usage data. This is verifiable: searching the `server/`, `windows/`, `ui/` and `installer/` trees for `telemetry`, `analytics`, `sentry`, `matomo`, `posthog`, `amplitude`, `mixpanel`, `crashpad` and `breakpad` returns nothing.
## Local input statistics (built-in, off by default)
-1
View File
@@ -47,7 +47,6 @@ Windows 端的全部一方源码都在本仓,一次 clone 就能拿到完整
| [`server/`](server/) | Server 端:算法调度、候选窗与工具栏宿主、设置程序、词库管理 |
| [`engine/`](engine/) | 输入引擎:输入会话、候选查询、辅助码、跨进程契约、语音模块 |
| [`ui/`](ui/) | 自研原生 GUI 框架(`msimeui`):Win32 宿主窗口,Direct2D / DirectWrite 渲染 |
| [`ui-html/`](ui-html/) | 界面资源(HTML / CSS / JS):候选窗、悬浮工具栏、托盘菜单、设置页 |
| [`installer/`](installer/) | 收集各组件产物、自签名、Inno Setup 打包 |
| [`log/`](log/) | 各模块共用的日志采集库 |
| [`experiments/tsf-edit-control/`](experiments/tsf-edit-control/) | 基于 Win32 TSF 的编辑控件实验工程,附最小宿主 demo |
+13 -24
View File
@@ -2,7 +2,7 @@ Metasequoia IME for Windows - Third-Party Notices
=================================================
The installer ships the TSF text service, the Server process and its helper
executables, the WebView2 interface resources, and the dictionary and helpcode
executables, the settings window's image assets, and the dictionary and helpcode
data. Every binary in it statically links the input engine in engine/, which is
licensed under the GNU General Public License version 3, so the product as a
whole is distributed under the GPLv3; see the LICENSE file for its full text.
@@ -36,40 +36,29 @@ dependencies are installed beside it as DLLs rather than linked into it.
Boost BSL-1.0 https://www.boost.org/
miniaudio MIT-0 https://github.com/mackron/miniaudio
Binaries redistributed unmodified
---------------------------------
WebView2Loader.dll Microsoft Edge WebView2 SDK
The Server links the WebView2 loader and the installer ships the loader DLL as
built by Microsoft. It is redistributed under the Microsoft Software License
Terms for the WebView2 SDK, which permit redistribution of the loader with an
application: https://developer.microsoft.com/microsoft-edge/webview2/
Interface resources
-------------------
ui-html GPL-3.0 ui-html/ in this repository
settings window assets GPL-3.0 server/assets/settings/ in this repository
The candidate window, floating toolbar, tray menu and settings page are HTML,
CSS and JavaScript loaded by the Server at runtime. They are part of this
project and carry the same license as the rest of it.
The icons and images the settings window draws are installed into the data
directory under assets\settings. They are part of this project and carry the
same license as the rest of it.
------------------------------------------------------------------------------
1. The input engine, msimeui and the interface resources - GNU General Public License version 3
------------------------------------------------------------------------------
The input engine, the msimeui GUI framework and the WebView2 interface resources
are all parts of this project and are licensed under the GNU General Public
License version 3. Its full text is reproduced in the LICENSE file at the root of
this repository and is not duplicated here. All three are directories of this
repository rather than separate projects; the GUI framework and the interface
resources were MSIME-UI and MSIME-UiHtml, and the engine was MSIME-Engine, until
those repositories were archived or, in the engine's case, imported and
specialized for Windows. See engine/UPSTREAM.md.
The input engine, the msimeui GUI framework and the settings window's image
assets are all parts of this project and are licensed under the GNU General
Public License version 3. Its full text is reproduced in the LICENSE file at the
root of this repository and is not duplicated here. All three are directories of
this repository rather than separate projects; the GUI framework was MSIME-UI
and the engine was MSIME-Engine, until those repositories were archived or, in
the engine's case, imported and specialized for Windows. See engine/UPSTREAM.md.
Sources:
engine/, ui/ and ui-html/ in this repository
engine/, ui/ and server/assets/settings/ in this repository
------------------------------------------------------------------------------
2. googlepinyinime-rev, OpenCC and cpp-pinyin - Apache License 2.0
+1 -1
View File
@@ -68,7 +68,7 @@ TSF DLL 被加载进宿主程序进程,采用静态 CRT 构建,不依赖运
### 候选窗始终不出现
先确认已安装 **Microsoft Edge WebView2 Runtime**(在「设置 → 应用 → 已安装的应用」里搜索 WebView2)。当前版本的候选窗由 WebView2 渲染,运行时缺失会导致窗口无法显示。
候选窗由 Server 用 Direct2D 绘制,不依赖 WebView2 Runtime。先按上一节确认 x64 运行库已安装,再在任务管理器里确认 `MetasequoiaImeServer.exe` 正在运行。在独占全屏的游戏里,候选窗会被有意隐藏,以免把游戏切出全屏。
### 手写识别没有候选结果
+2 -2
View File
@@ -161,8 +161,8 @@ if(MSVC)
# what the TCHAR-flavoured entry points resolve to, so a consumer that disagrees with the engine
# would be compiling against a different API surface. WIN32_LEAN_AND_MEAN is only an
# include-time trim of Windows.h, and forcing it on consumers breaks any of them that go on to
# include a header needing the fuller surface - WebView2.h, for one, relies on the `interface`
# macro that combaseapi.h provides.
# include a header needing the fuller surface - COM-based SDK headers, for instance, rely on the
# `interface` macro that combaseapi.h provides.
target_compile_definitions(MetasequoiaImeEngine PUBLIC NOMINMAX UNICODE _UNICODE)
target_compile_definitions(MetasequoiaImeEngine PRIVATE WIN32_LEAN_AND_MEAN)
target_compile_options(MetasequoiaImeEngine PRIVATE /W4 /permissive- /utf-8 /Zc:__cplusplus)
+2 -2
View File
@@ -9,7 +9,7 @@ This directory used to be a submodule of `metasequoiaime/MSIME-Engine`. It is no
| Directory | Purpose |
| --- | --- |
| `core/`, `quanpin/`, `shuangpin/`, `english/`, `japanese/` | Input sessions and candidate lookup |
| `contracts/` | The authoritative IPC wire format, dictionary format, punctuation policy and WebView message schema |
| `contracts/` | The authoritative IPC wire format, dictionary format, punctuation policy and settings-window message schema |
| `helpcode/` | Helpcode (形码) tables and generators |
| `local_modes/`, `providers/`, `user_dictionary/` | Local modes, online candidate sources, the user dictionary |
| `voice/` | Recording, WAV encoding, recognition and text cleanup; builds independently |
@@ -36,7 +36,7 @@ Formatting is driven from the repository root by `scripts/format.sh`, using the
## Contract rules
The IPC wire format, opcodes, voice framing and WebView message definitions live in `contracts/` and are the single source of truth. Consumers include the same headers; do not restate them on either side. Dictionary naming is defined by `contracts/dictionary/format.json`, shared with the repository root's `scripts/dictionary_product.py`.
The IPC wire format, opcodes, voice framing and settings-window message definitions live in `contracts/` and are the single source of truth. Consumers include the same headers; do not restate them on either side. Dictionary naming is defined by `contracts/dictionary/format.json`, shared with the repository root's `scripts/dictionary_product.py`.
## Data and licensing
+16 -17
View File
@@ -1,25 +1,24 @@
# WebView messages
`messages.json` owns the message envelope, payload shape, and allowed sender windows.
Change it here, run `python3 generate.py`, and re-sync the copy under `ui-html/` in the same
commit. The generated TypeScript union and embedded C++ schema must pass `generate.py --check`.
Change it here and run `python3 generate.py` in the same commit. The generated
TypeScript union and embedded C++ schema must pass `generate.py --check`.
`runtime.js` and `validator.h` implement the documented schema subset and run the same
`fixtures.json` compatibility cases. The standalone CMake test requires Boost.JSON;
the input engine itself does not acquire that dependency.
New messages carry `protocolVersion: 1`. A missing version is accepted as legacy v1
for existing skins; an explicit unknown version, unknown message, invalid payload,
or message sent from the wrong window is rejected before dispatch. This is envelope
validation, not a replacement for domain checks such as dictionary key validation,
allowed configuration values, or the host's HTTPS URL policy.
New messages carry `protocolVersion: 1`. A missing version is accepted as legacy v1;
an explicit unknown version, unknown message, invalid payload, or message sent from
the wrong window is rejected before dispatch. This is envelope validation, not a
replacement for domain checks such as dictionary key validation, allowed configuration
values, or the host's HTTPS URL policy.
`ui-html/scripts/sync-contracts.py` materializes these bindings into
`ui-html/webview2/shared` and checks byte equality; CI runs it with `--check`, which
now compares against this directory directly rather than against a pinned submodule.
Settings imports the typed serializer and checks incoming messages. Classic candidate,
toolbar and menu pages load the same runtime from the host's `https://msime-contracts/`
virtual mapping; packaging must include `webview2/shared`.
Server-to-page candidate rendering currently uses the existing view functions;
this contract covers WebMessage actions and settings responses. Candidate view
model ownership is documented separately from transport envelopes.
The name is historical: the contract was written for the WebView2 pages, which have
been removed. Its live consumer is the Direct2D settings window
(`server/src/settings/native/`), which posts client messages to and receives server
messages from `server/src/settings/settings_app.cpp` in exactly this format. The
`candidate`, `toolbar` and `menu` surfaces and the window-chrome messages (`dragStart`,
`resizeHitTest`, `windowControl`, `maximizeButtonRect`, `windowState`, `maxButtonEvent`)
no longer have a sender or receiver in the product; they stay in the schema so old
fixtures keep validating. The generated JavaScript and TypeScript bindings likewise have
no in-repo consumer any more.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

-872
View File
@@ -1,872 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>(MVI)Settings</title>
<link rel="icon" href="./imgs/app.png">
<style>
:root {
--accent-color: #8e8cd8;
--bg-color: #202020;
--sidebar-hover: rgba(255, 255, 255, 0.08);
--item-active: rgba(255, 255, 255, 0.06);
--text-color: #f3f3f3;
--subtext-color: #c7c7c7;
--border-color: rgba(255, 255, 255, 0.08);
--card-bg: rgba(38, 38, 38, 0.6);
--input-bg: rgba(45, 45, 45, 0.8);
--danger: #c95f5f;
--ok: #4caf81;
}
* {
box-sizing: border-box;
font-family: "Noto Sans SC", "Segoe UI Variable", "Segoe UI", sans-serif;
}
body {
margin: 0;
width: 100vw;
height: 100vh;
display: flex;
color: var(--text-color);
background: var(--bg-color);
overflow: hidden;
}
.sidebar {
width: 220px;
padding: 16px 10px;
overflow-y: auto;
position: relative;
/* scrollbar-width: thin; */
scrollbar-color: rgba(170, 170, 170, 0.35) transparent;
}
.sidebar-header {
display: flex;
align-items: center;
gap: 10px;
padding: 0 10px 16px;
margin-bottom: 6px;
}
.sidebar-header .logo {
width: 28px;
height: 28px;
border-radius: 6px;
object-fit: cover;
display: block;
}
.sidebar-header .name {
font-size: 18px;
font-weight: 600;
}
.nav-item {
margin: 4px 6px;
padding: 10px 12px;
border-radius: 8px;
color: #e8e8e8;
cursor: default;
display: flex;
align-items: center;
gap: 10px;
position: relative;
user-select: none;
}
.nav-item:hover {
background: var(--sidebar-hover);
}
.nav-item.active {
background: var(--item-active);
}
.nav-indicator {
position: absolute;
left: 0;
top: 0;
width: 4px;
height: 18px;
border-radius: 4px;
background: var(--accent-color);
pointer-events: none;
opacity: 0;
z-index: 2;
}
.nav-indicator.ready {
transition: transform 0.24s ease, opacity 0.18s ease;
}
.content {
flex: 1;
overflow-y: auto;
padding: 20px;
/* scrollbar-width: thin; */
scrollbar-color: rgba(170, 170, 170, 0.35) transparent;
display: flex;
flex-direction: column;
align-items: center;
}
.sidebar::-webkit-scrollbar,
.content::-webkit-scrollbar {
width: 8px;
height: 8px;
}
.sidebar::-webkit-scrollbar-track,
.content::-webkit-scrollbar-track {
background: transparent;
}
.sidebar::-webkit-scrollbar-thumb,
.content::-webkit-scrollbar-thumb {
background: rgba(170, 170, 170, 0.35);
border-radius: 3px;
}
.sidebar::-webkit-scrollbar-thumb:hover,
.content::-webkit-scrollbar-thumb:hover {
background: rgba(205, 205, 205, 0.45);
}
.content-page {
display: none;
width: 100%;
max-width: 800px;
}
.content-page.active {
display: block;
}
.page-title {
margin: 0 0 14px;
font-size: 20px;
font-weight: 600;
}
.section {
background: var(--card-bg);
border: 1px solid var(--border-color);
border-radius: 10px;
padding: 16px;
margin-bottom: 16px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.22);
}
.section-title {
margin: 0 0 12px;
color: #d2d2d2;
font-size: 15px;
font-weight: 600;
}
.about-text {
margin: 0;
color: #dfdfdf;
line-height: 1.7;
font-size: 14px;
}
.about-meta {
margin: 10px 0 0;
color: var(--subtext-color);
font-size: 13px;
line-height: 1.7;
}
.about-link-row {
margin-top: 12px;
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
}
.about-link-label {
color: var(--subtext-color);
font-size: 13px;
}
.about-link {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 12px;
border-radius: 999px;
border: 1px solid var(--border-color);
background: rgba(255, 255, 255, 0.04);
color: #e8e4ff;
font-size: 13px;
text-decoration: none;
transition: background 0.2s ease, border-color 0.2s ease, transform 0.18s ease;
}
.about-link:hover {
background: rgba(142, 140, 216, 0.2);
border-color: rgba(142, 140, 216, 0.55);
transform: translateY(-1px);
}
.about-link:focus-visible {
outline: 2px solid rgba(142, 140, 216, 0.55);
outline-offset: 2px;
}
.field {
display: grid;
gap: 6px;
margin-bottom: 12px;
}
.field:last-child {
margin-bottom: 0;
}
.field label {
font-size: 13px;
color: var(--subtext-color);
}
input[type="text"],
input[type="password"] {
width: 100%;
background: var(--input-bg);
border: 1px solid var(--border-color);
border-radius: 8px;
color: #f3f3f3;
padding: 9px 10px;
font-size: 14px;
outline: none;
}
input[type="text"]:focus,
input[type="password"]:focus {
border-color: var(--accent-color);
}
input[type="password"] {
letter-spacing: 2px;
font-family: "Segoe UI", sans-serif;
}
.dropdown {
position: relative;
width: 100%;
}
.dropdown-toggle {
width: 100%;
background: var(--input-bg);
border: 1px solid var(--border-color);
border-radius: 8px;
color: #f3f3f3;
padding: 9px 10px;
font-size: 14px;
display: flex;
justify-content: space-between;
align-items: center;
cursor: pointer;
text-align: left;
}
.dropdown-toggle:hover {
background: rgba(50, 50, 50, 0.9);
}
.dropdown.open .dropdown-toggle {
border-color: var(--accent-color);
}
.arrow {
width: 18px;
height: 18px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.arrow svg {
width: 18px;
height: 18px;
display: block;
transition: transform 0.4s ease;
transform-origin: center;
}
.dropdown.open .arrow svg {
transform: rotate(180deg);
}
.dropdown-menu {
display: none;
position: absolute;
top: calc(100% + 4px);
left: 0;
width: 100%;
background: #282828;
border: 1px solid var(--border-color);
border-radius: 10px;
padding: 2px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.35);
z-index: 20;
}
.dropdown.open .dropdown-menu {
display: block;
}
.dropdown-item {
width: 100%;
border: 0;
background: transparent;
color: #eee;
border-radius: 8px;
padding: 7px 8px;
text-align: left;
cursor: pointer;
font-size: 14px;
}
.dropdown-item:hover {
background: rgba(255, 255, 255, 0.08);
color: #fff;
}
.switch-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 0;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
}
.switch-row:last-child {
border-bottom: 0;
}
.switch-label {
display: flex;
flex-direction: column;
gap: 2px;
}
.switch-label strong {
font-size: 14px;
font-weight: 500;
}
.switch-label span {
font-size: 12px;
color: var(--subtext-color);
}
.switch-input {
appearance: none;
-webkit-appearance: none;
width: 38px;
height: 20px;
border-radius: 999px;
background: #555;
position: relative;
cursor: pointer;
transition: background 0.2s ease;
border: 1px solid transparent;
flex-shrink: 0;
}
.switch-input::after {
content: "";
position: absolute;
top: 50%;
left: 1px;
width: 16px;
height: 16px;
border-radius: 50%;
background: #fff;
transform: translateY(-50%);
transition: transform 0.2s ease;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
}
.switch-input:checked {
background: var(--accent-color);
}
.switch-input:checked::after {
transform: translate(18px, -50%);
}
.actions {
margin-top: 18px;
display: flex;
gap: 10px;
align-items: center;
width: 100%;
max-width: 800px;
}
.actions.hidden {
display: none;
}
button {
border: 0;
border-radius: 8px;
padding: 9px 14px;
cursor: default;
font-size: 14px;
}
.btn-primary {
background: #3c2d47;
color: #fff;
}
.btn-secondary {
background: #3a3a3a;
color: #f3f3f3;
}
.status {
font-size: 13px;
color: var(--subtext-color);
min-height: 18px;
}
.status.ok {
color: var(--ok);
}
.status.err {
color: var(--danger);
}
@media (max-width: 760px) {
body {
flex-direction: column;
}
.sidebar {
width: 100%;
border-right: 0;
border-bottom: 1px solid var(--border-color);
padding: 10px;
}
.content {
padding: 12px;
}
}
</style>
</head>
<body>
<aside class="sidebar">
<div class="sidebar-header">
<img class="logo" src="imgs/app.png" alt="App Logo" />
<div class="name">水杉记言</div>
</div>
<div class="nav-item active" data-target="api-page">🔌 API 配置</div>
<div class="nav-item" data-target="basic-page">🧩 基础设置</div>
<div class="nav-item" data-target="about-page">ℹ️ 关于</div>
</aside>
<main class="content">
<section id="api-page" class="content-page active">
<h1 class="page-title">API 配置</h1>
<div class="section">
<h2 class="section-title">ASR 接口([asr_api])</h2>
<div class="field">
<label for="asr-provider">provider</label>
<input id="asr-provider" type="text" value="siliconflow" />
</div>
<div class="field">
<label for="asr-token">token</label>
<input id="asr-token" type="password" value="" placeholder="填入你自己的 SiliconFlow API key" />
</div>
<div class="field">
<label for="asr-endpoint">endpoint</label>
<input id="asr-endpoint" type="text" value="https://api.siliconflow.cn/v1/audio/transcriptions" />
</div>
</div>
<div class="section">
<h2 class="section-title">润色接口([polish_api])</h2>
<div class="field">
<label for="polish-provider">provider</label>
<input id="polish-provider" type="text" value="siliconflow" />
</div>
<div class="field">
<label for="polish-token">token</label>
<input id="polish-token" type="password" value="" placeholder="填入你自己的 SiliconFlow API key" />
</div>
<div class="field">
<label for="polish-endpoint">endpoint</label>
<input id="polish-endpoint" type="text" value="https://api.siliconflow.cn/v1/chat/completions" />
</div>
</div>
</section>
<section id="basic-page" class="content-page">
<h1 class="page-title">基础设置</h1>
<div class="section">
<h2 class="section-title">[settings]</h2>
<div class="field">
<label for="language-btn">language</label>
<div class="dropdown" id="language-dropdown">
<button id="language-btn" class="dropdown-toggle" type="button">
<span class="dropdown-value">zh-cn</span>
<span class="arrow" aria-hidden="true">
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5.5 7.5L10 12L14.5 7.5" stroke="#cfcfcf" stroke-width="1.8" stroke-linecap="round"
stroke-linejoin="round" />
</svg>
</span>
</button>
<div class="dropdown-menu">
<button class="dropdown-item" type="button" data-value="zh-cn">zh-cn</button>
<button class="dropdown-item" type="button" data-value="en-us">en-us</button>
</div>
<input id="language" type="hidden" value="zh-cn" />
</div>
</div>
<div class="field">
<label for="stt-provider-btn">云端语音识别</label>
<div class="dropdown" id="stt-provider-dropdown">
<button id="stt-provider-btn" class="dropdown-toggle" type="button">
<span class="dropdown-value">cloud_siliconflow</span>
<span class="arrow" aria-hidden="true">
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5.5 7.5L10 12L14.5 7.5" stroke="#cfcfcf" stroke-width="1.8" stroke-linecap="round"
stroke-linejoin="round" />
</svg>
</span>
</button>
<div class="dropdown-menu">
<button class="dropdown-item" type="button" data-value="cloud_siliconflow">cloud_siliconflow</button>
</div>
<input id="stt-provider" type="hidden" value="cloud_siliconflow" />
</div>
</div>
<div class="switch-row">
<div class="switch-label">
<strong>上屏前先进行文本润色</strong>
<span>polish_text</span>
</div>
<input id="polish-text" class="switch-input" type="checkbox" />
</div>
<div class="switch-row">
<div class="switch-label">
<strong>触发时播放提示音</strong>
<span>notification_sound</span>
</div>
<input id="notification-sound" class="switch-input" type="checkbox" checked />
</div>
</div>
</section>
<section id="about-page" class="content-page">
<h1 class="page-title">关于</h1>
<div class="section">
<h2 class="section-title">项目简介</h2>
<p class="about-text">
<span>水杉记言</span>(Metasequoia Voice
Input)是一个面向中文输入场景的开源语音输入工具,源代码在 GitHub 上可查看。本项目通过语音识别(ASR)将语音实时转换为文本,并可选接入文本润色能力,帮助你以更自然、更高效的方式完成输入。
</p>
<br>
<p class="about-text">
当前设置窗口用于管理 ASR API、润色 API 和基础行为配置。您可以在“API 配置”和“基础设置”中修改参数,并通过“保存设置”提交到宿主程序。
</p>
<br>
<div class="about-link-row">
<span class="about-link-label">GitHub:</span>
<a class="about-link" href="https://github.com/metasequoiaime/MSIME-Engine/tree/main/voice" target="_blank"
rel="noopener noreferrer">MetasequoiaVoiceInput ↗</a>
</div>
<div class="about-link-row">
<span class="about-link-label">作者的更多作品,Windows 文字输入法:</span>
<a class="about-link" href="https://github.com/fanlumaster/MetasequoiaImeTsf" target="_blank"
rel="noopener noreferrer">水杉输入法 ↗</a>
</div>
</div>
</section>
<div class="actions">
<button id="save-btn" class="btn-primary" type="button">保存设置</button>
<button id="reset-btn" class="btn-secondary" type="button">恢复默认</button>
<span id="status" class="status"></span>
</div>
</main>
<script>
const defaultConfig = {
asr_api: {
provider: "siliconflow",
token: "",
endpoint: "https://api.siliconflow.cn/v1/audio/transcriptions"
},
polish_api: {
provider: "siliconflow",
token: "",
endpoint: "https://api.siliconflow.cn/v1/chat/completions"
},
settings: {
language: "zh-cn",
polish_text: false,
notification_sound: true,
stt_provider: "cloud_siliconflow"
}
};
const $ = (id) => document.getElementById(id);
function setStatus(text, type) {
const el = $("status");
el.textContent = text;
el.classList.remove("ok", "err");
if (type) {
el.classList.add(type);
}
}
function mergeConfigWithDefaults(config) {
const safe = config && typeof config === "object" ? config : {};
const asr = safe.asr_api && typeof safe.asr_api === "object" ? safe.asr_api : {};
const polish = safe.polish_api && typeof safe.polish_api === "object" ? safe.polish_api : {};
const settings = safe.settings && typeof safe.settings === "object" ? safe.settings : {};
return {
asr_api: {
provider: typeof asr.provider === "string" ? asr.provider : defaultConfig.asr_api.provider,
token: typeof asr.token === "string" ? asr.token : defaultConfig.asr_api.token,
endpoint: typeof asr.endpoint === "string" ? asr.endpoint : defaultConfig.asr_api.endpoint
},
polish_api: {
provider: typeof polish.provider === "string" ? polish.provider : defaultConfig.polish_api.provider,
token: typeof polish.token === "string" ? polish.token : defaultConfig.polish_api.token,
endpoint: typeof polish.endpoint === "string" ? polish.endpoint : defaultConfig.polish_api.endpoint
},
settings: {
language: typeof settings.language === "string" ? settings.language : defaultConfig.settings.language,
polish_text: typeof settings.polish_text === "boolean" ? settings.polish_text : defaultConfig.settings.polish_text,
notification_sound: typeof settings.notification_sound === "boolean" ? settings.notification_sound : defaultConfig.settings.notification_sound,
stt_provider: defaultConfig.settings.stt_provider
}
};
}
function readConfigFromForm() {
return {
asr_api: {
provider: $("asr-provider").value.trim(),
token: $("asr-token").value.trim(),
endpoint: $("asr-endpoint").value.trim()
},
polish_api: {
provider: $("polish-provider").value.trim(),
token: $("polish-token").value.trim(),
endpoint: $("polish-endpoint").value.trim()
},
settings: {
language: $("language").value,
polish_text: $("polish-text").checked,
notification_sound: $("notification-sound").checked,
stt_provider: $("stt-provider").value
}
};
}
function applyConfigToForm(config) {
$("asr-provider").value = config.asr_api.provider;
$("asr-token").value = config.asr_api.token;
$("asr-endpoint").value = config.asr_api.endpoint;
$("polish-provider").value = config.polish_api.provider;
$("polish-token").value = config.polish_api.token;
$("polish-endpoint").value = config.polish_api.endpoint;
$("language").value = config.settings.language;
$("polish-text").checked = config.settings.polish_text;
$("notification-sound").checked = config.settings.notification_sound;
$("stt-provider").value = config.settings.stt_provider;
syncDropdownText("language-dropdown");
syncDropdownText("stt-provider-dropdown");
}
function closeAllDropdowns() {
document.querySelectorAll(".dropdown.open").forEach((el) => {
el.classList.remove("open");
});
}
function syncDropdownText(dropdownId) {
const dropdown = document.getElementById(dropdownId);
if (!dropdown) {
return;
}
const hiddenInput = dropdown.querySelector("input[type='hidden']");
const valueEl = dropdown.querySelector(".dropdown-value");
const activeItem = dropdown.querySelector(`.dropdown-item[data-value="${hiddenInput.value}"]`);
valueEl.textContent = activeItem ? activeItem.textContent : hiddenInput.value;
}
function initDropdown(dropdownId) {
const dropdown = document.getElementById(dropdownId);
if (!dropdown) {
return;
}
const toggle = dropdown.querySelector(".dropdown-toggle");
const hiddenInput = dropdown.querySelector("input[type='hidden']");
toggle.addEventListener("click", (event) => {
event.stopPropagation();
const willOpen = !dropdown.classList.contains("open");
closeAllDropdowns();
if (willOpen) {
dropdown.classList.add("open");
}
});
dropdown.querySelectorAll(".dropdown-item").forEach((item) => {
item.addEventListener("click", () => {
hiddenInput.value = item.dataset.value;
syncDropdownText(dropdownId);
dropdown.classList.remove("open");
});
});
syncDropdownText(dropdownId);
}
function ensureNavIndicator() {
const sidebar = document.querySelector(".sidebar");
if (!sidebar) {
return null;
}
let indicator = sidebar.querySelector(".nav-indicator");
if (!indicator) {
indicator = document.createElement("div");
indicator.className = "nav-indicator";
sidebar.appendChild(indicator);
}
return indicator;
}
function moveNavIndicatorToItem(item) {
const sidebar = document.querySelector(".sidebar");
const indicator = ensureNavIndicator();
if (!sidebar || !indicator || !item) {
return;
}
const barLeft = item.offsetLeft - 2;
const barTop = item.offsetTop + (item.offsetHeight - 18) / 2;
if (!indicator.classList.contains("ready")) {
indicator.classList.add("ready");
indicator.style.transition = "none";
indicator.style.transform = `translate(${barLeft}px, ${barTop}px)`;
indicator.style.opacity = "1";
requestAnimationFrame(() => {
indicator.style.transition = "";
});
return;
}
indicator.style.transform = `translate(${barLeft}px, ${barTop}px)`;
indicator.style.opacity = "1";
}
function updateActionsVisibility(targetId) {
const actions = document.querySelector(".actions");
if (!actions) {
return;
}
const shouldHide = targetId === "about-page";
actions.classList.toggle("hidden", shouldHide);
}
document.querySelectorAll(".nav-item").forEach((item) => {
item.addEventListener("click", () => {
document.querySelector(".nav-item.active")?.classList.remove("active");
item.classList.add("active");
document.querySelector(".content-page.active")?.classList.remove("active");
const targetId = item.dataset.target;
document.getElementById(targetId)?.classList.add("active");
updateActionsVisibility(targetId);
moveNavIndicatorToItem(item);
});
});
$("save-btn").addEventListener("click", () => {
const config = readConfigFromForm();
try {
if (window.chrome?.webview) {
window.chrome.webview.postMessage({ action: "save_settings", config });
} else {
setStatus("当前不在宿主环境,无法写入配置文件", "err");
return;
}
} catch (err) {
setStatus("保存失败: " + String(err), "err");
}
});
$("reset-btn").addEventListener("click", () => {
applyConfigToForm(defaultConfig);
setStatus("已恢复默认值", "ok");
});
document.addEventListener("click", () => {
closeAllDropdowns();
});
window.__applyHostConfig = (config) => {
const normalized = mergeConfigWithDefaults(config);
applyConfigToForm(normalized);
setStatus("已加载本地配置", "ok");
};
window.__onHostSaveResult = (result) => {
if (!result || typeof result !== "object") {
setStatus("保存结果无效", "err");
return;
}
const message = typeof result.message === "string" ? result.message : (result.success ? "设置已保存" : "保存失败");
setStatus(message, result.success ? "ok" : "err");
};
initDropdown("language-dropdown");
initDropdown("stt-provider-dropdown");
applyConfigToForm(defaultConfig);
updateActionsVisibility("api-page");
moveNavIndicatorToItem(document.querySelector(".nav-item.active"));
if (window.chrome?.webview) {
window.chrome.webview.postMessage({ action: "load_settings" });
}
window.addEventListener("resize", () => {
moveNavIndicatorToItem(document.querySelector(".nav-item.active"));
});
</script>
</body>
</html>
-98
View File
@@ -1,98 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>(MetasequoiaVoiceInput)TrayMenu</title>
<style>
body {
background-color: transparent;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
color: #fff;
margin: 0;
padding: 0;
}
.menu {
background-color: #2b2b2b;
border: 1px solid #3a3a3a;
border-radius: 8px;
padding: 0.2em 0;
width: 6em;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);
user-select: none;
}
.menu-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.4em 0.4em;
font-size: 14px;
color: #e0e0e0;
border-radius: 6px;
margin: 1px 6px;
}
.menu-item:hover {
background-color: #3b3b3b;
color: #fff;
}
.menu-item-left {
display: flex;
align-items: center;
gap: 6px;
}
</style>
</head>
<body>
<div class="menu" id="menuContainer">
<div class="menu-item" id="settings">
<div class="menu-item-left">
<span>⚙️</span>
设置
</div>
</div>
<div class="menu-item" id="about">
<div class="menu-item-left">
<span>ℹ️</span>
关于
</div>
</div>
<div class="menu-item" id="exit">
<div class="menu-item-left">
<span>⏹️</span>
退出
</div>
</div>
</div>
<script>
// 设置:向 C++ 端发送消息,打开设置
const settings = document.getElementById('settings');
settings.addEventListener('click', () => {
console.log("settings");
window.chrome.webview.postMessage("open_settings");
});
// 关于:向 C++ 端发送消息,打开设置并切到关于页面
const about = document.getElementById('about');
about.addEventListener('click', () => {
console.log("about");
window.chrome.webview.postMessage("open_about");
});
// 退出:向 C++ 端发送消息,退出程序
const exit = document.getElementById('exit');
exit.addEventListener('click', () => {
console.log("exit");
window.chrome.webview.postMessage("exit");
});
</script>
</body>
</html>
@@ -3,10 +3,9 @@ if(NOT WIN32 OR NOT MSIME_VOICE_CAPTURE)
endif()
find_package(fmt CONFIG REQUIRED)
find_package(tomlplusplus CONFIG REQUIRED)
find_path(WEBVIEW2_INCLUDE_DIR WebView2.h REQUIRED)
add_executable(MetasequoiaVoiceInput WIN32 src/main.cpp src/cue_player.cpp src/mvi_config.cpp src/mvi_utils.cpp src/send_input.cpp src/wave_overlay.cpp src/window_webview2.cpp src/resource/app.rc)
add_executable(MetasequoiaVoiceInput WIN32 src/main.cpp src/cue_player.cpp src/mvi_config.cpp src/mvi_utils.cpp src/send_input.cpp src/wave_overlay.cpp src/tray_ui.cpp src/resource/app.rc)
target_compile_definitions(MetasequoiaVoiceInput PRIVATE UNICODE _UNICODE NOMINMAX)
target_compile_options(MetasequoiaVoiceInput PRIVATE /utf-8)
target_link_options(MetasequoiaVoiceInput PRIVATE /ENTRY:mainCRTStartup)
target_include_directories(MetasequoiaVoiceInput PRIVATE src src/resource ../../third_party/miniaudio ${WEBVIEW2_INCLUDE_DIR})
target_include_directories(MetasequoiaVoiceInput PRIVATE src src/resource ../../third_party/miniaudio)
target_link_libraries(MetasequoiaVoiceInput PRIVATE MetasequoiaIme::Voice MetasequoiaIme::VoiceCapture fmt::fmt nlohmann_json::nlohmann_json tomlplusplus::tomlplusplus d2d1 User32 dwmapi shell32)
+1 -11
View File
@@ -65,17 +65,7 @@ polish_text = false
stt_provider = "cloud_siliconflow"
```
You can also change these config in settings window:
![](https://i.imgur.com/Q3Jct2Z.png)
![](https://i.imgur.com/9j2IV9X.png)
![](https://i.imgur.com/1F47neV.png)
## Notice
- Only implemented dark-mode UI now
The tray icon's right-click menu has **设置** (opens `config.toml` in the editor associated with `.toml`, falling back to Notepad, and writes the defaults first if the file does not exist), **关于** and **退出**. Double-clicking the tray icon toggles recording. Changes to `config.toml` take effect the next time the host starts.
## License
+5 -7
View File
@@ -21,7 +21,7 @@
#include "wave_overlay.h"
#include "cue_player.h"
#include "text_polisher.h"
#include "window_webview2.h"
#include "tray_ui.h"
#include <fmt/format.h>
#include <fmt/xchar.h>
#include <windows.h>
@@ -392,14 +392,12 @@ int main()
MSG init_msg{};
PeekMessage(&init_msg, nullptr, WM_USER, WM_USER, PM_NOREMOVE);
window_webview2::TrayMenuConfig tray_config{};
tray_ui::TrayMenuConfig tray_config{};
tray_config.main_thread_id = g_main_thread_id;
tray_config.msg_toggle_record = WM_APP_TOGGLE_RECORD;
tray_config.msg_exit = WM_APP_EXIT;
tray_config.app_name = L"MetasequoiaVoiceInput";
tray_config.default_width = 300;
tray_config.default_height = 360;
if (!window_webview2::InitializeTrayUi(app_instance, tray_config))
if (!tray_ui::InitializeTrayUi(app_instance, tray_config))
{
throw std::runtime_error("Tray UI init failed");
}
@@ -612,7 +610,7 @@ int main()
force_release_ralt_key();
UnhookWindowsHookEx(keyboard_hook);
window_webview2::ShutdownTrayUi(app_instance);
tray_ui::ShutdownTrayUi(app_instance);
wave_overlay.shutdown();
// 通知 stt 线程停止
@@ -627,7 +625,7 @@ int main()
}
catch (const std::exception &e)
{
window_webview2::ShutdownTrayUi(app_instance);
tray_ui::ShutdownTrayUi(app_instance);
printf("FATAL ERROR: %s\n", e.what());
fflush(stdout);
MessageBoxA(nullptr, e.what(), "MetasequoiaVoiceInput - Fatal Error", MB_ICONERROR);
@@ -0,0 +1,248 @@
#include "tray_ui.h"
#include "mvi_config.h"
#include "resource/resource.h"
#include <atomic>
#include <cstdio>
#include <objbase.h>
#include <shellapi.h>
#include <thread>
namespace tray_ui
{
namespace
{
constexpr UINT WM_APP_TRAY_ICON = WM_APP + 120;
constexpr UINT k_tray_icon_id = 1;
constexpr wchar_t k_tray_window_class[] = L"MetasequoiaVoiceInput.TrayWindow";
constexpr wchar_t k_tray_tooltip[] = L"MetasequoiaVoiceInput";
constexpr wchar_t k_project_url[] = L"https://github.com/metasequoiaime/MSIME-Engine/tree/main/voice";
enum TrayCommand : UINT
{
k_cmd_settings = 1,
k_cmd_about,
k_cmd_exit,
};
struct TrayUiState
{
TrayMenuConfig config{};
HWND tray_window = nullptr;
NOTIFYICONDATAW notify_icon{};
bool notify_icon_added = false;
HICON tray_icon = nullptr;
};
TrayUiState g_state;
std::atomic<bool> g_about_open{false};
std::wstring AnsiToWide(const std::string &text)
{
if (text.empty())
{
return L"";
}
const int size = MultiByteToWideChar(CP_ACP, 0, text.c_str(), static_cast<int>(text.size()), nullptr, 0);
if (size <= 0)
{
return L"";
}
std::wstring result(size, L'\0');
MultiByteToWideChar(CP_ACP, 0, text.c_str(), static_cast<int>(text.size()), result.data(), size);
return result;
}
// There is no settings form: the menu opens config.toml in the user's editor. A missing file is
// first written with the defaults so there is something to edit.
void OpenConfigFile()
{
const std::string config_path = mvi_config::GetConfigPath();
if (config_path.empty())
{
printf("[SETTINGS] LOCALAPPDATA is empty; cannot locate config.toml.\n");
fflush(stdout);
return;
}
const std::wstring wide_path = AnsiToWide(config_path);
if (GetFileAttributesW(wide_path.c_str()) == INVALID_FILE_ATTRIBUTES)
{
std::string error_message;
if (!mvi_config::WriteConfigFromJson("{}", &error_message))
{
printf("[SETTINGS] Failed to create default config.toml: %s\n", error_message.c_str());
fflush(stdout);
return;
}
}
const auto result = reinterpret_cast<INT_PTR>(ShellExecuteW(nullptr, L"open", wide_path.c_str(), nullptr, nullptr, SW_SHOWNORMAL));
if (result > 32)
{
return;
}
// .toml frequently has no association; fall back to Notepad.
const std::wstring quoted = L"\"" + wide_path + L"\"";
ShellExecuteW(nullptr, L"open", L"notepad.exe", quoted.c_str(), nullptr, SW_SHOWNORMAL);
}
// A MessageBox on the main thread would run a modal loop that drops the thread messages the
// keyboard hook posts, so the About box lives on its own short-lived thread.
void ShowAbout()
{
bool expected = false;
if (!g_about_open.compare_exchange_strong(expected, true))
{
return;
}
std::thread([]() {
const HRESULT hr = CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE);
const int choice = MessageBoxW(nullptr,
L"水杉记言 (Metasequoia Voice Input) 是一个面向中文输入场景的开源语音输入工具,"
L"通过云端语音识别 (ASR) 将语音转换为文本,并可选接入文本润色。\n\n"
L"设置项保存在 %LOCALAPPDATA%\\MetasequoiaVoiceInput\\config.toml。\n\n"
L"是否在浏览器中打开项目主页?",
L"关于 MetasequoiaVoiceInput", MB_YESNO | MB_ICONINFORMATION | MB_SETFOREGROUND);
if (choice == IDYES)
{
ShellExecuteW(nullptr, L"open", k_project_url, nullptr, nullptr, SW_SHOWNORMAL);
}
if (SUCCEEDED(hr))
{
CoUninitialize();
}
g_about_open = false;
}).detach();
}
void ShowTrayMenu(HWND hwnd)
{
HMENU menu = CreatePopupMenu();
if (menu == nullptr)
{
return;
}
AppendMenuW(menu, MF_STRING, k_cmd_settings, L"设置(编辑 config.toml)");
AppendMenuW(menu, MF_STRING, k_cmd_about, L"关于");
AppendMenuW(menu, MF_SEPARATOR, 0, nullptr);
AppendMenuW(menu, MF_STRING, k_cmd_exit, L"退出");
POINT cursor{};
GetCursorPos(&cursor);
// The foreground/WM_NULL pair is what makes the menu dismiss on an outside click.
SetForegroundWindow(hwnd);
UINT flags = TPM_RIGHTBUTTON | TPM_RETURNCMD | TPM_NONOTIFY;
flags |= GetSystemMetrics(SM_MENUDROPALIGNMENT) != 0 ? TPM_RIGHTALIGN : TPM_LEFTALIGN;
const UINT command = static_cast<UINT>(TrackPopupMenuEx(menu, flags, cursor.x, cursor.y, hwnd, nullptr));
PostMessageW(hwnd, WM_NULL, 0, 0);
DestroyMenu(menu);
switch (command)
{
case k_cmd_settings:
OpenConfigFile();
break;
case k_cmd_about:
ShowAbout();
break;
case k_cmd_exit:
printf("[TRAY] Exit selected from tray menu.\n");
fflush(stdout);
PostThreadMessageW(g_state.config.main_thread_id, g_state.config.msg_exit, 0, 0);
break;
default:
break;
}
}
LRESULT CALLBACK TrayWindowProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
if (message == WM_APP_TRAY_ICON)
{
const UINT event = LOWORD(lParam);
if (event == WM_RBUTTONUP || event == WM_CONTEXTMENU)
{
ShowTrayMenu(hwnd);
return 0;
}
if (event == WM_LBUTTONDBLCLK && g_state.config.msg_toggle_record != 0)
{
PostThreadMessageW(g_state.config.main_thread_id, g_state.config.msg_toggle_record, 0, 0);
return 0;
}
return 0;
}
return DefWindowProcW(hwnd, message, wParam, lParam);
}
} // namespace
bool InitializeTrayUi(HINSTANCE instance, const TrayMenuConfig &config)
{
g_state = {};
g_state.config = config;
WNDCLASSW wc{};
wc.lpfnWndProc = TrayWindowProc;
wc.hInstance = instance;
wc.lpszClassName = k_tray_window_class;
RegisterClassW(&wc);
// A hidden top-level window rather than HWND_MESSAGE: TrackPopupMenu needs an owner that can
// become the foreground window, which a message-only window cannot.
g_state.tray_window = CreateWindowExW(WS_EX_TOOLWINDOW, k_tray_window_class, L"MetasequoiaVoiceInput.TrayHost", WS_POPUP, 0, 0, 0, 0, nullptr, nullptr, instance, nullptr);
if (g_state.tray_window == nullptr)
{
printf("[TRAY] Failed to create tray host window: %lu\n", GetLastError());
fflush(stdout);
return false;
}
g_state.tray_icon = static_cast<HICON>(LoadImageW(instance, MAKEINTRESOURCEW(IDI_APP_ICON), IMAGE_ICON, 0, 0, LR_DEFAULTSIZE));
if (g_state.tray_icon == nullptr)
{
g_state.tray_icon = LoadIconW(nullptr, IDI_APPLICATION);
}
g_state.notify_icon = {};
g_state.notify_icon.cbSize = sizeof(NOTIFYICONDATAW);
g_state.notify_icon.hWnd = g_state.tray_window;
g_state.notify_icon.uID = k_tray_icon_id;
g_state.notify_icon.uFlags = NIF_MESSAGE | NIF_ICON | NIF_TIP;
g_state.notify_icon.uCallbackMessage = WM_APP_TRAY_ICON;
g_state.notify_icon.hIcon = g_state.tray_icon;
wcscpy_s(g_state.notify_icon.szTip, k_tray_tooltip);
if (!Shell_NotifyIconW(NIM_ADD, &g_state.notify_icon))
{
printf("[TRAY] Failed to add tray icon.\n");
fflush(stdout);
return false;
}
g_state.notify_icon_added = true;
g_state.notify_icon.uVersion = NOTIFYICON_VERSION_4;
Shell_NotifyIconW(NIM_SETVERSION, &g_state.notify_icon);
printf("[TRAY] Tray icon initialized.\n");
fflush(stdout);
return true;
}
void ShutdownTrayUi(HINSTANCE instance)
{
if (g_state.notify_icon_added)
{
Shell_NotifyIconW(NIM_DELETE, &g_state.notify_icon);
g_state.notify_icon_added = false;
}
if (g_state.tray_window != nullptr)
{
DestroyWindow(g_state.tray_window);
g_state.tray_window = nullptr;
}
UnregisterClassW(k_tray_window_class, instance);
}
} // namespace tray_ui
@@ -3,7 +3,7 @@
#include <string>
#include <windows.h>
namespace window_webview2
namespace tray_ui
{
struct TrayMenuConfig
{
@@ -11,11 +11,8 @@ struct TrayMenuConfig
UINT msg_toggle_record = 0;
UINT msg_exit = 0;
std::wstring app_name = L"MetasequoiaVoiceInput";
std::wstring tray_menu_html_path;
int default_width = 194;
int default_height = 299;
};
bool InitializeTrayUi(HINSTANCE instance, const TrayMenuConfig &config);
void ShutdownTrayUi(HINSTANCE instance);
} // namespace window_webview2
} // namespace tray_ui
File diff suppressed because it is too large. Load diff
+1 -2
View File
@@ -8,8 +8,7 @@
"description": "Standalone Windows host dependencies",
"dependencies": [
"fmt",
"tomlplusplus",
"webview2"
"tomlplusplus"
]
}
}
+1 -1
View File
@@ -98,7 +98,7 @@ icon = "#f6e4eb"
- `font_family` 排在用户设置的字体前面,缺字时回落到用户字体;字号始终由用户设置决定。
- `page_arrows = true` 可以打开候选框里的翻页箭头;不写时沿用 `base` 内置皮肤的设置(出厂为关闭)。
- `fit_content = true` 让卡片严格按候选文字的宽度收紧,不再垫出厂最小宽度;`min_width_dip` 和装饰图宽度仍然生效。不写时沿用 `base` 内置皮肤的设置(出厂为关闭)。
- `[toolbar]` 下的所有键都可以省略,没写的沿用 `base` 皮肤;配置对 D2D 和 WebView2 两种工具栏渲染器都生效。
- `[toolbar]` 下的所有键都可以省略,没写的沿用 `base` 皮肤。
- `[supports].themes` 里声明的每套主题都要有对应的 `[candidate.<theme>]`;写了 `[toolbar]` 的话,也要有对应的 `[toolbar.<theme>]`。
完整字段见 [schema/README.md](schema/README.md)。
+7 -7
View File
@@ -1,6 +1,6 @@
# 外部皮肤 manifest(TOML)
每个皮肤目录需要 `skin.toml`,`id` 必须与文件夹名一致。`schema_version` 支持 `1` 与 `2`(v2 为面向全贴图与复杂立绘皮肤的高级规范,仅在 WebView2 端全面启用,Direct2D 原生端保底降级)。
每个皮肤目录需要 `skin.toml`,`id` 必须与文件夹名一致。`schema_version` 支持 `1` 与 `2`(v2 为面向全贴图与复杂立绘皮肤的高级规范)。候选框由 Direct2D 原生渲染器绘制;v2 的部分字段在原生端仍在补全,下文标「v2」的字段以实际效果为准。
候选框和悬浮工具栏都由 `skin.toml` 声明,不要再提供 `cand.css` 或工具栏 CSS。所有图片路径都相对于皮肤目录,且不能指向目录之外。
@@ -20,7 +20,7 @@
- `shadow`:卡片阴影,`none` | `soft` | `strong`;不写则沿用 `base`。只影响候选卡片,不影响右键菜单
- `font_family`:候选字体名,UTF-8 编码后最长 64 字节(约 21 个汉字),不能含引号、反斜杠、逗号、分号、尖括号、花括号和反引号。排在用户设置的字体前面,缺字时仍回落到用户字体;字号不由皮肤决定
- `page_arrows`:候选框里的翻页箭头,`true` | `false`。横排时是候选右侧贴着最后一行的一小列,竖排时是最后一行下方的一矮行、“‹”与序号左对齐;颜色取 `number`,悬停底色取 `hover`。不写则沿用 `base` 的默认皮肤设置(数据目录 `skins\default\<base>\skin.toml`,出厂为 `false`)
- `fit_content`:卡片是否严格按候选文字所需的宽度收紧,`true` | `false`。打开后不再垫出厂的最小宽度(D2D 160 DIP、WebView2 `7em`),同一次输入里后到的翻译等内容消失时宽度也立刻缩回;`min_width_dip` 和装饰图 `width_dip` 仍然生效。不写则沿用 `base` 的默认皮肤设置(出厂为 `false`)
- `fit_content`:卡片是否严格按候选文字所需的宽度收紧,`true` | `false`。打开后不再垫出厂的最小宽度(160 DIP),同一次输入里后到的翻译等内容消失时宽度也立刻缩回;`min_width_dip` 和装饰图 `width_dip` 仍然生效。不写则沿用 `base` 的默认皮肤设置(出厂为 `false`)
### `[candidate_window.decoration]`(可选)
@@ -38,7 +38,7 @@
- `fit`:`cover`(铺满裁切)| `contain`(完整显示)| `stretch`(拉伸)
- `opacity`:`0`–`1`
### `[candidate_window.border_image]`(v2 可选,仅 WebView2)
### `[candidate_window.border_image]`(v2 可选)
九宫格背景拉伸,面板整体贴图,用于全贴图与复杂纹理面板。
@@ -48,7 +48,7 @@
- `width_dip`:可选,边框绘制宽度,支持 1~4 个值
- `fill`:可选,布尔值,是否保留九宫格中心内容区域,默认为 `true`
### `[candidate_window.content_insets_dip]`(v2 可选,仅 WebView2)
### `[candidate_window.content_insets_dip]`(v2 可选)
候选框内容与文字的安全内边距,**从面板外缘量起**,用来避开贴图四周自带的花纹或立绘。渲染时会把该方向的边框宽度扣掉,落到内容盒上。
@@ -66,7 +66,7 @@
**不声明时窗口只按卡片大小开,挂到卡片外面的立绘会被窗口边界裁掉。**
### `[[candidate_window.decorations]]`(v2 可选,数组表,仅 WebView2)
### `[[candidate_window.decorations]]`(v2 可选,数组表)
自由锚定立绘挂件层,支持叠加角色立绘与 APNG 动图,层数不限。
@@ -114,7 +114,7 @@ translation_separator = "#529cca73"
## `[toolbar]`
悬浮工具栏样式,D2D 与 WebView2 两个渲染器都生效。所有键都可省略,不写的沿用 `base`。
悬浮工具栏样式。所有键都可省略,不写的沿用 `base`。
- `corner_radius_dip`:`0`–`32`
- `[toolbar.dark]` / `[toolbar.light]`:`background`、`border`、`handle`(左侧拖动条)、`divider`、`icon`、`hover`
@@ -131,5 +131,5 @@ translation_separator = "#529cca73"
| `rgba(r, g, b, a)` | `rgba(224, 138, 168, 0.28)` | `a` 为 0–1 |
| `transparent` | `transparent` | 全透明,等同 `#0000` |
- 十六进制大小写均可,但**必须带 `#`**:原生(D2D)渲染器不带 `#` 也能识别,WebView2 渲染器按 CSS 解析,会把它当作无效值,两种渲染结果就不一致了。
- 十六进制大小写均可,但**必须带 `#`**:这是清单格式的约定,与 CSS 写法一致;渲染器对不带 `#` 的写法有容错,但不要依赖它。
- 不透明度在**末尾**(`#RRGGBBAA`),不是 Windows 常见的 `#AARRGGBB`。例如 `#e9e8e89d` 约为 62% 不透明,`rgba(224, 138, 168, 0.28)` 与 `#e08aa847` 等价。
@@ -5,7 +5,7 @@
# 颜色写法:所有颜色键([candidate.*]、[candidate.*.menu]、[toolbar.*] 里除 show_selected_bar 外的键)
# 都接受下面任意一种,不能写颜色的是数字、布尔、枚举和路径类的键。
# - 十六进制:#rgb、#rgba、#rrggbb、#rrggbbaa,大小写均可。
# 必须带 #:原生渲染器不带 # 也能认,但 WebView2 按 CSS 解析,会把它当成无效值。
# 必须带 #:这是清单格式的约定,与 CSS 写法一致;渲染器不带 # 也能认,但不要依赖它。
# 不透明度在末尾(#RRGGBBAA),不是 Windows 常见的 #AARRGGBB;例如 #e9e8e89d ≈ 62% 不透明。
# - rgb(r, g, b) / rgba(r, g, b, a):r、g、b 为 0–255,a 为 0–1。
# 例如 rgba(224, 138, 168, 0.28) 与 #e08aa847 等价。
@@ -139,7 +139,7 @@ text = "#3a2a30"
hover = "rgba(196, 92, 122, 0.12)"
# ---------------------------------------------------------------------------
# 悬浮工具栏:D2D 与 WebView2 两个渲染器都生效,所有键都可省略,不写的沿用 base
# 悬浮工具栏:所有键都可省略,不写的沿用 base
# ---------------------------------------------------------------------------
[toolbar]
corner_radius_dip = 8 # 工具栏圆角,0–32
+5 -16
View File
@@ -12,8 +12,8 @@
[CmdletBinding()]
param(
# 轻量包:跳过词库、辅助码、拼音表和出厂配置,只刷新 TSF、Server、HTML。
# 适用:只改了 TSF、Server 或 HTML,本机已有完整安装(词库已在
# 轻量包:跳过词库、辅助码、拼音表和出厂配置,只刷新 TSF、Server 和设置窗口图片资源。
# 适用:只改了 TSF 或 Server,本机已有完整安装(词库已在
# %LOCALAPPDATA%\metasequoiaime)。不要用轻量包做首次安装。
[switch]$Light,
# 把 PDB 打进安装包。PDB 有 ~140 MB,而真正的二进制只有 ~20 MB,Inno Setup 还要对它们做
@@ -34,12 +34,11 @@ try {
$repoRoot = Split-Path -Parent $PSScriptRoot
$tsfCompile = Join-Path $repoRoot 'windows\scripts\lcompile-release-both.ps1'
$serverCompile = Join-Path $repoRoot 'server\scripts\lcompile-release.ps1'
$settingsDir = Join-Path $repoRoot 'ui-html\webview2\settings\ime-settings'
# All three are directories of this repository now, so a missing one is a broken checkout rather
# Both are directories of this repository now, so a missing one is a broken checkout rather
# than a neighbour nobody cloned. Skipping the build and packaging whatever binaries happen to be
# lying around would produce an installer you then run on your own machine.
foreach ($required in @($tsfCompile, $serverCompile, (Join-Path $settingsDir 'package.json'))) {
foreach ($required in @($tsfCompile, $serverCompile)) {
if (-not (Test-Path -LiteralPath $required -PathType Leaf)) {
throw "缺少组件构建入口:$required"
}
@@ -51,16 +50,6 @@ try {
Push-Location (Join-Path $repoRoot 'server')
try { & $serverCompile -Reconfigure:$Reconfigure } finally { Pop-Location }
Push-Location $settingsDir
try {
# 首次运行或 node_modules 缺失时需要安装依赖
if (-not (Test-Path -LiteralPath 'node_modules' -PathType Container)) {
pnpm install
if ($LASTEXITCODE -ne 0) { throw "Settings page dependencies installation failed ($LASTEXITCODE)" }
}
pnpm run build
if ($LASTEXITCODE -ne 0) { throw "Settings page build failed ($LASTEXITCODE)" }
} finally { Pop-Location }
# 轻量包不带任何数据文件,所以只有完整包需要语言模型。脚本是幂等的:摘要对得上时
# 直接返回,不重下也不重转,因此这里无条件调用不会让日常 test.ps1 变慢。首次运行
@@ -79,7 +68,7 @@ try {
}
& (Join-Path $PSScriptRoot 'Prepare-PackageFiles.ps1') -Light:$Light -IncludeSymbols:$IncludeSymbols `
-RepoRoot $repoRoot -TsfDirectory windows -ServerDirectory server -UiHtmlDirectory ui-html -NoticesDirectory .
-RepoRoot $repoRoot -TsfDirectory windows -ServerDirectory server -NoticesDirectory .
& (Join-Path $PSScriptRoot 'Sign-PackageBinaries-Local.ps1')
& (Join-Path $PSScriptRoot 'Compile-Installer.ps1') -Light:$Light
& (Join-Path $PSScriptRoot 'Sign-Installer-Local.ps1') -Light:$Light
+14 -32
View File
@@ -6,7 +6,6 @@ param(
# Historical or custom layouts remain available through explicit overrides.
[string]$TsfDirectory = 'windows',
[string]$ServerDirectory = 'server',
[string]$UiHtmlDirectory = 'ui-html',
[string]$HelpCodeDirectory = 'engine/helpcode',
# U 模式的部件拆字反查表(decomposition.txt)随包发布到数据目录的 radical\ 下。
[string]$RadicalDirectory = 'engine/radical',
@@ -65,7 +64,8 @@ $tsf32Release = Join-Path $RepoRoot (Join-Path $TsfDirectory 'build32-release\Re
$tsf64Release = Join-Path $RepoRoot (Join-Path $TsfDirectory 'build64-release\Release\MetasequoiaImeTsf.dll')
$tsf32Pdb = Join-Path $RepoRoot (Join-Path $TsfDirectory 'build32-release\Release\MetasequoiaImeTsf.pdb')
$tsf64Pdb = Join-Path $RepoRoot (Join-Path $TsfDirectory 'build64-release\Release\MetasequoiaImeTsf.pdb')
$webviewRoot = Join-Path $RepoRoot (Join-Path $UiHtmlDirectory 'webview2')
# 原生(Direct2D)设置窗口用到的图标与图片,按原相对路径装到数据目录的 assets\settings\。
$settingsAssets = Join-Path $RepoRoot (Join-Path $ServerDirectory 'assets\settings')
$defaultSkinsSource = Join-Path $RepoRoot 'skins\default'
$bundledSkinsSource = Join-Path $RepoRoot 'skins'
$serverConfig = Join-Path $RepoRoot (Join-Path $ServerDirectory 'assets\config\config.toml')
@@ -117,11 +117,7 @@ Assert-PathExists -LiteralPath $appIcon -Description '应用图标'
Assert-PathExists -LiteralPath $tipIcon -Description '输入法语言配置图标 msime.ico'
Assert-PathExists -LiteralPath $thirdPartyNotices -Description '第三方声明 THIRD_PARTY_NOTICES.txt'
Assert-PathExists -LiteralPath $license -Description '许可证 LICENSE'
Assert-PathExists -LiteralPath (Join-Path $webviewRoot 'shared') -Description '共享 WebView 消息契约'
Assert-PathExists -LiteralPath (Join-Path $webviewRoot 'candwnd') -Description '候选窗 HTML 目录'
Assert-PathExists -LiteralPath (Join-Path $webviewRoot 'ftb') -Description '悬浮工具栏 HTML 目录'
Assert-PathExists -LiteralPath (Join-Path $webviewRoot 'menu') -Description '菜单 HTML 目录'
Assert-PathExists -LiteralPath (Join-Path $webviewRoot 'settings\ime-settings\dist') -Description '设置页面 dist 目录'
Assert-PathExists -LiteralPath $settingsAssets -Description '设置窗口图片资源目录'
Assert-PathExists -LiteralPath $defaultSkinsSource -Description '内置皮肤设置清单 skins\default'
if (-not $Light) {
@@ -160,7 +156,7 @@ $targetServer = Join-Path $PSScriptRoot 'server_exe'
$targetTsf = Join-Path $PSScriptRoot 'tsf_dll'
if ($Light) {
Write-Host '轻量模式:跳过词库、辅助码、拼音表和出厂配置,只刷新 TSF、Server、HTML。'
Write-Host '轻量模式:跳过词库、辅助码、拼音表和出厂配置,只刷新 TSF、Server 和设置窗口图片资源。'
New-Item -ItemType Directory -Path $targetAppData -Force | Out-Null
}
else {
@@ -235,17 +231,17 @@ else {
Copy-Item -LiteralPath $radicalTable -Destination (Join-Path $targetRadical 'decomposition.txt') -Force
}
$targetHtml = Join-Path $targetAppData 'html'
if (Test-Path -LiteralPath $targetHtml) {
Remove-Item -LiteralPath $targetHtml -Recurse -Force
# 轻量模式不清空 app_data,旧版本留下的 html\(WebView2 页面)要在这里删掉,免得被打进包里。
$legacyHtml = Join-Path $targetAppData 'html'
if (Test-Path -LiteralPath $legacyHtml) {
Remove-Item -LiteralPath $legacyHtml -Recurse -Force
}
$targetWebview = Join-Path $targetHtml 'webview2'
Copy-DirectoryContents -Source (Join-Path $webviewRoot 'shared') -Destination (Join-Path $targetWebview 'shared')
Copy-DirectoryContents -Source (Join-Path $webviewRoot 'candwnd') -Destination (Join-Path $targetWebview 'candwnd')
Copy-DirectoryContents -Source (Join-Path $webviewRoot 'ftb') -Destination (Join-Path $targetWebview 'ftb')
Copy-DirectoryContents -Source (Join-Path $webviewRoot 'menu') -Destination (Join-Path $targetWebview 'menu')
Copy-DirectoryContents -Source (Join-Path $webviewRoot 'settings\ime-settings\dist') `
-Destination (Join-Path $targetWebview 'settings\ime-settings\dist')
$targetAssets = Join-Path $targetAppData 'assets'
$targetSettingsAssets = Join-Path $targetAssets 'settings'
if (Test-Path -LiteralPath $targetAssets) {
Remove-Item -LiteralPath $targetAssets -Recurse -Force
}
Copy-DirectoryContents -Source $settingsAssets -Destination $targetSettingsAssets
# 内置皮肤的设置清单(翻页箭头等开关)。用户会就地改这些 skin.toml,安装脚本用
# onlyifdoesntexist 落到数据目录的 skins\default,升级只补缺失的文件、不覆盖已有的。
@@ -356,20 +352,6 @@ $updatedIss = [regex]::Replace(
$updatedIss = $updatedIss.TrimEnd("`r", "`n") + "`n"
Set-Content -LiteralPath $targetIss -Value $updatedIss -Encoding utf8NoBOM -NoNewline
# 设置页是已构建的静态资源,同步其“当前版本”展示。
$settingsDist = Join-Path $targetWebview 'settings\ime-settings\dist'
Get-ChildItem -LiteralPath $settingsDist -Recurse -File -Include '*.js', '*.html' | ForEach-Object {
$content = Get-Content -LiteralPath $_.FullName -Raw
$updated = [regex]::Replace(
$content,
'(<div class="about-version">)v\d+(?:\.\d+)+(</div>)',
"`$1v$TargetVersion`$2"
)
if ($updated -ne $content) {
Set-Content -LiteralPath $_.FullName -Value $updated -Encoding utf8NoBOM
}
}
$serverBinaryCount = @(Get-ChildItem -LiteralPath $targetServer -Recurse -File -Include '*.exe', '*.dll').Count
$tsfBinaryCount = @(Get-ChildItem -LiteralPath $targetTsf -Recurse -File -Include '*.exe', '*.dll').Count
$symbolCount = @(
+9 -11
View File
@@ -13,14 +13,14 @@
## CI 契约
根 `.github/workflows/release.yml` **不加修改地调用本目录的 `Prepare-PackageFiles.ps1` 和 `Compile-Installer.ps1`**。源目录名是 `Prepare-PackageFiles.ps1` 的参数,不是写死在脚本里的字面量:默认值为 `windows/`、`server/`、`ui-html/` 和仓根授权文件;旧目录或自定义布局通过参数覆盖。回归检查同时执行默认路径和显式参数的打包。辅助码默认读取 `engine/helpcode/`,词库缓存位于 `MetasequoiaImeDict/out/`。
根 `.github/workflows/release.yml` **不加修改地调用本目录的 `Prepare-PackageFiles.ps1` 和 `Compile-Installer.ps1`**。源目录名是 `Prepare-PackageFiles.ps1` 的参数,不是写死在脚本里的字面量:默认值为 `windows/`、`server/` 和仓根授权文件;旧目录或自定义布局通过参数覆盖。回归检查同时执行默认路径和显式参数的打包。辅助码默认读取 `engine/helpcode/`,词库缓存位于 `MetasequoiaImeDict/out/`。
合仓后 release workflow 传的是本仓的目录名,辅助码随固定 Engine gitlink 检出,词库按产品锁下载到仓根缓存:
```powershell
pwsh -File ./Prepare-PackageFiles.ps1 -TargetVersion 1.2.3 -RepoRoot .. `
-TsfDirectory windows -ServerDirectory server `
-UiHtmlDirectory ui-html -NoticesDirectory .
-NoticesDirectory .
```
由此产生几条约束:
@@ -34,14 +34,14 @@ pwsh -File ./Prepare-PackageFiles.ps1 -TargetVersion 1.2.3 -RepoRoot .. `
## 数据目录
程序本体固定装在 `Program Files\metasequoiaime`(Server 带 `uiAccess=true`,只有装在受信任目录里这个标志才生效),**用户数据目录可以在安装向导里改**:词库、`config.toml`、用户词库、皮肤和前端资源都在那里,整包几百 MB,装 C 盘吃紧的用户可以放到别的盘。
程序本体固定装在 `Program Files\metasequoiaime`(Server 带 `uiAccess=true`,只有装在受信任目录里这个标志才生效),**用户数据目录可以在安装向导里改**:词库、`config.toml`、用户词库、皮肤和设置窗口的图片资源都在那里,整包几百 MB,装 C 盘吃紧的用户可以放到别的盘。
- 默认值 `%LOCALAPPDATA%\metasequoiaime`;升级安装时默认沿用上次的位置
- 选择写进 `HKLM\Software\Metasequoia\MetasequoiaIME` 的 `DataDir`。这是运行期唯一的权威来源:Server(`server/src/utils/ime_paths.cpp`)、TSF DLL(`windows/src/Utils/FanyUtils.cpp`)和引擎(`engine/core/data_path.h`)各自按 `METASEQUOIA_IME_DATA_DIR` 环境变量 → 该注册表值 → `%LOCALAPPDATA%\metasequoiaime` 的顺序解析,三处必须保持一致。32 位 TSF DLL 用 `KEY_WOW64_64KEY` 读,所以这个值必须写在 64 位视图里
- 升级时改了位置,安装器会把用户词库(`msime_user.db` 及其 WAL/SHM)、`config.toml`、`config.base.toml` 和 `skins\` 搬到新目录,再删掉旧目录;词库和前端资源由本次安装重新写入,不搬
- 升级时改了位置,安装器会把用户词库(`msime_user.db` 及其 WAL/SHM)、`config.toml`、`config.base.toml` 和 `skins\` 搬到新目录,再删掉旧目录;词库和设置窗口的图片资源由本次安装重新写入,不搬
- **数据目录必须整个归输入法**([#537](https://github.com/metasequoiaime/MSIME-Windows/issues/537))。用户选的目录里已经有别的文件、或者选了驱动器根目录时,安装器改用其中的 `metasequoiaime` 子目录,并在向导里把实际位置告诉用户;连那个子目录也不空就拒绝。输入法用过的目录(有标记,或历史默认位置)原样沿用
- 安装器在数据目录里放一个 `.metasequoiaime-data` 标记文件。只有标记里带 `exclusive: adopted empty by the installer` 这一行(安装时目录是空的),或目录就是历史默认位置,覆盖安装才整目录清理、卸载才整目录删除
- 旧版安装器不管目录里原来有什么都写标记,所以只有旧标记(没有那一行)的目录不能再信:覆盖安装、换目录和卸载都只按 `IsShippedAppDataItem` / `IsRuntimeAppDataItem` / `IsPreservedAppDataItem` 名单删输入法自己的条目,`html`、`logs`、`skins` 这种通用目录名只删输入法写进去的部分,最后目录空了才删目录。包里新增顶层文件要同步 `IsShippedAppDataItem`,`tests/package-files.ps1` 会拿真实打出的 `app_data` 核对;Server 在数据目录根下新写文件要同步 `IsRuntimeAppDataItem`
- 旧版安装器不管目录里原来有什么都写标记,所以只有旧标记(没有那一行)的目录不能再信:覆盖安装、换目录和卸载都只按 `IsShippedAppDataItem` / `IsRuntimeAppDataItem` / `IsPreservedAppDataItem` 名单删输入法自己的条目,`assets`、`logs`、`skins` 这种通用目录名(以及已退役的 `html`)只删输入法写进去的部分,最后目录空了才删目录。包里新增顶层文件要同步 `IsShippedAppDataItem`,`tests/package-files.ps1` 会拿真实打出的 `app_data` 核对;Server 在数据目录根下新写文件要同步 `IsRuntimeAppDataItem`
- 仓库 `skins\` 下的外部皮肤(除 `default\` 和素材授权为 `UNVERIFIED*` 的)随包装到 `skins\<id>`,完整包和轻量包都带,卸载不删。`Prepare-PackageFiles.ps1` 同时写出 `bundled_skins.manifest`(每行 `id|相对路径|sha256`),安装器在 `PrepareToInstall` 里拿它核对用户机器上的同名目录:文件集合和内容完全一致就直接覆盖,否则先改名成 `<id>.bak`(已有就 `<id>.2.bak`……,不覆盖旧备份)再装。Server 扫描皮肤时跳过 `*.bak` 目录。`tests/bundled-skins.ps1` 把这段 Pascal 编进探针安装器真跑一遍
- 静默安装用 `/DATADIR="D:\MetasequoiaIME"` 指定;该值在 `PrepareToInstall` 里和向导页走同一套换子目录和校验
@@ -68,20 +68,18 @@ Windows CI 运行 `installer/tests/cloud-choice.ps1`,用 Inno Setup 跑八个
## 运行时依赖检查
安装包不携带 WebView2 Runtime 和 VC 运行库——前者有自己的 Evergreen 更新通道,后者是系统级共享组件。缺 VC 运行库 Server 起不来;缺 WebView2 设置页打不开,但默认的 D2D 候选窗(以及表情 / 键盘 / 手写面板,都是 `msimeui` 原生绘制)照常可用,选了 `appearance.ui_backend = "webview2"` 的 Server 启动时检测不到运行时会临时退回 D2D(`FallBackToD2dIfWebView2RuntimeMissing`,不改配置,装好后下次启动自动恢复)。这些故障都出现在安装结束之后,用户看不到任何解释。所以 `msime_setup.iss` 的 `InitializeSetup` 在安装开始前先查一遍,缺哪个就当场说清楚,并给出下载地址。
安装包不携带 VC 运行库,它是系统级共享组件。缺了它 Server 起不来,而这个故障出现在安装结束之后,用户看不到任何解释。所以 `msime_setup.iss` 的 `InitializeSetup` 在安装开始前先查一遍,缺了就当场说清楚,并给出下载地址。界面全部由 `msimeui` 以 Direct2D 绘制,不再依赖 WebView2 Runtime;旧版本留下的 WebView2 用户数据(`ProgramData\metasequoiaime\webview2`、`webview2-settings`,以及 `%LOCALAPPDATA%\metasequoiaime` 下的同名回退目录)由安装器在升级和卸载时清掉。
- **Microsoft Edge WebView2 Runtime**:读 EdgeUpdate 客户端键 `{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}` 的 `pv`。机器级安装由 32 位的 EdgeUpdate 写入,所以主查 `HKLM32`,再看 `HKLM64` 和 `HKCU`(用户级安装)。`pv` 缺失或残留成 `0.0.0.0` 都算没装
- **Visual C++ 2015–2022 可再发行组件包(x64)**:读 `Software\Microsoft\VisualStudio\14.0\VC\Runtimes\x64` 的 `Installed` / `Major` / `Minor`,`HKLM32` 和 `HKLM64` 两个视图都查。要求 **14.40 以上**(常量 `VCRuntimeMinMinor`):`server_exe\` 里的 exe 用 MSVC 14.40+(VS 2022 17.10 起)编译并导入 `msvcp140.dll`,这一版起 `std::mutex` 的构造函数是 constexpr,配上更旧的 `msvcp140.dll` 会在第一次加锁时访问冲突、无声闪退——设置页「点了没反应」就是这个现象。装了 2019 / 早期 2022 运行库的机器同样有 `Installed=1`,所以必须比版本号。构建工具链升级到要求更高 redist 时同步调高这个常量。32 位 TSF DLL 用静态 CRT(`windows/CMakeLists.txt` 的 `CMAKE_MSVC_RUNTIME_LIBRARY`),所以不检查 x86 redist
- **不要改成查 `System32` 里的 DLL。** Setup.exe 是 32 位进程,Pascal Script 的 `FileExists` 会被 WOW64 重定向到 `SysWOW64`,查到的是 x86 运行库;只装了 x64 redist 的机器会被误判成缺少 x64 运行库。绕开重定向的办法不可移植:`EnableFsRedirection` 在 Inno Setup 7 里已被移除,而它的替代品在 CI 用的 6.x 里还不存在
- 缺组件或版本过旧时弹一次对话框,区分「没装」和「已装 x.y 但过旧」,列出直接下载链接(VC++:`https://aka.ms/vc14/vc_redist.x64.exe`;WebView2 Evergreen Bootstrapper:`https://go.microsoft.com/fwlink/p/?LinkId=2124703`)和对应的说明页:选「是」直接下载这些安装包并结束安装,选「否」继续装。**不阻断安装**——用户可能马上就要去补组件,也可能只是在准备环境。静默安装(CI、批量部署)用 `SuppressibleMsgBox` 默认「否」直接继续,缺什么已经写进安装日志(`/LOG=`)
- 安装时检查只是第一道关:用户可能选了「否」,也可能事后卸掉或损坏了 WebView2。所以设置进程在 WebView2 environment / controller 创建失败(含运行时缺失时的同步失败)时会弹框说明原因和错误码、给出同一条 WebView2 直链并退出,而不是留一个空白窗口
- 缺组件或版本过旧时弹一次对话框,区分「没装」和「已装 x.y 但过旧」,列出直接下载链接(`https://aka.ms/vc14/vc_redist.x64.exe`)和说明页:选「是」直接下载安装包并结束安装,选「否」继续装。**不阻断安装**——用户可能马上就要去补组件,也可能只是在准备环境。静默安装(CI、批量部署)用 `SuppressibleMsgBox` 默认「否」直接继续,缺什么已经写进安装日志(`/LOG=`)
## 依赖
- Windows 10/11,PowerShell 7(`pwsh`)
- [Inno Setup](https://jrsoftware.org/isinfo.php) 6.6 或更高版本
- Windows SDK(提供 `signtool.exe`)
- 先初始化本仓 submodule,并完成 `windows/`、`server/` 的 Release 编译以及 `ui-html/` 设置页构建。Release 构建必须生成同目录 PDB;打包脚本会拒绝缺少匹配符号的产物(符号是否随包安装是另一回事,由 `-IncludeSymbols` 决定)。
- 先初始化本仓 submodule,并完成 `windows/`、`server/` 的 Release 编译。Release 构建必须生成同目录 PDB;打包脚本会拒绝缺少匹配符号的产物(符号是否随包安装是另一回事,由 `-IncludeSymbols` 决定)。
- 本地测试包的词库从与本仓同级的 `msime-dictionary` 检出构建(见下文 `build-dictionary.py`),`Invoke-LocalTest.ps1` 打完整包前会自动调用,不再下载产品锁的 Release;需要复现发布用的词库时再手动运行 `python scripts/product_lock.py fetch-dictionaries --staging-root .`。
- 词格整句打分用的 `sc.lm` 由 `scripts\build-language-model.ps1` 从 `language-model\lock.json` 钉住的上游语料转换而来。`Invoke-LocalTest.ps1` 打完整包前会自动调它,一般不用手工执行;首次运行要下 ~75 MB 并转换一次,之后摘要对得上就直接跳过。
- 本地打完整包前,可以直接运行 `scripts\prepare-package-inputs.ps1`,一次准备好神经整句模型、`sc.lm` 和词库。模型按各自的锁校验 SHA256,已就绪的直接跳过;词库用 `build-dictionary.py --if-stale` 从本地 `msime-dictionary` 构建。
@@ -152,7 +150,7 @@ pwsh -File .\package-simplysign-symbols.ps1 1.2.3
### Hotfix 轻量包
只修 TSF / Server / HTML 的 hotfix 可以发轻量包,签名流程与完整包相同(包内 EXE/DLL 和最终安装包都签):
只修 TSF / Server / 设置窗口资源的 hotfix 可以发轻量包,签名流程与完整包相同(包内 EXE/DLL 和最终安装包都签):
```powershell
pwsh -File .\package-simplysign-light.ps1 1.2.4
@@ -174,15 +174,9 @@ linear_step = 1
[appearance]
# 主题模式,可选:"dark", "light", "system"
theme_mode = "system"
# 候选窗 / 悬浮工具栏 / 托盘菜单渲染,可选:"d2d", "webview2"
# 更改后需重启输入法进程生效
ui_backend = "d2d"
# 设置窗口关闭后在后台驻留多久再退出,驻留期间重新打开无需冷启动
# 可选:"off"(立即退出), "1m", "5m", "10m", "30m", "60m", "forever"(一直驻留)
settings_window_linger = "off"
# 设置窗口渲染方式,可选:"webview2", "d2d"(Direct2D,实验性)
# 更改后设置窗口会自动重新打开
settings_ui_backend = "webview2"
# 分表面主题,可选:"follow", "dark", "light"(follow = 跟随 theme_mode)
theme_settings = "follow"
theme_cand = "follow"
+79 -125
View File
@@ -13,7 +13,7 @@
; 4. Sign-Installer-Local.ps1 用同一张本机测试证书给安装包签名
;
; 也可以直接运行 .\test.ps1 走完整测试流程。
; 只改 TSF / Server / HTML 时用 .\test-light.ps1:ISCC /DLightPackage=1,
; 只改 TSF / Server 时用 .\test-light.ps1:ISCC /DLightPackage=1,
; 打出不含词库的轻量包,安装时也不会删本机已有词库。
; 上面两条都不把 PDB 打进包;要带符号用 .\test-symbols.ps1。
; 本仓库不包含任何预置代码签名证书。
@@ -79,17 +79,13 @@ Name: "english"; MessagesFile: "compiler:Default.isl"
Name: "{#MyTsfRoot32}\{code:GetVersionDir}"
Name: "{#MyTsfRoot64}\{code:GetVersionDir}"
Name: "{commonpf64}\metasequoiaime\server"
; 用户数据(词库、配置、皮肤、前端资源)。默认在 LocalAppData,安装时可以改到别的盘,
; 用户数据(词库、配置、皮肤、界面资源)。默认在 LocalAppData,安装时可以改到别的盘,
; 选择写进 HKLM 的 DataDir,Server / TSF DLL / 引擎三方都从那里读。
Name: "{code:GetDataDir}"; Permissions: users-modify
; 用户自己的辅助码方案放这里,Server 扫描后列进设置页,升级时保留。
Name: "{code:GetDataDir}\helpcodes\custom"
; 用户自己的双拼方案(.toml)放这里,同样由 Server 扫描、升级时保留。
Name: "{code:GetDataDir}\shuangpin\custom"
; WebView2 子进程是中完整性,写不进内置 Administrator 的高完整性 LocalAppData。
Name: "{commonappdata}\metasequoiaime"
Name: "{commonappdata}\metasequoiaime\webview2"; Permissions: users-modify
Name: "{commonappdata}\metasequoiaime\webview2-settings"; Permissions: users-modify
[Files]
; 独立安装应用图标,供 Windows“已安装的应用”列表稳定显示。
@@ -155,9 +151,9 @@ Source: "{#MySourceRoot}\bundled_skins\*"; \
Source: "{#MySourceRoot}\bundled_skins.manifest"; Flags: dontcopy
#ifdef LightPackage
; 轻量包只覆盖前端 HTML,不带词库/辅助码/出厂配置。
Source: "{#MySourceRoot}\app_data\html\*"; \
DestDir: "{code:GetDataDir}\html"; \
; 轻量包只覆盖设置窗口的图片资源,不带词库/辅助码/出厂配置。
Source: "{#MySourceRoot}\app_data\assets\settings\*"; \
DestDir: "{code:GetDataDir}\assets\settings"; \
Flags: ignoreversion recursesubdirs createallsubdirs uninsneveruninstall
#else
; 包内故意不带 config.toml。通配复制再排除一次,防止以后又把用户配置打进包内。
@@ -208,16 +204,9 @@ const
{ 用户选的目录里已有别的文件时,数据改放到这个子目录里。}
DataDirSubfolderName = 'metasequoiaime';
{ WebView2 Evergreen Runtime 在 EdgeUpdate 里的固定客户端 ID。}
WebView2ClientKey =
'Software\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}';
{ 安装器界面本身就是中文(InitializeSetup 在选语言对话框之前跑,拿不到用户选的语言),
下载页也统一给简体中文版,省得用户落到英文页上再自己切。}
WebView2DownloadUrl =
'https://developer.microsoft.com/zh-cn/microsoft-edge/webview2';
{ Evergreen Bootstrapper 直链,点开就是安装器。}
WebView2DirectUrl = 'https://go.microsoft.com/fwlink/p/?LinkId=2124703';
VCRuntimeKey = 'Software\Microsoft\VisualStudio\14.0\VC\Runtimes\x64';
{ 安装器界面本身就是中文(InitializeSetup 在选语言对话框之前跑,拿不到用户选的语言),
说明页也统一给简体中文版,省得用户落到英文页上再自己切。}
VCRuntimeDownloadUrl =
'https://learn.microsoft.com/zh-cn/cpp/windows/latest-supported-vc-redist?view=msvc-170';
{ 微软给的「最新受支持 v14 x64 运行库」固定直链。}
@@ -235,29 +224,10 @@ var
UserConfigExistedBeforeInstall: Boolean;
DataDirAdoptedEmpty: Boolean;
{ WebView2 Runtime 与 VC 运行库都不随包分发:前者有自己的 Evergreen 更新通道,
后者是系统级共享组件,安装器不该替用户装。缺 VC 运行库 Server 直接起不来;
缺 WebView2 设置页打不开(默认 D2D 候选窗照常可用,选了 WebView2 渲染的
Server 启动时会临时退回 D2D)。故障现场都在安装结束之后,用户看不到任何解释,
所以在安装开始前查一遍,把原因当场说清楚。
返回空串表示没装;否则是 pv 里的版本号。}
function ReadWebView2Version: String;
begin
Result := '';
{ 机器级安装由 32 位的 EdgeUpdate 写入,键落在 WOW6432Node 下。本安装器跑在
64 位模式,HKLM 默认就是 64 位视图,所以主查 HKLM32;HKLM64 作为后备,
以防将来的 ARM64 / 原生 64 位布局换了位置。用户级安装写在 HKCU。
卸载没清干净的机器上 pv 可能残留成 0.0.0.0。}
if not RegQueryStringValue(HKLM32, WebView2ClientKey, 'pv', Result) then
if not RegQueryStringValue(HKLM64, WebView2ClientKey, 'pv', Result) then
if not RegQueryStringValue(HKCU, WebView2ClientKey, 'pv', Result) then
Result := '';
Result := Trim(Result);
if Result = '0.0.0.0' then
Result := '';
end;
// VC 运行库不随包分发:它是系统级共享组件,安装器不该替用户装。缺了它 Server 直接
// 起不来,而故障现场在安装结束之后,用户看不到任何解释,所以在安装开始前查一遍,
// 把原因当场说清楚。
//
// 用注册表判定,而不是去 System32 里找 vcruntime140.dll:Setup.exe 是 32 位进程,
// Pascal Script 的 FileExists 会被 WOW64 重定向到 SysWOW64([Files] 和 Exec 里的
// {sys} 由 Inno 自己处理,脚本函数不管),结果查的是 x86 运行库——只装了 x64 redist
@@ -319,21 +289,12 @@ end;
function InitializeSetup: Boolean;
var
WebView2Version: String;
NeedsWebView2: Boolean;
NeedsVCRuntime: Boolean;
VCRuntimeVersion: String;
Prompt: String;
begin
Result := True;
WebView2Version := ReadWebView2Version;
NeedsWebView2 := WebView2Version = '';
if NeedsWebView2 then
Log('Prerequisite missing: Microsoft Edge WebView2 Runtime.')
else
Log('WebView2 Runtime found: ' + WebView2Version);
NeedsVCRuntime := not VCRuntimeIsInstalled;
{ 有版本号却没通过判定,就是装了但太旧,提示里要和「没装」分开说。}
VCRuntimeVersion := ReadVCRuntimeVersion;
@@ -343,45 +304,30 @@ begin
else
Log('Visual C++ runtime found: ' + VCRuntimeVersion);
if not (NeedsWebView2 or NeedsVCRuntime) then
if not NeedsVCRuntime then
exit;
Prompt := '这台电脑缺少输入法用到的系统组件,或版本过旧:' + #13#10;
if NeedsVCRuntime then
begin
Prompt := Prompt + #13#10 +
'● Visual C++ 2015-2022 可再发行组件包(x64),需要 14.' +
IntToStr(VCRuntimeMinMinor) + ' 或更高版本' + #13#10;
if VCRuntimeVersion <> '' then
Prompt := Prompt +
' 当前已安装 ' + VCRuntimeVersion + ',版本过旧,设置页等程序会启动即闪退。' + #13#10
else
Prompt := Prompt +
' 缺少它输入法主程序根本无法启动(提示找不到 VCRUNTIME140.dll 之类)。' + #13#10;
Prompt := '这台电脑缺少输入法用到的系统组件,或版本过旧:' + #13#10 + #13#10 +
'● Visual C++ 2015-2022 可再发行组件包(x64),需要 14.' +
IntToStr(VCRuntimeMinMinor) + ' 或更高版本' + #13#10;
if VCRuntimeVersion <> '' then
Prompt := Prompt +
' 直接下载:' + VCRuntimeDirectUrl + #13#10 +
' 说明页面:' + VCRuntimeDownloadUrl + #13#10;
end;
if NeedsWebView2 then
Prompt := Prompt + #13#10 +
'● Microsoft Edge WebView2 Runtime' + #13#10 +
' 缺少它设置页面打不开。默认的 D2D 候选窗不依赖它,打字不受影响;' + #13#10 +
' 选了 WebView2 渲染的候选窗会临时改用 D2D,装好后自动恢复。' + #13#10 +
' 直接下载:' + WebView2DirectUrl + #13#10 +
' 说明页面:' + WebView2DownloadUrl + #13#10;
Prompt := Prompt + #13#10 +
'点击「是」直接下载上述组件并结束本次安装,装好后重新运行本安装程序;' + #13#10 +
'点击「否」继续安装(装完仍需自行补齐上述组件,对应功能才能正常使用)。';
' 当前已安装 ' + VCRuntimeVersion + ',版本过旧,设置页等程序会启动即闪退。' + #13#10
else
Prompt := Prompt +
' 缺少它输入法主程序根本无法启动(提示找不到 VCRUNTIME140.dll 之类)。' + #13#10;
Prompt := Prompt +
' 直接下载:' + VCRuntimeDirectUrl + #13#10 +
' 说明页面:' + VCRuntimeDownloadUrl + #13#10 + #13#10 +
'点击「是」直接下载该组件并结束本次安装,装好后重新运行本安装程序;' + #13#10 +
'点击「否」继续安装(装完仍需自行补齐该组件,输入法才能正常使用)。';
{ 静默安装(CI、企业批量部署)不该被一个对话框卡住:默认继续,缺什么已经写进日志了。}
if
SuppressibleMsgBox(Prompt, mbConfirmation, MB_YESNO, IDNO) = IDYES
then
begin
if NeedsVCRuntime then
OpenDownloadPage(VCRuntimeDirectUrl);
if NeedsWebView2 then
OpenDownloadPage(WebView2DirectUrl);
OpenDownloadPage(VCRuntimeDirectUrl);
Result := False;
end;
end;
@@ -642,7 +588,7 @@ end;
升级时跳过:那时 config.toml 已经属于用户,安装器不该替他重新决定。}
procedure InitializeWizard;
begin
{ 词库、用户配置、皮肤和前端资源都在这个目录下,整包有几百 MB,
{ 词库、用户配置、皮肤和界面资源都在这个目录下,整包有几百 MB,
所以要让用户能把它放到别的盘。程序本体仍然装在 Program Files:
Server 带 uiAccess=true,只有装在受信任目录里这个标志才生效。}
DataDirPage := CreateInputDirPage(
@@ -892,7 +838,7 @@ begin
(CompareText(FileName, 'config.default.toml') = 0) or
(CompareText(FileName, 'helpcodes') = 0) or
(CompareText(FileName, 'radical') = 0) or
(CompareText(FileName, 'html') = 0);
(CompareText(FileName, 'assets') = 0);
end;
{ Server、引擎和安装器自己运行时在数据目录根下写出来的条目(不含 IsPreservedAppDataItem 已列的)。}
@@ -919,14 +865,18 @@ begin
(CompareText(FileName, 'logs') = 0);
end;
{ 数据目录不整个归我们时,只有这些名字允许删(#537)。html、logs 这种通用目录名
{ 数据目录不整个归我们时,只有这些名字允许删(#537)。assets、html、logs 这种通用目录名
由 DeleteKnownAppDataDir 只删输入法自己写进去的部分。log\、webview2\ 等只会出现在
历史默认位置,那里整个目录归我们,不必列。}
{ 旧版随包装进数据目录根下、现已不再分发的条目。留在名单里,覆盖安装与卸载才会继续清掉它们。
神经整句效果档模型改为设置页按需下载到 models\neural-desktop\(归 IsCollocationModelDirectory 保留)。}
神经整句效果档模型改为设置页按需下载到 models\neural-desktop\(归 IsCollocationModelDirectory 保留)。
html\webview2\ 是旧版 WebView2 界面的页面,界面改由 Direct2D 绘制后不再分发;设置窗口
用到的图片改装在 assets\settings\。}
function IsRetiredAppDataItem(const FileName: String): Boolean;
begin
Result := CompareText(FileName, 'sentence-model-desktop.safetensors') = 0;
Result :=
(CompareText(FileName, 'sentence-model-desktop.safetensors') = 0) or
(CompareText(FileName, 'html') = 0);
end;
function IsKnownAppDataItem(const FileName: String): Boolean;
@@ -994,7 +944,7 @@ var
SettingsWindow: HWND;
WaitedMs: Integer;
begin
// 设置窗口关闭后只是隐藏,进程还要驻留十分钟保住已导航完的 WebView2。
// 设置窗口关闭后可能只是隐藏,进程按 appearance.settings_window_linger 继续驻留。
// 覆盖安装要删掉整个 server 目录,所以这里必须把它请走:先投 kQuitSettings
// 让它自己收尾(Server 已被强杀,没人替我们发这条消息了),再强杀兜底。
SettingsWindow := FindWindowByClassName('{#MySettingsWindowClass}');
@@ -1072,34 +1022,6 @@ begin
);
end;
procedure EnsureSharedWebView2DataDir;
var
RootPath: String;
ResultCode: Integer;
begin
{ Edge 子进程需要 Users 可写、中完整性的目录。安装器本身是高完整性,
只 CreateDir 会带上高完整性标签,所以还要降完整性。 }
RootPath := ExpandConstant('{commonappdata}\metasequoiaime');
ForceDirectories(RootPath + '\webview2');
ForceDirectories(RootPath + '\webview2-settings');
Exec(
ExpandConstant('{sys}\icacls.exe'),
'"' + RootPath + '" /grant *S-1-5-32-545:(OI)(CI)M /T /C /Q',
'',
SW_HIDE,
ewWaitUntilTerminated,
ResultCode
);
Exec(
ExpandConstant('{sys}\icacls.exe'),
'"' + RootPath + '" /setintegritylevel (OI)(CI)M /T /C /Q',
'',
SW_HIDE,
ewWaitUntilTerminated,
ResultCode
);
end;
procedure CreateWatchdogLogonTask;
var
WatchdogPath: String;
@@ -1137,7 +1059,7 @@ begin
end;
{ 不独占的数据目录里删一个名单上的子目录。名字通用的几个只删我们写进去的内容,
目录空了才删掉它:用户自己恰好也有一个 html、logs 或 skins 文件夹时不能跟着没了。
目录空了才删掉它:用户自己恰好也有一个 assets、html、logs 或 skins 文件夹时不能跟着没了。
skins 里分不清哪些是外部皮肤、哪些是别人的东西,宁可留下。}
procedure DeleteKnownAppDataDir(const ItemPath, FileName: String);
begin
@@ -1145,7 +1067,10 @@ begin
begin
{ 只删空目录。}
end
else if CompareText(FileName, 'assets') = 0 then
TryDeleteTree(AddBackslash(ItemPath) + 'settings')
else if CompareText(FileName, 'html') = 0 then
{ 旧版的 WebView2 页面,见 IsRetiredAppDataItem。}
TryDeleteTree(AddBackslash(ItemPath) + 'webview2')
else if CompareText(FileName, 'logs') = 0 then
begin
@@ -1204,6 +1129,33 @@ begin
RemoveDir(Directory);
end;
{ 旧版 WebView2 的用户数据(浏览器缓存、Cookie 等)。界面改由 Direct2D 绘制后不再有人
读写它们,覆盖安装和卸载都清掉。正常情况下在 ProgramData 下;那里不可写时旧版 Server
退到历史默认数据目录下的同名子目录,那个目录整个归我们,按名字删是安全的。}
procedure RemoveLegacyWebView2UserData;
begin
TryDeleteTree(ExpandConstant('{commonappdata}\metasequoiaime\webview2'));
TryDeleteTree(ExpandConstant('{commonappdata}\metasequoiaime\webview2-settings'));
RemoveDir(ExpandConstant('{commonappdata}\metasequoiaime'));
TryDeleteTree(ExpandConstant('{localappdata}\metasequoiaime\webview2'));
TryDeleteTree(ExpandConstant('{localappdata}\metasequoiaime\webview2-settings'));
end;
#ifdef LightPackage
{ 轻量包只替换数据目录里的个别子目录。整个目录归我们时整个删;只有旧版标记时(#537)
按 DeleteKnownAppDataDir 的规则只删输入法自己写进去的部分。}
procedure CleanLightPackageItem(const FileName: String);
var
AppDataPath: String;
begin
AppDataPath := GetDataDir('');
if OwnsDataDirExclusively(AppDataPath) then
TryDeleteTree(AddBackslash(AppDataPath) + FileName)
else if DataDirHasMarker(AppDataPath) then
DeleteKnownAppDataDir(AddBackslash(AppDataPath) + FileName, FileName);
end;
#endif
{ helpcodes 里的内置表由本次安装重写,custom 子目录是用户自己放的辅助码,升级不得清掉。}
procedure CleanHelpcodesExceptCustom(const HelpcodesPath: String);
var
@@ -1394,7 +1346,7 @@ begin
end;
{ 用户改了数据目录:把上一处的用户数据搬过来。只搬真正属于用户、装不回来的东西——
词库主体、前端资源和辅助码都会由本次安装重新写入新目录。
词库主体、界面资源和辅助码都会由本次安装重新写入新目录。
用 robocopy 而不是 RenameFile:跨盘移动目录时 MoveFile 会直接失败。}
function MigrateUserDataDir(const OldDir, NewDir: String): String;
var
@@ -1494,7 +1446,7 @@ begin
exit;
end;
{ 旧目录里剩下的是可重建的资源(词库、html、cache 等)。}
{ 旧目录里剩下的是可重建的资源(词库、界面资源、cache 等)。}
RemoveDataDir(OldDir);
end;
@@ -1658,8 +1610,10 @@ begin
{ 轻量包不清根目录,但新 Server 同样只认 models\neural-desktop\,所以两种包都要挪。}
AdoptRetiredDesktopModel('0a6ecba69bf1d39c7eb49549c716477dd6c03fdf05757773e1f50435262fb469');
#ifdef LightPackage
{ 轻量包不替换词库:只清 HTML 和 Server/TSF,保留本机 msime.db 等。}
TryDeleteTree(AddBackslash(GetDataDir('')) + 'html');
{ 轻量包不替换词库:只清设置窗口图片资源和 Server/TSF,保留本机 msime.db 等。
旧版留下的 html(WebView2 页面)也一并清掉。}
CleanLightPackageItem('assets');
CleanLightPackageItem('html');
#else
{ 不能让旧 WAL/SHM 与即将复制的新主数据库混用。}
if not RemoveOldTargetDatabaseFiles(FailedPath) then
@@ -1669,13 +1623,10 @@ begin
'它可能仍被输入法相关进程占用。请关闭相关程序后重试安装。';
exit;
end;
{ 目标词库已安全移除,再清理旧前端资源与 WebView2 用户数据。
用户配置和外部皮肤目录在这里保留;webview2 目录故意重建,
由 Server 冷启动路径保证 FTB/候选窗仍能稳定揭罩。}
{ 目标词库已安全移除,再清理旧的随包资源。用户配置和外部皮肤目录在这里保留。}
CleanAppDataExceptUserFiles;
#endif
TryDeleteTree(ExpandConstant('{commonappdata}\metasequoiaime\webview2'));
TryDeleteTree(ExpandConstant('{commonappdata}\metasequoiaime\webview2-settings'));
RemoveLegacyWebView2UserData;
TryDeleteTree(ExpandConstant(
'{commonpf64}\metasequoiaime\server'));
TryDeleteOldVersionDirs(TsfRoot32);
@@ -1703,7 +1654,6 @@ begin
#endif
CreateWatchdogLogonTask;
EnsureImeUserDataDir;
EnsureSharedWebView2DataDir;
{ Keep the old autostart intact until its scheduled-task replacement has
been created successfully, then remove the Explorer-delayed Run entry. }
RegDeleteValue(
@@ -1759,6 +1709,10 @@ begin
TryDeleteTree(ExpandConstant('{commonpf64}\metasequoiaime'));
{ 数据目录可能被用户指到了别的盘,甚至指到一个本来就有文件的文件夹(#537)。}
RemoveDataDir(ResolvePreviousDataDir);
{ 旧版 WebView2 的用户数据,见 RemoveLegacyWebView2UserData。}
RemoveLegacyWebView2UserData;
{ 数据目录在别处时,历史默认位置里可能只剩上面删掉的那两样;还有别的东西就删不掉,原样留着。}
RemoveDir(ExpandConstant('{localappdata}\metasequoiaime'));
TryDeleteTree(ExpandConstant('{commonappdata}\metasequoiaime'));
end;
end;
+1 -1
View File
@@ -1,4 +1,4 @@
# Build a light (hotfix) installer that only replaces TSF, Server and HTML, then sign its payload
# Build a light (hotfix) installer that only replaces TSF, Server and the settings window assets, then sign its payload
# and the finished installer with the real Certum certificate exposed by SimplySign Desktop.
#
# The light installer carries no dictionaries, neural models or factory config template, so it is
+2 -11
View File
@@ -21,7 +21,7 @@ param(
# so this local entry keeps them opt-in, like test-symbols.ps1.
[switch]$IncludeSymbols,
# Hotfix package: only TSF, Server and HTML, like test-light.ps1. It carries no dictionaries,
# Hotfix package: only TSF, Server and the settings window assets, like test-light.ps1. It carries no dictionaries,
# neural models or factory config template, so it is only valid on top of a full install whose
# data files still match this build.
[switch]$Light,
@@ -217,8 +217,7 @@ Write-Host "时间戳服务:$TimestampUrl"
$repoRoot = Split-Path -Parent $PSScriptRoot
$tsfCompile = Join-Path $repoRoot 'windows\scripts\lcompile-release-both.ps1'
$serverCompile = Join-Path $repoRoot 'server\scripts\lcompile-release.ps1'
$settingsDir = Join-Path $repoRoot 'ui-html\webview2\settings\ime-settings'
foreach ($required in @($tsfCompile, $serverCompile, (Join-Path $settingsDir 'package.json'))) {
foreach ($required in @($tsfCompile, $serverCompile)) {
if (-not (Test-Path -LiteralPath $required -PathType Leaf)) {
throw "缺少组件构建入口:$required"
}
@@ -249,13 +248,6 @@ try {
Push-Location (Join-Path $repoRoot 'server')
try { & $serverCompile -Reconfigure:$Reconfigure } finally { Pop-Location }
Push-Location $settingsDir
try {
pnpm run build
if ($LASTEXITCODE -ne 0) { throw "Settings page build failed ($LASTEXITCODE)" }
}
finally { Pop-Location }
# 完整包的数据输入和 Invoke-LocalTest.ps1 一样准备:词库从本仓同级的 msime-dictionary 构建,
# 不下载 Release;源数据提交和构建脚本都没变、工作区干净时直接跳过。不跑这一步,
# Prepare-PackageFiles 只会原样拿 MetasequoiaImeDict\out\ 里上次留下的东西。
@@ -275,7 +267,6 @@ try {
-RepoRoot $repoRoot `
-TsfDirectory windows `
-ServerDirectory server `
-UiHtmlDirectory ui-html `
-NoticesDirectory . `
-HelpCodeDirectory engine/helpcode
+2 -2
View File
@@ -1,6 +1,6 @@
# 轻量本地测试:只编译 TSF / Server / 设置页,收集这三块产物,打不含词库的安装包。
# 轻量本地测试:只编译 TSF / Server,收集它们的产物和设置窗口图片资源,打不含词库的安装包。
#
# 适用:只改了 TSF、Server 或 HTML,本机已有完整安装(词库已在
# 适用:只改了 TSF 或 Server,本机已有完整安装(词库已在
# %LOCALAPPDATA%\metasequoiaime)。不要用这个脚本做首次安装。
#
# 完整流程(含词库)仍用 .\test.ps1;需要包内带 PDB 时用 .\test-symbols.ps1。
+1 -1
View File
@@ -11,7 +11,7 @@
[CmdletBinding()]
param(
# 默认走轻量包,和日常改 TSF / Server / HTML 的节奏一致;要连词库一起装就加 -Full。
# 默认走轻量包,和日常改 TSF / Server 的节奏一致;要连词库一起装就加 -Full。
[switch]$Full,
[switch]$Reconfigure
)
+1 -1
View File
@@ -2,7 +2,7 @@
# 本机自签名 → Inno Setup 打包 → 签安装包 → 启动安装程序。
#
# 不使用任何预置证书。签名脚本会在本机生成并复用自签名测试证书。
# 只改 TSF / Server / HTML、且本机已有词库时,用 .\test-light.ps1。
# 只改 TSF / Server、且本机已有词库时,用 .\test-light.ps1。
# 需要包内带 PDB 时用 .\test-symbols.ps1 -Full。
#
# 编译是增量的,实现见 Invoke-LocalTest.ps1。
+6 -15
View File
@@ -70,34 +70,25 @@ try {
throw 'CI manifest embedding did not verify resource 1'
}
Remove-Item Function:/cmake
# Each installer entry must stop before staging/signing when the settings build fails.
foreach ($relative in @('windows/scripts/lcompile-release-both.ps1', 'server/scripts/lcompile-release.ps1')) {
[IO.File]::WriteAllText((Join-Path $fixture $relative), '# successful synthetic component build')
}
$settings = Join-Path $fixture 'ui-html/webview2/settings/ime-settings'
New-Item -ItemType Directory -Force $settings | Out-Null
[IO.File]::WriteAllText((Join-Path $settings 'package.json'), '{}')
# Each installer entry must stop before staging/signing when the Server build fails.
[IO.File]::WriteAllText((Join-Path $fixture 'windows/scripts/lcompile-release-both.ps1'), '# successful synthetic component build')
[IO.File]::WriteAllText((Join-Path $fixture 'server/scripts/lcompile-release.ps1'), "throw 'Server build failed (23)'")
$installer = Join-Path $fixture 'installer'
New-Item -ItemType Directory -Force $installer | Out-Null
[IO.File]::WriteAllText((Join-Path $installer 'Prepare-PackageFiles.ps1'), "throw 'Reached package staging after failure'")
function global:pnpm {
# 只让 'pnpm run build' 失败,允许 'pnpm install' 成功(用于首次依赖安装)
if ($args -contains 'build') { $global:LASTEXITCODE = 23 }
else { $global:LASTEXITCODE = 0 }
}
Copy-Item (Join-Path $source 'installer/Invoke-LocalTest.ps1') $installer
foreach ($entry in @('test.ps1', 'test-light.ps1', 'test-symbols.ps1')) {
Copy-Item (Join-Path $source "installer/$entry") $installer
$rejected = $false
try { & (Join-Path $installer $entry) }
catch { $rejected = $_.Exception.Message -eq 'Settings page build failed (23)' }
if (-not $rejected) { throw "$entry continued after the settings build failed" }
catch { $rejected = $_.Exception.Message -eq 'Server build failed (23)' }
if (-not $rejected) { throw "$entry continued after the Server build failed" }
}
$global:LASTEXITCODE = 0
Write-Host 'Configure/build failures stop local packaging before staging, signing or installation'
} finally {
Set-Location $originalLocation
Remove-Item Function:/cmake, Function:/pnpm, Function:/Invoke-MsimeManifestProbe -ErrorAction SilentlyContinue
Remove-Item Function:/cmake, Function:/Invoke-MsimeManifestProbe -ErrorAction SilentlyContinue
Remove-Variable MsimeBuildCalls, MsimeFailAt, MsimeManifestExit, MsimeManifestArguments -Scope Global -ErrorAction SilentlyContinue
if (Test-Path $fixture) { Remove-Item $fixture -Recurse -Force }
}
+16 -13
View File
@@ -42,11 +42,8 @@ try {
'neural-model/sentence-model-desktop.safetensors',
'neural-model/sentence-model.safetensors',
'neural-model/NOTICE.md',
'ui-html/webview2/shared/runtime.js',
'ui-html/webview2/candwnd/index.html',
'ui-html/webview2/menu/index.html',
'ui-html/webview2/ftb/index.html',
'ui-html/webview2/settings/ime-settings/dist/index.html',
'server/assets/settings/arrow.svg',
'server/assets/settings/sidebar/about.svg',
'skins/default/fluent/skin.toml'
)) { Write-Fixture $file }
Write-Fixture 'skins/demo-skin/skin.toml' "schema_version = 1`nid = `"demo-skin`"`n"
@@ -58,7 +55,8 @@ try {
python -c "import sqlite3,sys; sqlite3.connect(sys.argv[1]).execute('CREATE TABLE english_words(word TEXT,display TEXT,weight INTEGER,PRIMARY KEY(word,display))')" $english
if ($LASTEXITCODE -ne 0) { throw 'Failed to create packaging fixture' }
& (Join-Path $installer 'Prepare-PackageFiles.ps1') -RepoRoot $fixture -TargetVersion '2026.9.1' -IncludeSymbols
foreach ($file in @('app_data/html/webview2/shared/runtime.js', 'app_data/dictionary-manifest.json',
foreach ($file in @('app_data/assets/settings/arrow.svg', 'app_data/assets/settings/sidebar/about.svg',
'app_data/dictionary-manifest.json',
'app_data/sc.lm', 'app_data/libime-lm-NOTICE.md', 'app_data/dict_pinyin.dat',
'app_data/sentence-model.safetensors', 'app_data/chinese-ime-lm-NOTICE.md',
'tsf_dll/32/MetasequoiaImeTsf.dll', 'tsf_dll/32/MetasequoiaImeTsf.pdb',
@@ -147,7 +145,7 @@ try {
}
# 默认不带符号:PDB 是安装包体积和打包耗时的大头,只有显式 -IncludeSymbols 才进包。
& (Join-Path $installer 'Prepare-PackageFiles.ps1') -RepoRoot $fixture -TargetVersion '2026.9.1'
foreach ($file in @('app_data/html/webview2/shared/runtime.js',
foreach ($file in @('app_data/assets/settings/sidebar/about.svg',
'tsf_dll/32/MetasequoiaImeTsf.dll', 'tsf_dll/64/MetasequoiaImeTsf.dll',
'server_exe/MetasequoiaImeServer.exe')) {
if (-not (Test-Path (Join-Path $installer $file))) { throw "Missing packaged file: $file" }
@@ -167,14 +165,19 @@ try {
[IO.File]::WriteAllText($serverPdbFixture, 'fixture')
$database = Join-Path $installer 'app_data/msime.db'
[IO.File]::WriteAllText($database, 'preserved user data')
& (Join-Path $installer 'Prepare-PackageFiles.ps1') -RepoRoot $fixture -TsfDirectory windows -ServerDirectory server -UiHtmlDirectory ui-html -NoticesDirectory . -Light
# 轻量包不清空 app_data:旧版本留下的 html\(WebView2 页面)必须被清掉,否则会被打进包里。
$legacyHtml = Join-Path $installer 'app_data/html/webview2/shared/runtime.js'
New-Item -ItemType Directory -Force -Path (Split-Path $legacyHtml) | Out-Null
[IO.File]::WriteAllText($legacyHtml, 'stale')
& (Join-Path $installer 'Prepare-PackageFiles.ps1') -RepoRoot $fixture -TsfDirectory windows -ServerDirectory server -NoticesDirectory . -Light
if ([IO.File]::ReadAllText($database) -ne 'preserved user data') { throw 'Light package replaced dictionary data' }
if (-not (Test-Path (Join-Path $installer 'app_data/html/webview2/shared/runtime.js'))) { throw 'Light package lost shared contracts' }
if (-not (Test-Path (Join-Path $installer 'app_data/assets/settings/sidebar/about.svg'))) { throw 'Light package lost settings assets' }
if (Test-Path (Join-Path $installer 'app_data/html')) { throw 'Light package kept the retired html directory' }
# 完整包必须带 sc.lm。缺了它引擎是静默降级的:整句候选只是变差,不会报错,所以打包
# 这一步是唯一能挡住它的地方。轻量包本来就不带数据文件,不受影响。
$languageModelFixture = Join-Path $fixture 'language-model/sc.lm'
Remove-Item $languageModelFixture -Force
& (Join-Path $installer 'Prepare-PackageFiles.ps1') -RepoRoot $fixture -TsfDirectory windows -ServerDirectory server -UiHtmlDirectory ui-html -NoticesDirectory . -Light
& (Join-Path $installer 'Prepare-PackageFiles.ps1') -RepoRoot $fixture -TsfDirectory windows -ServerDirectory server -NoticesDirectory . -Light
$rejected = $false
try { & (Join-Path $installer 'Prepare-PackageFiles.ps1') -RepoRoot $fixture } catch { $rejected = $_.Exception.Message -match 'sc\.lm' }
if (-not $rejected) { throw 'Missing language model was accepted' }
@@ -195,10 +198,10 @@ try {
try { & (Join-Path $installer 'Prepare-PackageFiles.ps1') -RepoRoot $fixture } catch { $rejected = $_.Exception.Message -match 'safetensors' }
if (-not $rejected) { throw 'Missing neural sentence model was accepted' }
[IO.File]::WriteAllText($neuralModelFixture, 'fixture')
Remove-Item (Join-Path $fixture 'ui-html/webview2/shared') -Recurse -Force
Remove-Item (Join-Path $fixture 'server/assets/settings') -Recurse -Force
$rejected = $false
try { & (Join-Path $installer 'Prepare-PackageFiles.ps1') -RepoRoot $fixture -TsfDirectory windows -ServerDirectory server -UiHtmlDirectory ui-html -NoticesDirectory . } catch { $rejected = $true }
if (-not $rejected) { throw 'Missing shared contracts were accepted' }
try { & (Join-Path $installer 'Prepare-PackageFiles.ps1') -RepoRoot $fixture -TsfDirectory windows -ServerDirectory server -NoticesDirectory . } catch { $rejected = $_.Exception.Message -match '设置窗口图片资源' }
if (-not $rejected) { throw 'Missing settings assets were accepted' }
if ([IO.File]::ReadAllText($database) -ne 'preserved user data') { throw 'Rejected package damaged previous staging' }
Write-Host 'Full/light package contracts, provenance, exclusions and failure staging passed'
} finally {
+13 -26
View File
@@ -13,32 +13,20 @@ project_root_path = os.path.dirname(cur_file_path)
user_home = os.path.expanduser("~")
Win32WebviewTemplate_root_path = normpath(project_root_path)
Win32WebviewTemplate_src_path = normpath(
os.path.join(Win32WebviewTemplate_root_path, "src")
MetasequoiaImeLog_root_path = normpath(project_root_path)
MetasequoiaImeLog_src_path = normpath(
os.path.join(MetasequoiaImeLog_root_path, "src")
)
vcpkg_include_path = normpath(
os.path.join(
Win32WebviewTemplate_root_path,
MetasequoiaImeLog_root_path,
"build",
"vcpkg_installed",
"x64-windows",
"include",
)
)
utfcpp_path = normpath(os.path.join(Win32WebviewTemplate_root_path, "utfcpp", "source"))
webview2_path = normpath(
os.path.join(
user_home,
".nuget",
"packages",
"microsoft.web.webview2",
"1.0.3240.44",
"build",
"native",
"include",
)
)
utfcpp_path = normpath(os.path.join(MetasequoiaImeLog_root_path, "utfcpp", "source"))
wim_path = normpath(
os.path.join(
user_home,
@@ -66,17 +54,16 @@ cmake_toolchain_file_path = normpath(
#
# project_root/.clangd
# line 7, 8, 9, 10
# line 7, 8, 10
#
dot_clangd_file = os.path.join(
Win32WebviewTemplate_root_path, "scripts", "config_files", ".clangd"
MetasequoiaImeLog_root_path, "scripts", "config_files", ".clangd"
)
dot_clangd_output_file = os.path.join(Win32WebviewTemplate_root_path, ".clangd")
dot_clangd_output_file = os.path.join(MetasequoiaImeLog_root_path, ".clangd")
with open(dot_clangd_file, "r", encoding="utf-8") as f:
lines = f.readlines()
lines[6] = f' "-I{Win32WebviewTemplate_src_path}",\n'
lines[6] = f' "-I{MetasequoiaImeLog_src_path}",\n'
lines[7] = f' "-I{vcpkg_include_path}",\n'
lines[8] = f' "-I{webview2_path}",\n'
lines[9] = f' "-I{wim_path}",\n'
with open(dot_clangd_output_file, "w", encoding="utf-8") as f:
f.writelines(lines)
@@ -85,9 +72,9 @@ with open(dot_clangd_output_file, "w", encoding="utf-8") as f:
# project_root/CMakeLists.txt
#
CMakeLists_file = os.path.join(
Win32WebviewTemplate_root_path, "scripts", "config_files", "CMakeLists.txt"
MetasequoiaImeLog_root_path, "scripts", "config_files", "CMakeLists.txt"
)
CMakeLists_output_file = os.path.join(Win32WebviewTemplate_root_path, "CMakeLists.txt")
CMakeLists_output_file = os.path.join(MetasequoiaImeLog_root_path, "CMakeLists.txt")
with open(CMakeLists_file, "r", encoding="utf-8") as f:
lines = f.readlines()
with open(CMakeLists_output_file, "w", encoding="utf-8") as f:
@@ -98,10 +85,10 @@ with open(CMakeLists_output_file, "w", encoding="utf-8") as f:
# lines 9 12
#
CMakePresets_file = os.path.join(
Win32WebviewTemplate_root_path, "scripts", "config_files", "CMakePresets.json"
MetasequoiaImeLog_root_path, "scripts", "config_files", "CMakePresets.json"
)
CMakePresets_file_output_file = os.path.join(
Win32WebviewTemplate_root_path, "CMakePresets.json"
MetasequoiaImeLog_root_path, "CMakePresets.json"
)
with open(CMakePresets_file, "r", encoding="utf-8") as f:
lines = f.readlines()
+20 -25
View File
@@ -2,13 +2,13 @@
组织级约定见 [组织 AGENTS.md](https://github.com/metasequoiaime/.github/blob/main/AGENTS.md),仓库地图和组件之间的边界见[仓库根 AGENTS.md](../AGENTS.md)。本文件补充本组件的实现、数据和验证规则,路径都相对于 `server/`。
本组件是常驻后端进程:输入引擎与候选状态、配置、词典、Named Pipe 服务,以及候选窗、悬浮工具栏、托盘菜单的原生宿主与 WebView2 控制器。设置页(`MetasequoiaImeSettings.exe`)由本组件的 `server/src/settings/` 单独构成一个进程,不在服务端进程内。
本组件是常驻后端进程:输入引擎与候选状态、配置、词典、Named Pipe 服务,以及候选窗、悬浮工具栏、托盘菜单(`msimeui` Direct2D presenter)。设置窗口(`MetasequoiaImeSettings.exe`)由本组件的 `server/src/settings/` 单独构成一个进程,不在服务端进程内。
## vcpkg_installed 的解析
`CMakeLists.txt` 四处引用 `vcpkg_installed`,全部走 `${CMAKE_BINARY_DIR}`。其中 `WEBVIEW2_VCPKG_ROOT` 供 WebView2 loader 的导入库和运行时 DLL 拷贝使用。
`CMakeLists.txt` 引用 `vcpkg_installed` 的地方(头文件目录、`sqlite3.dll` 拷贝)全部走 `${CMAKE_BINARY_DIR}`。
**不要改回 `${CMAKE_SOURCE_DIR}/build/...`。** 那样 binary dir 就只能叫字面的 `build`,本仓自带的 `vcpkg-release` preset(`binaryDir` 是 `build-release`)和 `scripts/lcompile-release.ps1` 都会在链接 `MetasequoiaImeSettings` 时报 LNK1181 找不到 `WebView2Loader.dll.lib`。
**不要改回 `${CMAKE_SOURCE_DIR}/build/...`。** 那样 binary dir 就只能叫字面的 `build`,本仓自带的 `vcpkg-release` preset(`binaryDir` 是 `build-release`)和 `scripts/lcompile-release.ps1` 都会找不到 vcpkg 装好的头文件与运行时 DLL。
CI 刻意构建到 `build-release` 而不是 `build`,为的就是让这条不会悄悄退回去——两者在目录名恰好是 `build` 时解析结果相同,所以只有构建到别处才测得出来。
@@ -43,9 +43,9 @@ ctest --test-dir build-release -C Release --output-on-failure
进程在 `src/main.cpp:90` 设 `DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2`。
**创建 HWND 时不能直接带 `WS_EX_TOPMOST`。** 在 `uiAccess=true` 进程里,Topmost 父窗口会让 WebView2 的跨进程 `SetParent` 返回 `E_INVALIDARG`,或者形成「窗口存在、尺寸正确但永远空白」。现有顺序是:非 Topmost 建窗 → DWM cloak 下预热 → 三个共享 environment 的 controller 依次创建并完成首屏导航 → lazy-topmost gate 打开 → 分阶段提升到 `HWND_TOPMOST`。提升后要用 `RenotifyControllerAfterPin` 重新同步 bounds 和 parent position。不要在布局函数里随手传 `HWND_TOPMOST`,也不要绕过 `EnsureSmallWindowsTopmost` / lazy pin。
三个小窗口的置顶由 `src/window/small_windows.cpp` 统一管理:建窗时不带 `WS_EX_TOPMOST`,以 DWM cloak 状态显示在真实显示器上;第一次真正显示时 `EnsureSmallWindowsTopmost` 打开置顶门,候选窗、悬浮工具栏、托盘菜单按固定间隔依次提升到 `HWND_TOPMOST`,托盘菜单最后(同一置顶带里后置顶的在上面)。这个分阶段顺序沿用自已移除的 WebView2 时期,D2D 窗口一直按它发布;要改成一次性置顶,先在签名并安装到受信位置的构建上验证 `uiAccess` 下的表现。不要在布局函数里随手传 `HWND_TOPMOST` 绕过这道门;托盘菜单需要回到最上面时用 `RaiseTrayMenuAboveSmallWindows`。
隐藏尚未完成首帧的 WebView2 host 会阻断 raster 初始化,预热阶段用 cloak,之后才按配置显示或隐藏。
隐藏候选窗用 cloak 而不是 `SW_HIDE`,presenter 在下一次显示时解除 cloak;悬浮工具栏在 presenter 绑定失败时只 cloak 不 hide。
## Boost
@@ -59,39 +59,34 @@ Boost 是静态链接但没有写进 `vcpkg.json`,所以只能用 classic 模
`CMakeLists.txt` 和 `tests/CMakeLists.txt` 不再由脚本生成,直接改就行。这两个文件曾经也是从 `scripts/config_files/` 覆盖过来的,但模板早已落后到 147 行对 400 行、以及一个无关的通用 `WinCppTemplate`,跑一次脚本就会毁掉构建,因此模板已删除。
`.clangd` 模板用 `@NAME@` 占位符,不再按固定行号替换——旧的行号索引已经漂移过一次,把 Boost 路径写到了 WebView2 的 include 上。新增 include 时在模板里加占位符、并在脚本的 `clangd_substitutions` 里补上对应项即可;有占位符没被替换脚本会直接报错。
`.clangd` 模板用 `@NAME@` 占位符,不再按固定行号替换——旧的行号索引已经漂移过一次,把 Boost 路径写到了别的 include 上。新增 include 时在模板里加占位符、并在脚本的 `clangd_substitutions` 里补上对应项即可;有占位符没被替换脚本会直接报错。
## 提交
提交信息用 `type(scope): 摘要`。不要添加 `Co-Authored-By`、`Generated with` 或其他 AI 生成标记。
## 窗口与页面的维护边界
## 窗口与设置窗口的维护边界
- **Server 拥有窗口,UiHtml 拥有页面**:候选窗、悬浮工具栏和托盘菜单的 HWND、尺寸、位置、显示、
DPI、Z-order、WebView2 environment/controller 生命周期在 `server/src/window/` 与
`server/src/webview2/`;页面结构、样式和浏览器端交互在 `ui-html/webview2/`。
- 消息定义以 Engine `contracts/webview/` 为准,双方必须校验。C++ → 页面更新主要经 WebView2 导航/脚本执行,页面 → C++ 动作经
`window.chrome.webview.postMessage(...)`。修改消息 `type`、JSON 字段、页面导出的 JS 函数或 DOM
标识时,必须同步检查 Server 的消息解析/脚本拼接与 UiHtml 的发送/接收代码。
- 候选内容和输入状态的权威仍在 Server/引擎,网页只负责展示与发出用户动作;不要在网页侧复制
- **界面只有 Direct2D 一套**:候选窗、悬浮工具栏和托盘菜单的 HWND、尺寸、位置、显示、DPI、Z-order 在
`src/window/`(`ime_windows*.cpp` 是宿主窗口过程,`*_presenter.cpp` 用 `msimeui` 绘制)。WebView2
已整体移除,`appearance.ui_backend` / `appearance.settings_ui_backend` 两个键也随之删除,不要恢复。
- 候选内容和输入状态的权威仍在 Server/引擎,presenter 只负责展示与发出用户动作;不要在界面侧复制
候选选择、翻页、输入模式或配置持久化的核心状态机。
- 设置页前端位于 `ui-html/webview2/settings/ime-settings/`,其中包含 Vite/TypeScript
工程;设置窗口的原生创建、激活、WebView2 承载及与配置系统的桥接都在本组件的独立进程
`MetasequoiaImeSettings.exe`(`server/src/settings/`)里,服务端进程内没有内嵌的设置宿主。
- 设置窗口另有实验性的 Direct2D 前端(`appearance.settings_ui_backend = "d2d"`,或命令行
`--d2d`),在 `server/src/settings/native/`,用 msimeui 1:1 复刻上面的网页。它和网页走同一套
webview 契约消息与 `HandleClientMessage`,配置读写映射仍只在 `settings_app.cpp`。**改设置页
的结构、文案或行为时两个前端要一起改**:每个 `native_page_*.cpp` 对应一个 partial + module。
- 设置窗口是独立进程 `MetasequoiaImeSettings.exe`(`src/settings/`):原生创建、激活、驻留和配置
读写映射在 `settings_app.cpp`,界面在 `src/settings/native/`,图片资源在 `assets/settings/`(安装到
数据目录的 `assets\settings\`)。界面与宿主之间走 `../engine/contracts/webview/` 的消息契约:宿主推送
`configSnapshot` 与各类响应,界面发 `configUpdate` 与请求,统一进 `HandleClientMessage` 校验和分发,
配置读写映射只在 `settings_app.cpp`。每个 `native_page_*.cpp` 顶部注明了它当初移植自旧网页设置页的
哪个 partial / module,那些文件只在 git 历史里(`ui-html/webview2/settings/ime-settings/src/`)。
本地调试时可设 `METASEQUOIA_IME_SETTINGS_INSTANCE=<任意名>` 让开发版与已安装的设置窗口并存,
并用 `METASEQUOIA_IME_CONFIG_DIR` 隔离配置。
并用 `METASEQUOIA_IME_CONFIG_DIR` 隔离配置。旧快捷方式传来的 `--d2d` / `--webview2` 参数会被忽略。
### 尺寸、坐标与 DPI
- **所有涉及尺寸或坐标的代码都必须明确考虑 DPI**。新增或修改窗口大小、位置、边距、间距、字体、
图标、命中区域、WebView2 bounds、候选布局及屏幕边界计算时,不得默认固定像素在所有显示器上等价。
图标、命中区域、候选布局及屏幕边界计算时,不得默认固定像素在所有显示器上等价。
- 明确每个数值使用逻辑单位还是物理像素,并在正确的窗口/显示器 DPI 下统一换算;不要混用来自不同
DPI 空间的坐标和尺寸,也不要长期缓存可能在窗口跨屏或 DPI 变化后失效的换算结果。
- 原生窗口需处理运行时 DPI 变化(包括跨显示器移动),同步更新窗口、子窗口、WebView2 controller
和相关布局;优先沿用项目已有的 DPI 获取与缩放辅助函数,避免各处自行实现不一致的换算规则。
- 原生窗口需处理运行时 DPI 变化(包括跨显示器移动),同步更新窗口、子窗口和相关布局;优先沿用项目已有的 DPI 获取与缩放辅助函数,避免各处自行实现不一致的换算规则。
- 涉及尺寸或布局的改动,至少检查 100%、125%、150%、200% 缩放,以及不同 DPI 显示器之间移动时
是否出现裁切、错位、模糊、点击区域偏移或窗口越出工作区。
+5 -38
View File
@@ -264,18 +264,8 @@ set(SRC_FILES
"./src/utils/serial_task_runner.h"
"./src/utils/shuangpin_preedit_display.cpp"
"./src/utils/shuangpin_preedit_display.h"
"./src/utils/webview_utils.cpp"
"./src/utils/webview_utils.h"
"./src/utils/window_utils.cpp"
"./src/utils/window_utils.h"
"./src/webview2/windows_webview2.cpp"
"./src/webview2/windows_webview2_candidate.cpp"
"./src/webview2/windows_webview2_internal.h"
"./src/webview2/windows_webview2_menu.cpp"
"./src/webview2/windows_webview2_skin.cpp"
"./src/webview2/windows_webview2_toolbar.cpp"
"./src/webview2/skin_css_policy.cpp"
"./src/webview2/windows_webview2.h"
"./src/window/candidate_presenter.cpp"
"./src/window/candidate_presenter.h"
"./src/window/candidate_skin_overlay.cpp"
@@ -293,11 +283,11 @@ set(SRC_FILES
"./src/window/ime_windows.cpp"
"./src/window/ime_windows.h"
"./src/window/ime_windows_candidate.cpp"
"./src/window/ime_windows_candidate_layout.cpp"
"./src/window/ime_windows_candidate_show.cpp"
"./src/window/ime_windows_internal.h"
"./src/window/ime_windows_menu.cpp"
"./src/window/ime_windows_toolbar.cpp"
"./src/window/small_windows.cpp"
"./src/window/small_windows.h"
"./src/window/window_hook.cpp"
"./src/window/window_hook.h"
"./src/voice-input/voice_input_service.cpp"
@@ -322,7 +312,7 @@ add_executable(${MY_EXECUTABLE_NAME} WIN32
${SRC_FILES}
)
# Tomlplusplus, Webview2 and other head-only libraries
# Tomlplusplus and other header-only libraries
target_include_directories(${PROJECT_NAME}
PRIVATE
"${CMAKE_BINARY_DIR}/vcpkg_installed/x64-windows/include"
@@ -342,7 +332,7 @@ target_link_libraries(${PROJECT_NAME} PRIVATE ZLIB::ZLIB)
target_link_libraries(${PROJECT_NAME} PRIVATE d2d1 dwrite dwmapi shell32 shcore ole32 bcrypt)
# The helper executables below already build at /W4. The server itself -- the process that owns
# every network call, the WebView2 hosts and the skin loader -- was still at the default /W3.
# every network call and the skin loader -- was still at the default /W3.
if(MSVC)
target_compile_options(${MY_EXECUTABLE_NAME} PRIVATE /W4)
endif()
@@ -447,15 +437,6 @@ if(MSVC)
endif()
add_dependencies(${PROJECT_NAME} ${HANDWRITING_EXECUTABLE_NAME})
# WebView2 loader (required for CreateCoreWebView2EnvironmentWithOptions)
set(WEBVIEW2_VCPKG_ROOT "${CMAKE_BINARY_DIR}/vcpkg_installed/x64-windows")
set(WEBVIEW2_LOADER_RELEASE "${WEBVIEW2_VCPKG_ROOT}/lib/WebView2Loader.dll.lib")
set(WEBVIEW2_LOADER_DEBUG "${WEBVIEW2_VCPKG_ROOT}/debug/lib/WebView2Loader.dll.lib")
target_link_libraries(${PROJECT_NAME} PRIVATE
$<$<CONFIG:Debug>:${WEBVIEW2_LOADER_DEBUG}>
$<$<CONFIG:Release>:${WEBVIEW2_LOADER_RELEASE}>
)
set(SETTINGS_EXECUTABLE_NAME "MetasequoiaImeSettings")
add_executable(${SETTINGS_EXECUTABLE_NAME} WIN32
"./src/settings/settings_app.cpp"
@@ -474,10 +455,6 @@ add_executable(${SETTINGS_EXECUTABLE_NAME} WIN32
"./src/settings/default_input_method.h"
"./src/settings/game_compat_name.cpp"
"./src/settings/game_compat_name.h"
"./src/settings/settings_mouse_input.cpp"
"./src/settings/settings_mouse_input.h"
"./src/settings/settings_splash.cpp"
"./src/settings/settings_splash.h"
"./src/settings/settings_launcher.cpp"
"./src/settings/settings_launcher.h"
"./src/settings/native/native_settings_bindings.cpp"
@@ -593,8 +570,6 @@ target_link_libraries(${SETTINGS_EXECUTABLE_NAME} PRIVATE
d2d1
dwrite
shell32
$<$<CONFIG:Debug>:${WEBVIEW2_LOADER_DEBUG}>
$<$<CONFIG:Release>:${WEBVIEW2_LOADER_RELEASE}>
)
if(MSVC)
target_compile_options(${SETTINGS_EXECUTABLE_NAME} PRIVATE /W4)
@@ -603,21 +578,13 @@ endif()
# Building the server target alone must therefore also produce Settings.
add_dependencies(${PROJECT_NAME} ${SETTINGS_EXECUTABLE_NAME})
add_custom_command(TARGET ${SETTINGS_EXECUTABLE_NAME} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
$<$<CONFIG:Debug>:${WEBVIEW2_VCPKG_ROOT}/debug/bin/WebView2Loader.dll>
$<$<CONFIG:Release>:${WEBVIEW2_VCPKG_ROOT}/bin/WebView2Loader.dll>
$<TARGET_FILE_DIR:${SETTINGS_EXECUTABLE_NAME}>
COMMAND ${CMAKE_COMMAND} -E copy_directory
"${MSIME_VENDOR_DIR}/cpp-pinyin/res/dict"
"$<TARGET_FILE_DIR:${SETTINGS_EXECUTABLE_NAME}>/dict"
)
# Ensure the loader DLL is next to the executable
# Runtime assets next to the executable
add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
$<$<CONFIG:Debug>:${WEBVIEW2_VCPKG_ROOT}/debug/bin/WebView2Loader.dll>
$<$<CONFIG:Release>:${WEBVIEW2_VCPKG_ROOT}/bin/WebView2Loader.dll>
$<TARGET_FILE_DIR:${PROJECT_NAME}>
COMMAND ${CMAKE_COMMAND} -E make_directory
$<TARGET_FILE_DIR:${PROJECT_NAME}>/assets/audios
COMMAND ${CMAKE_COMMAND} -E copy_if_different
+6 -19
View File
@@ -11,7 +11,6 @@ This is the server end of [MSIME-Windows](https://github.com/metasequoiaime/MSIM
- vcpkg
- Python3.10+
- Boost
- Node.js and pnpm 10 (settings page build)
Make sure vcpkg and Boost are installed by **Scoop**.
@@ -31,21 +30,9 @@ Copy-Item MetasequoiaImeDict/out/* $devData -Force
Copy-Item engine/helpcode/helpcodes $devData -Recurse -Force
Copy-Item server/assets/tables/* $devData -Force
Copy-Item server/assets/config/config.toml $devData -Force
Push-Location ui-html/webview2/settings/ime-settings
try {
pnpm install --frozen-lockfile
if ($LASTEXITCODE -ne 0) { throw 'Settings dependencies failed' }
pnpm build
if ($LASTEXITCODE -ne 0) { throw 'Settings page build failed' }
} finally { Pop-Location }
$devWebview = Join-Path $devData 'html/webview2'
New-Item -ItemType Directory -Force $devWebview | Out-Null
foreach ($part in @('shared', 'candwnd', 'ftb', 'menu')) {
Copy-Item "ui-html/webview2/$part" $devWebview -Recurse -Force
}
$devSettings = Join-Path $devWebview 'settings/ime-settings'
New-Item -ItemType Directory -Force $devSettings | Out-Null
Copy-Item ui-html/webview2/settings/ime-settings/dist $devSettings -Recurse -Force
$devAssets = Join-Path $devData 'assets'
New-Item -ItemType Directory -Force $devAssets | Out-Null
Copy-Item server/assets/settings $devAssets -Recurse -Force
python server/scripts/prepare_env.py
if ($LASTEXITCODE -ne 0) { throw 'Server environment setup failed' }
$env:LOCALAPPDATA = $devLocalData
@@ -54,10 +41,10 @@ cd server
```
Use a dedicated PowerShell terminal. These steps stage the same locked data as CI
and the current product pages in a separate development directory. Both
`LOCALAPPDATA` (Windows config/pages) and `METASEQUOIA_IME_DATA_DIR` (Engine data)
and the settings window assets in a separate development directory. Both
`LOCALAPPDATA` (Windows config/assets) and `METASEQUOIA_IME_DATA_DIR` (Engine data)
are set for that terminal; setting only the latter would still use installed
Windows configuration and pages. Close the terminal after development.
Windows configuration and assets. Close the terminal after development.
Dictionary source changes belong in Engine's `dictionary/`; use its root `build_profile.py`
to build data instead of calling internal stages in the archived Dict repository.
-6
View File
@@ -174,15 +174,9 @@ linear_step = 1
[appearance]
# 主题模式,可选:"dark", "light", "system"
theme_mode = "system"
# 候选窗 / 悬浮工具栏 / 托盘菜单渲染,可选:"d2d", "webview2"
# 更改后需重启输入法进程生效
ui_backend = "d2d"
# 设置窗口关闭后在后台驻留多久再退出,驻留期间重新打开无需冷启动
# 可选:"off"(立即退出), "1m", "5m", "10m", "30m", "60m", "forever"(一直驻留)
settings_window_linger = "off"
# 设置窗口渲染方式,可选:"webview2", "d2d"(Direct2D,实验性)
# 更改后设置窗口会自动重新打开
settings_ui_backend = "webview2"
# 分表面主题,可选:"follow", "dark", "light"(follow = 跟随 theme_mode)
theme_settings = "follow"
theme_cand = "follow"
File renamed without changes.

Before

Width:  |  Height:  |  Size: 145 KiB

After

Width:  |  Height:  |  Size: 145 KiB

File renamed without changes.

Before

Width:  |  Height:  |  Size: 813 B

After

Width:  |  Height:  |  Size: 813 B

File renamed without changes.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

File renamed without changes.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 32 KiB

File renamed without changes.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 33 KiB

File renamed without changes.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

File renamed without changes.

Before

Width:  |  Height:  |  Size: 1005 B

After

Width:  |  Height:  |  Size: 1005 B

File renamed without changes.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 50 KiB

File renamed without changes.

Before

Width:  |  Height:  |  Size: 73 KiB

After

Width:  |  Height:  |  Size: 73 KiB

File renamed without changes.

Before

Width:  |  Height:  |  Size: 200 KiB

After

Width:  |  Height:  |  Size: 200 KiB

File renamed without changes.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

File renamed without changes.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

File renamed without changes.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

File renamed without changes.

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

File renamed without changes.

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

File renamed without changes.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

File renamed without changes.

Before

Width:  |  Height:  |  Size: 335 B

After

Width:  |  Height:  |  Size: 335 B

File renamed without changes.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

File renamed without changes.

Before

Width:  |  Height:  |  Size: 796 B

After

Width:  |  Height:  |  Size: 796 B

File renamed without changes.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

File renamed without changes.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

File renamed without changes.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

File renamed without changes.

Before

Width:  |  Height:  |  Size: 930 B

After

Width:  |  Height:  |  Size: 930 B

File renamed without changes.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

File renamed without changes.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

File renamed without changes.

Before

Width:  |  Height:  |  Size: 234 B

After

Width:  |  Height:  |  Size: 234 B

File renamed without changes.

Before

Width:  |  Height:  |  Size: 1023 B

After

Width:  |  Height:  |  Size: 1023 B

File renamed without changes.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

File renamed without changes.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 37 KiB

File renamed without changes.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 35 KiB

+37 -46
View File
@@ -1,59 +1,50 @@
# UI ownership and compatibility
# UI ownership
| Surface | Long-term backend | Compatibility scope |
| Surface | Renderer | Owner |
|---|---|---|
| Candidate, floating toolbar, tray menu | msimeui native Direct2D/DirectWrite | `appearance.ui_backend=webview2` explicitly selects existing WebView skins |
| Settings | UiHtml hosted by the separate native settings application | `appearance.settings_ui_backend=d2d` selects the experimental Direct2D frontend; small-window backend settings never change this host |
| Candidate, floating toolbar, tray menu | msimeui native Direct2D/DirectWrite | `src/window/` presenters in the Server process |
| Settings | msimeui native Direct2D/DirectWrite | `src/settings/native/` in the separate `MetasequoiaImeSettings.exe` |
| Keyboard, handwriting, emoji and voice panels | Their existing native application/presenter | Shared actions/configuration stay in Server; panels do not own input sessions |
`window/ui_backend_policy.h` owns backend selection. D2D is the default; existing
WebView aliases remain readable. Unknown/retired stored values migrate to D2D,
while new invalid configuration writes fail. A backend setting applies at process
startup, preserving the existing controller lifetime and restart behavior.
Direct2D is the only renderer. The WebView2 small windows, the WebView2 settings host and
the `ui-html/` pages have been removed, together with the `appearance.ui_backend` and
`appearance.settings_ui_backend` configuration keys; existing values are dropped by the
template merge on upgrade.
The candidate worker resolves output text, auxiliary codes, source badges and
translations once into `CandidateViewItem`. Both native and Web renderers consume
these values. HTML escaping and the legacy comma framing are confined to the Web
adapter; native rendering receives plain text. Both click paths enter the existing
`EnqueueCandidateUiAction` queue with the same focus owner checks and action policy.
The ten-entry mouse range is distinct from the 1–9 keyboard shortcuts.
translations once into `CandidateViewItem`, and the native renderer consumes these
values as plain text. Clicks enter the existing `EnqueueCandidateUiAction` queue with the
focus owner checks and action policy. The ten-entry mouse range is distinct from the
1–9 keyboard shortcuts. New business behavior belongs in shared actions/session/view
models, not in a presenter.
WebView small windows remain a compatibility backend for existing skins. Their
removal requires: a supported conversion path for CSS-dependent external skins;
parity for candidate/context-menu actions, translations and accessibility; and
recorded mixed-DPI, cold-start and signed/uiAccess host validation of the native
replacement. Until those conditions are met, keep compatibility tests and do not
silently reinterpret a configured WebView skin as a native skin. New business
behavior belongs in shared actions/session/view models, not either renderer.
## Settings window
## Direct2D settings window (experimental)
`MetasequoiaImeSettings.exe` draws its window with msimeui in `src/settings/native/`.
The window speaks the message contract in `engine/contracts/webview/` (the name is
historical): it receives `configSnapshot` and the response messages as JSON and posts
`configUpdate` and the request messages through `HandleClientMessage` in
`settings_app.cpp`, so config paths are mapped in one place. Its image assets live in
`server/assets/settings/` and are installed to `<data dir>\assets\settings\`.
`MetasequoiaImeSettings.exe` has two frontends for the same window. WebView2 hosting
`ui-html/webview2/settings/ime-settings` stays the default; `appearance.settings_ui_backend
= "d2d"` (or `--d2d` on the command line for one run) draws the window with msimeui
instead, in `server/src/settings/native/`. Both frontends speak the same webview contract:
the native window receives `configSnapshot` and the response messages as JSON and posts
`configUpdate` and the request messages through the dispatcher the WebView2 page uses
(`HandleClientMessage` in `settings_app.cpp`), so config paths are still mapped in one
place. Switching the setting reopens the settings process in the other renderer.
The pages were ported one to one from the former HTML/CSS settings page: each page
builder in `native_page_*.cpp` names the partial and module it came from, and the
controls in `native_settings_controls.*` name the CSS rule they mirror. Those sources
remain in git history under `ui-html/webview2/settings/ime-settings/src/`.
The native pages reproduce the HTML/CSS page one to one: each page builder in
`native_page_*.cpp` follows its partial and module, and the controls in
`native_settings_controls.*` name the CSS rule they mirror. A change to a settings page
therefore has to be made on both sides until the WebView2 frontend is retired.
Sciter is not an active product backend. The msimeui library owns generic controls and
rendering, never Server configuration, pipe sessions, dictionary access, or global
input state.
Sciter is not an active product backend. The old unreferenced handwritten D2D
prototype is removed; native small windows use msimeui presenters. The msimeui
library owns generic controls and rendering, never Server configuration, pipe
sessions, dictionary access, or global input state.
## v2 skins
## v2 皮肤在两个后端下的渲染
`schema_version = 2` skins (nine-slice panel images, freely anchored decorations, space
outside the panel) are rendered by `CandidatePresenter`'s native scene: the panel is drawn
by `msimeui::Card::SetBackgroundImageNineSlice`, decorations by the `CandidateSkinOverlay`
layer above the card, and `outer_insets_dip` becomes extra window space around the card.
Parts of v2 are still being completed on the native side; see the `TODO(v2 皮肤)` notes in
`candidate_presenter.cpp` and `candidate_skin_overlay.cpp`.
`schema_version = 2` 的皮肤(九宫格贴图面板、自由锚定挂件、面板外空白)两个后端都支持,但走的是不同的实现:
- WebView2 把面板翻成 CSS:`border-image` 铺九宫格,`content_insets_dip` 落成容器内边距,挂件用 `.containerParent` 的伪元素绝对定位。
- D2D 用 `CandidatePresenter` 的本地场景:面板由 `msimeui::Card::SetBackgroundImageNineSlice` 画成九宫格,挂件由 `CandidateSkinOverlay` 这一层浮在卡片上,`outer_insets_dip` 换成窗口在卡片四边多留的空白。
两侧读同一份 `skin.toml`,字段语义必须一致,尤其是 `anchor` 与 `offset_*_dip`:正数向右、向下。改一侧的定位规则要同时改另一侧。
`content_insets_dip` 在两个后端都从面板外缘量起,D2D 侧由卡片内边距承担,不需要再扣边框。
`anchor` and `offset_*_dip` follow one convention: positive values move right and down.
`content_insets_dip` is measured from the outer edge of the panel and is carried by the
card padding.
+1 -2
View File
@@ -14,8 +14,7 @@ CompileFlags:
"-I@VOICE_INCLUDE@",
"-I@MINIAUDIO@",
"-I@GUI_INCLUDE@",
# WebView2 includes
"-I@WEBVIEW2@",
# Windows Implementation Library includes
"-I@WIL@",
# Boost includes
"-I@BOOST@",
+3 -16
View File
@@ -31,18 +31,6 @@ vcpkg_include_path = normpath(
monorepo_root_path = normpath(os.path.dirname(project_root_path))
engine_path = normpath(os.path.join(monorepo_root_path, "engine"))
utfcpp_path = normpath(os.path.join(engine_path, "utfcpp", "source"))
webview2_path = normpath(
os.path.join(
user_home,
".nuget",
"packages",
"microsoft.web.webview2",
"1.0.3240.44",
"build",
"native",
"include",
)
)
wil_path = normpath(
os.path.join(
user_home,
@@ -63,9 +51,9 @@ boost_path = normpath(
# project_root/.clangd
#
# The template carries @NAME@ placeholders rather than fixed line numbers. The
# previous version addressed lines by index, and the indices had drifted: the
# Boost path was written over the WebView2 include and the Sciter path over a
# comment, which left clangd unable to resolve the WebView2 headers.
# previous version addressed lines by index, and the indices had drifted, so
# paths were written over the wrong include lines and comments and clangd could
# not resolve the headers they were meant for.
#
clangd_substitutions = {
"@SERVER_ROOT@": MetasequoiaImeServer_root_path,
@@ -76,7 +64,6 @@ clangd_substitutions = {
"@VOICE_INCLUDE@": engine_path + "/voice/include",
"@MINIAUDIO@": engine_path + "/voice/third_party/miniaudio",
"@GUI_INCLUDE@": monorepo_root_path + "/ui/include",
"@WEBVIEW2@": webview2_path,
"@WIL@": wil_path,
"@BOOST@": boost_path,
}
+2 -2
View File
@@ -173,8 +173,8 @@ function Test-Activation([IntPtr]$Target, [IntPtr]$Settings, [uint32]$SettingsTh
$finalActive = if ($activeValues.Count) { $activeValues[$activeValues.Count - 1] } else { 0 }
$finalFocus = if ($focusValues.Count) { $focusValues[$focusValues.Count - 1] } else { 0 }
$requiresSettingsFocus = $targetValue -eq $Settings.ToInt64()
# MoveFocus transfers keyboard focus from the host HWND into WebView2's
# Composition input HWND, so a non-zero focus HWND is expected here.
# Once the settings window is active it owns keyboard focus (on itself or
# one of its child HWNDs), so a non-zero focus HWND is expected here.
$settingsFocusCorrect = -not $requiresSettingsFocus -or `
($finalActive -eq $Settings.ToInt64() -and $finalFocus -ne 0)
-22
View File
@@ -1,4 +1,3 @@
#include "window/ui_backend_policy.h"
#include "window/caret_state_indicator_policy.h"
#include "window/candidate_view_model.h"
#include "ime_config.h"
@@ -203,15 +202,12 @@ std::string g_punctuation_lock = "follow";
std::string g_candidate_window_layout = "vertical";
bool g_candidate_window_follow_cursor = true;
bool g_candidate_window_fit_content = false;
std::string g_ui_backend = "d2d";
std::string g_ui_backend_active = "d2d";
std::string g_candidate_skin = "fluent";
std::string g_candidate_window_preedit_style = "pinyin";
bool g_candidate_fixed_badge = true;
std::string g_candidate_fixed_badge_style = "paperclip";
std::string g_candidate_ai_badge_style = "robot";
std::string g_settings_window_linger = "off";
std::string g_settings_ui_backend = "webview2";
std::string g_theme_mode = "system";
std::string g_theme_settings = "follow";
std::string g_theme_cand = "follow";
@@ -275,13 +271,6 @@ void PublishVoiceInput(VoiceInputConfig voice)
namespace ime_config_detail
{
std::string NormalizeSmallWindowUiBackend(const std::string &value)
{
if (UiBackendPolicy::Resolve(UiBackendPolicy::Surface::Candidate, value) == UiBackendPolicy::Backend::WebView2)
return "webview2";
return "d2d";
}
bool IsValidCandidateSkinId(const std::string &skin)
{
if (skin.empty() || skin.size() > 64 || !std::isalnum(static_cast<unsigned char>(skin.front())))
@@ -313,11 +302,6 @@ bool IsValidSettingsWindowLinger(const std::string &linger)
linger == "60m" || linger == "forever";
}
bool IsValidSettingsUiBackend(const std::string &backend)
{
return backend == "webview2" || backend == "d2d";
}
bool IsValidTranslationTargetLanguage(const std::string &language)
{
static constexpr std::string_view kLanguages[] = {"en", "fr", "ja", "es", "ru", "de", "ko", "th", "vi", "it"};
@@ -743,7 +727,6 @@ bool LoadImeConfig()
g_candidate_window_layout = layout == "horizontal" ? "horizontal" : "vertical";
g_candidate_window_follow_cursor = tbl["appearance"]["candidate_window_follow_cursor"].value_or(true);
g_candidate_window_fit_content = tbl["appearance"]["candidate_window_fit_content"].value_or(false);
g_ui_backend = NormalizeSmallWindowUiBackend(tbl["appearance"]["ui_backend"].value_or(std::string("d2d")));
const std::string skin = tbl["appearance"]["candidate_skin"].value_or(std::string("fluent"));
g_candidate_skin = IsValidCandidateSkinId(skin) ? skin : "fluent";
{
@@ -770,10 +753,6 @@ bool LoadImeConfig()
const std::string linger = tbl["appearance"]["settings_window_linger"].value_or(std::string("off"));
g_settings_window_linger = IsValidSettingsWindowLinger(linger) ? linger : "off";
}
{
const std::string backend = tbl["appearance"]["settings_ui_backend"].value_or(std::string("webview2"));
g_settings_ui_backend = IsValidSettingsUiBackend(backend) ? backend : "webview2";
}
{
const std::string theme_mode = tbl["appearance"]["theme_mode"].value_or(std::string("system"));
if (theme_mode == "light" || theme_mode == "system" || theme_mode == "auto")
@@ -1197,7 +1176,6 @@ void InitImeConfig()
(void)0;
#endif
}
g_ui_backend_active = g_ui_backend;
}
bool ReloadImeConfigIfChanged()
-14
View File
@@ -614,20 +614,6 @@ bool SetConfiguredCandidateAiBadgeStyle(const std::string &style);
// "off" | "1m" | "5m" | "10m" | "30m" | "60m" | "forever",默认 "10m"。
const std::string &GetConfiguredSettingsWindowLinger();
bool SetConfiguredSettingsWindowLinger(const std::string &linger);
// "webview2" | "d2d" — renderer of the settings window, applied when it next opens.
// Direct2D is experimental; WebView2 stays the default.
const std::string &GetConfiguredSettingsUiBackend();
bool SetConfiguredSettingsUiBackend(const std::string &backend);
// "d2d" | "webview2" — candidate, floating toolbar, and tray menu renderer.
// Changing this writes config immediately; the process snapshot does not switch
// until the IME server is restarted.
const std::string &GetConfiguredUiBackend();
bool SetConfiguredUiBackend(const std::string &backend);
// Process-lifetime renderer chosen at InitImeConfig. Independent of later reloads.
bool UseD2dSmallWindowUi();
// Switch this process to the D2D renderer without touching the configured value, so the user's
// choice comes back on the next start. Only valid before the candidate window is created.
void FallBackToD2dSmallWindowUi();
// "fluent" | "wechat" | "graphite" | "willow_green" | "autumn_osmanthus" | "microsoft"
// — candidate-window and floating-toolbar skin.
const std::string &GetConfiguredCandidateSkin();
+1 -50
View File
@@ -1,6 +1,5 @@
// 候选窗外观配置:每页候选数、字体与字号、文字颜色、布局与跟随光标、小窗 UI 后端、
// 候选窗外观配置:每页候选数、字体与字号、文字颜色、布局与跟随光标、
// 皮肤、预编辑样式、固定徽标和设置窗口保留时长。
#include "window/ui_backend_policy.h"
#include "config/ime_config_internal.h"
#include <algorithm>
#include <cctype>
@@ -222,35 +221,6 @@ bool SetConfiguredCandidateWindowFitContent(bool enabled)
return true;
}
const std::string &GetConfiguredUiBackend()
{
return g_ui_backend;
}
bool SetConfiguredUiBackend(const std::string &backend)
{
if (!UiBackendPolicy::IsSupported(backend))
return false;
const std::string normalized = NormalizeSmallWindowUiBackend(backend);
if (!WriteConfiguredValue("appearance", "ui_backend", EscapeTomlBasicString(normalized)))
{
return false;
}
g_ui_backend = normalized;
return true;
}
bool UseD2dSmallWindowUi()
{
return UiBackendPolicy::Resolve(UiBackendPolicy::Surface::Candidate, g_ui_backend_active) ==
UiBackendPolicy::Backend::Native;
}
void FallBackToD2dSmallWindowUi()
{
g_ui_backend_active = "d2d";
}
const std::string &GetConfiguredCandidateSkin()
{
return g_candidate_skin;
@@ -375,22 +345,3 @@ bool SetConfiguredSettingsWindowLinger(const std::string &linger)
g_settings_window_linger = linger;
return true;
}
const std::string &GetConfiguredSettingsUiBackend()
{
return g_settings_ui_backend;
}
bool SetConfiguredSettingsUiBackend(const std::string &backend)
{
if (!IsValidSettingsUiBackend(backend))
{
return false;
}
if (!WriteConfiguredValue("appearance", "settings_ui_backend", EscapeTomlBasicString(backend)))
{
return false;
}
g_settings_ui_backend = backend;
return true;
}
+1 -1
View File
@@ -71,7 +71,7 @@ bool SetConfiguredCharacterSet(const std::string &character_set)
return false;
}
g_character_set = character_set;
// Marshal WebView/native toolbar refreshes to their owner thread. Settings
// Marshal toolbar refreshes to their owner thread. Settings
// in a separate process reaches the same refresh through ConfigChanged.
NotifyImeServer(WM_REFRESH_CHARACTER_SET, L"ConfigChanged");
return true;
-5
View File
@@ -243,15 +243,12 @@ extern std::string g_punctuation_lock;
extern std::string g_candidate_window_layout;
extern bool g_candidate_window_follow_cursor;
extern bool g_candidate_window_fit_content;
extern std::string g_ui_backend;
extern std::string g_ui_backend_active;
extern std::string g_candidate_skin;
extern std::string g_candidate_window_preedit_style;
extern bool g_candidate_fixed_badge;
extern std::string g_candidate_fixed_badge_style;
extern std::string g_candidate_ai_badge_style;
extern std::string g_settings_window_linger;
extern std::string g_settings_ui_backend;
extern std::string g_theme_mode;
extern std::string g_theme_settings;
extern std::string g_theme_cand;
@@ -281,12 +278,10 @@ void RefreshEffectiveTsfPreeditStyle();
// 文件缺失或写错时返回 false,调用方据此回落到小鹤。
bool LoadShuangpinSchema(const std::string &schema);
VoiceInputConfig SnapshotVoiceInput();
std::string NormalizeSmallWindowUiBackend(const std::string &value);
bool IsValidCandidateSkinId(const std::string &skin);
bool IsValidCandidateFixedBadgeStyle(const std::string &style);
bool IsValidCandidateAiBadgeStyle(const std::string &style);
bool IsValidSettingsWindowLinger(const std::string &linger);
bool IsValidSettingsUiBackend(const std::string &backend);
bool IsValidTranslationTargetLanguage(const std::string &language);
std::string AiAssistantTokenSlotKey(std::string_view provider);
// ime_config_services.cpp:按当前各段全局配置重建候选词翻译快照。改动 provider、腾讯云、
-1
View File
@@ -20,7 +20,6 @@ inline const UINT UPDATE_FTB_STATUS = WM_USER + 108;
inline const UINT WM_PIN_TO_TOP_CANDIDATE = WM_USER + 109;
inline const UINT WM_COMMIT_CANDIDATE = WM_USER + 110;
inline const UINT WM_CLEAR_IME_ENGINE_CACHE = WM_USER + 111;
inline const UINT WM_REFRESH_MENU_SIZE = WM_USER + 112;
inline const UINT WM_FIX_CANDIDATE_POSITION = WM_USER + 113;
inline const UINT WM_CLEAR_CANDIDATE_POSITION = WM_USER + 114;
inline const UINT UPDATE_FTB_ENGLISH_INPUT_MODE = WM_USER + 115;
+4 -14
View File
@@ -6,14 +6,6 @@
inline float SCALE = 1.0f;
inline int POP_UP_WND_WIDTH = 43;
inline int POP_UP_WND_HEIGHT = 55;
// WebView2 candidate shadow reserve in CSS DIPs. Keep these in sync with the
// two-layer shadows in ui-html/webview2/candwnd/skins. The shadow is biased to
// the lower right, so symmetric padding either clips its soft tail or reserves
// too much transparent hit-test area on the upper-left side.
inline constexpr int CANDIDATE_SHADOW_PAD_LEFT = 16;
inline constexpr int CANDIDATE_SHADOW_PAD_TOP = 14;
inline constexpr int CANDIDATE_SHADOW_PAD_RIGHT = 32;
inline constexpr int CANDIDATE_SHADOW_PAD_BOTTOM = 34;
//
// 候选窗口
@@ -23,8 +15,6 @@ inline constexpr int CANDIDATE_SHADOW_PAD_BOTTOM = 34;
//
inline int CANDIDATE_WINDOW_WIDTH = 120;
inline int CANDIDATE_WINDOW_HEIGHT = 232;
// Horizontal (and long vertical) candidates wrap beyond this CSS DIP width.
inline constexpr int CANDIDATE_WINDOW_MAX_WIDTH_DIP = 720;
inline int DEFAULT_WINDOW_WIDTH = CANDIDATE_WINDOW_WIDTH;
inline int DEFAULT_WINDOW_HEIGHT = CANDIDATE_WINDOW_HEIGHT;
@@ -41,7 +31,7 @@ inline bool is_global_wnd_cand_shown = false;
//
// 托盘区语言栏菜单窗口
// MENU_CONTENT_*_DIP is the measured CSS size; MENU_WINDOW_* is the derived
// MENU_CONTENT_*_DIP is the measured content size; MENU_WINDOW_* is the derived
// physical host size (dip * current scale). Caching DIPs lets live DPI changes
// and each show recompute physical size without a stale pixel cache.
//
@@ -57,11 +47,11 @@ inline bool is_global_wnd_menu_shown = false;
// 悬浮工具栏窗口
//
inline int FTB_WND_WIDTH = 207;
// Content bar height in DIPs (matches CSS --ftb-bar-height). Shadow is added on
// the host separately so box-shadow is not clipped.
// Content bar height in DIPs. The initial host adds the shadow margin
// separately so the shadow is not clipped.
inline int FTB_WND_HEIGHT = 35;
inline int FTB_WND_SHADOW_WIDTH = 8;
// Last measured .status-bar size in CSS DIPs (0 until first successful measure).
// Last measured bar size in DIPs (0 until first successful measure).
inline double FTB_CONTENT_WIDTH_DIP = 0.0;
inline double FTB_CONTENT_HEIGHT_DIP = 0.0;
// Off-screen margin (physical px) the host was last placed with, so a resize
+1 -4
View File
@@ -248,9 +248,6 @@ inline std::string NumHanSeparator = " "; // Number and Hanzi separator
namespace Global
{
inline LONG INVALID_Y = -100000;
inline int MarginTop = 0;
// Horizontal offset of the opaque card inside the stable (often 720 DIP-wide) host.
inline int MarginLeft = 0;
// Candidate pages are published by the IPC worker thread but painted by the UI thread. The worker
// stamps every published page (candidate_page_generation), the UI echoes back the generation it
@@ -418,7 +415,7 @@ inline CandidateUiState candidate_ui;
// candidate_ui belongs to the IPC worker thread, which rebuilds and destroys its vectors while the UI thread paints;
// the handoff is an asynchronous PostMessage, so nothing serialises the two. The worker therefore copies each finished
// page into one of these immutable snapshots and publishes it atomically, and every UI-thread reader (candidate
// presenter, its mouse callbacks, the WebView2 payload) loads a snapshot once and reads only from that copy.
// presenter and its mouse callbacks) loads a snapshot once and reads only from that copy.
struct CandidatePageSnapshot
{
std::vector<CandidateViewItem> page_views;
+4 -5
View File
@@ -14,7 +14,7 @@ namespace FanyImeIpc
// condition variable timeout (rounded up to one timer tick, ~16ms) is the only slack on top of
// this bound; together with candidate resolution it must stay well inside that budget. The
// frequency write runs after the reply and no longer competes for it. On a slow machine (battery,
// WebView2 paint) a paint regularly takes longer than 30ms, and settling against the unpainted
// busy GPU) a paint regularly takes longer than 30ms, and settling against the unpainted
// page is exactly the "committed something I did not see" bug, so the bound leaves room for a slow
// paint. On timeout the caller continues with the current data and the diagnostic log records the
// miss. Do not raise this without re-deriving the TSF budget.
@@ -37,10 +37,9 @@ constexpr bool ShouldWaitForCandidateRender(std::uint64_t rendered, std::uint64_
return rendered < current;
}
// The render echo only moves forward. WebView2 echoes from async DOM callbacks, so a content-only
// update's callback can land after a dedup of the same content already echoed the newer generation;
// storing the older one left rendered one behind published and every selection waited out the full
// bound.
// The render echo only moves forward. An echo for an older page can arrive after a dedup of the
// same content already echoed the newer generation; storing the older one would leave rendered one
// behind published, and every selection would wait out the full bound.
constexpr std::uint64_t AdvanceRenderedCandidateGeneration(std::uint64_t rendered, std::uint64_t echoed) noexcept
{
return echoed > rendered ? echoed : rendered;
+5 -5
View File
@@ -57,7 +57,7 @@ bool g_english_input_mode = false;
namespace
{
// Sticky UILess for the active Main-pipe client. When set, never raise the
// WebView2 candidate HWND — hosts (games) draw via ITfUIElementSink instead.
// candidate HWND — hosts (games) draw via ITfUIElementSink instead.
bool g_activate_uiless = false;
bool g_session_uiless = false;
} // namespace
@@ -120,8 +120,8 @@ void ApplyUiLessFromPacket(const FanyImeNamedpipeData &pipe_data)
g_candidate_translation_signature.clear();
g_candidate_translation_glosses.clear();
EnglishIme::ClearTranslations();
// A prior non-UILess session may have left the WebView2 candidate HWND
// visible; hide it immediately when the host takes over drawing.
// A prior non-UILess session may have left the candidate HWND visible;
// hide it immediately when the host takes over drawing.
::is_global_wnd_cand_shown = false;
Global::SetCandidateWindowRenderedVisible(false);
if (::global_hwnd && IsWindow(::global_hwnd))
@@ -194,8 +194,8 @@ void RequestShowCandidateWindow()
// because there is nothing left to offer as candidates.
void HideCandidateWindowAndDropItems()
{
// Drop published candidates before any in-flight FineTuneWindow callback can
// re-inflate an empty-preedit + stale-candidate view.
// Drop published candidates before a queued show can re-render an
// empty-preedit + stale-candidate view.
Global::CandidateString.clear();
Global::ClearCandidatePageSnapshot();
Global::candidate_ui.set_items({});
+3 -3
View File
@@ -16,9 +16,9 @@
inline std::condition_variable pipe_queueCv;
inline std::atomic_bool pipe_running = true;
inline std::shared_ptr<IInputSession> g_inputSession;
// Collapse bursty TSF show/move packets so the UI thread runs one FineTune
// against the latest Global::Point / CandidateString instead of restarting
// the WebView2 measure chain on every cloud refresh or layout notification.
// Collapse bursty TSF show/move packets so the UI thread lays out once against
// the latest Global::Point / CandidateString instead of on every cloud refresh
// or layout notification.
inline std::atomic<bool> g_candidate_show_msg_pending{false};
inline std::atomic<bool> g_candidate_move_msg_pending{false};
+3 -5
View File
@@ -21,12 +21,12 @@
#include "engine/contracts/assets/assets.h"
#include "engine/core/runtime_paths.h"
#include "engine/local_modes/radical_query.h"
#include "global/globals.h"
#include "utils/common_utils.h"
#include "utils/hanzi_pinyin.h"
#include "utils/single_instance.h"
#include "session/session_factory.h"
#include "statistics/stats_pipe.h"
#include "webview2/windows_webview2.h"
#include "voice-input/voice_input_service.h"
namespace
@@ -120,7 +120,6 @@ int CALLBACK WinMain(_In_ HINSTANCE hInstance, _In_ HINSTANCE /*hPrevInstance*/,
// Initialize config
InitImeConfig();
FallBackToD2dIfWebView2RuntimeMissing();
VoiceInput::Initialize();
Global::candidate_ui.page_size = GetConfiguredCandidatePageSize();
@@ -170,8 +169,8 @@ int CALLBACK WinMain(_In_ HINSTANCE hInstance, _In_ HINSTANCE /*hPrevInstance*/,
{
// The release runner is a Windows service without an interactive
// window station. The pipe probe still exercises the real Server and
// all IPC worker threads, but must not create candidate/WebView2
// windows or start UI-bound background services.
// all IPC worker threads, but must not create candidate windows or
// start UI-bound background services.
Sleep(INFINITE);
}
@@ -218,7 +217,6 @@ int CALLBACK WinMain(_In_ HINSTANCE hInstance, _In_ HINSTANCE /*hPrevInstance*/,
radical_warmup.join();
FanyNamedPipe::RegisterStatusSnapshotWindow(nullptr);
ShutdownWebviews();
EnglishIme::Stop();
CloudTranslation::Stop();
+1 -1
View File
@@ -486,7 +486,7 @@ bool ImportModel(const std::string &model_id, const std::filesystem::path &sourc
std::filesystem::path PromptForModelFile(HWND owner)
{
// 设置窗口的 UI 线程早已为 WebView2 初始化过 STA,这里的 CoInitializeEx 只是配平引用计数。
// 设置窗口的 UI 线程启动时已初始化过 STA,这里的 CoInitializeEx 只是配平引用计数。
const HRESULT init = CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE);
std::filesystem::path selected;
IFileOpenDialog *dialog = nullptr;
@@ -545,11 +545,8 @@ VisualPtr BuildAppearancePage(PageContext &context)
});
}
// 界面渲染
// 输入法进程与设置窗口
{
Dropdown::Options backend;
backend.minWidth = 120.0f;
backend.matchToggleWidth = true;
Dropdown::Options linger;
linger.minWidth = 150.0f;
linger.matchToggleWidth = true;
@@ -568,13 +565,7 @@ VisualPtr BuildAppearancePage(PageContext &context)
SetTimer(host->Hwnd(), 0x4E60, 3000, &RestoreRestartButton);
});
page.AddSection(MakeRows({
MakeRow(L"界面渲染", L"用于候选窗、悬浮工具栏和托盘菜单。更改后需重启输入法进程才会生效。",
BindDropdown(context, "appearance.ui_backend",
Options({{"d2d", L"Direct2D"}, {"webview2", L"WebView2"}}), backend)),
MakeRow(L"设置窗口渲染", L"Direct2D 为实验性功能。切换后设置窗口会自动重新打开。",
BindDropdown(context, "appearance.settings_ui_backend",
Options({{"webview2", L"WebView2"}, {"d2d", L"Direct2D(实验性)"}}), backend)),
MakeRow(L"重启输入法进程", L"立即重启输入法后台进程,使渲染方式等需要重启的设置生效。", restart),
MakeRow(L"重启输入法进程", L"立即重启输入法后台进程,使需要重启的设置生效。", restart),
MakeRow(L"设置窗口后台驻留",
L"关闭设置窗口后在后台保留一段时间,期间重新打开无需等待加载;驻留越久占用内存越久。",
BindDropdown(context, "appearance.settings_window_linger",
@@ -81,7 +81,7 @@ class DictTabs : public msimeui::Visual
private:
// Chrome lays the 15px system font out in a ~21.3px line, a little taller than
// DirectWrite's natural line (measured against the WebView2 page).
// DirectWrite's natural line (measured against the former web page).
static float TabLineHeight()
{
return std::max(LineHeight(TabStyle()), 21.33f);
@@ -10,7 +10,7 @@
#include <vector>
// Pieces shared by the dictionary page and the quick-phrase card of the tools page, both
// built on the WebView2 page's dict.css: `.dict-card`, `.dict-toolbar`, `.dict-input`,
// ported from the former web page's dict.css: `.dict-card`, `.dict-toolbar`, `.dict-input`,
// `.dict-table`, `.dict-pagination`, `.dict-dialog`, and the dictionaryRequest client.
namespace native_settings
{
@@ -217,7 +217,7 @@ struct VoicePage : std::enable_shared_from_this<VoicePage>
void Write(const std::string &path, Json value)
{
// The page applies host snapshots only, as the WebView2 page does; its own writes
// The page applies host snapshots only; its own writes
// do not loop back through Sync.
++localWrites;
context.model.Update(path, std::move(value));
@@ -3370,7 +3370,7 @@ void BitmapImage::Render(msimeui::DeviceResources &dr)
std::wstring AssetPath(const std::wstring &relativePath)
{
static const std::filesystem::path root =
std::filesystem::path(CommonUtils::get_ime_data_path_w()) / L"html/webview2/settings/ime-settings/dist/assets";
std::filesystem::path(CommonUtils::get_ime_data_path_w()) / L"assets/settings";
return (root / relativePath).wstring();
}
@@ -14,9 +14,9 @@
#include <string>
#include <vector>
// Controls of the Direct2D settings window. Each one reproduces a component of the
// WebView2 settings page; the CSS selector it mirrors is named on the class, and the
// numbers inside come from that rule. Every color is read from CurrentPalette() at
// Controls of the Direct2D settings window. Each one was ported from a component of the
// former web settings page (see native_settings_pages.h); the CSS selector it came from
// is named on the class, and the numbers inside come from that rule. Every color is read from CurrentPalette() at
// render time, so switching the theme only needs a repaint.
//
// Layout units are DIPs, which is what CSS px are at the page's zoom factor 1.0.
@@ -882,7 +882,7 @@ class BitmapImage : public msimeui::Visual
float radius_ = 0.0f;
};
// Reads an SVG asset of the WebView2 page (".../dist/assets/<relative>").
// Reads an SVG asset of the settings window (<data dir>\assets\settings\<relative>).
std::string LoadAssetSvg(const std::wstring &relativePath);
std::wstring AssetPath(const std::wstring &relativePath);
// CSS `filter: invert(1) brightness(b)` applied to every color literal of an SVG,
@@ -9,8 +9,8 @@
#include <string_view>
#include <vector>
// Client side of the settings bridge for the Direct2D window. It speaks exactly the
// protocol the WebView2 page speaks (engine/contracts/webview): the host pushes
// Client side of the settings bridge for the Direct2D window. It speaks the settings
// protocol of engine/contracts/webview: the host pushes
// `configSnapshot` and response messages, the window posts `configUpdate` and request
// messages. settings_app.cpp stays the only place that maps config paths to setters.
namespace native_settings
@@ -10,8 +10,11 @@
#include <string>
#include <vector>
// Page builders of the Direct2D settings window, one per sidebar entry of the WebView2
// page (ui-html/.../partials/<id>.html + modules/<id>.ts). A builder returns the page
// Page builders of the Direct2D settings window, one per sidebar entry. They were ported
// one to one from the former WebView2 settings page, which was removed together with
// WebView2; the "partials/<id>.html + modules/<id>.ts" notes at the top of each
// native_page_*.cpp name its sources, still in git history under
// ui-html/webview2/settings/ime-settings/src/. A builder returns the page
// content once; afterwards the page keeps itself in sync through PageContext::OnConfig
// and the model's message listeners.
namespace native_settings
@@ -3,9 +3,9 @@
#include <d2d1.h>
#include <string>
// Colors and type scale of the Direct2D settings window. Every value mirrors a CSS
// custom property of the WebView2 settings page (ui-html/.../styles/variables.css);
// the property name is noted where it is not obvious, so the two stay comparable.
// Colors and type scale of the Direct2D settings window. Every value was taken from a CSS
// custom property of the former web settings page (styles/variables.css, see
// native_settings_pages.h); the property name is noted where it is not obvious.
namespace native_settings
{
constexpr D2D1_COLOR_F Hex(unsigned rgb, float alpha = 1.0f)
@@ -1075,7 +1075,7 @@ class SettingsWindowImpl : public HostServices
window_->SetQuitOnDestroy(false);
// The frame is extended into the client area for the custom caption; DWM does not
// compose an HWND render target's output there, a composition swap chain it does.
// Opaque, so text can use ClearType like the WebView2 page does.
// Opaque, so text can use ClearType.
window_->SetCompositionRendering(true, true);
window_->SetTextAntialiasMode(D2D1_TEXT_ANTIALIAS_MODE_CLEARTYPE);
SetLayoutScale(static_cast<float>(GetDpiForWindow(hwnd_)) / 96.0f);
@@ -6,10 +6,9 @@
#include <memory>
#include <string>
// The Direct2D settings window (appearance.settings_ui_backend = "d2d"). It adopts the
// HWND that settings_app.cpp creates, so the window class, single-instance activation,
// lingering and the worker that applies config writes stay shared with the WebView2
// frontend; only painting and input are handled here.
// The Direct2D settings window. It adopts the HWND that settings_app.cpp creates; the
// window class, single-instance activation, lingering and the worker that applies config
// writes live there, and only painting and input are handled here.
namespace native_settings
{
class SettingsWindowImpl;
@@ -19,7 +18,7 @@ class SettingsWindow
public:
struct Callbacks
{
// A client message, serialized exactly as the WebView2 page would post it.
// A client message of the webview contract (engine/contracts/webview), serialized.
std::function<void(const std::string &message)> postToHost;
};
Loaded 100 of 342 files, more files were not shown because too many files have changed in this diff. Show more