* 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
270 lines
12 KiB
PowerShell
270 lines
12 KiB
PowerShell
param(
|
|
[int]$SettingsActivations = 100,
|
|
[int]$DelayMs = 600,
|
|
[int]$SampleMs = 350,
|
|
[ValidateSet('Mouse', 'AltTab')]
|
|
[string]$SwitchMode = 'Mouse'
|
|
)
|
|
|
|
$ErrorActionPreference = 'Stop'
|
|
|
|
Add-Type @'
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Runtime.InteropServices;
|
|
using System.Text;
|
|
|
|
public static class SettingsFocusNative {
|
|
public delegate bool EnumWindowsProc(IntPtr hwnd, IntPtr lParam);
|
|
|
|
[StructLayout(LayoutKind.Sequential)]
|
|
public struct RECT { public int Left, Top, Right, Bottom; }
|
|
[StructLayout(LayoutKind.Sequential)]
|
|
public struct POINT { public int X, Y; }
|
|
|
|
[StructLayout(LayoutKind.Sequential)]
|
|
public struct WINDOWPLACEMENT {
|
|
public int length;
|
|
public int flags;
|
|
public int showCmd;
|
|
public POINT ptMinPosition;
|
|
public POINT ptMaxPosition;
|
|
public RECT rcNormalPosition;
|
|
}
|
|
|
|
[StructLayout(LayoutKind.Sequential)]
|
|
public struct GUITHREADINFO {
|
|
public int cbSize;
|
|
public int flags;
|
|
public IntPtr hwndActive;
|
|
public IntPtr hwndFocus;
|
|
public IntPtr hwndCapture;
|
|
public IntPtr hwndMenuOwner;
|
|
public IntPtr hwndMoveSize;
|
|
public IntPtr hwndCaret;
|
|
public RECT rcCaret;
|
|
}
|
|
|
|
[DllImport("user32.dll")]
|
|
public static extern bool EnumWindows(EnumWindowsProc callback, IntPtr lParam);
|
|
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
|
|
public static extern int GetWindowText(IntPtr hwnd, StringBuilder text, int maxCount);
|
|
[DllImport("user32.dll")]
|
|
public static extern bool IsWindowVisible(IntPtr hwnd);
|
|
[DllImport("user32.dll")]
|
|
public static extern uint GetWindowThreadProcessId(IntPtr hwnd, out uint processId);
|
|
[DllImport("user32.dll")]
|
|
public static extern IntPtr GetForegroundWindow();
|
|
[DllImport("user32.dll")]
|
|
public static extern bool GetWindowRect(IntPtr hwnd, out RECT rect);
|
|
[DllImport("user32.dll")]
|
|
public static extern bool GetWindowPlacement(IntPtr hwnd, ref WINDOWPLACEMENT placement);
|
|
[DllImport("user32.dll")]
|
|
public static extern bool SetWindowPlacement(IntPtr hwnd, ref WINDOWPLACEMENT placement);
|
|
[DllImport("user32.dll")]
|
|
public static extern bool MoveWindow(IntPtr hwnd, int x, int y, int width, int height, bool repaint);
|
|
[DllImport("user32.dll")]
|
|
public static extern bool SetWindowPos(IntPtr hwnd, IntPtr insertAfter, int x, int y, int width, int height, uint flags);
|
|
[DllImport("user32.dll")]
|
|
public static extern bool ShowWindow(IntPtr hwnd, int command);
|
|
[DllImport("user32.dll")]
|
|
public static extern bool SetCursorPos(int x, int y);
|
|
[DllImport("user32.dll")]
|
|
public static extern void mouse_event(uint flags, uint dx, uint dy, uint data, UIntPtr extraInfo);
|
|
[DllImport("user32.dll")]
|
|
public static extern void keybd_event(byte virtualKey, byte scanCode, uint flags, UIntPtr extraInfo);
|
|
[DllImport("user32.dll")]
|
|
public static extern bool GetGUIThreadInfo(uint threadId, ref GUITHREADINFO info);
|
|
[DllImport("user32.dll")]
|
|
public static extern int GetSystemMetrics(int index);
|
|
[DllImport("user32.dll")]
|
|
public static extern IntPtr WindowFromPoint(POINT point);
|
|
[DllImport("user32.dll")]
|
|
public static extern IntPtr GetAncestor(IntPtr hwnd, uint flags);
|
|
|
|
public static IntPtr[] TopLevelWindows() {
|
|
var windows = new List<IntPtr>();
|
|
EnumWindows((hwnd, _) => { windows.Add(hwnd); return true; }, IntPtr.Zero);
|
|
return windows.ToArray();
|
|
}
|
|
}
|
|
'@
|
|
|
|
function Get-WindowInfo([IntPtr]$Hwnd) {
|
|
$text = [Text.StringBuilder]::new(512)
|
|
[void][SettingsFocusNative]::GetWindowText($Hwnd, $text, $text.Capacity)
|
|
$pidValue = [uint32]0
|
|
$threadId = [SettingsFocusNative]::GetWindowThreadProcessId($Hwnd, [ref]$pidValue)
|
|
[pscustomobject]@{
|
|
Hwnd = $Hwnd
|
|
Pid = $pidValue
|
|
ThreadId = $threadId
|
|
Title = $text.ToString()
|
|
Visible = [SettingsFocusNative]::IsWindowVisible($Hwnd)
|
|
}
|
|
}
|
|
|
|
function Invoke-RealClick([IntPtr]$Hwnd) {
|
|
$rect = [SettingsFocusNative+RECT]::new()
|
|
if (-not [SettingsFocusNative]::GetWindowRect($Hwnd, [ref]$rect)) {
|
|
throw "GetWindowRect failed for $Hwnd"
|
|
}
|
|
$x = [int](($rect.Left + $rect.Right) / 2)
|
|
# Click below Chromium's tab/title strip and the settings custom title bar.
|
|
$y = [Math]::Min($rect.Bottom - 80, $rect.Top + 180)
|
|
[void][SettingsFocusNative]::SetCursorPos($x, $y)
|
|
Start-Sleep -Milliseconds 80
|
|
$point = [SettingsFocusNative+POINT]::new()
|
|
$point.X = $x
|
|
$point.Y = $y
|
|
$windowAtPoint = [SettingsFocusNative]::WindowFromPoint($point)
|
|
$rootAtPoint = [SettingsFocusNative]::GetAncestor($windowAtPoint, 2)
|
|
[SettingsFocusNative]::mouse_event(0x0002, 0, 0, 0, [UIntPtr]::Zero)
|
|
[SettingsFocusNative]::mouse_event(0x0004, 0, 0, 0, [UIntPtr]::Zero)
|
|
return [pscustomobject]@{
|
|
X = $x
|
|
Y = $y
|
|
Window = $windowAtPoint.ToInt64()
|
|
RootWindow = $rootAtPoint.ToInt64()
|
|
}
|
|
}
|
|
|
|
function Test-Activation([IntPtr]$Target, [IntPtr]$Settings, [uint32]$SettingsThreadId) {
|
|
if ($SwitchMode -eq 'AltTab') {
|
|
[SettingsFocusNative]::keybd_event(0x12, 0, 0, [UIntPtr]::Zero)
|
|
[SettingsFocusNative]::keybd_event(0x09, 0, 0, [UIntPtr]::Zero)
|
|
[SettingsFocusNative]::keybd_event(0x09, 0, 2, [UIntPtr]::Zero)
|
|
[SettingsFocusNative]::keybd_event(0x12, 0, 2, [UIntPtr]::Zero)
|
|
$clickPoint = [pscustomobject]@{ Method = 'AltTab' }
|
|
}
|
|
else {
|
|
$clickPoint = Invoke-RealClick $Target
|
|
}
|
|
|
|
$samples = [Math]::Max(1, [int]($SampleMs / 10))
|
|
$foregrounds = [Collections.Generic.List[IntPtr]]::new()
|
|
$activeWindows = [Collections.Generic.List[IntPtr]]::new()
|
|
$focusWindows = [Collections.Generic.List[IntPtr]]::new()
|
|
for ($sample = 0; $sample -lt $samples; $sample++) {
|
|
$foregrounds.Add([SettingsFocusNative]::GetForegroundWindow())
|
|
$info = [SettingsFocusNative+GUITHREADINFO]::new()
|
|
$info.cbSize = [Runtime.InteropServices.Marshal]::SizeOf($info)
|
|
if ([SettingsFocusNative]::GetGUIThreadInfo($SettingsThreadId, [ref]$info)) {
|
|
$activeWindows.Add($info.hwndActive)
|
|
$focusWindows.Add($info.hwndFocus)
|
|
}
|
|
Start-Sleep -Milliseconds 10
|
|
}
|
|
|
|
$targetValue = $Target.ToInt64()
|
|
$foregroundValues = @($foregrounds | ForEach-Object { $_.ToInt64() })
|
|
$activeValues = @($activeWindows | ForEach-Object { $_.ToInt64() })
|
|
$focusValues = @($focusWindows | ForEach-Object { $_.ToInt64() })
|
|
$targetReached = $foregroundValues -contains $targetValue
|
|
$leftAfterReach = $false
|
|
if ($targetReached) {
|
|
$first = [Array]::IndexOf($foregroundValues, $targetValue)
|
|
for ($index = $first; $index -lt $foregroundValues.Count; $index++) {
|
|
if ($foregroundValues[$index] -ne $targetValue) { $leftAfterReach = $true; break }
|
|
}
|
|
}
|
|
|
|
$finalForeground = $foregroundValues[$foregroundValues.Count - 1]
|
|
$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()
|
|
# 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)
|
|
|
|
[pscustomobject]@{
|
|
Passed = $targetReached -and -not $leftAfterReach -and $finalForeground -eq $targetValue -and $settingsFocusCorrect
|
|
TargetReached = $targetReached
|
|
LeftAfterReach = $leftAfterReach
|
|
Target = $targetValue
|
|
Click = $clickPoint
|
|
FinalForeground = $finalForeground
|
|
FinalActive = $finalActive
|
|
FinalFocus = $finalFocus
|
|
SettingsFocusCorrect = $settingsFocusCorrect
|
|
SettingsActiveChanges = @($activeWindows | Select-Object -Unique).Count
|
|
SettingsFocusChanges = @($focusWindows | Select-Object -Unique).Count
|
|
}
|
|
}
|
|
|
|
$windows = [SettingsFocusNative]::TopLevelWindows() | ForEach-Object { Get-WindowInfo $_ }
|
|
$settingsProcess = Get-Process MetasequoiaImeSettings -ErrorAction Stop | Select-Object -First 1
|
|
$settingsInfo = $windows | Where-Object {
|
|
$_.Pid -eq $settingsProcess.Id -and $_.Title -eq 'Metasequoia IME Settings'
|
|
} | Select-Object -First 1
|
|
if (-not $settingsInfo) { throw 'Could not find the Settings top-level window.' }
|
|
|
|
$foreground = [SettingsFocusNative]::GetForegroundWindow()
|
|
$chromeInfo = $windows | Where-Object {
|
|
$_.Visible -and $_.Title -and (Get-Process -Id $_.Pid -ErrorAction SilentlyContinue).ProcessName -eq 'chrome'
|
|
} | Sort-Object { if ($_.Hwnd -eq $foreground) { 0 } else { 1 } } | Select-Object -First 1
|
|
if (-not $chromeInfo) { throw 'Could not find a visible Chrome top-level window.' }
|
|
|
|
$chromePlacement = [SettingsFocusNative+WINDOWPLACEMENT]::new()
|
|
$chromePlacement.length = [Runtime.InteropServices.Marshal]::SizeOf($chromePlacement)
|
|
[void][SettingsFocusNative]::GetWindowPlacement($chromeInfo.Hwnd, [ref]$chromePlacement)
|
|
$settingsPlacement = [SettingsFocusNative+WINDOWPLACEMENT]::new()
|
|
$settingsPlacement.length = [Runtime.InteropServices.Marshal]::SizeOf($settingsPlacement)
|
|
[void][SettingsFocusNative]::GetWindowPlacement($settingsInfo.Hwnd, [ref]$settingsPlacement)
|
|
$screenWidth = [SettingsFocusNative]::GetSystemMetrics(0)
|
|
$screenHeight = [SettingsFocusNative]::GetSystemMetrics(1)
|
|
$halfWidth = [int]($screenWidth / 2)
|
|
|
|
# Arrange both ordinary windows side by side. TOPMOST is used only for the two
|
|
# setup clicks needed to put them above the IDE, and is removed before testing.
|
|
[void][SettingsFocusNative]::ShowWindow($chromeInfo.Hwnd, 9)
|
|
[void][SettingsFocusNative]::MoveWindow($chromeInfo.Hwnd, 0, 0, $halfWidth, $screenHeight - 80, $true)
|
|
[void][SettingsFocusNative]::ShowWindow($settingsInfo.Hwnd, 4)
|
|
[void][SettingsFocusNative]::MoveWindow($settingsInfo.Hwnd, $halfWidth, 0, $screenWidth - $halfWidth, $screenHeight - 80, $true)
|
|
[void][SettingsFocusNative]::SetWindowPos($settingsInfo.Hwnd, [IntPtr](-1), 0, 0, 0, 0, 0x0013)
|
|
[void](Invoke-RealClick $settingsInfo.Hwnd)
|
|
[void][SettingsFocusNative]::SetWindowPos($chromeInfo.Hwnd, [IntPtr](-1), 0, 0, 0, 0, 0x0013)
|
|
[void](Invoke-RealClick $chromeInfo.Hwnd)
|
|
Start-Sleep -Milliseconds 500
|
|
Write-Output ("chromeHwnd={0} chromeTitle={1} settingsHwnd={2} foreground={3} foregroundTitle={4}" -f `
|
|
$chromeInfo.Hwnd.ToInt64(), $chromeInfo.Title, $settingsInfo.Hwnd.ToInt64(), `
|
|
[SettingsFocusNative]::GetForegroundWindow().ToInt64(), `
|
|
(Get-WindowInfo ([SettingsFocusNative]::GetForegroundWindow())).Title)
|
|
|
|
$failures = [Collections.Generic.List[string]]::new()
|
|
try {
|
|
for ($iteration = 1; $iteration -le $SettingsActivations; $iteration++) {
|
|
if ($SwitchMode -eq 'AltTab') {
|
|
$settingsResult = Test-Activation $settingsInfo.Hwnd $settingsInfo.Hwnd $settingsInfo.ThreadId
|
|
Start-Sleep -Milliseconds $DelayMs
|
|
$chromeResult = Test-Activation $chromeInfo.Hwnd $settingsInfo.Hwnd $settingsInfo.ThreadId
|
|
}
|
|
else {
|
|
$chromeResult = Test-Activation $chromeInfo.Hwnd $settingsInfo.Hwnd $settingsInfo.ThreadId
|
|
Start-Sleep -Milliseconds $DelayMs
|
|
$settingsResult = Test-Activation $settingsInfo.Hwnd $settingsInfo.Hwnd $settingsInfo.ThreadId
|
|
}
|
|
if (-not $chromeResult.Passed) {
|
|
$failures.Add("$iteration Chrome: $($chromeResult | ConvertTo-Json -Compress)")
|
|
}
|
|
if (-not $settingsResult.Passed) {
|
|
$failures.Add("$iteration Settings: $($settingsResult | ConvertTo-Json -Compress)")
|
|
}
|
|
Write-Output ("iteration={0} chrome={1} settings={2} transient={3} activeStates={4}" -f `
|
|
$iteration, $chromeResult.Passed, $settingsResult.Passed, $settingsResult.LeftAfterReach, `
|
|
$settingsResult.SettingsActiveChanges)
|
|
Start-Sleep -Milliseconds $DelayMs
|
|
}
|
|
}
|
|
finally {
|
|
[void][SettingsFocusNative]::SetWindowPos($settingsInfo.Hwnd, [IntPtr](-2), 0, 0, 0, 0, 0x0013)
|
|
[void][SettingsFocusNative]::SetWindowPos($chromeInfo.Hwnd, [IntPtr](-2), 0, 0, 0, 0, 0x0013)
|
|
[void][SettingsFocusNative]::SetWindowPlacement($chromeInfo.Hwnd, [ref]$chromePlacement)
|
|
[void][SettingsFocusNative]::SetWindowPlacement($settingsInfo.Hwnd, [ref]$settingsPlacement)
|
|
}
|
|
|
|
Write-Output "settings_activations=$SettingsActivations failures=$($failures.Count)"
|
|
$failures | ForEach-Object { Write-Output "FAIL $_" }
|
|
if ($failures.Count -gt 0) { exit 1 }
|