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
No files matched your search
@@ -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
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 生成标记。
|
||||
@@ -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 和引擎**,界面只负责展示。
|
||||
|
||||
## 提交前
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -68,7 +68,7 @@ TSF DLL 被加载进宿主程序进程,采用静态 CRT 构建,不依赖运
|
||||
|
||||
### 候选窗始终不出现
|
||||
|
||||
先确认已安装 **Microsoft Edge WebView2 Runtime**(在「设置 → 应用 → 已安装的应用」里搜索 WebView2)。当前版本的候选窗由 WebView2 渲染,运行时缺失会导致窗口无法显示。
|
||||
候选窗由 Server 用 Direct2D 绘制,不依赖 WebView2 Runtime。先按上一节确认 x64 运行库已安装,再在任务管理器里确认 `MetasequoiaImeServer.exe` 正在运行。在独占全屏的游戏里,候选窗会被有意隐藏,以免把游戏切出全屏。
|
||||
|
||||
### 手写识别没有候选结果
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
Before Width: | Height: | Size: 2.3 KiB |
@@ -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>
|
||||
@@ -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)
|
||||
@@ -65,17 +65,7 @@ polish_text = false
|
||||
stt_provider = "cloud_siliconflow"
|
||||
```
|
||||
|
||||
You can also change these config in settings window:
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
## 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
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -8,8 +8,7 @@
|
||||
"description": "Standalone Windows host dependencies",
|
||||
"dependencies": [
|
||||
"fmt",
|
||||
"tomlplusplus",
|
||||
"webview2"
|
||||
"tomlplusplus"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)。
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 = @(
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# 轻量本地测试:只编译 TSF / Server / 设置页,收集这三块产物,打不含词库的安装包。
|
||||
# 轻量本地测试:只编译 TSF / Server,收集它们的产物和设置窗口图片资源,打不含词库的安装包。
|
||||
#
|
||||
# 适用:只改了 TSF、Server 或 HTML,本机已有完整安装(词库已在
|
||||
# 适用:只改了 TSF 或 Server,本机已有完整安装(词库已在
|
||||
# %LOCALAPPDATA%\metasequoiaime)。不要用这个脚本做首次安装。
|
||||
#
|
||||
# 完整流程(含词库)仍用 .\test.ps1;需要包内带 PDB 时用 .\test-symbols.ps1。
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
# 默认走轻量包,和日常改 TSF / Server / HTML 的节奏一致;要连词库一起装就加 -Full。
|
||||
# 默认走轻量包,和日常改 TSF / Server 的节奏一致;要连词库一起装就加 -Full。
|
||||
[switch]$Full,
|
||||
[switch]$Reconfigure
|
||||
)
|
||||
|
||||
@@ -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。
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
@@ -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,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()
|
||||
|
||||
@@ -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 显示器之间移动时
|
||||
是否出现裁切、错位、模糊、点击区域偏移或窗口越出工作区。
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
Before Width: | Height: | Size: 145 KiB After Width: | Height: | Size: 145 KiB |
|
Before Width: | Height: | Size: 813 B After Width: | Height: | Size: 813 B |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 1005 B After Width: | Height: | Size: 1005 B |
|
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 73 KiB After Width: | Height: | Size: 73 KiB |
|
Before Width: | Height: | Size: 200 KiB After Width: | Height: | Size: 200 KiB |
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 335 B After Width: | Height: | Size: 335 B |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 796 B After Width: | Height: | Size: 796 B |
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 930 B After Width: | Height: | Size: 930 B |
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 234 B After Width: | Height: | Size: 234 B |
|
Before Width: | Height: | Size: 1023 B After Width: | Height: | Size: 1023 B |
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 35 KiB |
@@ -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.
|
||||
@@ -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@",
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,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;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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、腾讯云、
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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({});
|
||||
|
||||
@@ -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};
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||