Files
PowerToys/src/settings-ui/Settings.UI.Library/LightSwitchProfileReferenceHelper.cs
moooyo 2b8e6247fc [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>
2026-07-16 14:33:21 +08:00

128 lines
4.0 KiB
C#

// 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.
#nullable enable
using System;
using PowerDisplay.Models;
namespace Microsoft.PowerToys.Settings.UI.Library
{
public static class LightSwitchProfileReferenceHelper
{
public const string NoneSentinel = "(None)";
public static int? GetProfileIdForTheme(LightSwitchProperties properties, bool isLightMode)
{
ArgumentNullException.ThrowIfNull(properties);
var enabled = isLightMode
? properties.EnableLightModeProfile.Value
: properties.EnableDarkModeProfile.Value;
var profileId = isLightMode
? properties.LightModeProfileId.Value
: properties.DarkModeProfileId.Value;
return enabled && profileId >= 1 ? profileId : null;
}
public static bool SetProfileId(
IntProperty idProperty,
StringProperty legacyNameProperty,
int profileId)
{
ArgumentNullException.ThrowIfNull(idProperty);
ArgumentNullException.ThrowIfNull(legacyNameProperty);
ArgumentOutOfRangeException.ThrowIfNegative(profileId);
if (idProperty.Value == profileId
&& string.IsNullOrEmpty(legacyNameProperty.Value))
{
return false;
}
idProperty.Value = profileId;
legacyNameProperty.Value = string.Empty;
return true;
}
public static bool ClearProfileIdReferences(
LightSwitchProperties properties,
int profileId)
{
ArgumentNullException.ThrowIfNull(properties);
ArgumentOutOfRangeException.ThrowIfLessThan(profileId, 1);
var changed = false;
if (properties.LightModeProfileId.Value == profileId)
{
properties.LightModeProfileId.Value = 0;
changed = true;
}
if (properties.DarkModeProfileId.Value == profileId)
{
properties.DarkModeProfileId.Value = 0;
changed = true;
}
return changed;
}
public static bool ReconcileReferences(
LightSwitchProperties properties,
PowerDisplayProfiles profiles)
{
ArgumentNullException.ThrowIfNull(properties);
ArgumentNullException.ThrowIfNull(profiles);
var changed = false;
changed |= ReconcileOne(
profiles,
properties.LightModeProfileId,
properties.LightModeProfile);
changed |= ReconcileOne(
profiles,
properties.DarkModeProfileId,
properties.DarkModeProfile);
return changed;
}
private static bool ReconcileOne(
PowerDisplayProfiles profiles,
IntProperty idProperty,
StringProperty legacyNameProperty)
{
var originalId = idProperty.Value;
var originalName = legacyNameProperty.Value;
if (originalId >= 1)
{
if (profiles.GetById(originalId) is null)
{
idProperty.Value = 0;
}
}
else if (!string.IsNullOrEmpty(originalName) && originalName != NoneSentinel)
{
var profile = profiles.GetLegacyProfileByName(originalName);
if (profile is not null && profile.Id >= 1)
{
idProperty.Value = profile.Id;
}
}
if (!string.IsNullOrEmpty(legacyNameProperty.Value))
{
legacyNameProperty.Value = string.Empty;
}
return idProperty.Value != originalId
|| legacyNameProperty.Value != originalName;
}
}
}