mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-07-04 09:30:04 +02:00
Compare commits
11 Commits
yuleng/ptr
...
copilot/fi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f9679b937d | ||
|
|
36300d3c75 | ||
|
|
1cde68ae04 | ||
|
|
e6d346a59b | ||
|
|
2aece74831 | ||
|
|
7861bc408c | ||
|
|
b835cde4d2 | ||
|
|
f79df0663c | ||
|
|
7211f7ed67 | ||
|
|
215dfaf236 | ||
|
|
f5a294bb66 |
9
.github/actions/spell-check/allow/code.txt
vendored
9
.github/actions/spell-check/allow/code.txt
vendored
@@ -330,7 +330,9 @@ MRUINFO
|
||||
REGSTR
|
||||
|
||||
# Misc Win32 APIs and PInvokes
|
||||
DEFAULTTONEAREST
|
||||
INVOKEIDLIST
|
||||
LCMAP
|
||||
MEMORYSTATUSEX
|
||||
ABE
|
||||
Mdt
|
||||
@@ -394,3 +396,10 @@ Nonpaged
|
||||
|
||||
# XAML
|
||||
Untargeted
|
||||
|
||||
# Program names
|
||||
SEARCHHOST
|
||||
SHELLEXPERIENCEHOST
|
||||
SHELLHOST
|
||||
STARTMENUEXPERIENCEHOST
|
||||
WIDGETBOARD
|
||||
|
||||
1
.github/actions/spell-check/expect.txt
vendored
1
.github/actions/spell-check/expect.txt
vendored
@@ -1368,6 +1368,7 @@ POINTERUPDATE
|
||||
Pokedex
|
||||
Pomodoro
|
||||
Popups
|
||||
popups
|
||||
POPUPWINDOW
|
||||
POSITIONITEM
|
||||
POWERBROADCAST
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,392 +0,0 @@
|
||||
# PowerToys Run 拆分为独立仓库 — 设计文档
|
||||
|
||||
**日期**:2026-04-28
|
||||
**状态**:Draft(pending user review)
|
||||
**作者**:yuleng@microsoft.com
|
||||
**议题**:将 PowerToys Run 模块从 PowerToys 主仓拆分到独立仓库 `microsoft/PowerToysRun`,使其能够独立 build、安装、运行,覆盖现有完整功能
|
||||
|
||||
---
|
||||
|
||||
## 1. 目标与非目标
|
||||
|
||||
### 目标
|
||||
|
||||
- 创建一个全新的独立仓库 `microsoft/PowerToysRun`
|
||||
- 该仓库可以独立 build 出可工作的 MSI 安装包
|
||||
- 用户安装独立 MSI 后能完整使用 PowerToys Run 现有功能(搜索、19 个保留内置插件、热键、主题、本地化、设置)
|
||||
- 现有社区 Run 插件 DLL 可 drop-in 使用,无需修改源码(仅可能需 rebuild 一次)
|
||||
|
||||
### 非目标(本阶段明确不做)
|
||||
|
||||
- ❌ 不修改 PowerToys 主仓代码(不删除 `src/modules/launcher/`、不改 installer、不动 Settings UI、不改 runner)
|
||||
- ❌ 不决定 PowerToys 主仓何时切除 Run(后续阶段处理)
|
||||
- ❌ 不做内置自动更新机制
|
||||
- ❌ 不做 Microsoft Store 提交
|
||||
- ❌ 不做 telemetry pipeline
|
||||
- ❌ 不重新设计插件 API
|
||||
|
||||
---
|
||||
|
||||
## 2. 关键决策摘要
|
||||
|
||||
| # | 决策点 | 选择 | 备注 |
|
||||
|---|---|---|---|
|
||||
| Q1 | 发行模型 | **完全独立**:从 PT 中分离,独立分发 | 不再走 PT installer |
|
||||
| Q2 | 共享依赖处理 | **裁剪 + Vendor**:只 fork 真正必需的子集 | 删 GPO、删 PT-Interop、删 Telemetry |
|
||||
| Q3 | 进程模型 | **真·单进程**:所有功能(后台、热键、托盘、主 UI、Settings UI)在一个 `PowerToys.PowerLauncher.exe` 中 | 关键升级:Settings 不再是单独 exe |
|
||||
| Q4 | 插件兼容性 | **完全二进制兼容** | 现有 Plugin DLL drop-in 可用 |
|
||||
| Q5 | Settings UI 实现 | **WPF 重写 + 集成单进程,极简自定义控件**(用 WPF 内置控件替代 WinUI3 控件) | 视觉接受经典 WPF 风格 |
|
||||
| Q6 | 用户数据迁移 | **新数据目录** `%LOCALAPPDATA%\PowerToys Run\` + Settings UI 内置 "Import from PowerToys" 按钮 | 自动检测、自动显示 InfoBar、不含插件 cache |
|
||||
| Q7 | 分发方式 | **MSI(per-user 默认 + per-machine 双模式)+ winget**;.NET self-contained | 不做 MSIX |
|
||||
| Q8 | 品牌命名 | **保留 "PowerToys Run"**;repo 名 `microsoft/PowerToysRun` | 显示名、命名空间、exe 名都不变 |
|
||||
| Q9 | Telemetry | **完全去掉** | 删 PowerLauncher.Telemetry 全部 |
|
||||
| 路径 | 执行节奏 | **一次性大爆炸**:新 repo 一次性建成 | 与 PT 主仓不耦合时序 |
|
||||
| Git | 历史保留 | **Clean slate(不保留历史)** | 单个 initial commit;丢弃 PT 历史与作者归属 |
|
||||
|
||||
---
|
||||
|
||||
## 3. 设计章节
|
||||
|
||||
### §1 目标 repo 布局与代码复制映射
|
||||
|
||||
新仓 `microsoft/PowerToysRun` 顶层结构:
|
||||
|
||||
```
|
||||
PowerToysRun/
|
||||
├── src/
|
||||
│ ├── PowerLauncher/ ← 复制自 PT/src/modules/launcher/PowerLauncher/
|
||||
│ │ ├── (现有代码)
|
||||
│ │ ├── Views/Settings/ ← 新增:所有 WPF 设置 UI
|
||||
│ │ │ ├── SettingsWindow.xaml(.cs)
|
||||
│ │ │ ├── PowerLauncherSettingsPage.xaml(.cs) ← 800→500 行简化的 WPF 版本
|
||||
│ │ │ └── DataTemplates/PluginOptionTemplates.xaml ← 9 种 plugin AdditionalOption 模板
|
||||
│ │ ├── Controls/ ← 极少量自定义 WPF 控件
|
||||
│ │ │ ├── InfoBar.xaml(.cs) ← 唯一新 UserControl
|
||||
│ │ │ └── NumberBox.cs ← 30-50 行
|
||||
│ │ ├── Helpers/
|
||||
│ │ │ └── NumberValidationRule.cs
|
||||
│ │ └── Services/
|
||||
│ │ └── SettingsImportService.cs ← 从旧 PT 路径导入
|
||||
│ ├── Wox.Plugin/ ← 复制(保留命名空间,关键的 Q4 兼容点)
|
||||
│ ├── Wox.Infrastructure/ ← 复制
|
||||
│ ├── Plugins/ ← 复制(19 个内置插件,删 Microsoft.PowerToys.Run.Plugin.PowerToys)
|
||||
│ ├── Common/ ← 从 PT 主仓 vendor 进来的最小子集
|
||||
│ │ ├── ManagedCommon/ ← 裁剪后的子集(删 RunnerHelper,改 PowerToysPathResolver)
|
||||
│ │ ├── Common.UI/ ← 子集(保留 ThemeManager/ThemeListener/NativeEventWaiter;删 SettingsDeepLink)
|
||||
│ │ └── Settings.Library/ ← 仅 PowerLauncher* 相关类(命名空间保留 Microsoft.PowerToys.Settings.UI.Library)
|
||||
│ └── Tests/
|
||||
│ ├── Wox.Test/ ← 复制
|
||||
│ └── Plugins/*.UnitTests/ ← 复制
|
||||
├── installer/
|
||||
│ └── PowerToysRunSetup/ ← 从 installer/PowerToysSetupVNext/Run.wxs 抽取并独立化
|
||||
├── winget/
|
||||
│ └── manifest/ ← winget-pkgs 提交模板
|
||||
├── doc/ ← 用户文档、插件开发指南、迁移指南
|
||||
├── tools/
|
||||
│ └── extract-localization.ps1 ← 一次性 resw 抽取脚本
|
||||
├── .github/workflows/
|
||||
│ ├── build.yml ← PR CI
|
||||
│ └── release.yml ← tag 触发的 release 流水线
|
||||
├── PowerToysRun.sln ← 新建独立 sln
|
||||
├── Directory.Build.props ← 从 PT 裁剪
|
||||
├── README.md
|
||||
└── LICENSE ← MIT(同 PT)
|
||||
```
|
||||
|
||||
**复制策略**:
|
||||
- **Clean slate**:直接 `cp -r` 把 `src/modules/launcher/{PowerLauncher,Wox.Plugin,Wox.Infrastructure,Plugins,Wox.Test}` 这 5 个子目录拷贝到新 repo,**不保留 PT 历史**
|
||||
- 新 repo 从一个 "Initial commit from PowerToys main repo (snapshot 2026-04-28)" 开始;commit message 注明源 commit SHA 以便追溯
|
||||
- 不保留作者归属(git blame 全部归到这次的初始 commit)
|
||||
- **不复制**:`Microsoft.Launcher/`(C++ 模块 DLL)、`PowerLauncher.Telemetry/`、`Plugins/Microsoft.PowerToys.Run.Plugin.PowerToys/`
|
||||
|
||||
**复制后修改**(仅在新 repo 内):
|
||||
- `PowerLauncher.csproj` 中所有 `..\..\..\common\*` ProjectReference 改为指向新仓的 `src/Common/*`
|
||||
- 移除对 `GPOWrapperProjection`、`PowerToys.Interop`、`PowerLauncher.Telemetry` 的引用
|
||||
- 删除调用 `Telemetry`/`GPO`/runner-IPC 的代码点(编译错误驱动,逐个清理)
|
||||
|
||||
**双跑期间冲突缓解**:用户同时安装 PT (含 Run) + 独立 PowerToys Run 时,两个进程都会注册全局热键 → 后启动者失败。独立 Run 启动时检测 PT 集成版进程(路径区分),冲突时弹提示。
|
||||
|
||||
### §2 依赖审计与 Vendor 策略
|
||||
|
||||
**实际使用情况(依据 grep 结果)**:
|
||||
|
||||
| 共享库 | 处置 | 落地位置 |
|
||||
|---|---|---|
|
||||
| `ManagedCommon` | **裁剪 vendor**(删 `RunnerHelper`,改写 `PowerToysPathResolver` 为新 Run 自己的路径解析) | `src/Common/ManagedCommon/` |
|
||||
| `Common.UI` | **裁剪 vendor**(保留 `NativeEventWaiter`、`ThemeManager`、`ThemeListener`、`CustomLibraryThemeProvider`;**删除 `SettingsDeepLink`**——因 PT 工具插件被删后无消费者) | `src/Common/Common.UI/` |
|
||||
| `Settings.UI.Library` | **小子集 vendor**(仅 `PowerLauncher*` 类 + 它们传递依赖到的 `SettingsUtils`、`HotkeySettings`、`PluginAdditionalOption` 等基础类);命名空间保留 `Microsoft.PowerToys.Settings.UI.Library` | `src/Common/Settings.Library/` |
|
||||
| `GPOWrapperProjection` / `GPOWrapper` | **删除**(启动检查、PluginManager、SettingsReader、PT 工具插件全部去 GPO 化) | — |
|
||||
| `PowerToys.Interop` | **删除**(hotkey 改 Win32 RegisterHotKey;centralized hook 路径删;RunExitEvent 删) | — |
|
||||
| `ManagedTelemetry` / `PowerLauncher.Telemetry` | **删除** | — |
|
||||
| C++ `logger` / `SettingsAPI` | **删除**(仅被已弃用的 `Microsoft.Launcher.dll` 用) | — |
|
||||
| `Microsoft.PowerToys.Run.Plugin.PowerToys` 插件 | **整个删除** | — |
|
||||
|
||||
**关键兼容性约束(确保 Q4)**:
|
||||
- `Microsoft.PowerToys.Settings.UI.Library` 命名空间必须保留(Plugins 各处用了)
|
||||
- `Wox.Plugin` 命名空间必须保留
|
||||
- vendor 来的类放到 `src/Common/` 下时,namespace 不要改
|
||||
|
||||
### §3 进程与激活模型(接管 runner 的职责)
|
||||
|
||||
**真·单进程**:
|
||||
|
||||
```
|
||||
PowerToys.PowerLauncher.exe(唯一 exe)
|
||||
├── TrayIconService ← P/Invoke Shell_NotifyIconW + 隐藏窗口接 WM_TRAYICON
|
||||
├── HotkeyService ← Win32 RegisterHotKey + WM_HOTKEY
|
||||
├── AutostartService ← HKCU\Software\Microsoft\Windows\CurrentVersion\Run
|
||||
├── SingleInstanceGuard ← 命名 Mutex(mutex 名带 "Standalone" 区分 PT 集成版)
|
||||
├── SearchWindow(MainWindow) ← 按 hotkey 弹出/隐藏
|
||||
└── SettingsWindow ← 托盘菜单"Settings"打开(同进程的 WPF Window)
|
||||
```
|
||||
|
||||
**关键删除点**:
|
||||
- `App.xaml.cs::GetPowerToysPId()` 检测父 runner pid → 删
|
||||
- `RunnerHelper.WaitForPowerToysRunner` → 删
|
||||
- `Constants.RunExitEvent()`、`PowerLauncherCentralizedHookSharedEvent` 监听 → 删
|
||||
- 所有 GPO 检查(启动 + per-plugin) → 删
|
||||
- `PowerToys.Interop` 所有调用点 → 删
|
||||
|
||||
**热键冲突兜底**:注册失败时显示 InfoBar/Toast 提示用户在设置中换一个热键(不再有 centralized hook 兜底)。
|
||||
|
||||
**托盘**:P/Invoke `Shell_NotifyIconW`,左键唤起搜索框,右键菜单:Settings / Restart / Exit。
|
||||
|
||||
**单实例**:保留现有 `SingleInstance<App>` 命名 Mutex 机制,mutex 名改为新名(如 `Local\PowerToys-Run-Standalone-XXX`)。
|
||||
|
||||
**自启**:写 `HKCU\...\Run\PowerToysRun`,Settings UI 提供开关(默认开)。
|
||||
|
||||
### §4 设置 UI 重新设计(WPF 极简版)
|
||||
|
||||
**总策略**:用 WPF 内置控件 + 极少量自定义控件替代 WinUI3,**功能一致 + 布局类似**,接受经典 WPF 视觉风格。
|
||||
|
||||
**控件替换映射**:
|
||||
|
||||
| WinUI3 控件 | 用什么替代 |
|
||||
|---|---|
|
||||
| `tkcontrols:SettingsCard` | `Border` + `Grid` inline |
|
||||
| `tkcontrols:SettingsExpander` | WPF 自带 `Expander` |
|
||||
| `controls:SettingsGroup` | WPF 自带 `GroupBox` 或 `StackPanel` + 标题 |
|
||||
| `ToggleSwitch` | WPF 自带 `CheckBox` |
|
||||
| `InfoBar` | 50 行 UserControl(Border + Icon + TextBlock + 可选 Button),4 种 severity |
|
||||
| `NumberBox` | `TextBox` + `NumberValidationRule` |
|
||||
| `AutoSuggestBox` | 普通 `TextBox` + TextChanged 触发 ViewModel 过滤命令 |
|
||||
| `ItemsRepeater` | WPF 自带 `ItemsControl` |
|
||||
| `IsEnabledTextBlock` | `TextBlock` + Trigger 绑 IsEnabled 改 Opacity |
|
||||
| `CheckBoxWithDescriptionControl` | `CheckBox`(Content = StackPanel 含主标题 + 说明) |
|
||||
| `ShortcutControl` | **复用 PowerLauncher 已有的热键 UI** |
|
||||
|
||||
**真正要新写的(仅 3 项)**:
|
||||
- `InfoBar.xaml(.cs)` — 0.5 天
|
||||
- `NumberBox.cs` + `NumberValidationRule` — 0.5 天
|
||||
- `ShortcutControl` 微调 — 0.5 天
|
||||
|
||||
**复用部分(约 90%)**:
|
||||
- `PowerLauncherViewModel.cs` (715 行) — 删除 GPO 引用、删除 IPC 调用 (`ShellPage.SendDefaultIPCMessage`),改为直接调 `SettingsRepository.Save()`
|
||||
- `PowerLauncherPluginViewModel.cs` (221 行) — 同上
|
||||
- 设置数据模型(`PowerLauncherSettings`、`PluginAdditionalOption` 等)— 完全原样 vendor
|
||||
|
||||
**主题**:复用 PowerLauncher 现有的 `ThemeManager`(监听 `WM_SETTINGCHANGE`),WPF `DynamicResource` + 两套 ResourceDictionary(Light.xaml / Dark.xaml)。
|
||||
|
||||
**IPC 简化(关键)**:
|
||||
- 删除整条 Named Pipe IPC 路径(`ShellPage.SendDefaultIPCMessage`)
|
||||
- ViewModel 直接调 `SettingsRepository.Save()` → 写 `%LOCALAPPDATA%\PowerToys Run\settings.json`
|
||||
- 同进程下:直接调 `SettingsReader.Reload()`(即时)
|
||||
- 同进程外(外部修改 settings.json):现有 `FileSystemWatcher` 兜底
|
||||
|
||||
**预估工作量**:10-12 工作日(含调试)。
|
||||
|
||||
### §5 Installer(WiX dual-mode)+ winget
|
||||
|
||||
**工程结构**:
|
||||
```
|
||||
installer/PowerToysRunSetup/
|
||||
├── PowerToysRunSetup.wixproj ← 独立 WiX 4 工程
|
||||
├── Product.wxs ← 顶层产品定义
|
||||
├── Run.wxs ← 从 PT Run.wxs 剥离/裁剪
|
||||
├── Common.wxi ← 从 PT Common.wxi 裁剪
|
||||
├── DualMode.wxi ← per-machine / per-user 切换
|
||||
├── ui/ ← 安装界面资源
|
||||
└── Strings/<lang>/
|
||||
```
|
||||
|
||||
**输出布局(安装目录)**:
|
||||
```
|
||||
<InstallDir>/
|
||||
├── PowerToys.PowerLauncher.exe ← 唯一主进程
|
||||
├── *.dll ← Wox.Plugin、Wox.Infrastructure、ManagedCommon (vendor) 等
|
||||
├── Assets/PowerLauncher/
|
||||
└── RunPlugins/
|
||||
├── Calculator/
|
||||
├── Folder/
|
||||
└── ...(19 个保留插件,已删 PowerToys 工具启动器)
|
||||
```
|
||||
|
||||
**安装路径**:
|
||||
- per-user 默认:`%LOCALAPPDATA%\Programs\PowerToys Run\`
|
||||
- per-machine:`C:\Program Files\PowerToys Run\`(命令行 `msiexec /i ... ALLUSERS=1`)
|
||||
|
||||
**关键决策**:
|
||||
- per-user 为默认(无 UAC 友好)
|
||||
- .NET runtime 走 self-contained(每个 exe 自带,无外部依赖)
|
||||
- 所有插件必装(不引入插件可选安装)
|
||||
- 卸载保留用户数据目录 `%LOCALAPPDATA%\PowerToys Run\`
|
||||
- 升级走 MSI Major Upgrade
|
||||
- ARP 显示名 "PowerToys Run",新 `UpgradeCode` GUID(与 PT 主 MSI 完全隔离)
|
||||
- 不做 Bootstrapper EXE、不做 MSIX、不做 Store 提交
|
||||
|
||||
**winget**:
|
||||
```
|
||||
winget/manifest/
|
||||
├── Microsoft.PowerToysRun.installer.yaml
|
||||
├── Microsoft.PowerToysRun.locale.en-US.yaml
|
||||
└── Microsoft.PowerToysRun.yaml
|
||||
```
|
||||
PR 提交到 `microsoft/winget-pkgs`。`InstallerType: msi` + 两份 Installer 节点(machine / user scope)。
|
||||
|
||||
### §6 从 PowerToys 导入设置(in-Settings 按钮)
|
||||
|
||||
**入口位置**:Settings 窗口顶部 InfoBar,**自动显示**于以下条件满足时:
|
||||
- 旧路径存在:`%LOCALAPPDATA%\Microsoft\PowerToys\PowerToys Run\settings.json`
|
||||
- 新路径为默认值或不存在:`%LOCALAPPDATA%\PowerToys Run\settings.json`
|
||||
|
||||
```
|
||||
┌────────────────────────────────────────────────────────────┐
|
||||
│ ⓘ Existing PowerToys Run settings detected on this PC. │
|
||||
│ [ Import ] [×] │
|
||||
└────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
也可在 About / Advanced 区块手动找到同一按钮。Dismissed 后写入 user-pref `ImportInfoBarDismissed=true`,下次不再显示。
|
||||
|
||||
**`SettingsImportService.cs`**(在 `src/PowerLauncher/Services/`):
|
||||
- `IsLegacyDataAvailable()`、`IsCurrentDataEmpty()`
|
||||
- `Import(IProgress<string> progress = null)` → `ImportResult`
|
||||
- 内部:`BackupCurrent` / `CopyDirectory` / `Rollback` / `NotifyReload`
|
||||
|
||||
**导入范围**:
|
||||
- ✅ `settings.json`(核心配置)
|
||||
- ✅ `Plugins/<Name>/settings.json`(每个插件配置)
|
||||
- ❌ `Plugins/<Name>/cache/*` 缓存(不复制;用户首次启动重新生成)
|
||||
- ❌ `Microsoft.PowerToys.Run.Plugin.PowerToys/` 配置(该插件已删)
|
||||
|
||||
**流程**:
|
||||
1. 确认 Dialog:"This will overwrite your current PowerToys Run settings. A backup will be saved. Continue?"
|
||||
2. 备份当前设置到 `%LOCALAPPDATA%\PowerToys Run\backups\settings-<timestamp>.json` + `Plugins.zip`
|
||||
3. 复制(不是移动)旧路径 → 新路径
|
||||
4. 失败时回滚(恢复 backup)
|
||||
5. 同进程下直接调 `SettingsReader.Reload()` 立即生效
|
||||
6. 显示成功 Toast "Settings imported. Restart may be needed for some plugins."(提供 "Restart now" 按钮)
|
||||
7. **不删除**旧 PT 数据
|
||||
|
||||
**失败模式**:
|
||||
- 旧 settings.json 损坏 → InfoBar 错误,引导用户在 PT 中重置
|
||||
- 部分插件 schema 漂移 → 跳过 + dialog 列出跳过项
|
||||
- 复制中断 → backup + atomic rename 写入策略保证不破坏目标
|
||||
|
||||
### §7 Build / CI、本地化、更新机制
|
||||
|
||||
**Solution & Build**:
|
||||
- 单一 `PowerToysRun.sln`,包含所有 csproj + wixproj
|
||||
- `Directory.Build.props` 沿用 PT 的 `Common.Dotnet.CsWinRT.props` 中相关项,删 PT-specific RepoRoot 引用
|
||||
- `TargetFramework=net8.0-windows10.0.20348.0`(与 PT 现状一致)
|
||||
- 输出路径:`$(SolutionDir)bin\$(Platform)\$(Configuration)\`
|
||||
- 目标平台:x64 + arm64
|
||||
|
||||
**GitHub Actions CI**:
|
||||
|
||||
`.github/workflows/build.yml`(PR 触发):
|
||||
- runs-on: windows-2022
|
||||
- matrix: [x64, arm64]
|
||||
- steps: checkout → setup-dotnet 8.x → nuget restore → msbuild sln → dotnet test → msbuild wixproj → upload-artifact MSI
|
||||
|
||||
`.github/workflows/release.yml`(tag `v*` 触发):
|
||||
- 同 build
|
||||
- 加签名步骤(ESRP/SignTool;详见 §8 R2)
|
||||
- 创建 GitHub Release,上传 x64/arm64 两份 MSI
|
||||
|
||||
**本地化**:
|
||||
- PowerLauncher / Plugins 的 `*.resx` 整体复制原样(已经独立于 PT)
|
||||
- 设置 UI 字符串:写 `tools/extract-localization.ps1` 从 PT `Settings.UI/Strings/<lang>/Resources.resw` 抽取以 `PowerLauncher_*` / `Run_*` / `Activation_*` / `Shortcut*` / `Radio_Theme_*` / `ColorModeHeader` / `ShowPluginsOverview_*` 等开头的 keys
|
||||
- 抽取后的 resw 转 `Resources.<lang>.resx`(schema 几乎一致)
|
||||
- 18 种语言全部抽取,人工 spot-check 三种(en/zh/de)
|
||||
- WPF 通过 `ResourceManager` + satellite assembly 加载;XAML 中用 `{x:Static prop:Resources.X}`
|
||||
- 启动时 `LanguageHelper.LoadLanguage()` 设 `CurrentUICulture`,运行中不切(保留现有行为)
|
||||
|
||||
**更新机制**:
|
||||
- 完全推迟。**不做应用内"检查更新"**、**不做启动检查**、**不做新版本提示 InfoBar**
|
||||
- 用户手动从 GitHub Releases 下载新 MSI 重装;或 `winget upgrade Microsoft.PowerToysRun`
|
||||
- WiX `MajorUpgrade` 自动卸旧装新
|
||||
|
||||
### §8 风险、开放项、推迟工作
|
||||
|
||||
**已识别的风险**:
|
||||
|
||||
- **R1:Q4 "完全二进制兼容" 实际可达性** — 插件依赖的程序集版本/strong name 变化可能破坏加载。缓解:vendor 出来的程序集保持原 namespace + 原 assembly name;发布前用 GitHub top 10 社区插件做兼容性测试。
|
||||
- **R2:签名(Code Signing)** — 独立 repo 接入 Microsoft ESRP 签名 pipeline 需要内部审批。本设计假设可拿到签名通道,但实操是 ops 工作。
|
||||
- **R3:本地化抽取的完整性** — 18 语言 resw 抽取漏抽 = label 显示空。缓解:脚本抽取 + 人工 spot-check。
|
||||
- **R4:双跑期间的进程冲突** — 用户同时装 PT (含 Run) + 独立 Run 时全局热键互踩。缓解:启动检测 + 提示。下一阶段 PT 切除 Run 后自动消失。
|
||||
- **R5:Microsoft.Plugin.Indexer 插件的 COM 依赖** — 用 Windows Search COM Interop。缓解:独立 build 后端到端验证。
|
||||
- **R6:测试覆盖率** — PT 主仓的集成测试可能依赖 runner.exe;本设计仅承诺单元测试全部通过,UI 自动化测试可能短期不可用。
|
||||
- **R7:CI 签名密钥** — GitHub Actions runner 接入签名证书的安全方式未定。
|
||||
|
||||
**开放项**:
|
||||
|
||||
| # | 项 | 解决责任 |
|
||||
|---|---|---|
|
||||
| O1 | ESRP 签名 pipeline 接入 | ops |
|
||||
| O2 | `git filter-repo` 历史保留细节(issue/PR 不可迁) | repo migration ops |
|
||||
| O3 | 独立 repo 名最终敲定(`PowerToysRun` vs `PowerToys-Run` vs `powertoys-run`) | 用户决策 |
|
||||
| O4 | OneNote 插件的 `Microsoft.Office.Interop.OneNote` 在 self-contained build 下打包 | 实现期验证 |
|
||||
| O5 | x64 vs arm64 在 winget 同一 manifest 内 `Architecture` 节点表达 | 实现期验证 |
|
||||
| O6 | 仓库治理基础设施(issue templates、CONTRIBUTING、CODE_OF_CONDUCT) | 设置阶段补 |
|
||||
|
||||
**推迟到下一阶段(本次明确不做)**:
|
||||
|
||||
| # | 内容 |
|
||||
|---|---|
|
||||
| D1 | 删除 PT 主仓中的 `src/modules/launcher/`、`Run.wxs`、`PowerLauncherPage.xaml`、`Settings.UI.Library/PowerLauncher*`、`runner` 中 `Microsoft.Launcher.dll` 加载逻辑 |
|
||||
| D2 | PT 中加 deprecation 提示引导用户去新 repo |
|
||||
| D3 | 内置自动更新 / 更新通知 |
|
||||
| D4 | Microsoft Store 上架 |
|
||||
| D5 | 集中键盘钩子(Centralized Keyboard Hook)兜底 |
|
||||
| D6 | telemetry(如未来想做,单独设计 opt-in pipeline) |
|
||||
| D7 | 独立 plugin marketplace |
|
||||
|
||||
---
|
||||
|
||||
## 4. 验收标准
|
||||
|
||||
新 repo "完整运行" 的判定:
|
||||
|
||||
1. ✅ `PowerToysRun.sln` 在 Windows + Visual Studio 可打开、x64 全 build green
|
||||
2. ✅ Wox.Test + 各 Plugins.UnitTests 全部 pass
|
||||
3. ✅ 安装 MSI 后能从 winget/installer 启动 `PowerToys.PowerLauncher.exe`
|
||||
4. ✅ 全局热键 `Alt+Space` 工作;搜索框正常弹出
|
||||
5. ✅ 至少 5 个内置插件功能正常(Calculator / Program / Folder / Shell / WindowWalker)
|
||||
6. ✅ Settings 窗口能从托盘打开;改 hotkey、改主题、enable/disable 插件能立即生效
|
||||
7. ✅ Import from PowerToys 按钮检测旧数据并成功导入
|
||||
8. ✅ 18 语言下 Settings UI 无空字符串
|
||||
9. ✅ 至少 3 个 top 社区插件 DLL drop-in 可用(验证 Q4 兼容性)
|
||||
10. ✅ MSI 可被卸载,卸载后保留用户数据目录
|
||||
|
||||
---
|
||||
|
||||
## 5. 工作量与时间估算
|
||||
|
||||
| 任务块 | 估时(工作日) |
|
||||
|---|---|
|
||||
| §1 repo 复制 + 项目引用修复 + 编译 green | 3-4 |
|
||||
| §2 共享依赖 vendor + 裁剪 | 2-3 |
|
||||
| §3 进程接管(hotkey、tray、autostart、单实例) | 3-4 |
|
||||
| §4 Settings UI(自定义控件 1.5 天 + Page 重写 3-4 天 + ViewModel 适配 1 天 + 主题 0.5 天 + 模板 2 天 + 联调 1.5-2 天) | 10-12 |
|
||||
| §5 Installer + winget manifest | 3-4 |
|
||||
| §6 Settings Import 服务 | 1-2 |
|
||||
| §7 CI + 本地化抽取脚本 | 2-3 |
|
||||
| §8 兼容性测试 + 端到端验证 | 3-4 |
|
||||
| **合计** | **27-36 个工作日(约 1.5-2 个开发月)** |
|
||||
|
||||
---
|
||||
|
||||
## 6. 后续步骤
|
||||
|
||||
设计文档 review 通过后,进入 `superpowers:writing-plans` skill 产出可执行的 step-by-step 实施计划。
|
||||
@@ -579,14 +579,16 @@ static void StopResizing()
|
||||
HideOverlay();
|
||||
}
|
||||
|
||||
static void ReplayAbsorbedAlt()
|
||||
static void ReplayAbsorbedModifier(bool alsoKeyUp)
|
||||
{
|
||||
INPUT keyboardInput = {};
|
||||
keyboardInput.type = INPUT_KEYBOARD;
|
||||
keyboardInput.ki.wVk = static_cast<WORD>(g_absorbedVk);
|
||||
keyboardInput.ki.wScan = static_cast<WORD>(g_absorbedScanCode);
|
||||
keyboardInput.ki.dwFlags = (g_absorbedFlags & LLKHF_EXTENDED) ? KEYEVENTF_EXTENDEDKEY : 0;
|
||||
SendInput(1, &keyboardInput, sizeof(INPUT));
|
||||
INPUT inputs[2] = {};
|
||||
inputs[0].type = INPUT_KEYBOARD;
|
||||
inputs[0].ki.wVk = static_cast<WORD>(g_absorbedVk);
|
||||
inputs[0].ki.wScan = static_cast<WORD>(g_absorbedScanCode);
|
||||
inputs[0].ki.dwFlags = (g_absorbedFlags & LLKHF_EXTENDED) ? KEYEVENTF_EXTENDEDKEY : 0;
|
||||
inputs[1] = inputs[0];
|
||||
inputs[1].ki.dwFlags |= KEYEVENTF_KEYUP;
|
||||
SendInput(alsoKeyUp ? 2 : 1, inputs, sizeof(INPUT));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -632,9 +634,65 @@ static void ShowTrayMenu(HWND hwnd)
|
||||
|
||||
static bool IsSystemClass(HWND hwnd)
|
||||
{
|
||||
wchar_t cls[64] = {};
|
||||
GetClassNameW(hwnd, cls, 64);
|
||||
return (wcscmp(cls, L"Progman") == 0 || wcscmp(cls, L"Shell_TrayWnd") == 0);
|
||||
wchar_t cls[256] = {};
|
||||
GetClassNameW(hwnd, cls, ARRAYSIZE(cls));
|
||||
|
||||
// Desktop and primary/secondary taskbars
|
||||
if (wcscmp(cls, L"Progman") == 0 ||
|
||||
wcscmp(cls, L"Shell_TrayWnd") == 0 ||
|
||||
wcscmp(cls, L"Shell_SecondaryTrayWnd") == 0)
|
||||
return true;
|
||||
|
||||
// System tray / notification area popups and overflow
|
||||
if (wcscmp(cls, L"NotifyIconOverflowWindow") == 0 ||
|
||||
wcscmp(cls, L"TopLevelWindowForOverflowXamlIsland") == 0)
|
||||
return true;
|
||||
|
||||
// Tooltips (e.g. "Show hidden icons" tooltip)
|
||||
if (wcscmp(cls, L"tooltips_class32") == 0)
|
||||
return true;
|
||||
|
||||
// Task View (Win+Tab)
|
||||
if (wcscmp(cls, L"MultitaskingViewFrame") == 0 ||
|
||||
wcscmp(cls, L"XamlExplorerHostIslandWindow") == 0)
|
||||
return true;
|
||||
|
||||
// System tray flyouts (Quick Settings, calendar, input switcher)
|
||||
if (wcscmp(cls, L"Windows.UI.Composition.DesktopWindowContentBridge") == 0 ||
|
||||
wcscmp(cls, L"Shell_InputSwitchTopLevelWindow") == 0)
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
std::wstring ToUpperInvariant(std::wstring_view input)
|
||||
{
|
||||
int required = LCMapStringEx(
|
||||
LOCALE_NAME_INVARIANT,
|
||||
LCMAP_UPPERCASE,
|
||||
input.data(),
|
||||
static_cast<int>(input.size()),
|
||||
nullptr,
|
||||
0,
|
||||
nullptr,
|
||||
nullptr,
|
||||
0);
|
||||
|
||||
std::wstring result(required, L'\0');
|
||||
|
||||
LCMapStringEx(
|
||||
LOCALE_NAME_INVARIANT,
|
||||
LCMAP_UPPERCASE,
|
||||
input.data(),
|
||||
static_cast<int>(input.size()),
|
||||
result.data(),
|
||||
required,
|
||||
nullptr,
|
||||
nullptr,
|
||||
0);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
static bool IsExcluded(HWND hwnd)
|
||||
@@ -642,6 +700,45 @@ static bool IsExcluded(HWND hwnd)
|
||||
if (IsSystemClass(hwnd))
|
||||
return true;
|
||||
|
||||
// To identify these for adding a new exception:
|
||||
// 1. Resolve the hwnd class name.
|
||||
// 2. Resolve the process path.
|
||||
// 3. Add OutputDebugStringW() for the class name and process path.
|
||||
// 4. Build the executable.
|
||||
// 5. Check with the debugger (or with Sysinternals DebugView) the outputs.
|
||||
// 6. Delete the added code.
|
||||
// 7. Add the exception below, according to the pattern there.
|
||||
//
|
||||
// Shell experience windows: Start menu, Notifications (Win+N), Search,
|
||||
// Quick Settings (volume / network / battery).
|
||||
// These use the generic Windows.UI.Core.CoreWindow class, so filter by process.
|
||||
{
|
||||
wchar_t cls[256] = {};
|
||||
GetClassNameW(hwnd, cls, ARRAYSIZE(cls));
|
||||
if (wcscmp(cls, L"Windows.UI.Core.CoreWindow") == 0)
|
||||
{
|
||||
std::wstring processPath = ToUpperInvariant(get_process_path(hwnd));
|
||||
if (processPath.find(L"STARTMENUEXPERIENCEHOST.EXE") != std::wstring::npos ||
|
||||
processPath.find(L"SHELLEXPERIENCEHOST.EXE") != std::wstring::npos ||
|
||||
processPath.find(L"SEARCHHOST.EXE") != std::wstring::npos)
|
||||
return true;
|
||||
}
|
||||
else if (wcscmp(cls, L"ControlCenterWindow") == 0)
|
||||
{
|
||||
// The Quick Settings flyout.
|
||||
std::wstring processPath = ToUpperInvariant(get_process_path(hwnd));
|
||||
if (processPath.find(L"SHELLHOST.EXE") != std::wstring::npos)
|
||||
return true;
|
||||
}
|
||||
else if (wcscmp(cls, L"WindowsDashboard") == 0)
|
||||
{
|
||||
// The Windows 11 Widgets flyout.
|
||||
std::wstring processPath = ToUpperInvariant(get_process_path(hwnd));
|
||||
if (processPath.find(L"WIDGETBOARD.EXE") != std::wstring::npos)
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
auto apps = g_excludedApps.load();
|
||||
if (!apps || apps->empty())
|
||||
return false;
|
||||
@@ -735,8 +832,9 @@ static LRESULT CALLBACK KeyboardProc(int nCode, WPARAM wParam, LPARAM lParam)
|
||||
g_dragConsumedAlt = false;
|
||||
return 1;
|
||||
}
|
||||
// No drag happened; replay the keydown, then let keyup through
|
||||
ReplayAbsorbedAlt();
|
||||
// No drag happened; replay the keydown, THEN the keyup
|
||||
ReplayAbsorbedModifier(true);
|
||||
return 1; // swallow this keyup since the replay already sent one
|
||||
}
|
||||
}
|
||||
goto forward; // let Win keyup pass through
|
||||
@@ -793,7 +891,7 @@ static LRESULT CALLBACK KeyboardProc(int nCode, WPARAM wParam, LPARAM lParam)
|
||||
return 1;
|
||||
}
|
||||
// No drag happened; replay the keydown, then let keyup through
|
||||
ReplayAbsorbedAlt();
|
||||
ReplayAbsorbedModifier(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -803,7 +901,8 @@ static LRESULT CALLBACK KeyboardProc(int nCode, WPARAM wParam, LPARAM lParam)
|
||||
if (g_altAbsorbed && !g_dragConsumedAlt)
|
||||
{
|
||||
g_altAbsorbed = false;
|
||||
ReplayAbsorbedAlt();
|
||||
g_altPressed = false;
|
||||
ReplayAbsorbedModifier(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -813,7 +912,7 @@ static LRESULT CALLBACK KeyboardProc(int nCode, WPARAM wParam, LPARAM lParam)
|
||||
{
|
||||
g_winAbsorbed = false;
|
||||
g_winPressed = false;
|
||||
ReplayAbsorbedAlt();
|
||||
ReplayAbsorbedModifier(false);
|
||||
}
|
||||
|
||||
// Track held non-modifier keys (used to suppress GrabAndMove when the modifier
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
|
||||
@@ -11,9 +12,21 @@ public record AppStateModel
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// STATE HERE
|
||||
// Make sure that any new types you add are added to JsonSerializationContext!
|
||||
public RecentCommandsManager RecentCommands { get; init; } = new();
|
||||
private RecentCommandsManager? _recentCommands = new();
|
||||
|
||||
public ImmutableList<string> RunHistory { get; init; } = ImmutableList<string>.Empty;
|
||||
public RecentCommandsManager RecentCommands
|
||||
{
|
||||
get => _recentCommands ?? new();
|
||||
init => _recentCommands = value;
|
||||
}
|
||||
|
||||
private ImmutableList<string>? _runHistory = ImmutableList<string>.Empty;
|
||||
|
||||
public ImmutableList<string> RunHistory
|
||||
{
|
||||
get => _runHistory ?? ImmutableList<string>.Empty;
|
||||
init => _runHistory = value;
|
||||
}
|
||||
|
||||
// END SETTINGS
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@@ -46,45 +46,6 @@ public partial class DockBandSettingsViewModel : ObservableObject
|
||||
|
||||
public IconInfoViewModel Icon => _adapter.IconViewModel;
|
||||
|
||||
private ShowLabelsOption _showLabels;
|
||||
|
||||
public ShowLabelsOption ShowLabels
|
||||
{
|
||||
get => _showLabels;
|
||||
set
|
||||
{
|
||||
if (value != _showLabels)
|
||||
{
|
||||
_showLabels = value;
|
||||
var newShowTitles = value switch
|
||||
{
|
||||
ShowLabelsOption.Default => (bool?)null,
|
||||
ShowLabelsOption.ShowLabels => true,
|
||||
ShowLabelsOption.HideLabels => false,
|
||||
_ => null,
|
||||
};
|
||||
UpdateModel(_dockSettingsModel with { ShowTitles = newShowTitles });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private ShowLabelsOption FetchShowLabels()
|
||||
{
|
||||
if (_dockSettingsModel.ShowLabels == null)
|
||||
{
|
||||
return ShowLabelsOption.Default;
|
||||
}
|
||||
|
||||
return _dockSettingsModel.ShowLabels.Value ? ShowLabelsOption.ShowLabels : ShowLabelsOption.HideLabels;
|
||||
}
|
||||
|
||||
// used to map to ComboBox selection
|
||||
public int ShowLabelsIndex
|
||||
{
|
||||
get => (int)ShowLabels;
|
||||
set => ShowLabels = (ShowLabelsOption)value;
|
||||
}
|
||||
|
||||
private DockPinSide PinSide
|
||||
{
|
||||
get => _pinSide;
|
||||
@@ -138,7 +99,6 @@ public partial class DockBandSettingsViewModel : ObservableObject
|
||||
_bandViewModel = bandViewModel;
|
||||
_settingsService = settingsService;
|
||||
_pinSide = FetchPinSide();
|
||||
_showLabels = FetchShowLabels();
|
||||
}
|
||||
|
||||
private DockPinSide FetchPinSide()
|
||||
|
||||
@@ -559,7 +559,7 @@ public sealed partial class DockViewModel
|
||||
}
|
||||
|
||||
// Create settings for the new band
|
||||
var bandSettings = new DockBandSettings { ProviderId = topLevel.CommandProviderId, CommandId = bandId, ShowLabels = null };
|
||||
var bandSettings = new DockBandSettings { ProviderId = topLevel.CommandProviderId, CommandId = bandId };
|
||||
var dockSettings = _settings;
|
||||
|
||||
// Create the band view model
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
@@ -18,12 +18,24 @@ public record ProviderSettings
|
||||
|
||||
public bool IsEnabled { get; init; } = true;
|
||||
|
||||
public ImmutableDictionary<string, FallbackSettings> FallbackCommands { get; init; }
|
||||
private ImmutableDictionary<string, FallbackSettings>? _fallbackCommands
|
||||
= ImmutableDictionary<string, FallbackSettings>.Empty;
|
||||
|
||||
public ImmutableList<string> PinnedCommandIds { get; init; }
|
||||
public ImmutableDictionary<string, FallbackSettings> FallbackCommands
|
||||
{
|
||||
get => _fallbackCommands ?? ImmutableDictionary<string, FallbackSettings>.Empty;
|
||||
init => _fallbackCommands = value;
|
||||
}
|
||||
|
||||
private ImmutableList<string>? _pinnedCommandIds
|
||||
= ImmutableList<string>.Empty;
|
||||
|
||||
public ImmutableList<string> PinnedCommandIds
|
||||
{
|
||||
get => _pinnedCommandIds ?? ImmutableList<string>.Empty;
|
||||
init => _pinnedCommandIds = value;
|
||||
}
|
||||
|
||||
[JsonIgnore]
|
||||
public string ProviderId { get; init; } = string.Empty;
|
||||
|
||||
@@ -37,7 +49,6 @@ public record ProviderSettings
|
||||
{
|
||||
}
|
||||
|
||||
[JsonConstructor]
|
||||
public ProviderSettings(bool isEnabled)
|
||||
{
|
||||
IsEnabled = isEnabled;
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
|
||||
public record RecentCommandsManager : IRecentCommandsManager
|
||||
{
|
||||
[JsonInclude]
|
||||
internal ImmutableList<HistoryItem> History { get; init; } = ImmutableList<HistoryItem>.Empty;
|
||||
private ImmutableList<HistoryItem>? _history = ImmutableList<HistoryItem>.Empty;
|
||||
|
||||
internal ImmutableList<HistoryItem> History
|
||||
{
|
||||
get => _history ?? ImmutableList<HistoryItem>.Empty;
|
||||
init => _history = value;
|
||||
}
|
||||
|
||||
public RecentCommandsManager()
|
||||
{
|
||||
|
||||
@@ -45,7 +45,7 @@ public record DockSettings
|
||||
public string? BackgroundImagePath { get; init; }
|
||||
|
||||
// </Theme settings>
|
||||
public ImmutableList<DockBandSettings> StartBands { get; init; } = ImmutableList.Create(
|
||||
private ImmutableList<DockBandSettings>? _startBands = ImmutableList.Create(
|
||||
new DockBandSettings
|
||||
{
|
||||
ProviderId = "com.microsoft.cmdpal.builtin.core",
|
||||
@@ -55,12 +55,24 @@ public record DockSettings
|
||||
{
|
||||
ProviderId = "WinGet",
|
||||
CommandId = "com.microsoft.cmdpal.winget",
|
||||
ShowLabels = false,
|
||||
ShowTitles = false,
|
||||
});
|
||||
|
||||
public ImmutableList<DockBandSettings> CenterBands { get; init; } = ImmutableList<DockBandSettings>.Empty;
|
||||
public ImmutableList<DockBandSettings> StartBands
|
||||
{
|
||||
get => _startBands ?? ImmutableList<DockBandSettings>.Empty;
|
||||
init => _startBands = value;
|
||||
}
|
||||
|
||||
public ImmutableList<DockBandSettings> EndBands { get; init; } = ImmutableList.Create(
|
||||
private ImmutableList<DockBandSettings>? _centerBands = ImmutableList<DockBandSettings>.Empty;
|
||||
|
||||
public ImmutableList<DockBandSettings> CenterBands
|
||||
{
|
||||
get => _centerBands ?? ImmutableList<DockBandSettings>.Empty;
|
||||
init => _centerBands = value;
|
||||
}
|
||||
|
||||
private ImmutableList<DockBandSettings>? _endBands = ImmutableList.Create(
|
||||
new DockBandSettings
|
||||
{
|
||||
ProviderId = "PerformanceMonitor",
|
||||
@@ -72,6 +84,12 @@ public record DockSettings
|
||||
CommandId = "com.microsoft.cmdpal.timedate.dockBand",
|
||||
});
|
||||
|
||||
public ImmutableList<DockBandSettings> EndBands
|
||||
{
|
||||
get => _endBands ?? ImmutableList<DockBandSettings>.Empty;
|
||||
init => _endBands = value;
|
||||
}
|
||||
|
||||
public bool ShowLabels { get; init; } = true;
|
||||
|
||||
[JsonIgnore]
|
||||
@@ -103,16 +121,6 @@ public record DockBandSettings
|
||||
/// </summary>
|
||||
public bool? ShowSubtitles { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value for backward compatibility. Maps to ShowTitles.
|
||||
/// </summary>
|
||||
[System.Text.Json.Serialization.JsonIgnore]
|
||||
public bool? ShowLabels
|
||||
{
|
||||
get => ShowTitles;
|
||||
init => ShowTitles = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolves the effective value of <see cref="ShowTitles"/> for this band.
|
||||
/// If this band doesn't have a specific value set, we'll fall back to the
|
||||
|
||||
@@ -55,8 +55,14 @@ public record HotkeySettings// : ICmdLineRepresentable
|
||||
|
||||
// This is currently needed for FancyZones, we need to unify these two objects
|
||||
// see src\common\settings_objects.h
|
||||
private string? _key = string.Empty;
|
||||
|
||||
[JsonPropertyName("key")]
|
||||
public string Key { get; init; } = string.Empty;
|
||||
public string Key
|
||||
{
|
||||
get => _key ?? string.Empty;
|
||||
init => _key = value;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
|
||||
@@ -39,17 +39,41 @@ public record SettingsModel
|
||||
|
||||
public bool AllowExternalReload { get; init; }
|
||||
|
||||
public ImmutableDictionary<string, ProviderSettings> ProviderSettings { get; init; }
|
||||
private ImmutableDictionary<string, ProviderSettings>? _providerSettings
|
||||
= ImmutableDictionary<string, ProviderSettings>.Empty;
|
||||
|
||||
public string[] FallbackRanks { get; init; } = [];
|
||||
public ImmutableDictionary<string, ProviderSettings> ProviderSettings
|
||||
{
|
||||
get => _providerSettings ?? ImmutableDictionary<string, ProviderSettings>.Empty;
|
||||
init => _providerSettings = value;
|
||||
}
|
||||
|
||||
public ImmutableDictionary<string, CommandAlias> Aliases { get; init; }
|
||||
private string[]? _fallbackRanks = [];
|
||||
|
||||
public string[] FallbackRanks
|
||||
{
|
||||
get => _fallbackRanks ?? [];
|
||||
init => _fallbackRanks = value;
|
||||
}
|
||||
|
||||
private ImmutableDictionary<string, CommandAlias>? _aliases
|
||||
= ImmutableDictionary<string, CommandAlias>.Empty;
|
||||
|
||||
public ImmutableList<TopLevelHotkey> CommandHotkeys { get; init; }
|
||||
public ImmutableDictionary<string, CommandAlias> Aliases
|
||||
{
|
||||
get => _aliases ?? ImmutableDictionary<string, CommandAlias>.Empty;
|
||||
init => _aliases = value;
|
||||
}
|
||||
|
||||
private ImmutableList<TopLevelHotkey>? _commandHotkeys
|
||||
= ImmutableList<TopLevelHotkey>.Empty;
|
||||
|
||||
public ImmutableList<TopLevelHotkey> CommandHotkeys
|
||||
{
|
||||
get => _commandHotkeys ?? ImmutableList<TopLevelHotkey>.Empty;
|
||||
init => _commandHotkeys = value;
|
||||
}
|
||||
|
||||
public MonitorBehavior SummonOn { get; init; } = MonitorBehavior.ToMouse;
|
||||
|
||||
public bool DisableAnimations { get; init; } = true;
|
||||
@@ -62,7 +86,13 @@ public record SettingsModel
|
||||
|
||||
public bool EnableDock { get; init; }
|
||||
|
||||
public DockSettings DockSettings { get; init; } = new();
|
||||
private DockSettings? _dockSettings = new();
|
||||
|
||||
public DockSettings DockSettings
|
||||
{
|
||||
get => _dockSettings ?? new();
|
||||
init => _dockSettings = value;
|
||||
}
|
||||
|
||||
// Theme settings
|
||||
public UserTheme Theme { get; init; } = UserTheme.Default;
|
||||
|
||||
@@ -196,7 +196,7 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem, IEx
|
||||
{
|
||||
ProviderId = this.CommandProviderId,
|
||||
CommandId = this.Id,
|
||||
ShowLabels = true,
|
||||
ShowTitles = true,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.InteropServices;
|
||||
using ManagedCommon;
|
||||
using Windows.Win32.Foundation;
|
||||
using static PowerDisplay.Common.Drivers.NativeConstants;
|
||||
using static PowerDisplay.Common.Drivers.PInvoke;
|
||||
@@ -27,32 +28,55 @@ namespace PowerDisplay.Common.Drivers.DDC
|
||||
{
|
||||
if (hPhysicalMonitor == IntPtr.Zero)
|
||||
{
|
||||
Logger.LogWarning("DDC: Monitor ignored - null physical monitor handle");
|
||||
return DdcCiValidationResult.Invalid;
|
||||
}
|
||||
|
||||
var handleHex = $"0x{hPhysicalMonitor:X}";
|
||||
|
||||
try
|
||||
{
|
||||
// Try to get capabilities string (slow I2C operation)
|
||||
var capsString = TryGetCapabilitiesString(hPhysicalMonitor);
|
||||
if (string.IsNullOrEmpty(capsString))
|
||||
{
|
||||
Logger.LogWarning($"DDC: Monitor ignored (handle={handleHex}) - empty capabilities string from DDC/CI");
|
||||
return DdcCiValidationResult.Invalid;
|
||||
}
|
||||
|
||||
Logger.LogInfo($"DDC: Capabilities raw (handle={handleHex}, length={capsString.Length}): {capsString}");
|
||||
|
||||
// Parse the capabilities string
|
||||
var parseResult = Utils.MccsCapabilitiesParser.Parse(capsString);
|
||||
var capabilities = parseResult.Capabilities;
|
||||
|
||||
if (capabilities == null || capabilities.SupportedVcpCodes.Count == 0)
|
||||
{
|
||||
Logger.LogWarning($"DDC: Monitor ignored (handle={handleHex}) - parsed capabilities have no VCP codes (parseErrors={parseResult.Errors.Count})");
|
||||
return DdcCiValidationResult.Invalid;
|
||||
}
|
||||
|
||||
// Check if brightness (VCP 0x10) is supported - determines DDC/CI validity
|
||||
bool supportsBrightness = capabilities.SupportsVcpCode(NativeConstants.VcpCodeBrightness);
|
||||
bool supportsContrast = capabilities.SupportsVcpCode(NativeConstants.VcpCodeContrast);
|
||||
bool supportsColorTemperature = capabilities.SupportsVcpCode(NativeConstants.VcpCodeSelectColorPreset);
|
||||
bool supportsVolume = capabilities.SupportsVcpCode(NativeConstants.VcpCodeVolume);
|
||||
|
||||
Logger.LogInfo(
|
||||
$"DDC: Capabilities parsed (handle={handleHex}) - " +
|
||||
$"Brightness={supportsBrightness} Contrast={supportsContrast} " +
|
||||
$"ColorTemperature={supportsColorTemperature} Volume={supportsVolume}");
|
||||
|
||||
if (!supportsBrightness)
|
||||
{
|
||||
Logger.LogWarning($"DDC: Monitor ignored (handle={handleHex}) - brightness (VCP 0x10) not advertised in capabilities");
|
||||
}
|
||||
|
||||
return new DdcCiValidationResult(supportsBrightness, capsString, capabilities);
|
||||
}
|
||||
catch (Exception ex) when (ex is not OutOfMemoryException)
|
||||
{
|
||||
Logger.LogError($"DDC: Monitor ignored (handle={handleHex}) - exception during FetchCapabilities: {ex.Message}");
|
||||
return DdcCiValidationResult.Invalid;
|
||||
}
|
||||
}
|
||||
@@ -74,6 +98,7 @@ namespace PowerDisplay.Common.Drivers.DDC
|
||||
// Get capabilities string length
|
||||
if (!GetCapabilitiesStringLength(hPhysicalMonitor, out uint length) || length == 0)
|
||||
{
|
||||
Logger.LogWarning($"DDC: GetCapabilitiesStringLength failed (handle=0x{hPhysicalMonitor:X}, length={length})");
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -83,6 +108,7 @@ namespace PowerDisplay.Common.Drivers.DDC
|
||||
{
|
||||
if (!CapabilitiesRequestAndCapabilitiesReply(hPhysicalMonitor, buffer, length))
|
||||
{
|
||||
Logger.LogWarning($"DDC: CapabilitiesRequestAndCapabilitiesReply failed (handle=0x{hPhysicalMonitor:X})");
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -95,6 +121,7 @@ namespace PowerDisplay.Common.Drivers.DDC
|
||||
}
|
||||
catch (Exception ex) when (ex is not OutOfMemoryException)
|
||||
{
|
||||
Logger.LogError($"DDC: TryGetCapabilitiesString exception (handle=0x{hPhysicalMonitor:X}): {ex.Message}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ using System.Threading.Tasks;
|
||||
using ManagedCommon;
|
||||
using PowerDisplay.Common.Interfaces;
|
||||
using PowerDisplay.Common.Models;
|
||||
using PowerDisplay.Common.Utils;
|
||||
using WmiLight;
|
||||
using Monitor = PowerDisplay.Common.Models.Monitor;
|
||||
|
||||
@@ -245,8 +244,9 @@ namespace PowerDisplay.Common.Drivers.WMI
|
||||
|
||||
/// <summary>
|
||||
/// Discover supported monitors.
|
||||
/// WMI brightness control is typically only available on internal laptop displays,
|
||||
/// which don't have meaningful UserFriendlyName in WmiMonitorID, so we use "Built-in Display".
|
||||
/// WMI brightness control is typically only available on internal laptop displays.
|
||||
/// The monitor Name is left blank here; the ViewModel layer fills in a localized
|
||||
/// "Built-in Display" string so it can be translated for the user's UI language.
|
||||
/// </summary>
|
||||
public async Task<IEnumerable<Monitor>> DiscoverMonitorsAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
@@ -294,13 +294,12 @@ namespace PowerDisplay.Common.Drivers.WMI
|
||||
? $"WMI_{edidId}_{monitorNumber}"
|
||||
: $"WMI_Unknown_{monitorNumber}";
|
||||
|
||||
// Get display name from PnP manufacturer ID (e.g., "Lenovo Built-in Display")
|
||||
var displayName = PnpIdHelper.GetBuiltInDisplayName(edidId);
|
||||
|
||||
// Name is left blank: MonitorViewModel injects a localized
|
||||
// "Built-in Display" string for internal displays.
|
||||
var monitor = new Monitor
|
||||
{
|
||||
Id = uniqueId,
|
||||
Name = displayName,
|
||||
Name = string.Empty,
|
||||
CurrentBrightness = currentBrightness,
|
||||
MinBrightness = 0,
|
||||
MaxBrightness = 100,
|
||||
|
||||
@@ -1,86 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Frozen;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace PowerDisplay.Common.Utils;
|
||||
|
||||
/// <summary>
|
||||
/// Helper class for mapping PnP (Plug and Play) manufacturer IDs to display names.
|
||||
/// PnP IDs are 3-character codes assigned by Microsoft to hardware manufacturers.
|
||||
/// See: https://uefi.org/pnp_id_list
|
||||
/// </summary>
|
||||
public static class PnpIdHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// Map of common laptop/monitor manufacturer PnP IDs to display names.
|
||||
/// Only includes manufacturers known to produce laptops with internal displays.
|
||||
/// </summary>
|
||||
private static readonly FrozenDictionary<string, string> ManufacturerNames = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
// Major laptop manufacturers
|
||||
{ "ACR", "Acer" },
|
||||
{ "AUO", "AU Optronics" },
|
||||
{ "BOE", "BOE" },
|
||||
{ "CMN", "Chi Mei Innolux" },
|
||||
{ "DEL", "Dell" },
|
||||
{ "HWP", "HP" },
|
||||
{ "IVO", "InfoVision" },
|
||||
{ "LEN", "Lenovo" },
|
||||
{ "LGD", "LG Display" },
|
||||
{ "NCP", "Nanjing CEC Panda" },
|
||||
{ "SAM", "Samsung" },
|
||||
{ "SDC", "Samsung Display" },
|
||||
{ "SEC", "Samsung Electronics" },
|
||||
{ "SHP", "Sharp" },
|
||||
{ "AUS", "ASUS" },
|
||||
{ "MSI", "MSI" },
|
||||
{ "APP", "Apple" },
|
||||
{ "SNY", "Sony" },
|
||||
{ "PHL", "Philips" },
|
||||
{ "HSD", "HannStar" },
|
||||
{ "CPT", "Chunghwa Picture Tubes" },
|
||||
{ "QDS", "Quanta Display" },
|
||||
{ "TMX", "Tianma Microelectronics" },
|
||||
{ "CSO", "CSOT" },
|
||||
|
||||
// Microsoft Surface
|
||||
{ "MSF", "Microsoft" },
|
||||
}.ToFrozenDictionary(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
/// <summary>
|
||||
/// Extract the 3-character PnP manufacturer ID from an EDID ID.
|
||||
/// </summary>
|
||||
/// <param name="edidId">EDID ID like "LEN4038" or "BOE0900".</param>
|
||||
/// <returns>The 3-character PnP ID (e.g., "LEN"), or null if invalid.</returns>
|
||||
public static string? ExtractPnpId(string? edidId)
|
||||
{
|
||||
if (string.IsNullOrEmpty(edidId) || edidId.Length < 3)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// PnP ID is the first 3 characters
|
||||
return edidId.Substring(0, 3).ToUpperInvariant();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a user-friendly display name for an internal display based on its EDID ID.
|
||||
/// </summary>
|
||||
/// <param name="edidId">EDID ID like "LEN4038" or "BOE0900".</param>
|
||||
/// <returns>Display name like "Lenovo Built-in Display" or "Built-in Display" as fallback.</returns>
|
||||
public static string GetBuiltInDisplayName(string? edidId)
|
||||
{
|
||||
var pnpId = ExtractPnpId(edidId);
|
||||
|
||||
if (pnpId != null && ManufacturerNames.TryGetValue(pnpId, out var manufacturer))
|
||||
{
|
||||
return $"{manufacturer} Built-in Display";
|
||||
}
|
||||
|
||||
return "Built-in Display";
|
||||
}
|
||||
}
|
||||
@@ -135,6 +135,10 @@
|
||||
<data name="BuiltInMonitorTooltip.ToolTipService.ToolTip" xml:space="preserve">
|
||||
<value>Built-in display</value>
|
||||
</data>
|
||||
<data name="BuiltInDisplayName" xml:space="preserve">
|
||||
<value>Built-in display</value>
|
||||
<comment>Display name shown in the monitor list for the laptop's internal/built-in display.</comment>
|
||||
</data>
|
||||
<data name="BrightnessTooltip.ToolTipService.ToolTip" xml:space="preserve">
|
||||
<value>Brightness</value>
|
||||
</data>
|
||||
|
||||
@@ -415,13 +415,17 @@ public partial class MainViewModel
|
||||
SupportsVolume = vm.VcpCapabilitiesInfo?.SupportedVcpCodes.ContainsKey(0x62) ?? false,
|
||||
SupportsPowerState = vm.VcpCapabilitiesInfo?.SupportedVcpCodes.ContainsKey(0xD6) ?? false,
|
||||
|
||||
// Default Enable* to match Supports* for new monitors (first-time setup)
|
||||
// ApplyPreservedUserSettings will override these with saved user preferences if they exist
|
||||
// Default Enable* for new monitors (first-time setup):
|
||||
// - Contrast / Volume: enabled if the monitor advertises the VCP code (low-risk features).
|
||||
// - InputSource / ColorTemperature / PowerState: always disabled by default. These can leave
|
||||
// the monitor in a state recoverable only via physical buttons; users opt-in via the
|
||||
// Settings UI checkbox, which raises a confirmation dialog (HandleDangerousFeatureClickAsync).
|
||||
// ApplyPreservedUserSettings will override these with saved user preferences if they exist.
|
||||
EnableContrast = vm.VcpCapabilitiesInfo?.SupportedVcpCodes.ContainsKey(0x12) ?? false,
|
||||
EnableVolume = vm.VcpCapabilitiesInfo?.SupportedVcpCodes.ContainsKey(0x62) ?? false,
|
||||
EnableInputSource = vm.VcpCapabilitiesInfo?.SupportedVcpCodes.ContainsKey(0x60) ?? false,
|
||||
EnableColorTemperature = vm.VcpCapabilitiesInfo?.SupportedVcpCodes.ContainsKey(0x14) ?? false,
|
||||
EnablePowerState = vm.VcpCapabilitiesInfo?.SupportedVcpCodes.ContainsKey(0xD6) ?? false,
|
||||
EnableInputSource = false,
|
||||
EnableColorTemperature = false,
|
||||
EnablePowerState = false,
|
||||
|
||||
// Monitor number for display name formatting
|
||||
MonitorNumber = vm.MonitorNumber,
|
||||
|
||||
@@ -215,13 +215,19 @@ public partial class MonitorViewModel : ObservableObject, IDisposable
|
||||
// Subscribe to underlying Monitor property changes (e.g., Orientation updates in mirror mode)
|
||||
_monitor.PropertyChanged += OnMonitorPropertyChanged;
|
||||
|
||||
// Initialize Show properties based on hardware capabilities
|
||||
// Initialize Show properties for first-time detection. ApplyFeatureVisibility will
|
||||
// override these whenever settings.json has a saved entry for this monitor, so these
|
||||
// values only take effect for brand-new monitors (no persisted preference yet).
|
||||
// Mirror CreateMonitorInfo's defaults to keep the flyout and settings.json in sync:
|
||||
// - Brightness / Contrast / Volume: enabled if the hardware advertises the VCP code.
|
||||
// - InputSource / ColorTemperature / PowerState: always disabled by default (dangerous
|
||||
// features); the user opts in via the Settings UI confirmation dialog.
|
||||
ShowBrightness = monitor.SupportsBrightness;
|
||||
ShowContrast = monitor.SupportsContrast;
|
||||
ShowVolume = monitor.SupportsVolume;
|
||||
ShowInputSource = monitor.SupportsInputSource;
|
||||
_showPowerState = monitor.SupportsPowerState;
|
||||
_showColorTemperature = monitor.SupportsColorTemperature;
|
||||
ShowInputSource = false;
|
||||
_showPowerState = false;
|
||||
_showColorTemperature = false;
|
||||
|
||||
// Initialize basic properties from monitor
|
||||
_brightness = monitor.CurrentBrightness;
|
||||
@@ -232,7 +238,9 @@ public partial class MonitorViewModel : ObservableObject, IDisposable
|
||||
|
||||
public string Id => _monitor.Id;
|
||||
|
||||
public string Name => _monitor.Name;
|
||||
public string Name => IsInternal
|
||||
? ResourceLoaderInstance.ResourceLoader.GetString("BuiltInDisplayName")
|
||||
: _monitor.Name;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the monitor number from the underlying monitor model (Windows DISPLAY number)
|
||||
|
||||
@@ -211,7 +211,11 @@
|
||||
<CheckBox x:Uid="PowerDisplay_Monitor_EnableVolume" IsChecked="{x:Bind EnableVolume, Mode=TwoWay}" />
|
||||
</tkcontrols:SettingsCard>
|
||||
<tkcontrols:SettingsCard ContentAlignment="Left" IsEnabled="{x:Bind SupportsInputSource, Mode=OneWay}">
|
||||
<CheckBox x:Uid="PowerDisplay_Monitor_EnableInputSource" IsChecked="{x:Bind EnableInputSource, Mode=TwoWay}" />
|
||||
<CheckBox
|
||||
x:Uid="PowerDisplay_Monitor_EnableInputSource"
|
||||
Click="EnableInputSource_Click"
|
||||
IsChecked="{x:Bind EnableInputSource, Mode=TwoWay}"
|
||||
Tag="{x:Bind}" />
|
||||
</tkcontrols:SettingsCard>
|
||||
<tkcontrols:SettingsCard ContentAlignment="Left">
|
||||
<CheckBox x:Uid="PowerDisplay_Monitor_EnableRotation" IsChecked="{x:Bind EnableRotation, Mode=TwoWay}" />
|
||||
@@ -224,7 +228,11 @@
|
||||
Tag="{x:Bind}" />
|
||||
</tkcontrols:SettingsCard>
|
||||
<tkcontrols:SettingsCard ContentAlignment="Left" IsEnabled="{x:Bind SupportsPowerState, Mode=OneWay}">
|
||||
<CheckBox x:Uid="PowerDisplay_Monitor_EnablePowerState" IsChecked="{x:Bind EnablePowerState, Mode=TwoWay}" />
|
||||
<CheckBox
|
||||
x:Uid="PowerDisplay_Monitor_EnablePowerState"
|
||||
Click="EnablePowerState_Click"
|
||||
IsChecked="{x:Bind EnablePowerState, Mode=TwoWay}"
|
||||
Tag="{x:Bind}" />
|
||||
</tkcontrols:SettingsCard>
|
||||
<tkcontrols:SettingsCard ContentAlignment="Left">
|
||||
<CheckBox x:Uid="PowerDisplay_Monitor_HideMonitor" IsChecked="{x:Bind IsHidden, Mode=TwoWay}" />
|
||||
|
||||
@@ -209,13 +209,40 @@ namespace Microsoft.PowerToys.Settings.UI.Views
|
||||
}
|
||||
}
|
||||
|
||||
// Flag to prevent reentrant handling during programmatic checkbox changes
|
||||
private bool _isRestoringColorTempCheckbox;
|
||||
// Flag to prevent reentrant Click handling while we programmatically restore
|
||||
// a checkbox after the user cancels a dangerous-feature confirmation dialog.
|
||||
private bool _isRestoringDangerousFeatureCheckbox;
|
||||
|
||||
private async void EnableColorTemperature_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
// Skip if we're programmatically restoring the checkbox state
|
||||
if (_isRestoringColorTempCheckbox)
|
||||
await HandleDangerousFeatureClickAsync(
|
||||
sender,
|
||||
"PowerDisplay_ColorTemperature",
|
||||
(monitor, value) => monitor.EnableColorTemperature = value);
|
||||
}
|
||||
|
||||
private async void EnablePowerState_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
await HandleDangerousFeatureClickAsync(
|
||||
sender,
|
||||
"PowerDisplay_PowerState",
|
||||
(monitor, value) => monitor.EnablePowerState = value);
|
||||
}
|
||||
|
||||
private async void EnableInputSource_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
await HandleDangerousFeatureClickAsync(
|
||||
sender,
|
||||
"PowerDisplay_InputSource",
|
||||
(monitor, value) => monitor.EnableInputSource = value);
|
||||
}
|
||||
|
||||
private async Task HandleDangerousFeatureClickAsync(
|
||||
object sender,
|
||||
string resourceKeyPrefix,
|
||||
Action<MonitorInfo, bool> setter)
|
||||
{
|
||||
if (_isRestoringDangerousFeatureCheckbox)
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -225,18 +252,17 @@ namespace Microsoft.PowerToys.Settings.UI.Views
|
||||
return;
|
||||
}
|
||||
|
||||
// Only show warning when enabling (checking the box)
|
||||
// Only show the warning when the user is enabling the feature.
|
||||
if (checkBox.IsChecked != true)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Show confirmation dialog with color temperature warning
|
||||
var resourceLoader = ResourceLoaderInstance.ResourceLoader;
|
||||
var dialog = new ContentDialog
|
||||
{
|
||||
XamlRoot = this.XamlRoot,
|
||||
Title = resourceLoader.GetString("PowerDisplay_ColorTemperature_WarningTitle"),
|
||||
Title = resourceLoader.GetString($"{resourceKeyPrefix}_WarningTitle"),
|
||||
Content = new StackPanel
|
||||
{
|
||||
Spacing = 12,
|
||||
@@ -244,31 +270,31 @@ namespace Microsoft.PowerToys.Settings.UI.Views
|
||||
{
|
||||
new TextBlock
|
||||
{
|
||||
Text = resourceLoader.GetString("PowerDisplay_ColorTemperature_WarningHeader"),
|
||||
Text = resourceLoader.GetString($"{resourceKeyPrefix}_WarningHeader"),
|
||||
FontWeight = Microsoft.UI.Text.FontWeights.Bold,
|
||||
Foreground = (Microsoft.UI.Xaml.Media.Brush)Application.Current.Resources["SystemFillColorCriticalBrush"],
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
},
|
||||
new TextBlock
|
||||
{
|
||||
Text = resourceLoader.GetString("PowerDisplay_ColorTemperature_WarningDescription"),
|
||||
Text = resourceLoader.GetString($"{resourceKeyPrefix}_WarningDescription"),
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
},
|
||||
new TextBlock
|
||||
{
|
||||
Text = resourceLoader.GetString("PowerDisplay_ColorTemperature_WarningList"),
|
||||
Text = resourceLoader.GetString($"{resourceKeyPrefix}_WarningList"),
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
Margin = new Thickness(20, 0, 0, 0),
|
||||
},
|
||||
new TextBlock
|
||||
{
|
||||
Text = resourceLoader.GetString("PowerDisplay_ColorTemperature_WarningConfirm"),
|
||||
Text = resourceLoader.GetString($"{resourceKeyPrefix}_WarningConfirm"),
|
||||
FontWeight = Microsoft.UI.Text.FontWeights.SemiBold,
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
},
|
||||
},
|
||||
},
|
||||
PrimaryButtonText = resourceLoader.GetString("PowerDisplay_ColorTemperature_EnableButton"),
|
||||
PrimaryButtonText = resourceLoader.GetString("PowerDisplay_Dialog_Enable"),
|
||||
CloseButtonText = resourceLoader.GetString("PowerDisplay_Dialog_Cancel"),
|
||||
DefaultButton = ContentDialogButton.Close,
|
||||
};
|
||||
@@ -277,16 +303,16 @@ namespace Microsoft.PowerToys.Settings.UI.Views
|
||||
|
||||
if (result != ContentDialogResult.Primary)
|
||||
{
|
||||
// User cancelled: revert checkbox to unchecked
|
||||
_isRestoringColorTempCheckbox = true;
|
||||
// User cancelled: revert checkbox to unchecked.
|
||||
_isRestoringDangerousFeatureCheckbox = true;
|
||||
try
|
||||
{
|
||||
checkBox.IsChecked = false;
|
||||
monitor.EnableColorTemperature = false;
|
||||
setter(monitor, false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isRestoringColorTempCheckbox = false;
|
||||
_isRestoringDangerousFeatureCheckbox = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5501,7 +5501,41 @@ The break timer font matches the text font.</value>
|
||||
<data name="PowerDisplay_ColorTemperature_WarningConfirm" xml:space="preserve">
|
||||
<value>Do you want to enable color temperature control for this monitor?</value>
|
||||
</data>
|
||||
<data name="PowerDisplay_ColorTemperature_EnableButton" xml:space="preserve">
|
||||
<data name="PowerDisplay_PowerState_WarningTitle" xml:space="preserve">
|
||||
<value>Confirm power state control</value>
|
||||
</data>
|
||||
<data name="PowerDisplay_PowerState_WarningHeader" xml:space="preserve">
|
||||
<value>⚠️ Warning: This action may be unsafe.</value>
|
||||
</data>
|
||||
<data name="PowerDisplay_PowerState_WarningDescription" xml:space="preserve">
|
||||
<value>Enabling power state control may lead to unexpected behavior, including:</value>
|
||||
</data>
|
||||
<data name="PowerDisplay_PowerState_WarningList" xml:space="preserve">
|
||||
<value>• Monitor may enter standby and not wake via software.
|
||||
• You may need to press the monitor’s power button or reconnect the cable to recover.
|
||||
• Some monitors may not restore the previous state correctly.</value>
|
||||
</data>
|
||||
<data name="PowerDisplay_PowerState_WarningConfirm" xml:space="preserve">
|
||||
<value>Enable power state control for this monitor?</value>
|
||||
</data>
|
||||
<data name="PowerDisplay_InputSource_WarningTitle" xml:space="preserve">
|
||||
<value>Confirm input source control</value>
|
||||
</data>
|
||||
<data name="PowerDisplay_InputSource_WarningHeader" xml:space="preserve">
|
||||
<value>⚠️ Warning: This action may be unsafe.</value>
|
||||
</data>
|
||||
<data name="PowerDisplay_InputSource_WarningDescription" xml:space="preserve">
|
||||
<value>Switching input sources may cause unintended results, including:</value>
|
||||
</data>
|
||||
<data name="PowerDisplay_InputSource_WarningList" xml:space="preserve">
|
||||
<value>• Screen may go black if the selected input has no signal.
|
||||
• Software control may be lost until you switch back using the monitor’s physical buttons.
|
||||
• Some monitors may not reliably expose all input sources.</value>
|
||||
</data>
|
||||
<data name="PowerDisplay_InputSource_WarningConfirm" xml:space="preserve">
|
||||
<value>Enable input source control for this monitor?</value>
|
||||
</data>
|
||||
<data name="PowerDisplay_Dialog_Enable" xml:space="preserve">
|
||||
<value>Enable</value>
|
||||
</data>
|
||||
<data name="PowerDisplay_Dialog_Cancel" xml:space="preserve">
|
||||
|
||||
@@ -9,6 +9,7 @@ using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Input;
|
||||
|
||||
using ManagedCommon;
|
||||
using Microsoft.PowerToys.Settings.UI.Helpers;
|
||||
using Microsoft.PowerToys.Settings.UI.Library;
|
||||
using Microsoft.PowerToys.Settings.UI.Library.Helpers;
|
||||
@@ -135,7 +136,18 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
|
||||
private void Frame_NavigationFailed(object sender, NavigationFailedEventArgs e)
|
||||
{
|
||||
throw e.Exception;
|
||||
var sourcePage = e.SourcePageType?.FullName ?? "<unknown>";
|
||||
|
||||
if (e.Exception is null)
|
||||
{
|
||||
Logger.LogWarning($"Navigation to '{sourcePage}' failed without an exception.");
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogError($"Navigation to '{sourcePage}' failed.", e.Exception);
|
||||
}
|
||||
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
private void Frame_Navigated(object sender, NavigationEventArgs e)
|
||||
|
||||
Reference in New Issue
Block a user