mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-08-29 10:09:43 +02:00
[PowerDisplay] Add stable profile IDs (#49175)
## Summary of the Pull Request Gives every saved PowerDisplay profile a stable, auto-incrementing integer ID and makes the app address profiles by that ID instead of by name. Duplicate profile names are allowed, renames preserve identity, and LightSwitch stores stable profile references. > Split out of the PowerDisplay CLI branch (#48632). CLI-specific contracts and commands remain in that stacked PR. ## PR Checklist - [x] **Closes:** N/A - split from #48632. - [x] **Communication:** Discussed with core contributors. - [x] **Tests:** Added and passing in `PowerDisplay.Lib.UnitTests`. - [x] **Localization:** The composed profile label uses a shared localized format resource. - [x] **New binaries:** None. - [x] **Documentation updated:** `doc/devdocs/modules/powerdisplay/design.md`. ## Implementation ### Profile model and persistence - `PowerDisplayProfile.Id` is the stable JSON `id`; `0` means unassigned. - `PowerDisplayProfiles.NextId` is monotonic and IDs are never reused. - `SetProfile` assigns IDs to new profiles and replaces existing profiles by ID. - Duplicate names are supported; name lookup remains only for migration of legacy references. - `ProfileStore` serializes cross-process load/modify/save operations with a named mutex and atomically replaces `profiles.json`. - Production callers use asynchronous `ProfileHelper` APIs. ### Migration and application - Initial PowerDisplay discovery assigns missing profile IDs and migrates legacy monitor IDs. - LightSwitch legacy name references are reconciled to IDs and written back to the current typed settings schema. - Native LightSwitch publishes pure light/dark theme events; PowerDisplay exclusively validates profile enablement and stable IDs. - Settings UI and Named Pipe ApplyProfile actions send invariant positive profile IDs. - PowerDisplay validates the ID, loads the current profile, and applies its monitor settings. ### Settings UI - Create, edit, apply, and delete operations use stable IDs. - LightSwitch selectors store profile IDs and keep legacy name fields only for migration. - Profile lists use a localized name-and-ID label so duplicate names remain distinguishable. ## Accepted Trade-offs - Profile ID migration remains dependent on the initial monitor discovery; a failed or delayed discovery can temporarily hide legacy ID-less profiles. - The one-time PowerDisplay LightSwitch migration rewrites the complete current typed settings object and does not add a new cross-process settings transaction. ## Validation - Built the affected x64 Debug projects with the repository build scripts. - `PowerDisplay.Lib.UnitTests`: 186 passed, 0 failed. --------- Co-authored-by: Yu Leng (from Dev Box) <yuleng@microsoft.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -193,26 +193,18 @@ src/modules/powerdisplay/
|
||||
│ │ └── PInvoke.cs # P/Invoke declarations
|
||||
│ ├── Interfaces/
|
||||
│ │ ├── IMonitorController.cs # Controller abstraction
|
||||
│ │ ├── IMonitorData.cs # Monitor data interface
|
||||
│ │ └── IProfileService.cs # Profile service interface
|
||||
│ │ └── IMonitorData.cs # Monitor data interface
|
||||
│ ├── Models/
|
||||
│ │ ├── Monitor.cs # Runtime monitor data
|
||||
│ │ ├── MonitorCapabilities.cs # Monitor capability flags
|
||||
│ │ ├── MonitorOperationResult.cs # Operation result
|
||||
│ │ ├── MonitorStateEntry.cs # Persisted monitor state
|
||||
│ │ ├── MonitorStateFile.cs # State file schema
|
||||
│ │ ├── PowerDisplayProfile.cs # Profile definition
|
||||
│ │ ├── PowerDisplayProfiles.cs # Profile collection
|
||||
│ │ ├── ProfileMonitorSetting.cs # Per-monitor profile settings
|
||||
│ │ ├── ColorPresetItem.cs # Color preset UI item
|
||||
│ │ ├── VcpCapabilities.cs # Parsed VCP capabilities
|
||||
│ │ └── VcpFeatureValue.cs # VCP feature value (current/min/max)
|
||||
│ ├── Serialization/
|
||||
│ │ └── ProfileSerializationContext.cs # JSON source generation
|
||||
│ ├── Services/
|
||||
│ │ ├── DisplayRotationService.cs # Display rotation via ChangeDisplaySettingsEx
|
||||
│ │ ├── MonitorStateManager.cs # State persistence (debounced save) and restore on startup
|
||||
│ │ └── ProfileService.cs # Profile persistence
|
||||
│ │ └── MonitorStateManager.cs # State persistence (debounced save) and restore on startup
|
||||
│ ├── Utils/
|
||||
│ │ ├── ColorTemperatureHelper.cs # Color temp utilities
|
||||
│ │ ├── EventHelper.cs # Windows Event utilities
|
||||
@@ -221,11 +213,19 @@ src/modules/powerdisplay/
|
||||
│ │ ├── MonitorMatchingHelper.cs # Profile-to-monitor matching
|
||||
│ │ ├── MonitorValueConverter.cs # Value conversion utilities
|
||||
│ │ ├── PnpIdHelper.cs # PnP manufacturer ID lookup
|
||||
│ │ ├── ProfileHelper.cs # Profile helper utilities
|
||||
│ │ ├── SimpleDebouncer.cs # Generic debouncer
|
||||
│ │ └── VcpNames.cs # VCP code and value name lookup
|
||||
│ └── PathConstants.cs # File path constants
|
||||
│
|
||||
├── PowerDisplay.Models/ # Shared profile models and persistence
|
||||
│ ├── ColorPresetItem.cs # Color preset UI item
|
||||
│ ├── PowerDisplayProfile.cs # Profile definition
|
||||
│ ├── PowerDisplayProfiles.cs # Profile collection
|
||||
│ ├── ProfileMonitorSetting.cs # Per-monitor profile settings
|
||||
│ ├── ProfileHelper.cs # Shared asynchronous profile entry points
|
||||
│ ├── ProfileStore.cs # Atomic cross-process profile persistence
|
||||
│ └── ProfileSerializationContext.cs # JSON source generation
|
||||
│
|
||||
├── PowerDisplay/ # WinUI 3 application
|
||||
│ ├── Assets/ # App icons and images
|
||||
│ ├── Configuration/
|
||||
@@ -304,7 +304,6 @@ flowchart TB
|
||||
|
||||
subgraph PowerDisplayLib["PowerDisplay.Lib"]
|
||||
subgraph Services
|
||||
ProfileService
|
||||
MonitorStateManager
|
||||
DisplayRotationService
|
||||
end
|
||||
@@ -316,6 +315,11 @@ flowchart TB
|
||||
PnpIdHelper["PnpIdHelper<br/>(Manufacturer Names)"]
|
||||
end
|
||||
end
|
||||
|
||||
subgraph PowerDisplayModels["PowerDisplay.Models"]
|
||||
ProfileHelper
|
||||
ProfileStore
|
||||
end
|
||||
end
|
||||
|
||||
subgraph Storage["Persistent Storage"]
|
||||
@@ -338,13 +342,14 @@ flowchart TB
|
||||
ThemeChangedEvent --> LightSwitchService
|
||||
|
||||
%% App internal
|
||||
LightSwitchService -.->|"Get profile name"| MainViewModel
|
||||
LightSwitchService -.->|"Get profile id"| MainViewModel
|
||||
MainViewModel --> MonitorViewModel
|
||||
MonitorViewModel --> MonitorManager
|
||||
DisplayChangeWatcher -.->|"DisplayChanged event"| MainViewModel
|
||||
|
||||
%% App to Lib services
|
||||
MainViewModel --> ProfileService
|
||||
%% App to services and profile persistence
|
||||
MainViewModel --> ProfileHelper
|
||||
ProfileHelper --> ProfileStore
|
||||
MonitorViewModel --> MonitorStateManager
|
||||
MonitorManager --> Drivers
|
||||
MonitorManager --> DisplayRotationService
|
||||
@@ -352,8 +357,8 @@ flowchart TB
|
||||
%% Utils used during discovery
|
||||
WmiController --> PnpIdHelper
|
||||
|
||||
%% Services to Storage
|
||||
ProfileService --> ProfilesJson
|
||||
%% Persistence to Storage
|
||||
ProfileStore --> ProfilesJson
|
||||
MonitorStateManager --> MonitorStateJson
|
||||
|
||||
%% Drivers to Hardware
|
||||
@@ -1080,7 +1085,7 @@ flowchart TB
|
||||
StateManager["LightSwitchStateManager"]
|
||||
ThemeEval["Theme Evaluation<br/>(Time/System)"]
|
||||
LightSwitchSettings["LightSwitchSettings"]
|
||||
NotifyPD["NotifyPowerDisplay(isLight)"]
|
||||
NotifyPD["NotifyPowerDisplayThemeChanged(isLight)"]
|
||||
end
|
||||
|
||||
subgraph PowerDisplayModule["PowerDisplay Module (C#)"]
|
||||
@@ -1090,7 +1095,8 @@ flowchart TB
|
||||
MainViewModel["MainViewModel"]
|
||||
end
|
||||
|
||||
ProfileService["ProfileService"]
|
||||
ProfileHelper["ProfileHelper<br/>(PowerDisplay.Models)"]
|
||||
ProfileStore["ProfileStore"]
|
||||
MonitorVMs["MonitorViewModels"]
|
||||
Controllers["IMonitorController"]
|
||||
end
|
||||
@@ -1113,17 +1119,18 @@ flowchart TB
|
||||
ThemeEval -->|"Time boundary<br/>or manual"| StateManager
|
||||
StateManager --> LightSwitchSettings
|
||||
StateManager --> NotifyPD
|
||||
NotifyPD -->|"isLight=true"| LightEvent
|
||||
NotifyPD -->|"isLight=false"| DarkEvent
|
||||
NotifyPD -->|"pure light theme event"| LightEvent
|
||||
NotifyPD -->|"pure dark theme event"| DarkEvent
|
||||
|
||||
%% PowerDisplay flow - theme determined from event
|
||||
LightEvent -->|"Event signaled"| EventWaiter
|
||||
DarkEvent -->|"Event signaled"| EventWaiter
|
||||
EventWaiter -->|"isLightMode"| LightSwitchSvc
|
||||
LightSwitchSvc -->|"GetProfileForTheme()"| LSSettingsJson
|
||||
LightSwitchSvc -->|"Profile name"| MainViewModel
|
||||
MainViewModel -->|"LoadProfiles()"| ProfileService
|
||||
ProfileService <--> PDProfilesJson
|
||||
LightSwitchSvc -->|"GetProfileIdForTheme()"| LSSettingsJson
|
||||
LightSwitchSvc -->|"Profile id"| MainViewModel
|
||||
MainViewModel -->|"LoadProfilesAsync()"| ProfileHelper
|
||||
ProfileHelper --> ProfileStore
|
||||
ProfileStore <--> PDProfilesJson
|
||||
MainViewModel -->|"ApplyProfileAsync()"| MonitorVMs
|
||||
MonitorVMs --> Controllers
|
||||
Controllers --> Monitors
|
||||
@@ -1135,20 +1142,25 @@ flowchart TB
|
||||
style FileSystem fill:#fffde7
|
||||
```
|
||||
|
||||
Native LightSwitch treats these named events as pure theme-change notifications and does not parse PowerDisplay profile enablement, names, or IDs. PowerDisplay reads the typed LightSwitch settings after receiving the event and is the sole authority that validates and applies the configured profile.
|
||||
|
||||
### LightSwitch Settings JSON Structure
|
||||
|
||||
```json
|
||||
{
|
||||
"properties": {
|
||||
"apply_monitor_settings": { "value": true },
|
||||
"enable_light_mode_profile": { "value": true },
|
||||
"light_mode_profile": { "value": "Productivity" },
|
||||
"enable_dark_mode_profile": { "value": true },
|
||||
"dark_mode_profile": { "value": "Night Mode" }
|
||||
"enableLightModeProfile": { "value": true },
|
||||
"lightModeProfile": { "value": "" },
|
||||
"lightModeProfileId": { "value": 3 },
|
||||
"enableDarkModeProfile": { "value": true },
|
||||
"darkModeProfile": { "value": "" },
|
||||
"darkModeProfileId": { "value": 7 }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The name fields are retained only for migration from pre-ID settings; current code persists and resolves the positive ID fields.
|
||||
|
||||
---
|
||||
|
||||
## Data Flow and Communication
|
||||
@@ -1354,7 +1366,8 @@ sequenceDiagram
|
||||
participant SettingsPage as PowerDisplayPage
|
||||
participant ViewModel as PowerDisplayViewModel
|
||||
participant ProfileDialog as ProfileEditorDialog
|
||||
participant ProfileService
|
||||
participant ProfileHelper
|
||||
participant ProfileStore
|
||||
participant FileSystem as profiles.json
|
||||
|
||||
User->>SettingsPage: Clicks "Add Profile" button
|
||||
@@ -1369,20 +1382,21 @@ sequenceDiagram
|
||||
User->>ProfileDialog: Clicks "Save"
|
||||
|
||||
ProfileDialog->>ProfileDialog: Validate inputs
|
||||
Note over ProfileDialog: Check name unique,<br/>at least one monitor selected
|
||||
Note over ProfileDialog: Check non-empty name,<br/>at least one monitor selected
|
||||
|
||||
ProfileDialog-->>ViewModel: ResultProfile (PowerDisplayProfile)
|
||||
|
||||
ViewModel->>ProfileService: AddOrUpdateProfile(profile)
|
||||
ViewModel->>ProfileHelper: ProfileHelper.AddOrUpdateProfileAsync(profile)
|
||||
ProfileHelper->>ProfileStore: AddOrUpdateProfileAsync(profile)
|
||||
|
||||
ProfileService->>ProfileService: lock(_lock)
|
||||
ProfileService->>FileSystem: Read profiles.json
|
||||
FileSystem-->>ProfileService: Existing profiles
|
||||
ProfileService->>ProfileService: Add/update profile in collection
|
||||
ProfileService->>ProfileService: Set LastUpdated = DateTime.Now
|
||||
ProfileService->>FileSystem: Write profiles.json
|
||||
FileSystem-->>ProfileService: Success
|
||||
ProfileService-->>ViewModel: true
|
||||
ProfileStore->>ProfileStore: Acquire process lock and named mutex
|
||||
ProfileStore->>FileSystem: Read profiles.json
|
||||
FileSystem-->>ProfileStore: Existing profiles
|
||||
ProfileStore->>ProfileStore: Assign id and update profile
|
||||
ProfileStore->>FileSystem: Write temp file and atomically replace profiles.json
|
||||
FileSystem-->>ProfileStore: Success
|
||||
ProfileStore-->>ProfileHelper: Completed
|
||||
ProfileHelper-->>ViewModel: Completed
|
||||
|
||||
ViewModel->>ViewModel: RefreshProfilesList()
|
||||
ViewModel-->>SettingsPage: PropertyChanged(Profiles)
|
||||
@@ -1401,7 +1415,7 @@ sequenceDiagram
|
||||
participant EventWaiter as NativeEventWaiter
|
||||
participant LSSvc as LightSwitchService
|
||||
participant MainVM as MainViewModel
|
||||
participant ProfileService
|
||||
participant ProfileHelper
|
||||
participant MonitorVM as MonitorViewModel
|
||||
participant Controller as IMonitorController
|
||||
participant Monitor as Physical Monitor
|
||||
@@ -1412,8 +1426,8 @@ sequenceDiagram
|
||||
LightSwitch->>LightSwitch: EvaluateAndApplyIfNeeded()
|
||||
LightSwitch->>LightSwitch: ApplyTheme(isLight)
|
||||
|
||||
LightSwitch->>LightSwitch: NotifyPowerDisplay(isLight)
|
||||
Note over LightSwitch: Check if profile enabled
|
||||
LightSwitch->>LightSwitch: NotifyPowerDisplayThemeChanged(isLight)
|
||||
Note over LightSwitch: Publish the resulting theme only;<br/>PowerDisplay owns profile validation
|
||||
|
||||
alt isLight == true
|
||||
LightSwitch->>WinEvent: SetEvent("Local\\PowerToys_LightSwitch_LightTheme")
|
||||
@@ -1425,16 +1439,16 @@ sequenceDiagram
|
||||
EventWaiter->>WinEvent: WaitAny([lightEvent, darkEvent]) returns index
|
||||
|
||||
Note over EventWaiter: Theme determined from event:<br/>index 0 = Light, index 1 = Dark
|
||||
EventWaiter->>LSSvc: GetProfileForTheme(isLightMode)
|
||||
EventWaiter->>LSSvc: GetProfileIdForTheme(isLightMode)
|
||||
LSSvc->>LSSvc: Read LightSwitch/settings.json
|
||||
LSSvc-->>EventWaiter: profileName (or null)
|
||||
LSSvc-->>EventWaiter: profileId (or null)
|
||||
|
||||
EventWaiter->>MainVM: Dispatch to UI thread with profileName
|
||||
EventWaiter->>MainVM: Dispatch to UI thread with profileId
|
||||
|
||||
MainVM->>ProfileService: LoadProfiles()
|
||||
ProfileService-->>MainVM: PowerDisplayProfiles
|
||||
MainVM->>ProfileHelper: LoadProfilesAsync()
|
||||
ProfileHelper-->>MainVM: PowerDisplayProfiles
|
||||
|
||||
MainVM->>MainVM: Find profile by name
|
||||
MainVM->>MainVM: Find profile by id
|
||||
MainVM->>MainVM: ApplyProfileAsync(profile.MonitorSettings)
|
||||
|
||||
loop For each ProfileMonitorSetting
|
||||
|
||||
Reference in New Issue
Block a user