Files
Lu Fan(SonnyCalcr) e701cebbe4 feat(server,ui,installer): 内置皮肤改由皮肤清单定义,新增微信绿(卷轴)内置皮肤 (#1061)
* feat(ui): CandidateList 支持等宽网格列与关闭选中态

- Appearance::gridColumns:横排时按可用宽度均分成等宽列(列数不超过 gridColumns,每列不窄于
  minItemWidth),放不下一列的项占多列。
- Appearance::selectable:为 false 时不画任何选中态,首项也能正常显示悬停与按下效果。

* feat(server,installer): 内置皮肤改由皮肤清单定义,新增微信绿(卷轴)内置皮肤

内置皮肤数据化:
- skins/default/<id>/skin.toml 从只放开关改为完整的出厂清单(配色、圆角、版式、工具栏、名称与说明),
  并新增 canvas 基底的清单。候选窗、悬浮工具栏、光标状态徽标不再在代码里写死任何内置皮肤的参数。
- 出厂清单在构建时编进程序;数据目录里的清单写了的键覆盖出厂值,缺失或写错时退回出厂外观。
  安装包改为每次整份覆盖 skins\default。
- 皮肤清单新增版式键(padding、item_gap、number_inset 等,长度可写 DIP 或 "0.3em"),以及
  [candidate_window.horizontal] / [candidate_window.vertical] 分排列覆盖、page_arrow_divider、
  toolbar.icon_corner_radius_dip。
- test_candidate_skin_tokens 保留改动前的内置分支作参照,逐个皮肤、主题、排列与字号比对清单解析结果。

共用候选窗场景:
- 候选框的场景搭建与套皮肤抽成 CandidateSkinScene,候选窗与设置页的皮肤预览共用;设置页预览改用
  真实的候选窗控件绘制,删除原先移植自网页样式的预览绘制与各皮肤硬编码。

卷轴展开(expand_panel)与微信绿(卷轴):
- 皮肤开关 expand_panel / expand_rows:横排候选行右端的展开按钮,展开后首行与后续候选网格一起滚动,
  右侧细滚动条;展开与收起只认按钮,展开时滚轮上下滚动、不翻页。
- 网格点选按绝对下标上屏并在工作线程核对显示文字;滚到底部时自动再展开一批候选。
- 新增内置皮肤 wechat_scroll「微信绿(卷轴)」。
2026-10-18 20:42:06 +08:00

213 lines
15 KiB
PowerShell
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
$ErrorActionPreference = 'Stop'
Set-StrictMode -Version Latest
$fixture = Join-Path ([IO.Path]::GetTempPath()) ('msime-package-' + [Guid]::NewGuid())
function Write-Fixture([string]$Relative, [string]$Text = 'fixture') {
$path = Join-Path $fixture $Relative
New-Item -ItemType Directory -Force -Path (Split-Path $path) | Out-Null
[IO.File]::WriteAllText($path, $Text)
}
try {
$installer = Join-Path $fixture 'installer'
New-Item -ItemType Directory -Force -Path $installer | Out-Null
Copy-Item (Join-Path $PSScriptRoot '../Prepare-PackageFiles.ps1') $installer
Copy-Item (Join-Path $PSScriptRoot '../msime_setup.iss') $installer
Copy-Item (Join-Path $PSScriptRoot '../default_config') $installer -Recurse
foreach ($file in @(
'server/build-release/bin/Release/MetasequoiaImeServer.exe',
'server/build-release/bin/Release/MetasequoiaImeServer.pdb',
'server/build-release/bin/Release/MetasequoiaImeDictionaryReplay.exe',
'server/build-release/bin/Release/MetasequoiaImeDictionaryReplay.pdb',
'server/build-release/bin/Release/MetasequoiaImeServerTests.exe',
'server/build-release/bin/Release/MetasequoiaImeServerTests.pdb',
'server/build-release/bin/Release/test_webview_contract.exe',
'server/build-release/bin/Release/test_webview_contract.pdb',
'windows/build32-release/Release/MetasequoiaImeTsf.dll',
'windows/build32-release/Release/MetasequoiaImeTsf.pdb',
'windows/build64-release/Release/MetasequoiaImeTsf.dll',
'windows/build64-release/Release/MetasequoiaImeTsf.pdb',
'THIRD_PARTY_NOTICES.txt',
'LICENSE',
'server/assets/config/config.toml',
'server/src/resource/MetasequoiaIME.ico',
'windows/image/msime.ico',
'engine/helpcode/helpcodes/helpcode.txt',
'engine/radical/decomposition.txt',
'engine/googlepinyinime-rev/data/dict_pinyin.dat',
'MetasequoiaImeDict/out/msime.db',
'MetasequoiaImeDict/out/others.db',
'MetasequoiaImeDict/out/dict_japanese.dat',
'MetasequoiaImeDict/source/mozc_dictionary_oss/README.txt',
'language-model/sc.lm',
'language-model/NOTICE.md',
'neural-model/sentence-model-desktop.safetensors',
'neural-model/sentence-model.safetensors',
'neural-model/NOTICE.md',
'server/assets/settings/arrow.svg',
'server/assets/settings/sidebar/about.svg',
'skins/default/fluent/skin.toml',
'skins/default/canvas/skin.toml'
)) { Write-Fixture $file }
Write-Fixture 'skins/demo-skin/skin.toml' "schema_version = 1`nid = `"demo-skin`"`n"
Write-Fixture 'skins/demo-skin/assets/background.png' 'png'
Write-Fixture 'skins/unverified/skin.toml' "schema_version = 1`nid = `"unverified`"`n[license]`nassets = `"UNVERIFIED-DEMO-ONLY`"`n"
Write-Fixture 'server/assets/tables/pinyin.txt' 'xing'
Write-Fixture 'MetasequoiaImeDict/out/dictionary-manifest.json' '{"manifest_version":1}'
$english = Join-Path $fixture 'MetasequoiaImeDict/out/english.db'
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/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',
'tsf_dll/64/MetasequoiaImeTsf.dll', 'tsf_dll/64/MetasequoiaImeTsf.pdb',
'server_exe/MetasequoiaImeServer.pdb',
'server_exe/MetasequoiaImeDictionaryReplay.pdb',
'app_data/helpcodes/helpcode.txt', 'app_data/radical/decomposition.txt',
'default_skins/fluent/skin.toml',
'THIRD_PARTY_NOTICES.txt', 'LICENSE.txt', 'MetasequoiaImeTip.ico')) {
if (-not (Test-Path (Join-Path $installer $file))) { throw "Missing packaged file: $file" }
}
# 语言配置图标必须在 TSF DLL 的 regserver 之前装好:DLL 注册时才看得到它,否则又会把图标
# 注册成随版本目录变化的 DLL 路径。32 位、64 位 DLL 各自找自己的上一级目录,两份都要有。
$issForIcon = [IO.File]::ReadAllText((Join-Path $installer 'msime_setup.iss'))
$dllEntry = $issForIcon.IndexOf('Source: "{#MySourceRoot}\tsf_dll\')
foreach ($root in @('{#MyTsfRoot32}', '{#MyTsfRoot64}')) {
$iconEntry = [regex]::Match($issForIcon,
'Source: "\{#MySourceRoot\}\\MetasequoiaImeTip\.ico";\s*\\\s*DestDir: "' + [regex]::Escape($root) + '";')
if (-not $iconEntry.Success) { throw "MetasequoiaImeTip.ico is not installed to $root" }
if ($dllEntry -lt 0 -or $iconEntry.Index -gt $dllEntry) {
throw "MetasequoiaImeTip.ico for $root must be installed before the TSF DLL entries"
}
}
# CS2 等 Trusted Mode 游戏只放行系统目录下的签名 DLL,TSF DLL 装回 Program Files 就进不了游戏。
foreach ($arch in @('32', '64')) {
if ($issForIcon -notmatch ('Source: "\{#MySourceRoot\}\\tsf_dll\\' + $arch + '\\\*\.dll";\s*\\\s*DestDir: "\{#MyTsfRoot' + $arch + '\}\\')) {
throw "The $arch-bit TSF DLL must be installed under MyTsfRoot$arch"
}
}
if ($issForIcon -notmatch '#define MyTsfRoot64\s+"\{sys\}\\IME\\' -or $issForIcon -notmatch '#define MyTsfRoot32\s+"\{syswow64\}\\IME\\') {
throw 'MyTsfRoot32/MyTsfRoot64 must point into the system directories'
}
# 神经整句效果档模型由设置页按需下载,仓库的 neural-model/ 里有它也不能进安装包。
if (Test-Path (Join-Path $installer 'app_data/sentence-model-desktop.safetensors')) {
throw 'The on-demand desktop neural model was packaged'
}
# 内置皮肤清单落在数据目录的 skins 下,那是用户目录(外部皮肤也在里面),卸载时不能删;
# 它若经 app_data 暂存,就得进 IsShippedAppDataItem 名单,卸载会把整个 skins 带走。
if (Test-Path (Join-Path $installer 'app_data/skins')) { throw 'Default skin settings were staged under app_data' }
$issText = [IO.File]::ReadAllText((Join-Path $installer 'msime_setup.iss'))
# 内置皮肤清单承载出厂外观,每次安装都要整份覆盖,否则老用户永远拿不到外观修正。
if ($issText -notmatch '(?s)Source: "\{#MySourceRoot\}\\default_skins\\\*";[^\r\n]*\\\s*DestDir: "\{code:GetDataDir\}\\skins\\default";[^\r\n]*\\\s*Flags: ignoreversion') {
throw 'Default skin manifests must be installed to skins\default with ignoreversion (overwritten on every install)'
}
if (-not (Test-Path (Join-Path $installer 'default_skins/canvas/skin.toml'))) { throw 'Missing packaged file: default_skins/canvas/skin.toml' }
# 仓库 skins\ 下的外部皮肤随包分发;素材授权未核实的不进包,skins\default 走上面那条。
foreach ($file in @('bundled_skins/demo-skin/skin.toml', 'bundled_skins/demo-skin/assets/background.png')) {
if (-not (Test-Path (Join-Path $installer $file))) { throw "Missing bundled skin file: $file" }
}
foreach ($excluded in @('bundled_skins/unverified', 'bundled_skins/default')) {
if (Test-Path (Join-Path $installer $excluded)) { throw "Bundled a skin that must not ship: $excluded" }
}
$expectedManifest = @(
"demo-skin|assets\background.png|$((Get-FileHash (Join-Path $fixture 'skins/demo-skin/assets/background.png') -Algorithm SHA256).Hash.ToLowerInvariant())",
"demo-skin|skin.toml|$((Get-FileHash (Join-Path $fixture 'skins/demo-skin/skin.toml') -Algorithm SHA256).Hash.ToLowerInvariant())"
)
$actualManifest = @(Get-Content -LiteralPath (Join-Path $installer 'bundled_skins.manifest'))
if (($actualManifest -join "`n") -cne ($expectedManifest -join "`n")) {
throw "Unexpected bundled skin manifest:`n$($actualManifest -join "`n")"
}
if ($issText -notmatch '(?s)Source: "\{#MySourceRoot\}\\bundled_skins\\\*";[^\r\n]*\\\s*DestDir: "\{code:GetDataDir\}\\skins";[^\r\n]*\\\s*Flags: ignoreversion') {
throw 'Bundled skins must overwrite skins\<id> in the data directory'
}
# 数据目录不整个归输入法时(旧版安装器把标记写进了用户原有的文件夹,#537),卸载只按
# IsShippedAppDataItem 等名单删。包里新增了顶层条目而名单没跟上,卸载就会把它漏在用户目录里。
$iss = [IO.File]::ReadAllText((Join-Path $installer 'msime_setup.iss'))
$shippedBody = [regex]::Match($iss, '(?s)function IsShippedAppDataItem\(.*?\r?\nend;').Value
if (-not $shippedBody) { throw 'msime_setup.iss is missing IsShippedAppDataItem' }
$shipped = [Collections.Generic.HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase)
foreach ($literal in [regex]::Matches($shippedBody, "CompareText\(\s*FileName\s*,\s*'([^']+)'")) {
[void]$shipped.Add($literal.Groups[1].Value)
}
foreach ($entry in Get-ChildItem -LiteralPath (Join-Path $installer 'app_data') -Force) {
if (-not $shipped.Contains($entry.Name)) {
throw "app_data\$($entry.Name) 不在 msime_setup.iss 的 IsShippedAppDataItem 名单里"
}
}
# user_dict.dat 是 role=user 的可写文件,清单里 profiles 为空。装进资源目录会把用户
# 自造词顶掉,而且每次升级顶一次,所以它必须由引擎在用户数据目录下自建。
if (Test-Path (Join-Path $installer 'app_data/user_dict.dat')) { throw 'Packaged the writable user dictionary' }
foreach ($testFile in @(
'server_exe/MetasequoiaImeServerTests.exe',
'server_exe/MetasequoiaImeServerTests.pdb',
'server_exe/test_webview_contract.exe',
'server_exe/test_webview_contract.pdb'
)) {
if (Test-Path (Join-Path $installer $testFile)) { throw "Packaged a test file: $testFile" }
}
# 默认不带符号:PDB 是安装包体积和打包耗时的大头,只有显式 -IncludeSymbols 才进包。
& (Join-Path $installer 'Prepare-PackageFiles.ps1') -RepoRoot $fixture -TargetVersion '2026.9.1'
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" }
}
$stagedSymbols = @(
Get-ChildItem -LiteralPath (Join-Path $installer 'server_exe'), (Join-Path $installer 'tsf_dll') `
-Recurse -File -Filter '*.pdb'
)
if ($stagedSymbols.Count -gt 0) {
throw "Packaged symbols without -IncludeSymbols: $($stagedSymbols.Name -join ', ')"
}
$serverPdbFixture = Join-Path $fixture 'server/build-release/bin/Release/MetasequoiaImeServer.pdb'
Remove-Item $serverPdbFixture -Force
$rejected = $false
try { & (Join-Path $installer 'Prepare-PackageFiles.ps1') -RepoRoot $fixture } catch { $rejected = $_.Exception.Message -match 'PDB' }
if (-not $rejected) { throw 'Missing production PDB was accepted' }
[IO.File]::WriteAllText($serverPdbFixture, 'fixture')
$database = Join-Path $installer 'app_data/msime.db'
[IO.File]::WriteAllText($database, 'preserved user data')
# 轻量包不清空 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/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 -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' }
[IO.File]::WriteAllText($languageModelFixture, 'fixture')
# dict_pinyin.dat 同理:缺了它 im_open_decoder 返回 false,Google 那条 Fallback 整句
# 候选悄无声息地消失。它长期没被打包进去,测试却是绿的,因为测试用的是
# server/assets/tables 那份开发副本 —— 所以这条断言要盯的是安装包,不是数据目录。
$pinyinModelFixture = Join-Path $fixture 'engine/googlepinyinime-rev/data/dict_pinyin.dat'
Remove-Item $pinyinModelFixture -Force
$rejected = $false
try { & (Join-Path $installer 'Prepare-PackageFiles.ps1') -RepoRoot $fixture } catch { $rejected = $_.Exception.Message -match 'dict_pinyin\.dat' }
if (-not $rejected) { throw 'Missing pinyin decoder model was accepted' }
[IO.File]::WriteAllText($pinyinModelFixture, 'fixture')
# 随包的神经整句速度档模型同理:缺文件时 shared_sentence_model 返回 nullptr,设置页开关能开但不出候选。
$neuralModelFixture = Join-Path $fixture 'neural-model/sentence-model.safetensors'
Remove-Item $neuralModelFixture -Force
$rejected = $false
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 'server/assets/settings') -Recurse -Force
$rejected = $false
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 {
if (Test-Path $fixture) { Remove-Item $fixture -Recurse -Force }
}