Introduce new utility PowerDisplay to control your monitor settings (#42642)

<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request
Introduce a new PowerToys' module PowerDisplay to let user can control
their monitor settings without touching monitor's button.

Support feature list:
Common:
1. Profiles support
2. Integration with LightSwitch (auto switch profile when theme change)
3. TrayIcon
4. Save and restore settings when startup
5. Shortcut
6. Rotation
7. GPO support
8. Auto re-discovery monitor when plugging and unplugging monitors.
9. Identify Monitors
10. Quick profile switch

Especially for DDC/CI monitor:
1. Brightness
2. Contrast
3. Volume
4. Color temperature (preset profile)
5. Input source
6. Power State (poweroff)


Design doc:
https://github.com/microsoft/PowerToys/blob/yuleng/display/pr/3/doc/devdocs/modules/powerdisplay/design.md

AOT compatibility:
I designed this module for AOT from the start, so I'm pretty sure at
least 95% of it is AOT compatible. But unfortunately, PowerToys still
have a AOT blocker to block this module publish with AOT.

Currently PowerToys will check the .net file version (file version not
lib version) to avoid crash. So, all modules should reference Common.UI
or add UseWPF to avoid overwrite the .net file with different version
(which may cause crash).

Todo:
- [ ] BugBash
- [ ] Icon
- [ ] IdentifyWindow UI improvement


Demo

Main UI:
<img width="546" height="671" alt="image"
src="https://github.com/user-attachments/assets/b0ad9ac5-8000-4365-a192-ab8c2d66d4f1"
/>

Input Source:
<img width="536" height="674" alt="image"
src="https://github.com/user-attachments/assets/80f9ccd7-4f8c-4201-b177-cc86c5bcc9e3"
/>


Settings UI:
<img width="1581" height="1191" alt="image"
src="https://github.com/user-attachments/assets/6a82e4bb-8f96-4f28-abf9-d7c45e1c8ef7"
/>

<img width="1525" height="1146" alt="image"
src="https://github.com/user-attachments/assets/aae81e65-08fd-453a-bf52-02a74f2fdea0"
/>



Closes: 
#42942
#42678
#41117
#38109
#35564
#34932
#28500
#1052
#18149

<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

- [x] Closes: #1052
- [x] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [x] **Tests:** Added/updated and all pass
- [x] **Localization:** All end-user-facing strings can be localized
- [x] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx

<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed

---------

Co-authored-by: Yu Leng <yuleng@microsoft.com>
Co-authored-by: Niels Laute <niels.laute@live.nl>
Co-authored-by: moooyo <lengyuchn@gmail.com>
Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
moooyo
2026-02-03 13:53:25 +08:00
committed by GitHub
parent b3e7c9d227
commit 18efa0559c
181 changed files with 20738 additions and 165 deletions

View File

@@ -65,6 +65,7 @@ namespace Microsoft.PowerToys.Settings.UI.Controls
AddFlyoutMenuItem(ModuleType.FancyZones);
AddFlyoutMenuItem(ModuleType.Hosts);
AddFlyoutMenuItem(ModuleType.LightSwitch);
AddFlyoutMenuItem(ModuleType.PowerDisplay);
AddFlyoutMenuItem(ModuleType.PowerLauncher);
AddFlyoutMenuItem(ModuleType.PowerOCR);
AddFlyoutMenuItem(ModuleType.RegistryPreview);
@@ -121,6 +122,7 @@ namespace Microsoft.PowerToys.Settings.UI.Controls
{
ModuleType.ColorPicker => SettingsRepository<ColorPickerSettings>.GetInstance(SettingsUtils.Default).SettingsConfig.Properties.ActivationShortcut.ToString(),
ModuleType.FancyZones => SettingsRepository<FancyZonesSettings>.GetInstance(SettingsUtils.Default).SettingsConfig.Properties.FancyzonesEditorHotkey.Value.ToString(),
ModuleType.PowerDisplay => SettingsRepository<PowerDisplaySettings>.GetInstance(SettingsUtils.Default).SettingsConfig.Properties.ActivationShortcut.ToString(),
ModuleType.LightSwitch => SettingsRepository<LightSwitchSettings>.GetInstance(SettingsUtils.Default).SettingsConfig.Properties.ToggleThemeHotkey.Value.ToString(),
ModuleType.PowerLauncher => SettingsRepository<PowerLauncherSettings>.GetInstance(SettingsUtils.Default).SettingsConfig.Properties.OpenPowerLauncher.ToString(),
ModuleType.PowerOCR => SettingsRepository<PowerOcrSettings>.GetInstance(SettingsUtils.Default).SettingsConfig.Properties.ActivationShortcut.ToString(),

View File

@@ -546,6 +546,23 @@ namespace Microsoft.PowerToys.Settings.UI.Library
}
}
private bool powerDisplay;
[JsonPropertyName("PowerDisplay")]
public bool PowerDisplay
{
get => powerDisplay;
set
{
if (powerDisplay != value)
{
LogTelemetryEvent(value);
powerDisplay = value;
NotifyChange();
}
}
}
private void NotifyChange()
{
notifyEnabledChangedAction?.Invoke();

View File

@@ -73,6 +73,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library.Helpers
ModuleType.MeasureTool => generalSettingsConfig.Enabled.MeasureTool,
ModuleType.ShortcutGuide => generalSettingsConfig.Enabled.ShortcutGuide,
ModuleType.PowerOCR => generalSettingsConfig.Enabled.PowerOcr,
ModuleType.PowerDisplay => generalSettingsConfig.Enabled.PowerDisplay,
ModuleType.Workspaces => generalSettingsConfig.Enabled.Workspaces,
ModuleType.ZoomIt => generalSettingsConfig.Enabled.ZoomIt,
ModuleType.GeneralSettings => generalSettingsConfig.EnableQuickAccess,
@@ -112,6 +113,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library.Helpers
case ModuleType.MeasureTool: generalSettingsConfig.Enabled.MeasureTool = isEnabled; break;
case ModuleType.ShortcutGuide: generalSettingsConfig.Enabled.ShortcutGuide = isEnabled; break;
case ModuleType.PowerOCR: generalSettingsConfig.Enabled.PowerOcr = isEnabled; break;
case ModuleType.PowerDisplay: generalSettingsConfig.Enabled.PowerDisplay = isEnabled; break;
case ModuleType.Workspaces: generalSettingsConfig.Enabled.Workspaces = isEnabled; break;
case ModuleType.ZoomIt: generalSettingsConfig.Enabled.ZoomIt = isEnabled; break;
case ModuleType.GeneralSettings: generalSettingsConfig.EnableQuickAccess = isEnabled; break;

View File

@@ -17,6 +17,10 @@ namespace Microsoft.PowerToys.Settings.UI.Library
public const string DefaultLatitude = "0.0";
public const string DefaultLongitude = "0.0";
public const string DefaultScheduleMode = "Off";
public const bool DefaultEnableDarkModeProfile = false;
public const bool DefaultEnableLightModeProfile = false;
public const string DefaultDarkModeProfile = "";
public const string DefaultLightModeProfile = "";
public static readonly HotkeySettings DefaultToggleThemeHotkey = new HotkeySettings(true, true, false, true, 0x44); // Ctrl+Win+Shift+D
public LightSwitchProperties()
@@ -31,6 +35,10 @@ namespace Microsoft.PowerToys.Settings.UI.Library
SunsetOffset = new IntProperty(DefaultSunsetOffset);
ScheduleMode = new StringProperty(DefaultScheduleMode);
ToggleThemeHotkey = new KeyboardKeysProperty(DefaultToggleThemeHotkey);
EnableDarkModeProfile = new BoolProperty(DefaultEnableDarkModeProfile);
EnableLightModeProfile = new BoolProperty(DefaultEnableLightModeProfile);
DarkModeProfile = new StringProperty(DefaultDarkModeProfile);
LightModeProfile = new StringProperty(DefaultLightModeProfile);
}
[JsonPropertyName("changeSystem")]
@@ -62,5 +70,17 @@ namespace Microsoft.PowerToys.Settings.UI.Library
[JsonPropertyName("toggle-theme-hotkey")]
public KeyboardKeysProperty ToggleThemeHotkey { get; set; }
[JsonPropertyName("enableDarkModeProfile")]
public BoolProperty EnableDarkModeProfile { get; set; }
[JsonPropertyName("enableLightModeProfile")]
public BoolProperty EnableLightModeProfile { get; set; }
[JsonPropertyName("darkModeProfile")]
public StringProperty DarkModeProfile { get; set; }
[JsonPropertyName("lightModeProfile")]
public StringProperty LightModeProfile { get; set; }
}
}

View File

@@ -60,6 +60,10 @@ namespace Microsoft.PowerToys.Settings.UI.Library
Latitude = new StringProperty(Properties.Latitude.Value),
Longitude = new StringProperty(Properties.Longitude.Value),
ToggleThemeHotkey = new KeyboardKeysProperty(Properties.ToggleThemeHotkey.Value),
EnableDarkModeProfile = new BoolProperty(Properties.EnableDarkModeProfile.Value),
EnableLightModeProfile = new BoolProperty(Properties.EnableLightModeProfile.Value),
DarkModeProfile = new StringProperty(Properties.DarkModeProfile.Value),
LightModeProfile = new StringProperty(Properties.LightModeProfile.Value),
},
};
}

View File

@@ -0,0 +1,694 @@
// 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.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text.Json.Serialization;
using Microsoft.PowerToys.Settings.UI.Library.Helpers;
using PowerDisplay.Common.Drivers;
using PowerDisplay.Common.Models;
using PowerDisplay.Common.Utils;
namespace Microsoft.PowerToys.Settings.UI.Library
{
public class MonitorInfo : Observable
{
private string _name = string.Empty;
private string _id = string.Empty;
private string _communicationMethod = string.Empty;
private int _currentBrightness;
private int _colorTemperatureVcp = 0x05; // Default to 6500K preset (VCP 0x14 value)
private int _contrast;
private int _volume;
private bool _isHidden;
private bool _enableContrast;
private bool _enableVolume;
private bool _enableInputSource;
private bool _enableRotation;
private bool _enableColorTemperature;
private bool _enablePowerState;
private string _capabilitiesRaw = string.Empty;
private List<VcpCodeDisplayInfo> _vcpCodesFormatted = new List<VcpCodeDisplayInfo>();
private int _monitorNumber;
private int _totalMonitorCount;
// Feature support status (determined from capabilities)
private bool _supportsBrightness = true; // Brightness always shown even if unsupported
private bool _supportsContrast;
private bool _supportsColorTemperature;
private bool _supportsVolume;
private bool _supportsInputSource;
private bool _supportsPowerState;
// Cached color temperature presets (computed from VcpCodesFormatted)
private ObservableCollection<ColorPresetItem> _availableColorPresetsCache;
private ObservableCollection<ColorPresetItem> _colorPresetsForDisplayCache;
private int _lastColorTemperatureVcpForCache = -1;
/// <summary>
/// Invalidates the color preset cache and notifies property changes.
/// Call this when VcpCodesFormatted or SupportsColorTemperature changes.
/// </summary>
private void InvalidateColorPresetCache()
{
_availableColorPresetsCache = null;
_colorPresetsForDisplayCache = null;
_lastColorTemperatureVcpForCache = -1;
OnPropertyChanged(nameof(ColorPresetsForDisplay));
}
public MonitorInfo()
{
}
[JsonPropertyName("name")]
public string Name
{
get => _name;
set
{
if (_name != value)
{
_name = value;
OnPropertyChanged();
OnPropertyChanged(nameof(DisplayName));
}
}
}
/// <summary>
/// Gets or sets the monitor number (Windows DISPLAY number, e.g., 1, 2, 3...).
/// </summary>
[JsonPropertyName("monitorNumber")]
public int MonitorNumber
{
get => _monitorNumber;
set
{
if (_monitorNumber != value)
{
_monitorNumber = value;
OnPropertyChanged();
OnPropertyChanged(nameof(DisplayName));
}
}
}
/// <summary>
/// Gets or sets the total number of monitors (used for dynamic display name).
/// This is not serialized; it's set by the ViewModel.
/// </summary>
[JsonIgnore]
public int TotalMonitorCount
{
get => _totalMonitorCount;
set
{
if (_totalMonitorCount != value)
{
_totalMonitorCount = value;
OnPropertyChanged(nameof(DisplayName));
}
}
}
/// <summary>
/// Gets the display name - includes monitor number when multiple monitors exist.
/// Follows the same logic as PowerDisplay UI's MonitorViewModel.DisplayName.
/// </summary>
[JsonIgnore]
public string DisplayName
{
get
{
// Show monitor number only when there are multiple monitors and MonitorNumber is valid
if (TotalMonitorCount > 1 && MonitorNumber > 0)
{
return $"{Name} {MonitorNumber}";
}
return Name;
}
}
public string MonitorIconGlyph => CommunicationMethod.Contains("WMI", StringComparison.OrdinalIgnoreCase)
? "\uE7F8" // Laptop icon for WMI
: "\uE7F4"; // External monitor icon for DDC/CI and others
[JsonPropertyName("id")]
public string Id
{
get => _id;
set
{
if (_id != value)
{
_id = value;
OnPropertyChanged();
}
}
}
[JsonPropertyName("communicationMethod")]
public string CommunicationMethod
{
get => _communicationMethod;
set
{
if (_communicationMethod != value)
{
_communicationMethod = value;
OnPropertyChanged();
}
}
}
[JsonPropertyName("currentBrightness")]
public int CurrentBrightness
{
get => _currentBrightness;
set
{
if (_currentBrightness != value)
{
_currentBrightness = value;
OnPropertyChanged();
}
}
}
/// <summary>
/// Gets or sets the color temperature VCP preset value (raw DDC/CI value from VCP code 0x14).
/// This stores the raw VCP value (e.g., 0x05 for 6500K preset), not the Kelvin temperature.
/// </summary>
[JsonPropertyName("colorTemperatureVcp")]
public int ColorTemperatureVcp
{
get => _colorTemperatureVcp;
set
{
if (_colorTemperatureVcp != value)
{
_colorTemperatureVcp = value;
OnPropertyChanged();
OnPropertyChanged(nameof(ColorPresetsForDisplay)); // Update display list when current value changes
}
}
}
/// <summary>
/// Gets or sets the current contrast value (0-100).
/// </summary>
[JsonPropertyName("contrast")]
public int Contrast
{
get => _contrast;
set
{
if (_contrast != value)
{
_contrast = value;
OnPropertyChanged();
}
}
}
/// <summary>
/// Gets or sets the current volume value (0-100).
/// </summary>
[JsonPropertyName("volume")]
public int Volume
{
get => _volume;
set
{
if (_volume != value)
{
_volume = value;
OnPropertyChanged();
}
}
}
[JsonPropertyName("isHidden")]
public bool IsHidden
{
get => _isHidden;
set
{
if (_isHidden != value)
{
_isHidden = value;
OnPropertyChanged();
}
}
}
[JsonPropertyName("enableContrast")]
public bool EnableContrast
{
get => _enableContrast;
set
{
if (_enableContrast != value)
{
_enableContrast = value;
OnPropertyChanged();
}
}
}
[JsonPropertyName("enableVolume")]
public bool EnableVolume
{
get => _enableVolume;
set
{
if (_enableVolume != value)
{
_enableVolume = value;
OnPropertyChanged();
}
}
}
[JsonPropertyName("enableInputSource")]
public bool EnableInputSource
{
get => _enableInputSource;
set
{
if (_enableInputSource != value)
{
_enableInputSource = value;
OnPropertyChanged();
}
}
}
[JsonPropertyName("enableRotation")]
public bool EnableRotation
{
get => _enableRotation;
set
{
if (_enableRotation != value)
{
_enableRotation = value;
OnPropertyChanged();
}
}
}
[JsonPropertyName("enableColorTemperature")]
public bool EnableColorTemperature
{
get => _enableColorTemperature;
set
{
if (_enableColorTemperature != value)
{
_enableColorTemperature = value;
OnPropertyChanged();
}
}
}
[JsonPropertyName("enablePowerState")]
public bool EnablePowerState
{
get => _enablePowerState;
set
{
if (_enablePowerState != value)
{
_enablePowerState = value;
OnPropertyChanged();
}
}
}
[JsonPropertyName("capabilitiesRaw")]
public string CapabilitiesRaw
{
get => _capabilitiesRaw;
set
{
if (_capabilitiesRaw != value)
{
_capabilitiesRaw = value ?? string.Empty;
OnPropertyChanged();
OnPropertyChanged(nameof(HasCapabilities));
}
}
}
[JsonPropertyName("vcpCodesFormatted")]
public List<VcpCodeDisplayInfo> VcpCodesFormatted
{
get => _vcpCodesFormatted;
set
{
var newValue = value ?? new List<VcpCodeDisplayInfo>();
// Only update if content actually changed (compare by VCP code list content)
if (AreVcpCodesEqual(_vcpCodesFormatted, newValue))
{
return;
}
_vcpCodesFormatted = newValue;
OnPropertyChanged();
InvalidateColorPresetCache();
}
}
/// <summary>
/// Compare two VcpCodesFormatted lists for equality by content.
/// Returns true if both lists have the same VCP codes (by code value).
/// </summary>
private static bool AreVcpCodesEqual(List<VcpCodeDisplayInfo> list1, List<VcpCodeDisplayInfo> list2)
{
if (list1 == null && list2 == null)
{
return true;
}
if (list1 == null || list2 == null)
{
return false;
}
if (list1.Count != list2.Count)
{
return false;
}
// Compare by code values - order matters for our use case
for (int i = 0; i < list1.Count; i++)
{
if (list1[i].Code != list2[i].Code)
{
return false;
}
// Also compare ValueList count to detect preset changes
var values1 = list1[i].ValueList;
var values2 = list2[i].ValueList;
if ((values1?.Count ?? 0) != (values2?.Count ?? 0))
{
return false;
}
}
return true;
}
[JsonPropertyName("supportsBrightness")]
public bool SupportsBrightness
{
get => _supportsBrightness;
set
{
if (_supportsBrightness != value)
{
_supportsBrightness = value;
OnPropertyChanged();
}
}
}
[JsonPropertyName("supportsContrast")]
public bool SupportsContrast
{
get => _supportsContrast;
set
{
if (_supportsContrast != value)
{
_supportsContrast = value;
OnPropertyChanged();
}
}
}
[JsonPropertyName("supportsColorTemperature")]
public bool SupportsColorTemperature
{
get => _supportsColorTemperature;
set
{
if (_supportsColorTemperature != value)
{
_supportsColorTemperature = value;
OnPropertyChanged();
InvalidateColorPresetCache(); // Notifies ColorPresetsForDisplay
}
}
}
[JsonPropertyName("supportsVolume")]
public bool SupportsVolume
{
get => _supportsVolume;
set
{
if (_supportsVolume != value)
{
_supportsVolume = value;
OnPropertyChanged();
}
}
}
[JsonPropertyName("supportsInputSource")]
public bool SupportsInputSource
{
get => _supportsInputSource;
set
{
if (_supportsInputSource != value)
{
_supportsInputSource = value;
OnPropertyChanged();
}
}
}
[JsonPropertyName("supportsPowerState")]
public bool SupportsPowerState
{
get => _supportsPowerState;
set
{
if (_supportsPowerState != value)
{
_supportsPowerState = value;
OnPropertyChanged();
}
}
}
/// <summary>
/// Gets available color temperature presets computed from VcpCodesFormatted (VCP code 0x14).
/// This is a computed property that parses the VCP capabilities data on-demand.
/// </summary>
private ObservableCollection<ColorPresetItem> AvailableColorPresets
{
get
{
// Return cached value if available
if (_availableColorPresetsCache != null)
{
return _availableColorPresetsCache;
}
// Compute from VcpCodesFormatted
_availableColorPresetsCache = ComputeAvailableColorPresets();
return _availableColorPresetsCache;
}
}
/// <summary>
/// Compute available color presets from VcpCodesFormatted (VCP code 0x14).
/// Uses ColorTemperatureHelper from PowerDisplay.Lib for shared computation logic.
/// </summary>
private ObservableCollection<ColorPresetItem> ComputeAvailableColorPresets()
{
// Check if color temperature is supported
if (!_supportsColorTemperature || _vcpCodesFormatted == null)
{
return new ObservableCollection<ColorPresetItem>();
}
// Find VCP code 0x14 (Color Temperature / Select Color Preset)
var colorTempVcp = _vcpCodesFormatted.FirstOrDefault(v =>
!string.IsNullOrEmpty(v.Code) &&
int.TryParse(
v.Code.StartsWith("0x", StringComparison.OrdinalIgnoreCase) ? v.Code[2..] : v.Code,
System.Globalization.NumberStyles.HexNumber,
System.Globalization.CultureInfo.InvariantCulture,
out int code) &&
code == NativeConstants.VcpCodeSelectColorPreset);
// No VCP 0x14 or no values
if (colorTempVcp == null || colorTempVcp.ValueList == null || colorTempVcp.ValueList.Count == 0)
{
return new ObservableCollection<ColorPresetItem>();
}
// Extract VCP values as tuples for ColorTemperatureHelper
var colorTempValues = colorTempVcp.ValueList
.Select(valueInfo =>
{
var hex = valueInfo.Value;
if (string.IsNullOrEmpty(hex))
{
return (VcpValue: 0, Name: valueInfo.Name);
}
var cleanHex = hex.StartsWith("0x", StringComparison.OrdinalIgnoreCase) ? hex[2..] : hex;
bool parsed = int.TryParse(cleanHex, System.Globalization.NumberStyles.HexNumber, System.Globalization.CultureInfo.InvariantCulture, out int vcpValue);
return (VcpValue: parsed ? vcpValue : 0, Name: valueInfo.Name);
})
.Where(x => x.VcpValue > 0);
// Use shared helper to compute presets, then convert to nested type for XAML compatibility
var basePresets = ColorTemperatureHelper.ComputeColorPresets(colorTempValues);
var presetList = basePresets.Select(p => new ColorPresetItem(p.VcpValue, p.DisplayName));
return new ObservableCollection<ColorPresetItem>(presetList);
}
/// <summary>
/// Gets color presets for display in ComboBox, includes current value if not in preset list.
/// Uses caching to avoid recreating collections on every access.
/// </summary>
[JsonIgnore]
public ObservableCollection<ColorPresetItem> ColorPresetsForDisplay
{
get
{
// Return cached value if available and color temperature hasn't changed
if (_colorPresetsForDisplayCache != null && _lastColorTemperatureVcpForCache == _colorTemperatureVcp)
{
return _colorPresetsForDisplayCache;
}
var presets = AvailableColorPresets;
if (presets == null || presets.Count == 0)
{
_colorPresetsForDisplayCache = new ObservableCollection<ColorPresetItem>();
_lastColorTemperatureVcpForCache = _colorTemperatureVcp;
return _colorPresetsForDisplayCache;
}
// Check if current value is in the preset list
var currentValueInList = presets.Any(p => p.VcpValue == _colorTemperatureVcp);
if (currentValueInList)
{
// Current value is in the list, return as-is
_colorPresetsForDisplayCache = presets;
}
else
{
// Current value is not in the preset list - add it at the beginning
var displayList = new List<ColorPresetItem>();
// Add current value with "Custom" indicator using shared helper
var displayName = ColorTemperatureHelper.FormatCustomColorTemperatureDisplayName(_colorTemperatureVcp);
displayList.Add(new ColorPresetItem(_colorTemperatureVcp, displayName));
// Add all supported presets
displayList.AddRange(presets);
_colorPresetsForDisplayCache = new ObservableCollection<ColorPresetItem>(displayList);
}
_lastColorTemperatureVcpForCache = _colorTemperatureVcp;
return _colorPresetsForDisplayCache;
}
}
[JsonIgnore]
public bool HasCapabilities => !string.IsNullOrEmpty(_capabilitiesRaw);
[JsonIgnore]
public bool ShowCapabilitiesWarning => _communicationMethod.Contains("WMI", StringComparison.OrdinalIgnoreCase);
/// <summary>
/// Generate formatted text of all VCP codes for clipboard
/// </summary>
public string GetVcpCodesAsText()
{
if (_vcpCodesFormatted == null || _vcpCodesFormatted.Count == 0)
{
return "No VCP codes detected";
}
var lines = new List<string>();
lines.Add($"VCP Capabilities for: {_name}");
lines.Add($"Monitor ID: {_id}");
lines.Add(string.Empty);
lines.Add("Detected VCP Codes:");
lines.Add(new string('-', 50));
foreach (var vcp in _vcpCodesFormatted)
{
lines.Add(string.Empty);
lines.Add(vcp.Title);
if (vcp.HasValues)
{
lines.Add($" {vcp.Values}");
}
}
lines.Add(string.Empty);
lines.Add(new string('-', 50));
lines.Add($"Total: {_vcpCodesFormatted.Count} VCP codes");
return string.Join(System.Environment.NewLine, lines);
}
/// <summary>
/// Update this monitor's properties from another MonitorInfo instance.
/// This preserves the object reference while updating all properties.
/// </summary>
/// <param name="other">The source MonitorInfo to copy properties from</param>
public void UpdateFrom(MonitorInfo other)
{
if (other == null)
{
return;
}
// Update all properties that can change
Name = other.Name;
Id = other.Id;
CommunicationMethod = other.CommunicationMethod;
CurrentBrightness = other.CurrentBrightness;
Contrast = other.Contrast;
Volume = other.Volume;
ColorTemperatureVcp = other.ColorTemperatureVcp;
IsHidden = other.IsHidden;
EnableContrast = other.EnableContrast;
EnableVolume = other.EnableVolume;
EnableInputSource = other.EnableInputSource;
EnableRotation = other.EnableRotation;
EnableColorTemperature = other.EnableColorTemperature;
EnablePowerState = other.EnablePowerState;
CapabilitiesRaw = other.CapabilitiesRaw;
VcpCodesFormatted = other.VcpCodesFormatted;
SupportsBrightness = other.SupportsBrightness;
SupportsContrast = other.SupportsContrast;
SupportsColorTemperature = other.SupportsColorTemperature;
SupportsVolume = other.SupportsVolume;
SupportsInputSource = other.SupportsInputSource;
SupportsPowerState = other.SupportsPowerState;
MonitorNumber = other.MonitorNumber;
}
}
}

View File

@@ -0,0 +1,32 @@
// 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.Text.Json.Serialization;
namespace Microsoft.PowerToys.Settings.UI.Library
{
/// <summary>
/// Message for PowerDisplay module actions
/// </summary>
public class PowerDisplayActionMessage
{
[JsonPropertyName("action")]
public ActionData Action { get; set; }
public class ActionData
{
[JsonPropertyName("PowerDisplay")]
public PowerDisplayAction PowerDisplay { get; set; }
}
public class PowerDisplayAction
{
[JsonPropertyName("action_name")]
public string ActionName { get; set; }
[JsonPropertyName("value")]
public string Value { get; set; }
}
}
}

View File

@@ -0,0 +1,65 @@
// 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.Generic;
using System.Text.Json.Serialization;
using PowerDisplay.Common.Models;
using Settings.UI.Library.Attributes;
namespace Microsoft.PowerToys.Settings.UI.Library
{
public class PowerDisplayProperties
{
[CmdConfigureIgnore]
public HotkeySettings DefaultActivationShortcut => new HotkeySettings(true, true, false, true, 0x44); // Ctrl+Shift+Win+D (win, ctrl, alt, shift, code)
public PowerDisplayProperties()
{
ActivationShortcut = DefaultActivationShortcut;
MonitorRefreshDelay = 5;
Monitors = new List<MonitorInfo>();
RestoreSettingsOnStartup = false;
ShowSystemTrayIcon = true;
ShowProfileSwitcher = true;
ShowIdentifyMonitorsButton = true;
// Note: saved_monitor_settings has been moved to monitor_state.json
// which is managed separately by PowerDisplay app
}
[JsonPropertyName("activation_shortcut")]
public HotkeySettings ActivationShortcut { get; set; }
/// <summary>
/// Gets or sets delay in seconds before refreshing monitors after display changes (hot-plug).
/// This allows hardware to stabilize before querying DDC/CI.
/// </summary>
[JsonPropertyName("monitor_refresh_delay")]
public int MonitorRefreshDelay { get; set; }
[JsonPropertyName("monitors")]
public List<MonitorInfo> Monitors { get; set; }
[JsonPropertyName("restore_settings_on_startup")]
public bool RestoreSettingsOnStartup { get; set; }
[JsonPropertyName("show_system_tray_icon")]
public bool ShowSystemTrayIcon { get; set; }
/// <summary>
/// Gets or sets whether to show the profile switcher button in the flyout UI.
/// Default is true. When false, the profile switcher is hidden (but profiles still work via Settings).
/// Note: Also hidden when no profiles exist.
/// </summary>
[JsonPropertyName("show_profile_switcher")]
public bool ShowProfileSwitcher { get; set; }
/// <summary>
/// Gets or sets whether to show the identify monitors button in the flyout UI.
/// Default is true.
/// </summary>
[JsonPropertyName("show_identify_monitors_button")]
public bool ShowIdentifyMonitorsButton { get; set; }
}
}

View File

@@ -0,0 +1,50 @@
// 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.Generic;
using System.Text.Json.Serialization;
using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Library.Helpers;
using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
namespace Microsoft.PowerToys.Settings.UI.Library
{
public class PowerDisplaySettings : BasePTModuleSettings, ISettingsConfig, IHotkeyConfig
{
public const string ModuleName = "PowerDisplay";
[JsonPropertyName("properties")]
public PowerDisplayProperties Properties { get; set; }
public PowerDisplaySettings()
{
Properties = new PowerDisplayProperties();
Version = "1";
Name = ModuleName;
}
public string GetModuleName()
=> Name;
// This can be utilized in the future if the settings.json file is to be modified/deleted.
public bool UpgradeSettingsConfiguration()
=> false;
public ModuleType GetModuleType() => ModuleType.PowerDisplay;
public HotkeyAccessor[] GetAllHotkeyAccessors()
{
var hotkeyAccessors = new List<HotkeyAccessor>
{
new HotkeyAccessor(
() => Properties.ActivationShortcut,
value => Properties.ActivationShortcut = value ?? Properties.DefaultActivationShortcut,
"Activation_Shortcut"),
};
return hotkeyAccessors.ToArray();
}
}
}

View File

@@ -23,6 +23,7 @@
<ProjectReference Include="..\..\common\ManagedCommon\ManagedCommon.csproj" />
<ProjectReference Include="..\..\common\ManagedTelemetry\Telemetry\ManagedTelemetry.csproj" />
<ProjectReference Include="..\..\modules\MouseUtils\MouseJump.Common\MouseJump.Common.csproj" />
<ProjectReference Include="..\..\modules\powerdisplay\PowerDisplay.Lib\PowerDisplay.Lib.csproj" />
</ItemGroup>
<ItemGroup>

View File

@@ -2,7 +2,9 @@
// 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.Generic;
using System.Text.Json.Serialization;
using PowerDisplay.Common.Models;
using SettingsUILibrary = Settings.UI.Library;
using SettingsUILibraryHelpers = Settings.UI.Library.Helpers;
@@ -65,6 +67,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library
[JsonSerializable(typeof(NewPlusSettings))]
[JsonSerializable(typeof(PeekSettings))]
[JsonSerializable(typeof(PowerAccentSettings))]
[JsonSerializable(typeof(PowerDisplaySettings))]
[JsonSerializable(typeof(PowerLauncherSettings))]
[JsonSerializable(typeof(PowerOcrSettings))]
[JsonSerializable(typeof(PowerPreviewSettings))]
@@ -102,6 +105,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library
[JsonSerializable(typeof(PeekProperties))]
[JsonSerializable(typeof(SettingsUILibrary.PeekPreviewSettings))]
[JsonSerializable(typeof(PowerAccentProperties))]
[JsonSerializable(typeof(PowerDisplayProperties))]
[JsonSerializable(typeof(PowerLauncherProperties))]
[JsonSerializable(typeof(PowerOcrProperties))]
[JsonSerializable(typeof(PowerPreviewProperties))]
@@ -134,13 +138,33 @@ namespace Microsoft.PowerToys.Settings.UI.Library
[JsonSerializable(typeof(AdvancedPasteAdditionalAction))]
[JsonSerializable(typeof(AdvancedPastePasteAsFileAction))]
[JsonSerializable(typeof(AdvancedPasteTranscodeAction))]
[JsonSerializable(typeof(PasteAIConfiguration))]
[JsonSerializable(typeof(PasteAIProviderDefinition))]
[JsonSerializable(typeof(ImageResizerSizes))]
[JsonSerializable(typeof(ImageResizerCustomSizeProperty))]
[JsonSerializable(typeof(KeyboardKeysProperty))]
[JsonSerializable(typeof(MonitorInfo))]
[JsonSerializable(typeof(PowerDisplayActionMessage))]
[JsonSerializable(typeof(PowerDisplayActionMessage.ActionData))]
[JsonSerializable(typeof(PowerDisplayActionMessage.PowerDisplayAction))]
[JsonSerializable(typeof(VcpCodeDisplayInfo))]
[JsonSerializable(typeof(VcpValueInfo))]
[JsonSerializable(typeof(List<string>))]
[JsonSerializable(typeof(List<MonitorInfo>))]
[JsonSerializable(typeof(List<VcpCodeDisplayInfo>))]
[JsonSerializable(typeof(List<VcpValueInfo>))]
[JsonSerializable(typeof(SettingsUILibraryHelpers.SearchLocation))]
// AdvancedPaste AI Provider Types (for AOT compatibility)
[JsonSerializable(typeof(PasteAIConfiguration))]
[JsonSerializable(typeof(PasteAIProviderDefinition))]
[JsonSerializable(typeof(System.Collections.ObjectModel.ObservableCollection<PasteAIProviderDefinition>))]
// PowerDisplay Profile Types (for AOT compatibility)
[JsonSerializable(typeof(PowerDisplayProfile))]
[JsonSerializable(typeof(List<PowerDisplayProfile>))]
[JsonSerializable(typeof(PowerDisplayProfiles))]
[JsonSerializable(typeof(ProfileMonitorSetting))]
[JsonSerializable(typeof(List<ProfileMonitorSetting>))]
// IPC Send Message Wrapper Classes (Snd*)
[JsonSerializable(typeof(SndAwakeSettings))]
[JsonSerializable(typeof(SndCursorWrapSettings))]

View File

@@ -0,0 +1,29 @@
// 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.Text.Json.Serialization;
namespace Microsoft.PowerToys.Settings.UI.Library
{
/// <summary>
/// Formatted VCP code display information
/// </summary>
public class VcpCodeDisplayInfo
{
[JsonPropertyName("code")]
public string Code { get; set; } = string.Empty;
[JsonPropertyName("title")]
public string Title { get; set; } = string.Empty;
[JsonPropertyName("values")]
public string Values { get; set; } = string.Empty;
[JsonPropertyName("hasValues")]
public bool HasValues { get; set; }
[JsonPropertyName("valueList")]
public System.Collections.Generic.List<VcpValueInfo> ValueList { get; set; } = new System.Collections.Generic.List<VcpValueInfo>();
}
}

View File

@@ -0,0 +1,20 @@
// 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.Text.Json.Serialization;
namespace Microsoft.PowerToys.Settings.UI.Library
{
/// <summary>
/// Individual VCP value information
/// </summary>
public class VcpValueInfo
{
[JsonPropertyName("value")]
public string Value { get; set; } = string.Empty;
[JsonPropertyName("name")]
public string Name { get; set; } = string.Empty;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 MiB

View File

@@ -44,6 +44,7 @@ namespace Microsoft.PowerToys.Settings.UI.Helpers
case ModuleType.MeasureTool: return GPOWrapper.GetConfiguredScreenRulerEnabledValue();
case ModuleType.ShortcutGuide: return GPOWrapper.GetConfiguredShortcutGuideEnabledValue();
case ModuleType.PowerOCR: return GPOWrapper.GetConfiguredTextExtractorEnabledValue();
case ModuleType.PowerDisplay: return GPOWrapper.GetConfiguredPowerDisplayEnabledValue();
case ModuleType.ZoomIt: return GPOWrapper.GetConfiguredZoomItEnabledValue();
default: return GpoRuleConfigured.Unavailable;
}
@@ -83,6 +84,7 @@ namespace Microsoft.PowerToys.Settings.UI.Helpers
ModuleType.MeasureTool => typeof(MeasureToolPage),
ModuleType.ShortcutGuide => typeof(ShortcutGuidePage),
ModuleType.PowerOCR => typeof(PowerOcrPage),
ModuleType.PowerDisplay => typeof(PowerDisplayPage),
ModuleType.ZoomIt => typeof(ZoomItPage),
_ => typeof(DashboardPage), // never called, all values listed above
};

View File

@@ -33,6 +33,7 @@ namespace Microsoft.PowerToys.Settings.UI.OOBE.Enums
Hosts,
Workspaces,
RegistryPreview,
PowerDisplay,
NewPlus,
ZoomIt,
}

View File

@@ -123,6 +123,7 @@
<ProjectReference Include="..\..\common\ManagedTelemetry\Telemetry\ManagedTelemetry.csproj" />
<ProjectReference Include="..\..\common\LanguageModelProvider\LanguageModelProvider.csproj" />
<ProjectReference Include="..\..\modules\MouseUtils\MouseJump.Common\MouseJump.Common.csproj" />
<ProjectReference Include="..\..\modules\powerdisplay\PowerDisplay.Lib\PowerDisplay.Lib.csproj" />
<ProjectReference Include="..\Settings.UI.Library\Settings.UI.Library.csproj" />
<ProjectReference Include="..\Settings.UI.Controls\Settings.UI.Controls.csproj" />
</ItemGroup>

View File

@@ -19,6 +19,7 @@ namespace Microsoft.PowerToys.Settings.UI.SerializationContext;
[JsonSerializable(typeof(AlwaysOnTopSettings))]
[JsonSerializable(typeof(ColorPickerSettings))]
[JsonSerializable(typeof(CropAndLockSettings))]
[JsonSerializable(typeof(CursorWrapSettings))]
[JsonSerializable(typeof(Dictionary<string, List<string>>))]
[JsonSerializable(typeof(FileLocksmithSettings))]
[JsonSerializable(typeof(FindMyMouseSettings))]
@@ -34,11 +35,16 @@ namespace Microsoft.PowerToys.Settings.UI.SerializationContext;
[JsonSerializable(typeof(PowerLauncherSettings))]
[JsonSerializable(typeof(PowerOcrSettings))]
[JsonSerializable(typeof(PowerOcrSettings))]
[JsonSerializable(typeof(PowerDisplaySettings))]
[JsonSerializable(typeof(RegistryPreviewSettings))]
[JsonSerializable(typeof(ShortcutConflictProperties))]
[JsonSerializable(typeof(ShortcutGuideSettings))]
[JsonSerializable(typeof(WINDOWPLACEMENT))]
[JsonSerializable(typeof(WorkspacesSettings))]
[JsonSerializable(typeof(ZoomItSettings))]
[JsonSerializable(typeof(PasteAIConfiguration))]
[JsonSerializable(typeof(PasteAIProviderDefinition))]
[JsonSerializable(typeof(System.Collections.ObjectModel.ObservableCollection<PasteAIProviderDefinition>))]
public sealed partial class SourceGenerationContextContext : JsonSerializerContext
{
}

View File

@@ -415,6 +415,7 @@ namespace Microsoft.PowerToys.Settings.UI
case "Workspaces": return typeof(WorkspacesPage);
case "CmdPal": return typeof(CmdPalPage);
case "ZoomIt": return typeof(ZoomItPage);
case "PowerDisplay": return typeof(PowerDisplayPage);
default:
// Fallback to Dashboard
Debug.Assert(false, "Unexpected SettingsWindow argument value");

View File

@@ -0,0 +1,36 @@
<Page
x:Class="Microsoft.PowerToys.Settings.UI.OOBE.Views.OobePowerDisplay"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:Microsoft.PowerToys.Settings.UI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:tkcontrols="using:CommunityToolkit.WinUI.Controls"
mc:Ignorable="d">
<controls:OOBEPageControl x:Uid="Oobe_PowerDisplay" HeroImage="ms-appx:///Assets/Settings/Modules/OOBE/PowerDisplay.gif">
<controls:OOBEPageControl.PageContent>
<StackPanel Orientation="Vertical" Spacing="12">
<TextBlock x:Uid="Oobe_HowToLaunch" Style="{ThemeResource OobeSubtitleStyle}" />
<controls:ShortcutWithTextLabelControl x:Name="HotkeyActivation" x:Uid="Oobe_PowerDisplay_Activation" />
<TextBlock x:Uid="Oobe_TipsAndTricks" Style="{ThemeResource OobeSubtitleStyle}" />
<tkcontrols:MarkdownTextBlock x:Uid="Oobe_PowerDisplay_TipsAndTricks" />
<StackPanel Orientation="Horizontal" Spacing="8">
<Button
x:Uid="Launch_PowerDisplay"
Click="Launch_PowerDisplay_Click"
Style="{StaticResource AccentButtonStyle}" />
<Button x:Uid="OOBE_Settings" Click="SettingsLaunchButton_Click" />
<HyperlinkButton NavigateUri="https://aka.ms/PowerToysOverview_PowerDisplay" Style="{StaticResource TextButtonStyle}">
<TextBlock x:Uid="LearnMore_PowerDisplay" TextWrapping="Wrap" />
</HyperlinkButton>
</StackPanel>
</StackPanel>
</controls:OOBEPageControl.PageContent>
</controls:OOBEPageControl>
</Page>

View File

@@ -0,0 +1,60 @@
// 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.Threading;
using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.PowerToys.Settings.UI.OOBE.Enums;
using Microsoft.PowerToys.Settings.UI.OOBE.ViewModel;
using Microsoft.PowerToys.Settings.UI.Views;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Navigation;
using PowerToys.Interop;
namespace Microsoft.PowerToys.Settings.UI.OOBE.Views
{
/// <summary>
/// An empty page that can be used on its own or navigated to within a Frame.
/// </summary>
public sealed partial class OobePowerDisplay : Page
{
public OobePowerToysModule ViewModel { get; set; }
public OobePowerDisplay()
{
this.InitializeComponent();
ViewModel = new OobePowerToysModule(OobeShellPage.OobeShellHandler.Modules[(int)PowerToysModules.PowerDisplay]);
DataContext = ViewModel;
}
private void Launch_PowerDisplay_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
{
using (var eventHandle = new EventWaitHandle(false, EventResetMode.AutoReset, Constants.TogglePowerDisplayEvent()))
{
eventHandle.Set();
}
}
private void SettingsLaunchButton_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
{
if (OobeShellPage.OpenMainWindowCallback != null)
{
OobeShellPage.OpenMainWindowCallback(typeof(PowerDisplayPage));
}
ViewModel.LogOpeningSettingsEvent();
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
ViewModel.LogOpeningModuleEvent();
HotkeyActivation.Keys = SettingsRepository<PowerDisplaySettings>.GetInstance(SettingsUtils.Default).SettingsConfig.Properties.ActivationShortcut.GetKeysList();
}
protected override void OnNavigatedFrom(NavigationEventArgs e)
{
ViewModel.LogClosingModuleEvent();
}
}
}

View File

@@ -109,6 +109,10 @@
x:Uid="Shell_MouseUtilities"
Icon="{ui:BitmapIcon Source=/Assets/Settings/Icons/MouseUtils.png}"
Tag="MouseUtils" />
<NavigationViewItem
x:Uid="Shell_PowerDisplay"
Icon="{ui:BitmapIcon Source=/Assets/Settings/Icons/PowerDisplay.png}"
Tag="PowerDisplay" />
<NavigationViewItem
x:Uid="Shell_MouseWithoutBorders"
Icon="{ui:BitmapIcon Source=/Assets/Settings/Icons/MouseWithoutBorders.png}"

View File

@@ -206,6 +206,12 @@ namespace Microsoft.PowerToys.Settings.UI.OOBE.Views
IsNew = false,
});
Modules.Insert((int)PowerToysModules.PowerDisplay, new OobePowerToysModule()
{
ModuleName = "PowerDisplay",
IsNew = true,
});
Modules.Insert((int)PowerToysModules.NewPlus, new OobePowerToysModule()
{
ModuleName = "NewPlus",
@@ -301,6 +307,7 @@ namespace Microsoft.PowerToys.Settings.UI.OOBE.Views
case "Peek": NavigationFrame.Navigate(typeof(OobePeek)); break;
case "NewPlus": NavigationFrame.Navigate(typeof(OobeNewPlus)); break;
case "Workspaces": NavigationFrame.Navigate(typeof(OobeWorkspaces)); break;
case "PowerDisplay": NavigationFrame.Navigate(typeof(OobePowerDisplay)); break;
case "ZoomIt": NavigationFrame.Navigate(typeof(OobeZoomIt)); break;
}
}

View File

@@ -191,29 +191,83 @@
IsOpen="True"
Severity="Informational"
Visibility="Collapsed" />
<controls:SettingsGroup x:Uid="LightSwitch_BehaviorSettingsGroup" IsEnabled="{x:Bind ViewModel.IsEnabled, Mode=OneWay}">
<tkcontrols:SettingsExpander
x:Uid="LightSwitch_ApplyDarkModeExpander"
HeaderIcon="{ui:FontIcon Glyph=&#xE790;}"
IsExpanded="True">
<tkcontrols:SettingsExpander.Items>
<tkcontrols:SettingsCard HorizontalContentAlignment="Stretch" ContentAlignment="Left">
<controls:CheckBoxWithDescriptionControl
x:Uid="LightSwitch_SystemCheckbox"
AutomationProperties.AutomationId="ChangeSystemCheckbox_LightSwitch"
IsChecked="{x:Bind ViewModel.ChangeSystem, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard HorizontalContentAlignment="Stretch" ContentAlignment="Left">
<controls:CheckBoxWithDescriptionControl
x:Uid="LightSwitch_AppsCheckbox"
AutomationProperties.AutomationId="ChangeAppsCheckbox_LightSwitch"
IsChecked="{x:Bind ViewModel.ChangeApps, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
</tkcontrols:SettingsExpander.Items>
</tkcontrols:SettingsExpander>
</controls:SettingsGroup>
<!-- Force mode buttons -->
<!--<tkcontrols:SettingsCard
</controls:SettingsGroup>
<controls:SettingsGroup x:Uid="LightSwitch_BehaviorSettingsGroup" IsEnabled="{x:Bind ViewModel.IsEnabled, Mode=OneWay}">
<tkcontrols:SettingsExpander
x:Uid="LightSwitch_ApplyDarkModeExpander"
HeaderIcon="{ui:FontIcon Glyph=&#xE790;}"
IsExpanded="True">
<tkcontrols:SettingsExpander.Items>
<tkcontrols:SettingsCard HorizontalContentAlignment="Stretch" ContentAlignment="Left">
<controls:CheckBoxWithDescriptionControl
x:Uid="LightSwitch_SystemCheckbox"
AutomationProperties.AutomationId="ChangeSystemCheckbox_LightSwitch"
IsChecked="{x:Bind ViewModel.ChangeSystem, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard HorizontalContentAlignment="Stretch" ContentAlignment="Left">
<controls:CheckBoxWithDescriptionControl
x:Uid="LightSwitch_AppsCheckbox"
AutomationProperties.AutomationId="ChangeAppsCheckbox_LightSwitch"
IsChecked="{x:Bind ViewModel.ChangeApps, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
</tkcontrols:SettingsExpander.Items>
</tkcontrols:SettingsExpander>
<InfoBar
x:Name="PowerDisplayDisabledWarningBar"
x:Uid="LightSwitch_PowerDisplayDisabledWarningBar"
Background="Transparent"
BorderBrush="Transparent"
IsClosable="False"
IsOpen="True"
IsTabStop="True"
Severity="Informational"
Visibility="{x:Bind ViewModel.ShowPowerDisplayDisabledWarning, Mode=OneWay}">
<InfoBar.ActionButton>
<HyperlinkButton
x:Uid="LightSwitch_NavigatePowerDisplaySettings"
HorizontalAlignment="Right"
Click="NavigatePowerDisplaySettings_Click" />
</InfoBar.ActionButton>
</InfoBar>
<tkcontrols:SettingsExpander
x:Uid="LightSwitch_ApplyMonitorSettingsExpander"
HeaderIcon="{ui:FontIcon Glyph=&#xE7F4;}"
IsExpanded="True">
<tkcontrols:SettingsExpander.Items>
<tkcontrols:SettingsCard>
<tkcontrols:SettingsCard.Header>
<CheckBox
x:Uid="LightSwitch_DarkModeProfileCheckbox"
IsChecked="{x:Bind ViewModel.EnableDarkModeProfile, Mode=TwoWay}"
IsEnabled="{x:Bind ViewModel.IsPowerDisplayEnabled, Mode=OneWay}" />
</tkcontrols:SettingsCard.Header>
<ComboBox
MinWidth="200"
DisplayMemberPath="Name"
IsEnabled="{x:Bind ViewModel.IsPowerDisplayEnabled, Mode=OneWay}"
ItemsSource="{x:Bind ViewModel.AvailableProfiles, Mode=OneWay}"
SelectedItem="{x:Bind ViewModel.SelectedDarkModeProfile, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard>
<tkcontrols:SettingsCard.Header>
<CheckBox
x:Uid="LightSwitch_LightModeProfileCheckbox"
IsChecked="{x:Bind ViewModel.EnableLightModeProfile, Mode=TwoWay}"
IsEnabled="{x:Bind ViewModel.IsPowerDisplayEnabled, Mode=OneWay}" />
</tkcontrols:SettingsCard.Header>
<ComboBox
MinWidth="200"
DisplayMemberPath="Name"
IsEnabled="{x:Bind ViewModel.IsPowerDisplayEnabled, Mode=OneWay}"
ItemsSource="{x:Bind ViewModel.AvailableProfiles, Mode=OneWay}"
SelectedItem="{x:Bind ViewModel.SelectedLightModeProfile, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
</tkcontrols:SettingsExpander.Items>
</tkcontrols:SettingsExpander>
</controls:SettingsGroup>
<!-- Force mode buttons -->
<!--<tkcontrols:SettingsCard
Header="Force mode now"
HeaderIcon="{ui:FontIcon Glyph=&#xE706;}"
Description="Apply light or dark mode immediately">
@@ -227,24 +281,24 @@
</StackPanel>
</tkcontrols:SettingsCard>-->
<ContentDialog
x:Name="LocationDialog"
x:Uid="LightSwitch_LocationDialog"
IsPrimaryButtonEnabled="False"
IsSecondaryButtonEnabled="True"
PrimaryButtonClick="LocationDialog_PrimaryButtonClick"
PrimaryButtonStyle="{StaticResource AccentButtonStyle}">
<Grid RowSpacing="16">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" MinHeight="64" />
</Grid.RowDefinitions>
<TextBlock
x:Uid="LightSwitch_LocationDialog_Description"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
TextWrapping="Wrap" />
<!--<AutoSuggestBox
<ContentDialog
x:Name="LocationDialog"
x:Uid="LightSwitch_LocationDialog"
IsPrimaryButtonEnabled="False"
IsSecondaryButtonEnabled="True"
PrimaryButtonClick="LocationDialog_PrimaryButtonClick"
PrimaryButtonStyle="{StaticResource AccentButtonStyle}">
<Grid RowSpacing="16">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" MinHeight="64" />
</Grid.RowDefinitions>
<TextBlock
x:Uid="LightSwitch_LocationDialog_Description"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
TextWrapping="Wrap" />
<!--<AutoSuggestBox
x:Name="CityAutoSuggestBox"
Grid.Row="1"
Margin="0,16,0,8"
@@ -272,100 +326,99 @@
</AutoSuggestBox.ItemTemplate>
</AutoSuggestBox>-->
<Grid
Grid.Row="1"
Margin="0,12,0,0"
ColumnSpacing="4">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="124" />
<ColumnDefinition Width="124" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<NumberBox
x:Name="LatitudeBox"
x:Uid="LightSwitch_LatitudeBox"
AutomationProperties.AutomationId="LatitudeBox_LightSwitch"
Maximum="90"
Minimum="-90"
ValueChanged="LatLonBox_ValueChanged"
Value="{x:Bind ViewModel.LocationPanelLatitude, Mode=TwoWay}" />
<NumberBox
x:Name="LongitudeBox"
x:Uid="LightSwitch_LongitudeBox"
Grid.Column="1"
AutomationProperties.AutomationId="LongitudeBox_LightSwitch"
Maximum="180"
Minimum="-180"
ValueChanged="LatLonBox_ValueChanged"
Value="{x:Bind ViewModel.LocationPanelLongitude, Mode=TwoWay}" />
<Button
x:Name="SyncButton"
x:Uid="LightSwitch_FindLocationAutomation"
Grid.Column="2"
Margin="4,0,0,0"
HorizontalAlignment="Stretch"
VerticalAlignment="Bottom"
AutomationProperties.AutomationId="SyncLocationButton_LightSwitch"
Click="GetGeoLocation_Click">
<StackPanel Orientation="Horizontal" Spacing="8">
<FontIcon FontSize="16" Glyph="&#xECAF;" />
<TextBlock x:Uid="LightSwitch_FindLocation" />
</StackPanel>
</Button>
</Grid>
<ProgressRing
x:Name="SyncLoader"
Grid.Row="2"
Width="40"
Height="40"
VerticalAlignment="Center"
IsActive="False"
Visibility="Collapsed" />
<Grid
x:Name="LocationResultPanel"
Grid.Row="2"
<Grid
Grid.Row="1"
Margin="0,12,0,0"
ColumnSpacing="4">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="124" />
<ColumnDefinition Width="124" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<NumberBox
x:Name="LatitudeBox"
x:Uid="LightSwitch_LatitudeBox"
AutomationProperties.AutomationId="LatitudeBox_LightSwitch"
Maximum="90"
Minimum="-90"
ValueChanged="LatLonBox_ValueChanged"
Value="{x:Bind ViewModel.LocationPanelLatitude, Mode=TwoWay}" />
<NumberBox
x:Name="LongitudeBox"
x:Uid="LightSwitch_LongitudeBox"
Grid.Column="1"
AutomationProperties.AutomationId="LongitudeBox_LightSwitch"
Maximum="180"
Minimum="-180"
ValueChanged="LatLonBox_ValueChanged"
Value="{x:Bind ViewModel.LocationPanelLongitude, Mode=TwoWay}" />
<Button
x:Name="SyncButton"
x:Uid="LightSwitch_FindLocationAutomation"
Grid.Column="2"
Margin="4,0,0,0"
HorizontalAlignment="Stretch"
VerticalAlignment="Bottom"
ColumnSpacing="16"
RowSpacing="12"
Visibility="Collapsed">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<FontIcon FontSize="20" Glyph="&#xED39;">
<ToolTipService.ToolTip>
<TextBlock x:Uid="LightSwitch_SunriseTooltip" />
</ToolTipService.ToolTip>
</FontIcon>
<TextBlock
Grid.Column="1"
AutomationProperties.AutomationId="SunriseText_LightSwitch"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Text="{x:Bind ViewModel.LocationPanelLightTime, Converter={StaticResource TimeSpanToFriendlyTimeConverter}, Mode=OneWay}"
TextAlignment="Left" />
<FontIcon
Grid.Row="1"
FontSize="20"
Glyph="&#xED3A;">
<ToolTipService.ToolTip>
<TextBlock x:Uid="LightSwitch_SunsetTooltip" />
</ToolTipService.ToolTip>
</FontIcon>
<TextBlock
Grid.Row="1"
Grid.Column="1"
AutomationProperties.AutomationId="SunsetText_LightSwitch"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Text="{x:Bind ViewModel.LocationPanelDarkTime, Converter={StaticResource TimeSpanToFriendlyTimeConverter}, Mode=OneWay}"
TextAlignment="Left" />
</Grid>
AutomationProperties.AutomationId="SyncLocationButton_LightSwitch"
Click="GetGeoLocation_Click">
<StackPanel Orientation="Horizontal" Spacing="8">
<FontIcon FontSize="16" Glyph="&#xECAF;" />
<TextBlock x:Uid="LightSwitch_FindLocation" />
</StackPanel>
</Button>
</Grid>
</ContentDialog>
</controls:SettingsGroup>
<ProgressRing
x:Name="SyncLoader"
Grid.Row="2"
Width="40"
Height="40"
VerticalAlignment="Center"
IsActive="False"
Visibility="Collapsed" />
<Grid
x:Name="LocationResultPanel"
Grid.Row="2"
VerticalAlignment="Bottom"
ColumnSpacing="16"
RowSpacing="12"
Visibility="Collapsed">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<FontIcon FontSize="20" Glyph="&#xED39;">
<ToolTipService.ToolTip>
<TextBlock x:Uid="LightSwitch_SunriseTooltip" />
</ToolTipService.ToolTip>
</FontIcon>
<TextBlock
Grid.Column="1"
AutomationProperties.AutomationId="SunriseText_LightSwitch"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Text="{x:Bind ViewModel.LocationPanelLightTime, Converter={StaticResource TimeSpanToFriendlyTimeConverter}, Mode=OneWay}"
TextAlignment="Left" />
<FontIcon
Grid.Row="1"
FontSize="20"
Glyph="&#xED3A;">
<ToolTipService.ToolTip>
<TextBlock x:Uid="LightSwitch_SunsetTooltip" />
</ToolTipService.ToolTip>
</FontIcon>
<TextBlock
Grid.Row="1"
Grid.Column="1"
AutomationProperties.AutomationId="SunsetText_LightSwitch"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Text="{x:Bind ViewModel.LocationPanelDarkTime, Converter={StaticResource TimeSpanToFriendlyTimeConverter}, Mode=OneWay}"
TextAlignment="Left" />
</Grid>
</Grid>
</ContentDialog>
</StackPanel>
</controls:SettingsPageControl.ModuleContent>

View File

@@ -8,6 +8,7 @@ using System.IO;
using System.IO.Abstractions;
using System.Linq;
using System.Threading.Tasks;
using Common.UI;
using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Helpers;
using Microsoft.PowerToys.Settings.UI.Library;
@@ -380,5 +381,10 @@ namespace Microsoft.PowerToys.Settings.UI.Views
this.LocationWarningBar.Visibility = Visibility.Visible;
}
}
private void NavigatePowerDisplaySettings_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
{
ShellPage.Navigate(typeof(PowerDisplayPage));
}
}
}

View File

@@ -0,0 +1,263 @@
<local:NavigablePage
x:Class="Microsoft.PowerToys.Settings.UI.Views.PowerDisplayPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:Microsoft.PowerToys.Settings.UI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:library="using:Microsoft.PowerToys.Settings.UI.Library"
xmlns:local="using:Microsoft.PowerToys.Settings.UI.Helpers"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:pdmodels="using:PowerDisplay.Common.Models"
xmlns:tkcontrols="using:CommunityToolkit.WinUI.Controls"
xmlns:ui="using:CommunityToolkit.WinUI"
AutomationProperties.LandmarkType="Main"
mc:Ignorable="d">
<controls:SettingsPageControl x:Uid="PowerDisplay" ModuleImageSource="ms-appx:///Assets/Settings/Modules/PowerDisplay.png">
<controls:SettingsPageControl.ModuleContent>
<StackPanel ChildrenTransitions="{StaticResource SettingsCardsAnimations}" Orientation="Vertical">
<controls:GPOInfoControl ShowWarning="{x:Bind ViewModel.IsEnabledGpoConfigured, Mode=OneWay}">
<tkcontrols:SettingsCard
x:Uid="PowerDisplay_Enable_PowerDisplay"
HeaderIcon="{ui:BitmapIcon Source=/Assets/Settings/Icons/PowerDisplay.png}"
IsEnabled="{x:Bind ViewModel.IsEnabledGpoConfigured, Mode=OneWay, Converter={StaticResource BoolNegationConverter}}">
<ToggleSwitch IsOn="{x:Bind ViewModel.IsEnabled, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
</tkcontrols:SettingsCard>
</controls:GPOInfoControl>
<controls:SettingsGroup x:Uid="Shortcut" IsEnabled="{x:Bind ViewModel.IsEnabled, Mode=OneWay}">
<tkcontrols:SettingsCard x:Uid="PowerDisplay_ActivationShortcut" HeaderIcon="{ui:FontIcon Glyph=&#xEDA7;}">
<controls:ShortcutControl MinWidth="{StaticResource SettingActionControlMinWidth}" HotkeySettings="{x:Bind Path=ViewModel.ActivationShortcut, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
</controls:SettingsGroup>
<controls:SettingsGroup x:Uid="PowerDisplay_Configuration_GroupSettings" IsEnabled="{x:Bind ViewModel.IsEnabled, Mode=OneWay}">
<tkcontrols:SettingsCard
x:Uid="PowerDisplay_LaunchButtonControl"
ActionIcon="{ui:FontIcon Glyph=&#xE8A7;}"
Command="{x:Bind ViewModel.LaunchEventHandler}"
HeaderIcon="{ui:FontIcon Glyph=&#xE770;}"
IsClickEnabled="True" />
<tkcontrols:SettingsCard x:Uid="PowerDisplay_RestoreSettingsOnStartup" HeaderIcon="{ui:FontIcon Glyph=&#xE7B8;}">
<ToggleSwitch x:Uid="PowerDisplay_RestoreSettingsOnStartup_ToggleSwitch" IsOn="{x:Bind ViewModel.RestoreSettingsOnStartup, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard x:Uid="PowerDisplay_ShowSystemTrayIcon" HeaderIcon="{ui:FontIcon Glyph=&#xE75B;}">
<ToggleSwitch IsOn="{x:Bind ViewModel.ShowSystemTrayIcon, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard x:Uid="PowerDisplay_MonitorRefreshDelay" HeaderIcon="{ui:FontIcon Glyph=&#xE916;}">
<ComboBox
MinWidth="120"
ItemsSource="{x:Bind ViewModel.MonitorRefreshDelayOptions}"
SelectedItem="{x:Bind ViewModel.MonitorRefreshDelay, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
</controls:SettingsGroup>
<controls:SettingsGroup x:Uid="PowerDisplay_FlyoutOptions_GroupSettings" IsEnabled="{x:Bind ViewModel.IsEnabled, Mode=OneWay}">
<tkcontrols:SettingsCard x:Uid="PowerDisplay_ShowProfileSwitcher" HeaderIcon="{ui:FontIcon Glyph=&#xE748;}">
<ToggleSwitch IsOn="{x:Bind ViewModel.ShowProfileSwitcher, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard x:Uid="PowerDisplay_ShowIdentifyMonitorsButton" HeaderIcon="{ui:FontIcon Glyph=&#xE9D9;}">
<ToggleSwitch IsOn="{x:Bind ViewModel.ShowIdentifyMonitorsButton, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
</controls:SettingsGroup>
<controls:SettingsGroup x:Uid="PowerDisplay_Profiles_GroupSettings" IsEnabled="{x:Bind ViewModel.IsEnabled, Mode=OneWay}">
<tkcontrols:SettingsExpander
x:Uid="PowerDisplay_QuickProfiles"
HeaderIcon="{ui:FontIcon Glyph=&#xE8B7;}"
IsExpanded="True"
ItemsSource="{x:Bind ViewModel.Profiles, Mode=OneWay}">
<tkcontrols:SettingsExpander.ItemTemplate>
<DataTemplate x:DataType="pdmodels:PowerDisplayProfile">
<tkcontrols:SettingsCard Header="{x:Bind Name}">
<StackPanel Orientation="Horizontal" Spacing="8">
<Button
x:Uid="PowerDisplay_Profile_ApplyButton"
Click="ProfileButton_Click"
Tag="{x:Bind}" />
<Button
x:Uid="PowerDisplay_Profile_MoreButton"
Content="{ui:FontIcon Glyph=&#xE712;,
FontSize=16}"
Style="{StaticResource SubtleButtonStyle}"
Tag="{x:Bind}">
<Button.Flyout>
<MenuFlyout>
<MenuFlyoutItem
x:Uid="PowerDisplay_Profile_EditMenuItem"
Click="EditProfile_Click"
Icon="{ui:FontIcon Glyph=&#xE70F;}"
Tag="{x:Bind}" />
<MenuFlyoutSeparator />
<MenuFlyoutItem
x:Uid="PowerDisplay_Profile_DeleteMenuItem"
Click="DeleteProfile_Click"
Icon="{ui:FontIcon Glyph=&#xE74D;}"
Tag="{x:Bind}" />
</MenuFlyout>
</Button.Flyout>
</Button>
</StackPanel>
</tkcontrols:SettingsCard>
</DataTemplate>
</tkcontrols:SettingsExpander.ItemTemplate>
<!-- Add profile button -->
<Button x:Uid="PowerDisplay_AddProfileButton" Click="AddProfileButton_Click">
<StackPanel Orientation="Horizontal" Spacing="6">
<FontIcon FontSize="14" Glyph="&#xE710;" />
<TextBlock x:Uid="PowerDisplay_AddProfile_Text" />
</StackPanel>
</Button>
</tkcontrols:SettingsExpander>
</controls:SettingsGroup>
<controls:SettingsGroup x:Uid="PowerDisplay_Monitors" IsEnabled="{x:Bind ViewModel.IsEnabled, Mode=OneWay}">
<!-- Empty state hint -->
<TextBlock
x:Uid="PowerDisplay_NoMonitorsDetected"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Visibility="{x:Bind ViewModel.HasMonitors, Mode=OneWay, Converter={StaticResource ReverseBoolToVisibilityConverter}}" />
<!-- Monitor list -->
<ItemsControl
x:Name="MonitorsList"
HorizontalAlignment="Stretch"
ItemsSource="{x:Bind ViewModel.Monitors, Mode=OneWay}"
Visibility="{x:Bind ViewModel.HasMonitors, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}">
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="library:MonitorInfo">
<tkcontrols:SettingsExpander
Margin="0,0,0,2"
Description="{x:Bind Id, Mode=OneWay}"
Header="{x:Bind DisplayName, Mode=OneWay}"
IsExpanded="False">
<tkcontrols:SettingsExpander.HeaderIcon>
<FontIcon Glyph="{x:Bind MonitorIconGlyph, Mode=OneWay}" />
</tkcontrols:SettingsExpander.HeaderIcon>
<TextBlock Text="{x:Bind CommunicationMethod, Mode=OneWay}" />
<tkcontrols:SettingsExpander.ItemsHeader>
<!-- Capabilities warning -->
<InfoBar
x:Uid="PowerDisplay_Monitor_CapabilitiesWarning"
BorderThickness="0"
CornerRadius="0"
IsClosable="False"
IsOpen="True"
Severity="Warning"
Visibility="{x:Bind ShowCapabilitiesWarning, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}" />
</tkcontrols:SettingsExpander.ItemsHeader>
<tkcontrols:SettingsExpander.Items>
<tkcontrols:SettingsCard ContentAlignment="Left" IsEnabled="{x:Bind SupportsContrast, Mode=OneWay}">
<CheckBox x:Uid="PowerDisplay_Monitor_EnableContrast" IsChecked="{x:Bind EnableContrast, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard ContentAlignment="Left" IsEnabled="{x:Bind SupportsVolume, Mode=OneWay}">
<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}" />
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard ContentAlignment="Left">
<CheckBox x:Uid="PowerDisplay_Monitor_EnableRotation" IsChecked="{x:Bind EnableRotation, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard ContentAlignment="Left" IsEnabled="{x:Bind SupportsColorTemperature, Mode=OneWay}">
<CheckBox
x:Uid="PowerDisplay_Monitor_EnableColorTemperature"
Click="EnableColorTemperature_Click"
IsChecked="{x:Bind EnableColorTemperature, Mode=TwoWay}"
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}" />
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard ContentAlignment="Left">
<CheckBox x:Uid="PowerDisplay_Monitor_HideMonitor" IsChecked="{x:Bind IsHidden, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
<!-- VCP Capabilities -->
<tkcontrols:SettingsCard x:Uid="PowerDisplay_Monitor_VcpCapabilities" Visibility="{x:Bind HasCapabilities, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}">
<Button
x:Uid="PowerDisplay_Monitor_VcpDetails_Button"
Content="&#xE946;"
FontFamily="{ThemeResource SymbolThemeFontFamily}"
Style="{StaticResource SubtleButtonStyle}">
<Button.Flyout>
<Flyout ShouldConstrainToRootBounds="False">
<Grid Width="420">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<!-- Header with Copy Button -->
<Grid Margin="0,0,0,12">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock
x:Uid="PowerDisplay_Monitor_VcpCodes_Header"
Grid.Column="0"
VerticalAlignment="Center"
FontSize="13"
FontWeight="SemiBold" />
<Button
x:Uid="PowerDisplay_Monitor_VcpCodes_CopyButton"
Grid.Column="1"
Padding="8,4"
VerticalAlignment="Center"
Click="CopyVcpCodes_Click"
Tag="{x:Bind}">
<StackPanel Orientation="Horizontal" Spacing="4">
<FontIcon FontSize="12" Glyph="&#xE8C8;" />
<TextBlock x:Uid="PowerDisplay_Monitor_VcpCodes_CopyText" FontSize="12" />
</StackPanel>
</Button>
</Grid>
<!-- VCP Codes List -->
<ScrollViewer
Grid.Row="1"
MaxHeight="480"
HorizontalScrollBarVisibility="Disabled"
HorizontalScrollMode="Disabled"
VerticalScrollBarVisibility="Auto">
<ItemsControl HorizontalAlignment="Stretch" ItemsSource="{x:Bind VcpCodesFormatted, Mode=OneWay}">
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="library:VcpCodeDisplayInfo">
<StackPanel HorizontalAlignment="Stretch" Orientation="Vertical">
<TextBlock
FontSize="12"
Text="{x:Bind Title}"
TextWrapping="Wrap" />
<TextBlock
Margin="0,0,0,8"
FontSize="12"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Text="{x:Bind Values}"
TextWrapping="Wrap"
Visibility="{x:Bind HasValues}" />
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</Grid>
</Flyout>
</Button.Flyout>
</Button>
</tkcontrols:SettingsCard>
</tkcontrols:SettingsExpander.Items>
</tkcontrols:SettingsExpander>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</controls:SettingsGroup>
</StackPanel>
</controls:SettingsPageControl.ModuleContent>
<controls:SettingsPageControl.PrimaryLinks>
<controls:PageLink x:Uid="LearnMore_PowerDisplay" Link="https://aka.ms/PowerToysOverview_PowerDisplay" />
</controls:SettingsPageControl.PrimaryLinks>
</controls:SettingsPageControl>
</local:NavigablePage>

View File

@@ -0,0 +1,218 @@
// 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.Generic;
using System.Linq;
using System.Threading.Tasks;
using CommunityToolkit.WinUI.Controls;
using Microsoft.PowerToys.Settings.UI.Helpers;
using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.PowerToys.Settings.UI.ViewModels;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using PowerDisplay.Common.Models;
using PowerDisplay.Common.Utils;
using Windows.ApplicationModel.DataTransfer;
namespace Microsoft.PowerToys.Settings.UI.Views
{
public sealed partial class PowerDisplayPage : NavigablePage, IRefreshablePage
{
private PowerDisplayViewModel ViewModel { get; set; }
public PowerDisplayPage()
{
var settingsUtils = SettingsUtils.Default;
ViewModel = new PowerDisplayViewModel(
settingsUtils,
SettingsRepository<GeneralSettings>.GetInstance(settingsUtils),
SettingsRepository<PowerDisplaySettings>.GetInstance(settingsUtils),
ShellPage.SendDefaultIPCMessage);
DataContext = ViewModel;
InitializeComponent();
Loaded += (s, e) => ViewModel.OnPageLoaded();
}
public void RefreshEnabledState()
{
ViewModel.RefreshEnabledState();
}
private void CopyVcpCodes_Click(object sender, RoutedEventArgs e)
{
if (sender is Button button && button.Tag is MonitorInfo monitor)
{
var vcpText = monitor.GetVcpCodesAsText();
var dataPackage = new DataPackage();
dataPackage.SetText(vcpText);
Clipboard.SetContent(dataPackage);
}
}
// Profile button event handlers
private void ProfileButton_Click(object sender, RoutedEventArgs e)
{
if (sender is Button button && button.Tag is PowerDisplayProfile profile)
{
ViewModel.ApplyProfile(profile);
}
}
private async void AddProfileButton_Click(object sender, RoutedEventArgs e)
{
if (ViewModel.Monitors == null || ViewModel.Monitors.Count == 0)
{
return;
}
var defaultName = GenerateDefaultProfileName();
var dialog = new ProfileEditorDialog(ViewModel.Monitors, defaultName);
dialog.XamlRoot = this.XamlRoot;
var result = await dialog.ShowAsync();
if (result == ContentDialogResult.Primary && dialog.ResultProfile != null)
{
ViewModel.CreateProfile(dialog.ResultProfile);
}
}
private async void EditProfile_Click(object sender, RoutedEventArgs e)
{
var menuItem = sender as MenuFlyoutItem;
if (menuItem?.Tag is PowerDisplayProfile profile)
{
var dialog = new ProfileEditorDialog(ViewModel.Monitors, profile.Name);
dialog.XamlRoot = this.XamlRoot;
// Pre-fill with existing profile settings
dialog.PreFillProfile(profile);
var result = await dialog.ShowAsync();
if (result == ContentDialogResult.Primary && dialog.ResultProfile != null)
{
ViewModel.UpdateProfile(profile.Name, dialog.ResultProfile);
}
}
}
private async void DeleteProfile_Click(object sender, RoutedEventArgs e)
{
var menuItem = sender as MenuFlyoutItem;
if (menuItem?.Tag is PowerDisplayProfile profile)
{
var resourceLoader = ResourceLoaderInstance.ResourceLoader;
var dialog = new ContentDialog
{
XamlRoot = this.XamlRoot,
Title = resourceLoader.GetString("PowerDisplay_DeleteProfile_Title"),
Content = string.Format(System.Globalization.CultureInfo.CurrentCulture, resourceLoader.GetString("PowerDisplay_DeleteProfile_Content"), profile.Name),
PrimaryButtonText = resourceLoader.GetString("PowerDisplay_DeleteProfile_PrimaryButton"),
CloseButtonText = resourceLoader.GetString("PowerDisplay_Dialog_Cancel"),
DefaultButton = ContentDialogButton.Close,
};
var result = await dialog.ShowAsync();
if (result == ContentDialogResult.Primary)
{
ViewModel.DeleteProfile(profile.Name);
}
}
}
private string GenerateDefaultProfileName()
{
// Use shared ProfileHelper for consistent profile name generation
var existingNames = ViewModel.Profiles.Select(p => p.Name).ToHashSet();
var resourceLoader = ResourceLoaderInstance.ResourceLoader;
var baseName = resourceLoader.GetString("PowerDisplay_Profile_DefaultBaseName");
return ProfileHelper.GenerateUniqueProfileName(existingNames, baseName);
}
// Flag to prevent reentrant handling during programmatic checkbox changes
private bool _isRestoringColorTempCheckbox;
private async void EnableColorTemperature_Click(object sender, RoutedEventArgs e)
{
// Skip if we're programmatically restoring the checkbox state
if (_isRestoringColorTempCheckbox)
{
return;
}
if (sender is not CheckBox checkBox || checkBox.Tag is not MonitorInfo monitor)
{
return;
}
// Only show warning when enabling (checking the box)
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"),
Content = new StackPanel
{
Spacing = 12,
Children =
{
new TextBlock
{
Text = resourceLoader.GetString("PowerDisplay_ColorTemperature_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"),
TextWrapping = TextWrapping.Wrap,
},
new TextBlock
{
Text = resourceLoader.GetString("PowerDisplay_ColorTemperature_WarningList"),
TextWrapping = TextWrapping.Wrap,
Margin = new Thickness(20, 0, 0, 0),
},
new TextBlock
{
Text = resourceLoader.GetString("PowerDisplay_ColorTemperature_WarningConfirm"),
FontWeight = Microsoft.UI.Text.FontWeights.SemiBold,
TextWrapping = TextWrapping.Wrap,
},
},
},
PrimaryButtonText = resourceLoader.GetString("PowerDisplay_ColorTemperature_EnableButton"),
CloseButtonText = resourceLoader.GetString("PowerDisplay_Dialog_Cancel"),
DefaultButton = ContentDialogButton.Close,
};
var result = await dialog.ShowAsync();
if (result != ContentDialogResult.Primary)
{
// User cancelled: revert checkbox to unchecked
_isRestoringColorTempCheckbox = true;
try
{
checkBox.IsChecked = false;
monitor.EnableColorTemperature = false;
}
finally
{
_isRestoringColorTempCheckbox = false;
}
}
}
}
}

View File

@@ -0,0 +1,132 @@
<ContentDialog
x:Class="Microsoft.PowerToys.Settings.UI.Views.ProfileEditorDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:tkcontrols="using:CommunityToolkit.WinUI.Controls"
xmlns:ui="using:CommunityToolkit.WinUI"
xmlns:viewmodels="using:Microsoft.PowerToys.Settings.UI.ViewModels"
Width="486"
MinWidth="486"
CloseButtonClick="ContentDialog_CloseButtonClick"
DefaultButton="Primary"
IsPrimaryButtonEnabled="{x:Bind ViewModel.CanSave, Mode=OneWay}"
PrimaryButtonClick="ContentDialog_PrimaryButtonClick"
Style="{StaticResource DefaultContentDialogStyle}"
mc:Ignorable="d">
<Grid MinWidth="486">
<Grid.Resources>
<x:Double x:Key="SettingsCardWrapThreshold">0</x:Double>
<x:Double x:Key="SettingsCardWrapNoIconThreshold">0</x:Double>
<Thickness x:Key="SettingsExpanderItemPadding">0,8,0,8</Thickness>
</Grid.Resources>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<TextBox
x:Name="ProfileNameTextBox"
x:Uid="PowerDisplay_ProfileEditor_ProfileName"
Margin="0,0,0,24"
MaxLength="50"
Text="{x:Bind ViewModel.ProfileName, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
<!-- Monitors List -->
<ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto">
<StackPanel Spacing="16">
<TextBlock x:Uid="PowerDisplay_ProfileEditor_Description" Style="{StaticResource BodyStrongTextBlockStyle}" />
<ItemsControl ItemsSource="{x:Bind ViewModel.Monitors, Mode=OneWay}">
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="viewmodels:MonitorSelectionItem">
<tkcontrols:SettingsExpander
Margin="0,0,0,8"
Description="{x:Bind Monitor.Id, Mode=OneWay}"
Header="{x:Bind Monitor.DisplayName, Mode=OneWay}"
IsExpanded="True">
<tkcontrols:SettingsExpander.HeaderIcon>
<FontIcon Glyph="{x:Bind Monitor.MonitorIconGlyph}" />
</tkcontrols:SettingsExpander.HeaderIcon>
<ToggleSwitch IsOn="{x:Bind IsSelected, Mode=TwoWay}" />
<tkcontrols:SettingsExpander.Items>
<tkcontrols:SettingsCard Padding="16,8,16,8" Visibility="{x:Bind Monitor.SupportsBrightness, Converter={StaticResource BoolToVisibilityConverter}}">
<tkcontrols:SettingsCard.Header>
<CheckBox IsChecked="{x:Bind IncludeBrightness, Mode=TwoWay}">
<StackPanel Orientation="Horizontal" Spacing="8">
<FontIcon FontSize="16" Glyph="&#xE706;" />
<TextBlock x:Uid="PowerDisplay_ProfileEditor_Brightness" VerticalAlignment="Center" />
</StackPanel>
</CheckBox>
</tkcontrols:SettingsCard.Header>
<Slider
MinWidth="{StaticResource SettingActionControlMinWidth}"
VerticalAlignment="Center"
Maximum="100"
Minimum="0"
Value="{x:Bind Brightness, Mode=TwoWay}" />
<tkcontrols:SettingsCard.Resources>
<x:Double x:Key="SettingsCardLeftIndention">0</x:Double>
</tkcontrols:SettingsCard.Resources>
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard Padding="16,8,16,8" Visibility="{x:Bind Monitor.SupportsContrast, Converter={StaticResource BoolToVisibilityConverter}}">
<tkcontrols:SettingsCard.Header>
<CheckBox IsChecked="{x:Bind IncludeContrast, Mode=TwoWay}">
<StackPanel Orientation="Horizontal" Spacing="8">
<FontIcon FontSize="16" Glyph="&#xE793;" />
<TextBlock x:Uid="PowerDisplay_ProfileEditor_Contrast" VerticalAlignment="Center" />
</StackPanel>
</CheckBox>
</tkcontrols:SettingsCard.Header>
<Slider
MinWidth="{StaticResource SettingActionControlMinWidth}"
VerticalAlignment="Center"
Maximum="100"
Minimum="0"
Value="{x:Bind Contrast, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard Padding="16,8,16,8" Visibility="{x:Bind Monitor.SupportsVolume, Converter={StaticResource BoolToVisibilityConverter}}">
<tkcontrols:SettingsCard.Header>
<CheckBox IsChecked="{x:Bind IncludeVolume, Mode=TwoWay}">
<StackPanel Orientation="Horizontal" Spacing="8">
<FontIcon FontSize="16" Glyph="&#xE767;" />
<TextBlock x:Uid="PowerDisplay_ProfileEditor_Volume" VerticalAlignment="Center" />
</StackPanel>
</CheckBox>
</tkcontrols:SettingsCard.Header>
<Slider
MinWidth="{StaticResource SettingActionControlMinWidth}"
VerticalAlignment="Center"
Maximum="100"
Minimum="0"
Value="{x:Bind Volume, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard Padding="16,8,16,8" Visibility="{x:Bind Monitor.SupportsColorTemperature, Converter={StaticResource BoolToVisibilityConverter}}">
<tkcontrols:SettingsCard.Header>
<CheckBox IsChecked="{x:Bind IncludeColorTemperature, Mode=TwoWay}">
<StackPanel Orientation="Horizontal" Spacing="8">
<FontIcon FontSize="16" Glyph="&#xE790;" />
<TextBlock x:Uid="PowerDisplay_ProfileEditor_ColorTemperature" VerticalAlignment="Center" />
</StackPanel>
</CheckBox>
</tkcontrols:SettingsCard.Header>
<ComboBox
x:Uid="PowerDisplay_ProfileEditor_ColorTemperature_ComboBox"
MinWidth="{StaticResource SettingActionControlMinWidth}"
VerticalAlignment="Center"
DisplayMemberPath="DisplayName"
ItemsSource="{x:Bind Monitor.ColorPresetsForDisplay, Mode=OneWay}"
SelectedValue="{x:Bind ColorTemperature, Mode=TwoWay}"
SelectedValuePath="VcpValue" />
</tkcontrols:SettingsCard>
</tkcontrols:SettingsExpander.Items>
</tkcontrols:SettingsExpander>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</ScrollViewer>
</Grid>
</ContentDialog>

View File

@@ -0,0 +1,104 @@
// 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.Collections.ObjectModel;
using System.Linq;
using Microsoft.PowerToys.Settings.UI.Helpers;
using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.PowerToys.Settings.UI.ViewModels;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using PowerDisplay.Common.Models;
namespace Microsoft.PowerToys.Settings.UI.Views
{
/// <summary>
/// Dialog for creating/editing PowerDisplay profiles
/// </summary>
public sealed partial class ProfileEditorDialog : ContentDialog
{
public ProfileEditorViewModel ViewModel { get; private set; }
public PowerDisplayProfile? ResultProfile { get; private set; }
public ProfileEditorDialog(ObservableCollection<MonitorInfo> availableMonitors, string defaultName = "")
{
this.InitializeComponent();
ViewModel = new ProfileEditorViewModel(availableMonitors, defaultName);
// Set localized strings for ContentDialog
var resourceLoader = ResourceLoaderInstance.ResourceLoader;
Title = resourceLoader.GetString("PowerDisplay_ProfileEditor_Title");
PrimaryButtonText = resourceLoader.GetString("PowerDisplay_Dialog_Save");
CloseButtonText = resourceLoader.GetString("PowerDisplay_Dialog_Cancel");
}
private void ContentDialog_PrimaryButtonClick(ContentDialog sender, ContentDialogButtonClickEventArgs args)
{
if (ViewModel.CanSave)
{
ResultProfile = ViewModel.CreateProfile();
}
}
private void ContentDialog_CloseButtonClick(ContentDialog sender, ContentDialogButtonClickEventArgs args)
{
ResultProfile = null;
}
/// <summary>
/// Pre-fill the dialog with existing profile data
/// </summary>
public void PreFillProfile(PowerDisplayProfile profile)
{
if (profile == null || ViewModel == null)
{
return;
}
// Set profile name
ViewModel.ProfileName = profile.Name;
// Pre-fill monitor settings from existing profile
foreach (var monitorSetting in profile.MonitorSettings)
{
var monitorItem = ViewModel.Monitors.FirstOrDefault(m => m.Monitor.Id == monitorSetting.MonitorId);
if (monitorItem != null)
{
monitorItem.IsSelected = true;
// Set brightness if included in profile
if (monitorSetting.Brightness.HasValue)
{
monitorItem.IncludeBrightness = true;
monitorItem.Brightness = monitorSetting.Brightness.Value;
}
// Set color temperature if included in profile
if (monitorSetting.ColorTemperatureVcp.HasValue)
{
monitorItem.IncludeColorTemperature = true;
monitorItem.ColorTemperature = monitorSetting.ColorTemperatureVcp.Value;
}
// Set contrast if included in profile
if (monitorSetting.Contrast.HasValue)
{
monitorItem.IncludeContrast = true;
monitorItem.Contrast = monitorSetting.Contrast.Value;
}
// Set volume if included in profile
if (monitorSetting.Volume.HasValue)
{
monitorItem.IncludeVolume = true;
monitorItem.Volume = monitorSetting.Volume.Value;
}
}
}
}
}
}

View File

@@ -179,6 +179,9 @@
AutomationProperties.AutomationId="SystemToolsNavItem"
Icon="{ui:BitmapIcon Source=/Assets/Settings/Icons/SystemTools.png}"
SelectsOnInvoked="False">
<NavigationViewItem.InfoBadge>
<InfoBadge Style="{StaticResource NewInfoBadge}" />
</NavigationViewItem.InfoBadge>
<NavigationViewItem.MenuItems>
<NavigationViewItem
x:Name="AdvancedPasteNavigationItem"
@@ -210,6 +213,16 @@
helpers:NavHelper.NavigateTo="views:LightSwitchPage"
AutomationProperties.AutomationId="LightSwitchNavItem"
Icon="{ui:BitmapIcon Source=/Assets/Settings/Icons/LightSwitch.png}" />
<NavigationViewItem
x:Name="PowerDisplayNavigationItem"
x:Uid="Shell_PowerDisplay"
helpers:NavHelper.NavigateTo="views:PowerDisplayPage"
AutomationProperties.AutomationId="PowerDisplayNavItem"
Icon="{ui:BitmapIcon Source=/Assets/Settings/Icons/PowerDisplay.png}">
<NavigationViewItem.InfoBadge>
<InfoBadge Style="{StaticResource NewInfoBadge}" />
</NavigationViewItem.InfoBadge>
</NavigationViewItem>
<NavigationViewItem
x:Name="PowerLauncherNavigationItem"
x:Uid="Shell_PowerLauncher"

View File

@@ -555,6 +555,10 @@ opera.exe</value>
<value>Color Picker</value>
<comment>Product name: Navigation view item name for Color Picker</comment>
</data>
<data name="Shell_PowerDisplay.Content" xml:space="preserve">
<value>Power Display</value>
<comment>Product name: Navigation view item name for Power Display</comment>
</data>
<data name="Shell_KeyboardManager.Content" xml:space="preserve">
<value>Keyboard Manager</value>
<comment>Product name: Navigation view item name for Keyboard Manager</comment>
@@ -5439,6 +5443,9 @@ The break timer font matches the text font.</value>
<data name="LightSwitch_BehaviorSettingsGroup.Header" xml:space="preserve">
<value>Behavior</value>
</data>
<data name="LightSwitch_MonitorSettingsGroup.Header" xml:space="preserve">
<value>Monitor settings</value>
</data>
<data name="LightSwitch_EnableSettingsCard.Header" xml:space="preserve">
<value>Light Switch</value>
</data>
@@ -5508,6 +5515,45 @@ The break timer font matches the text font.</value>
<data name="LightSwitch_AppsCheckbox.Description" xml:space="preserve">
<value>Supported applications</value>
</data>
<data name="LightSwitch_PowerDisplayDisabledWarningBar.Title" xml:space="preserve">
<value>Enable PowerDisplay module to use automatic monitor profile switching</value>
</data>
<data name="LightSwitch_PowerDisplayDisabledWarningBar.Message" xml:space="preserve">
<value />
</data>
<data name="LightSwitch_PowerDisplayDisabledWarning.Text" xml:space="preserve">
<value>Enable PowerDisplay module to use automatic monitor profile switching</value>
</data>
<data name="LightSwitch_ApplyMonitorSettingsExpander.Header" xml:space="preserve">
<value>Apply monitor settings to</value>
</data>
<data name="LightSwitch_ApplyMonitorSettingsExpander.Description" xml:space="preserve">
<value>Automatically switch PowerDisplay profiles when theme changes</value>
</data>
<data name="LightSwitch_EnableMonitorSettings.Content" xml:space="preserve">
<value>Enable monitor settings integration</value>
</data>
<data name="LightSwitch_DarkModeProfileCheckbox.Content" xml:space="preserve">
<value>Dark mode profile</value>
</data>
<data name="LightSwitch_LightModeProfileCheckbox.Content" xml:space="preserve">
<value>Light mode profile</value>
</data>
<data name="LightSwitch_DarkModeProfileCard.Header" xml:space="preserve">
<value>Dark mode profile</value>
</data>
<data name="LightSwitch_DarkModeProfileCard.Description" xml:space="preserve">
<value>Profile to apply when switching to dark mode</value>
</data>
<data name="LightSwitch_LightModeProfileCard.Header" xml:space="preserve">
<value>Light mode profile</value>
</data>
<data name="LightSwitch_LightModeProfileCard.Description" xml:space="preserve">
<value>Profile to apply when switching to light mode</value>
</data>
<data name="LightSwitch_NavigatePowerDisplaySettings.Content" xml:space="preserve">
<value>Open PowerDisplay settings</value>
</data>
<data name="LightSwitch_LocationDialog.Title" xml:space="preserve">
<value>Select a location</value>
</data>
@@ -5687,6 +5733,265 @@ The break timer font matches the text font.</value>
<data name="Shortcut_Conflict_LearnMore.Content" xml:space="preserve">
<value>Learn more</value>
</data>
<data name="PowerDisplay.ModuleTitle" xml:space="preserve">
<value>Power Display</value>
</data>
<data name="PowerDisplay.ModuleDescription" xml:space="preserve">
<value>A display management utility for brightness and power control</value>
</data>
<data name="PowerDisplay_Enable_PowerDisplay.Header" xml:space="preserve">
<value>Enable Power Display</value>
</data>
<data name="PowerDisplay_Configuration_GroupSettings.Header" xml:space="preserve">
<value>Configuration</value>
</data>
<data name="PowerDisplay_ToggleWindow" xml:space="preserve">
<value>Toggle Power Display</value>
<comment>Dashboard: Label for the PowerDisplay activation hotkey</comment>
</data>
<data name="PowerDisplay_ActivationShortcut.Header" xml:space="preserve">
<value>Activation shortcut</value>
<comment>Header for the PowerDisplay activation shortcut setting</comment>
</data>
<data name="PowerDisplay_LaunchButtonControl.Header" xml:space="preserve">
<value>Open Power Display</value>
</data>
<data name="PowerDisplay_LaunchButtonControl.Description" xml:space="preserve">
<value>Launch the Power Display utility</value>
</data>
<data name="PowerDisplay_LaunchAtStartup.Header" xml:space="preserve">
<value>Launch at startup</value>
</data>
<data name="PowerDisplay_LaunchAtStartup.Description" xml:space="preserve">
<value>Automatically start Power Display when Windows starts</value>
</data>
<data name="PowerDisplay_LaunchAtStartup_ToggleSwitch.OnContent" xml:space="preserve">
<value>On</value>
</data>
<data name="PowerDisplay_LaunchAtStartup_ToggleSwitch.OffContent" xml:space="preserve">
<value>Off</value>
</data>
<data name="PowerDisplay_RestoreSettingsOnStartup.Header" xml:space="preserve">
<value>Restore settings on startup</value>
</data>
<data name="PowerDisplay_RestoreSettingsOnStartup.Description" xml:space="preserve">
<value>Restore monitor brightness and color temperature when Power Display launches</value>
</data>
<data name="PowerDisplay_RestoreSettingsOnStartup_ToggleSwitch.OnContent" xml:space="preserve">
<value>On</value>
</data>
<data name="PowerDisplay_RestoreSettingsOnStartup_ToggleSwitch.OffContent" xml:space="preserve">
<value>Off</value>
</data>
<data name="PowerDisplay_ShowSystemTrayIcon.Header" xml:space="preserve">
<value>Show system tray icon</value>
</data>
<data name="PowerDisplay_ShowSystemTrayIcon.Description" xml:space="preserve">
<value>Choose if PowerDisplay is visible in the system tray</value>
</data>
<data name="PowerDisplay_MonitorRefreshDelay.Header" xml:space="preserve">
<value>Monitor refresh delay</value>
</data>
<data name="PowerDisplay_MonitorRefreshDelay.Description" xml:space="preserve">
<value>Number of seconds to wait after display changes before refreshing monitors. Increase if monitors are not detected after hot-plug.</value>
</data>
<data name="PowerDisplay_Profiles_GroupSettings.Header" xml:space="preserve">
<value>Profiles</value>
</data>
<data name="PowerDisplay_QuickProfiles.Header" xml:space="preserve">
<value>Quick apply profiles</value>
</data>
<data name="PowerDisplay_QuickProfiles.Description" xml:space="preserve">
<value>Click a profile button to quickly apply saved monitor settings</value>
</data>
<data name="PowerDisplay_AddProfile_Text.Text" xml:space="preserve">
<value>Add profile</value>
</data>
<data name="PowerDisplay_Monitors.Header" xml:space="preserve">
<value>Monitors</value>
</data>
<data name="PowerDisplay_NoMonitorsDetected.Text" xml:space="preserve">
<value>No monitors detected. Ensure your external monitors are connected and powered on.</value>
</data>
<data name="LearnMore_PowerDisplay.Text" xml:space="preserve">
<value>Learn more about Power Display</value>
</data>
<data name="Oobe_PowerDisplay.Title" xml:space="preserve">
<value>Power Display</value>
</data>
<data name="Oobe_PowerDisplay.Description" xml:space="preserve">
<value>Power Display provides unified control over display settings across multiple monitors. Adjust brightness, contrast, volume, color temperature, and input source for all connected displays from a single overlay. Supports DDC/CI for external monitors and WMI for laptop displays, with profile support for quick configuration switching and LightSwitch integration for automatic theme-based adjustments.</value>
</data>
<data name="Oobe_PowerDisplay_Activation.Text" xml:space="preserve">
<value>to open the Power Display overlay.</value>
</data>
<data name="Oobe_PowerDisplay_TipsAndTricks.Text" xml:space="preserve">
<value>**Create profiles** to save your preferred display settings, then configure them in **LightSwitch** to automatically switch profiles when the system theme changes.</value>
</data>
<data name="Launch_PowerDisplay.Content" xml:space="preserve">
<value>Open Power Display</value>
</data>
<data name="PowerDisplay_Monitor_EnableContrast.Content" xml:space="preserve">
<value>Display contrast slider</value>
</data>
<data name="PowerDisplay_Monitor_EnableVolume.Content" xml:space="preserve">
<value>Display volume slider</value>
</data>
<data name="PowerDisplay_Monitor_EnableInputSource.Content" xml:space="preserve">
<value>Show input source control</value>
</data>
<data name="PowerDisplay_Monitor_EnableRotation.Content" xml:space="preserve">
<value>Show rotation control</value>
</data>
<data name="PowerDisplay_Monitor_EnableColorTemperature.Content" xml:space="preserve">
<value>Show color temperature switcher</value>
</data>
<data name="PowerDisplay_Monitor_EnablePowerState.Content" xml:space="preserve">
<value>Show power state control</value>
</data>
<data name="PowerDisplay_Monitor_HideMonitor.Content" xml:space="preserve">
<value>Hide monitor</value>
</data>
<data name="PowerDisplay_ColorTemperature_WarningTitle" xml:space="preserve">
<value>Confirm Color Temperature Change</value>
</data>
<data name="PowerDisplay_ColorTemperature_WarningHeader" xml:space="preserve">
<value>⚠️ Warning: This is a potentially dangerous operation!</value>
</data>
<data name="PowerDisplay_ColorTemperature_WarningDescription" xml:space="preserve">
<value>Changing the color temperature setting may cause unpredictable results including:</value>
</data>
<data name="PowerDisplay_ColorTemperature_WarningList" xml:space="preserve">
<value>• Incorrect display colors
• Display malfunction
• Settings that cannot be reverted</value>
</data>
<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">
<value>Enable</value>
</data>
<data name="PowerDisplay_Dialog_Cancel" xml:space="preserve">
<value>Cancel</value>
</data>
<data name="PowerDisplay_Dialog_Save" xml:space="preserve">
<value>Save</value>
</data>
<data name="PowerDisplay_ProfileEditor_Title" xml:space="preserve">
<value>Edit Profile</value>
</data>
<data name="PowerDisplay_ProfileEditor_ProfileName.Header" xml:space="preserve">
<value>Profile name</value>
</data>
<data name="PowerDisplay_ProfileEditor_ProfileName.PlaceholderText" xml:space="preserve">
<value>Enter profile name (e.g., 'Gaming Mode', 'Work')</value>
</data>
<data name="PowerDisplay_ProfileEditor_Description.Text" xml:space="preserve">
<value>Select what monitors and settings to include for this profile</value>
</data>
<data name="PowerDisplay_ProfileEditor_Brightness.Text" xml:space="preserve">
<value>Brightness</value>
</data>
<data name="PowerDisplay_ProfileEditor_Contrast.Text" xml:space="preserve">
<value>Contrast</value>
</data>
<data name="PowerDisplay_ProfileEditor_Volume.Text" xml:space="preserve">
<value>Volume</value>
</data>
<data name="PowerDisplay_ProfileEditor_ColorTemperature.Text" xml:space="preserve">
<value>Color temperature</value>
</data>
<data name="PowerDisplay_ProfileEditor_ColorTemperature_ComboBox.PlaceholderText" xml:space="preserve">
<value>Select..</value>
</data>
<data name="PowerDisplay_Profile_DefaultBaseName" xml:space="preserve">
<value>Profile</value>
</data>
<data name="PowerDisplay_DeleteProfile_Title" xml:space="preserve">
<value>Delete Profile</value>
</data>
<data name="PowerDisplay_DeleteProfile_Content" xml:space="preserve">
<value>Are you sure you want to delete '{0}'?</value>
</data>
<data name="PowerDisplay_DeleteProfile_PrimaryButton" xml:space="preserve">
<value>Delete</value>
</data>
<data name="PowerDisplay_Profile_ApplyButton.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
<value>Apply profile</value>
</data>
<data name="PowerDisplay_Profile_MoreButton.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
<value>More profile options</value>
</data>
<data name="PowerDisplay_Profile_EditMenuItem.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
<value>Edit profile</value>
</data>
<data name="PowerDisplay_Profile_DeleteMenuItem.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
<value>Delete profile</value>
</data>
<data name="PowerDisplay_Profile_AddButton.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
<value>Add new profile</value>
</data>
<data name="PowerDisplay_Profile_ApplyButton.Content" xml:space="preserve">
<value>Apply</value>
</data>
<data name="PowerDisplay_Profile_MoreButton.ToolTipService.ToolTip" xml:space="preserve">
<value>More settings</value>
</data>
<data name="PowerDisplay_Profile_EditMenuItem.Text" xml:space="preserve">
<value>Edit</value>
</data>
<data name="PowerDisplay_Profile_DeleteMenuItem.Text" xml:space="preserve">
<value>Delete</value>
</data>
<data name="PowerDisplay_AddProfileButton.ToolTipService.ToolTip" xml:space="preserve">
<value>Save current settings as new profile</value>
</data>
<data name="PowerDisplay_Monitor_CapabilitiesWarning.Title" xml:space="preserve">
<value>Monitor capabilities unavailable</value>
</data>
<data name="PowerDisplay_Monitor_CapabilitiesWarning.Message" xml:space="preserve">
<value>This monitor did not report DDC/CI capabilities. Advanced controls may be limited.</value>
</data>
<data name="PowerDisplay_Monitor_VcpCapabilities.Header" xml:space="preserve">
<value>VCP capabilities</value>
</data>
<data name="PowerDisplay_Monitor_VcpCapabilities.Description" xml:space="preserve">
<value>DDC/CI VCP codes and supported values (for debugging purposes)</value>
</data>
<data name="PowerDisplay_Monitor_VcpDetails_Button.ToolTipService.ToolTip" xml:space="preserve">
<value>View VCP details</value>
</data>
<data name="PowerDisplay_Monitor_VcpDetails_Button.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
<value>View VCP details</value>
</data>
<data name="PowerDisplay_Monitor_VcpCodes_Header.Text" xml:space="preserve">
<value>Detected VCP Codes</value>
</data>
<data name="PowerDisplay_Monitor_VcpCodes_CopyButton.ToolTipService.ToolTip" xml:space="preserve">
<value>Copy all VCP codes to clipboard</value>
</data>
<data name="PowerDisplay_Monitor_VcpCodes_CopyButton.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
<value>Copy VCP codes to clipboard</value>
</data>
<data name="PowerDisplay_Monitor_VcpCodes_CopyText.Text" xml:space="preserve">
<value>Copy</value>
</data>
<data name="PowerDisplay_FlyoutOptions_GroupSettings.Header" xml:space="preserve">
<value>Flyout options</value>
</data>
<data name="PowerDisplay_ShowProfileSwitcher.Header" xml:space="preserve">
<value>Show profile switcher</value>
</data>
<data name="PowerDisplay_ShowProfileSwitcher.Description" xml:space="preserve">
<value>Show or hide the profile switcher button in the Power Display flyout</value>
</data>
<data name="PowerDisplay_ShowIdentifyMonitorsButton.Header" xml:space="preserve">
<value>Show identify monitors button</value>
</data>
<data name="PowerDisplay_ShowIdentifyMonitorsButton.Description" xml:space="preserve">
<value>Show or hide the identify monitors button in the Power Display flyout</value>
</data>
<data name="Hosts_Backup_GroupSettings.Header" xml:space="preserve">
<value>Backup</value>
</data>

View File

@@ -8,7 +8,6 @@ using System.Collections.ObjectModel;
using System.IO.Abstractions;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Threading;
using CommunityToolkit.WinUI.Controls;
using global::PowerToys.GPOWrapper;
using ManagedCommon;
@@ -21,6 +20,7 @@ using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
using Microsoft.PowerToys.Settings.UI.Library.Utilities;
using Microsoft.PowerToys.Settings.UI.Services;
using Microsoft.PowerToys.Settings.UI.Views;
using Microsoft.UI.Dispatching;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Settings.UI.Library;
@@ -33,7 +33,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
protected override string ModuleName => "Dashboard";
private Dispatcher dispatcher;
private DispatcherQueue dispatcher;
public Func<string, int> SendConfigMSG { get; }
@@ -107,7 +107,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
public DashboardViewModel(ISettingsRepository<GeneralSettings> settingsRepository, Func<string, int> ipcMSGCallBackFunc)
{
dispatcher = Dispatcher.CurrentDispatcher;
dispatcher = DispatcherQueue.GetForCurrentThread();
_settingsRepository = settingsRepository;
generalSettingsConfig = settingsRepository.SettingsConfig;
@@ -132,7 +132,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
private void OnSettingsChanged(GeneralSettings newSettings)
{
dispatcher.BeginInvoke(() =>
dispatcher.TryEnqueue(() =>
{
generalSettingsConfig = newSettings;
@@ -149,7 +149,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
protected override void OnConflictsUpdated(object sender, AllHotkeyConflictsEventArgs e)
{
dispatcher.BeginInvoke(() =>
dispatcher.TryEnqueue(() =>
{
var allConflictData = e.Conflicts;
foreach (var inAppConflict in allConflictData.InAppConflicts)
@@ -199,7 +199,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
IsEnabled = gpo == GpoRuleConfigured.Enabled || (gpo != GpoRuleConfigured.Disabled && ModuleHelper.GetIsModuleEnabled(generalSettingsConfig, moduleType)),
IsLocked = gpo == GpoRuleConfigured.Enabled || gpo == GpoRuleConfigured.Disabled,
Icon = ModuleHelper.GetModuleTypeFluentIconName(moduleType),
IsNew = moduleType == ModuleType.CursorWrap,
IsNew = moduleType == ModuleType.CursorWrap || moduleType == ModuleType.PowerDisplay,
DashboardModuleItems = GetModuleItems(moduleType),
ClickCommand = new RelayCommand<object>(DashboardListItemClick),
};
@@ -258,7 +258,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
finally
{
// Use dispatcher to reset flag after UI updates complete
dispatcher.BeginInvoke(System.Windows.Threading.DispatcherPriority.Background, () =>
dispatcher.TryEnqueue(DispatcherQueuePriority.Low, () =>
{
_isSorting = false;
});
@@ -459,6 +459,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
ModuleType.MouseJump => GetModuleItemsMouseJump(),
ModuleType.MousePointerCrosshairs => GetModuleItemsMousePointerCrosshairs(),
ModuleType.Peek => GetModuleItemsPeek(),
ModuleType.PowerDisplay => GetModuleItemsPowerDisplay(),
ModuleType.PowerLauncher => GetModuleItemsPowerLauncher(),
ModuleType.PowerAccent => GetModuleItemsPowerAccent(),
ModuleType.Workspaces => GetModuleItemsWorkspaces(),
@@ -738,6 +739,18 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
return new ObservableCollection<DashboardModuleItem>(list);
}
private ObservableCollection<DashboardModuleItem> GetModuleItemsPowerDisplay()
{
ISettingsRepository<PowerDisplaySettings> moduleSettingsRepository = SettingsRepository<PowerDisplaySettings>.GetInstance(SettingsUtils.Default);
var settings = moduleSettingsRepository.SettingsConfig;
var list = new List<DashboardModuleItem>
{
new DashboardModuleShortcutItem() { Label = resourceLoader.GetString("PowerDisplay_ToggleWindow"), Shortcut = settings.Properties.ActivationShortcut.GetKeysList() },
new DashboardModuleButtonItem() { ButtonTitle = resourceLoader.GetString("PowerDisplay_LaunchButtonControl/Header"), IsButtonDescriptionVisible = true, ButtonDescription = resourceLoader.GetString("PowerDisplay_LaunchButtonControl/Description"), ButtonGlyph = "ms-appx:///Assets/Settings/Icons/PowerDisplay.png", ButtonClickHandler = PowerDisplayLaunchClicked },
};
return new ObservableCollection<DashboardModuleItem>(list);
}
internal void SWVersionButtonClicked()
{
NavigationService.Navigate(typeof(GeneralPage));
@@ -775,6 +788,12 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
SendConfigMSG("{\"action\":{\"RegistryPreview\":{\"action_name\":\"" + actionName + "\", \"value\":\"\"}}}");
}
private void PowerDisplayLaunchClicked(object sender, RoutedEventArgs e)
{
var actionName = "Launch";
SendConfigMSG("{\"action\":{\"PowerDisplay\":{\"action_name\":\"" + actionName + "\", \"value\":\"\"}}}");
}
internal void DashboardListItemClick(object sender)
{
if (sender is ModuleType moduleType)

View File

@@ -2,10 +2,13 @@
// 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 System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text.Json;
@@ -17,6 +20,8 @@ using Microsoft.PowerToys.Settings.UI.Library.Helpers;
using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
using Microsoft.PowerToys.Settings.UI.SerializationContext;
using Newtonsoft.Json.Linq;
using PowerDisplay.Common.Models;
using PowerDisplay.Common.Services;
using PowerToys.GPOWrapper;
using Settings.UI.Library;
using Settings.UI.Library.Helpers;
@@ -33,14 +38,14 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
public ObservableCollection<SearchLocation> SearchLocations { get; } = new();
public LightSwitchViewModel(ISettingsRepository<GeneralSettings> settingsRepository, LightSwitchSettings initialSettings = null, Func<string, int> ipcMSGCallBackFunc = null)
public LightSwitchViewModel(ISettingsRepository<GeneralSettings> settingsRepository, LightSwitchSettings? initialSettings = null, Func<string, int>? ipcMSGCallBackFunc = null)
{
ArgumentNullException.ThrowIfNull(settingsRepository);
GeneralSettingsConfig = settingsRepository.SettingsConfig;
InitializeEnabledValue();
_moduleSettings = initialSettings ?? new LightSwitchSettings();
SendConfigMSG = ipcMSGCallBackFunc;
SendConfigMSG = ipcMSGCallBackFunc ?? (_ => 0);
ForceLightCommand = new RelayCommand(ForceLightNow);
ForceDarkCommand = new RelayCommand(ForceDarkNow);
@@ -53,7 +58,11 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
"FollowNightLight",
};
_toggleThemeHotkey = _moduleSettings.Properties.ToggleThemeHotkey.Value;
// Load PowerDisplay profiles
LoadPowerDisplayProfiles();
// Check if PowerDisplay is enabled
CheckPowerDisplayEnabled();
}
public override Dictionary<string, HotkeySettings[]> GetAllHotkeySettings()
@@ -98,6 +107,16 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
SendConfigMSG("{\"action\":{\"LightSwitch\":{\"action_name\":\"" + actionName + "\", \"value\":\"\"}}}");
}
private void SaveSettings()
{
SendConfigMSG(
string.Format(
CultureInfo.InvariantCulture,
"{{ \"powertoys\": {{ \"{0}\": {1} }} }}",
LightSwitchSettings.ModuleName,
JsonSerializer.Serialize(_moduleSettings, SourceGenerationContextContext.Default.LightSwitchSettings)));
}
public LightSwitchSettings ModuleSettings
{
get => _moduleSettings;
@@ -428,9 +447,9 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
}
}
private SearchLocation _selectedSearchLocation;
private SearchLocation? _selectedSearchLocation;
public SearchLocation SelectedCity
public SearchLocation? SelectedCity
{
get => _selectedSearchLocation;
set
@@ -440,7 +459,10 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
_selectedSearchLocation = value;
NotifyPropertyChanged();
UpdateSunTimes(_selectedSearchLocation.Latitude, _selectedSearchLocation.Longitude, _selectedSearchLocation.City);
if (_selectedSearchLocation != null)
{
UpdateSunTimes(_selectedSearchLocation.Latitude, _selectedSearchLocation.Longitude, _selectedSearchLocation.City);
}
}
}
}
@@ -554,12 +576,248 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
}
}
public void NotifyPropertyChanged([CallerMemberName] string propertyName = null)
public void NotifyPropertyChanged([CallerMemberName] string? propertyName = null)
{
Logger.LogInfo($"Changed the property {propertyName}");
OnPropertyChanged(propertyName);
}
// PowerDisplay Integration Properties and Methods
public ObservableCollection<PowerDisplayProfile> AvailableProfiles
{
get => _availableProfiles;
set
{
if (_availableProfiles != value)
{
_availableProfiles = value;
NotifyPropertyChanged();
}
}
}
public bool IsPowerDisplayEnabled
{
get => _isPowerDisplayEnabled;
set
{
if (_isPowerDisplayEnabled != value)
{
_isPowerDisplayEnabled = value;
NotifyPropertyChanged();
NotifyPropertyChanged(nameof(ShowPowerDisplayDisabledWarning));
}
}
}
public bool ShowPowerDisplayDisabledWarning => !IsPowerDisplayEnabled;
public bool EnableDarkModeProfile
{
get => ModuleSettings.Properties.EnableDarkModeProfile.Value;
set
{
if (ModuleSettings.Properties.EnableDarkModeProfile.Value != value)
{
ModuleSettings.Properties.EnableDarkModeProfile.Value = value;
NotifyPropertyChanged();
NotifyPropertyChanged(nameof(ShowPowerDisplayDisabledWarning));
SaveSettings();
}
}
}
public bool EnableLightModeProfile
{
get => ModuleSettings.Properties.EnableLightModeProfile.Value;
set
{
if (ModuleSettings.Properties.EnableLightModeProfile.Value != value)
{
ModuleSettings.Properties.EnableLightModeProfile.Value = value;
NotifyPropertyChanged();
NotifyPropertyChanged(nameof(ShowPowerDisplayDisabledWarning));
SaveSettings();
}
}
}
public PowerDisplayProfile? SelectedDarkModeProfile
{
get => _selectedDarkModeProfile;
set
{
if (_selectedDarkModeProfile != value)
{
_selectedDarkModeProfile = value;
// Sync with the string property stored in settings
var newProfileName = value?.Name ?? string.Empty;
if (ModuleSettings.Properties.DarkModeProfile.Value != newProfileName)
{
ModuleSettings.Properties.DarkModeProfile.Value = newProfileName;
SaveSettings();
}
NotifyPropertyChanged();
}
}
}
public PowerDisplayProfile? SelectedLightModeProfile
{
get => _selectedLightModeProfile;
set
{
if (_selectedLightModeProfile != value)
{
_selectedLightModeProfile = value;
// Sync with the string property stored in settings
var newProfileName = value?.Name ?? string.Empty;
if (ModuleSettings.Properties.LightModeProfile.Value != newProfileName)
{
ModuleSettings.Properties.LightModeProfile.Value = newProfileName;
SaveSettings();
}
NotifyPropertyChanged();
}
}
}
// Legacy string properties for backwards compatibility with settings persistence
public string DarkModeProfile
{
get => ModuleSettings.Properties.DarkModeProfile.Value;
set
{
if (ModuleSettings.Properties.DarkModeProfile.Value != value)
{
ModuleSettings.Properties.DarkModeProfile.Value = value;
// Sync with the object property
UpdateSelectedProfileFromName(value, isDarkMode: true);
NotifyPropertyChanged();
}
}
}
public string LightModeProfile
{
get => ModuleSettings.Properties.LightModeProfile.Value;
set
{
if (ModuleSettings.Properties.LightModeProfile.Value != value)
{
ModuleSettings.Properties.LightModeProfile.Value = value;
// Sync with the object property
UpdateSelectedProfileFromName(value, isDarkMode: false);
NotifyPropertyChanged();
}
}
}
private void LoadPowerDisplayProfiles()
{
try
{
var profilesData = ProfileService.LoadProfiles();
AvailableProfiles.Clear();
foreach (var profile in profilesData.Profiles)
{
AvailableProfiles.Add(profile);
}
Logger.LogInfo($"Loaded {profilesData.Profiles.Count} PowerDisplay profiles");
// Sync selected profiles from settings
UpdateSelectedProfileFromName(ModuleSettings.Properties.DarkModeProfile.Value, isDarkMode: true);
UpdateSelectedProfileFromName(ModuleSettings.Properties.LightModeProfile.Value, isDarkMode: false);
}
catch (Exception ex)
{
Logger.LogError($"Failed to load PowerDisplay profiles: {ex.Message}");
AvailableProfiles.Clear();
}
}
/// <summary>
/// Helper method to sync the selected profile object from the profile name stored in settings.
/// If the configured profile no longer exists, clears the selection and updates settings.
/// </summary>
private void UpdateSelectedProfileFromName(string profileName, bool isDarkMode)
{
PowerDisplayProfile? matchingProfile = null;
if (!string.IsNullOrEmpty(profileName))
{
matchingProfile = AvailableProfiles.FirstOrDefault(p =>
p.Name.Equals(profileName, StringComparison.OrdinalIgnoreCase));
// If the configured profile no longer exists, clear it from settings
if (matchingProfile == null)
{
Logger.LogWarning($"Configured {(isDarkMode ? "dark" : "light")} mode profile '{profileName}' no longer exists, clearing selection");
if (isDarkMode)
{
ModuleSettings.Properties.DarkModeProfile.Value = string.Empty;
}
else
{
ModuleSettings.Properties.LightModeProfile.Value = string.Empty;
}
SaveSettings();
}
}
if (isDarkMode)
{
if (_selectedDarkModeProfile != matchingProfile)
{
_selectedDarkModeProfile = matchingProfile;
NotifyPropertyChanged(nameof(SelectedDarkModeProfile));
}
}
else
{
if (_selectedLightModeProfile != matchingProfile)
{
_selectedLightModeProfile = matchingProfile;
NotifyPropertyChanged(nameof(SelectedLightModeProfile));
}
}
}
private void CheckPowerDisplayEnabled()
{
try
{
var settingsUtils = SettingsUtils.Default;
var generalSettings = settingsUtils.GetSettingsOrDefault<GeneralSettings>(string.Empty);
IsPowerDisplayEnabled = generalSettings?.Enabled?.PowerDisplay ?? false;
Logger.LogInfo($"PowerDisplay enabled status: {IsPowerDisplayEnabled}");
}
catch (Exception ex)
{
Logger.LogError($"Failed to check PowerDisplay enabled status: {ex.Message}");
IsPowerDisplayEnabled = false;
}
}
public void RefreshPowerDisplayStatus()
{
CheckPowerDisplayEnabled();
NotifyPropertyChanged(nameof(ShowPowerDisplayDisabledWarning));
}
public void RefreshEnabledState()
{
OnPropertyChanged(nameof(IsEnabled));
@@ -576,6 +834,10 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
OnPropertyChanged(nameof(Latitude));
OnPropertyChanged(nameof(Longitude));
OnPropertyChanged(nameof(ScheduleMode));
OnPropertyChanged(nameof(EnableDarkModeProfile));
OnPropertyChanged(nameof(EnableLightModeProfile));
OnPropertyChanged(nameof(DarkModeProfile));
OnPropertyChanged(nameof(LightModeProfile));
}
private void UpdateSunTimes(double latitude, double longitude, string city = "n/a")
@@ -630,10 +892,15 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
private GpoRuleConfigured _enabledGpoRuleConfiguration;
private LightSwitchSettings _moduleSettings;
private bool _isEnabled;
private HotkeySettings _toggleThemeHotkey;
private TimeSpan? _sunriseTimeSpan;
private TimeSpan? _sunsetTimeSpan;
// PowerDisplay integration
private ObservableCollection<PowerDisplayProfile> _availableProfiles = new ObservableCollection<PowerDisplayProfile>();
private bool _isPowerDisplayEnabled;
private PowerDisplayProfile? _selectedDarkModeProfile;
private PowerDisplayProfile? _selectedLightModeProfile;
public ICommand ForceLightCommand { get; }
public ICommand ForceDarkCommand { get; }

View File

@@ -0,0 +1,178 @@
// 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.ComponentModel;
using System.Runtime.CompilerServices;
using Microsoft.PowerToys.Settings.UI.Library;
namespace Microsoft.PowerToys.Settings.UI.ViewModels
{
/// <summary>
/// ViewModel for monitor selection in profile editor
/// </summary>
public class MonitorSelectionItem : INotifyPropertyChanged
{
private bool _isSelected;
private int _brightness = 100;
private int _contrast = 50;
private int _volume = 50;
private int _colorTemperature = 6500;
private bool _includeBrightness;
private bool _includeContrast;
private bool _includeVolume;
private bool _includeColorTemperature;
public required MonitorInfo Monitor { get; set; }
public bool SuppressAutoSelection { get; set; }
public bool IsSelected
{
get => _isSelected;
set
{
if (_isSelected != value)
{
_isSelected = value;
OnPropertyChanged();
}
}
}
public int Brightness
{
get => _brightness;
set
{
if (_brightness != value)
{
_brightness = value;
OnPropertyChanged();
if (!SuppressAutoSelection)
{
IncludeBrightness = true;
}
}
}
}
public int Contrast
{
get => _contrast;
set
{
if (_contrast != value)
{
_contrast = value;
OnPropertyChanged();
if (!SuppressAutoSelection)
{
IncludeContrast = true;
}
}
}
}
public int Volume
{
get => _volume;
set
{
if (_volume != value)
{
_volume = value;
OnPropertyChanged();
if (!SuppressAutoSelection)
{
IncludeVolume = true;
}
}
}
}
public int ColorTemperature
{
get => _colorTemperature;
set
{
if (_colorTemperature != value)
{
_colorTemperature = value;
OnPropertyChanged();
if (!SuppressAutoSelection)
{
IncludeColorTemperature = true;
}
}
}
}
public bool SupportsContrast => Monitor?.SupportsContrast ?? false;
public bool SupportsVolume => Monitor?.SupportsVolume ?? false;
public bool SupportsColorTemperature => Monitor?.SupportsColorTemperature ?? false;
public bool IncludeBrightness
{
get => _includeBrightness;
set
{
if (_includeBrightness != value)
{
_includeBrightness = value;
OnPropertyChanged();
}
}
}
public bool IncludeContrast
{
get => _includeContrast;
set
{
if (_includeContrast != value)
{
_includeContrast = value;
OnPropertyChanged();
}
}
}
public bool IncludeVolume
{
get => _includeVolume;
set
{
if (_includeVolume != value)
{
_includeVolume = value;
OnPropertyChanged();
}
}
}
public bool IncludeColorTemperature
{
get => _includeColorTemperature;
set
{
if (_includeColorTemperature != value)
{
_includeColorTemperature = value;
OnPropertyChanged();
}
}
}
public event PropertyChangedEventHandler? PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}

View File

@@ -0,0 +1,671 @@
// 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.Generic;
using System.Collections.ObjectModel;
using System.Globalization;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text.Json;
using global::PowerToys.GPOWrapper;
using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Helpers;
using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.PowerToys.Settings.UI.Library.Helpers;
using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
using Microsoft.PowerToys.Settings.UI.Library.ViewModels.Commands;
using PowerDisplay.Common.Models;
using PowerDisplay.Common.Services;
using PowerDisplay.Common.Utils;
using PowerToys.Interop;
namespace Microsoft.PowerToys.Settings.UI.ViewModels
{
public partial class PowerDisplayViewModel : PageViewModelBase
{
protected override string ModuleName => PowerDisplaySettings.ModuleName;
private GeneralSettings GeneralSettingsConfig { get; set; }
private SettingsUtils SettingsUtils { get; set; }
public ButtonClickCommand LaunchEventHandler => new ButtonClickCommand(Launch);
public PowerDisplayViewModel(SettingsUtils settingsUtils, ISettingsRepository<GeneralSettings> settingsRepository, ISettingsRepository<PowerDisplaySettings> powerDisplaySettingsRepository, Func<string, int> ipcMSGCallBackFunc)
{
// To obtain the general settings configurations of PowerToys Settings.
ArgumentNullException.ThrowIfNull(settingsRepository);
SettingsUtils = settingsUtils;
GeneralSettingsConfig = settingsRepository.SettingsConfig;
_settings = powerDisplaySettingsRepository.SettingsConfig;
InitializeEnabledValue();
// Initialize monitors collection using property setter for proper subscription setup
var loadedMonitors = _settings.Properties.Monitors;
Logger.LogInfo($"[Constructor] Initializing with {loadedMonitors.Count} monitors from settings");
Monitors = new ObservableCollection<MonitorInfo>(loadedMonitors);
// set the callback functions value to handle outgoing IPC message.
SendConfigMSG = ipcMSGCallBackFunc;
// Load profiles
LoadProfiles();
// Listen for monitor refresh events from PowerDisplay.exe
NativeEventWaiter.WaitForEventLoop(
Constants.RefreshPowerDisplayMonitorsEvent(),
() =>
{
Logger.LogInfo("Received refresh monitors event from PowerDisplay.exe");
ReloadMonitorsFromSettings();
});
}
private GpoRuleConfigured _enabledGpoRuleConfiguration;
private bool _enabledStateIsGPOConfigured;
private void InitializeEnabledValue()
{
_enabledGpoRuleConfiguration = GPOWrapper.GetConfiguredPowerDisplayEnabledValue();
if (_enabledGpoRuleConfiguration == GpoRuleConfigured.Disabled || _enabledGpoRuleConfiguration == GpoRuleConfigured.Enabled)
{
// Get the enabled state from GPO
_enabledStateIsGPOConfigured = true;
_isEnabled = _enabledGpoRuleConfiguration == GpoRuleConfigured.Enabled;
}
else
{
_isEnabled = GeneralSettingsConfig.Enabled.PowerDisplay;
}
}
public bool IsEnabled
{
get => _isEnabled;
set
{
if (_enabledStateIsGPOConfigured)
{
// If it's GPO configured, shouldn't be able to change this state.
return;
}
if (_isEnabled != value)
{
_isEnabled = value;
OnPropertyChanged(nameof(IsEnabled));
GeneralSettingsConfig.Enabled.PowerDisplay = value;
OutGoingGeneralSettings outgoing = new OutGoingGeneralSettings(GeneralSettingsConfig);
SendConfigMSG(outgoing.ToString());
}
}
}
public bool IsEnabledGpoConfigured
{
get => _enabledStateIsGPOConfigured;
}
public bool RestoreSettingsOnStartup
{
get => _settings.Properties.RestoreSettingsOnStartup;
set => SetSettingsProperty(_settings.Properties.RestoreSettingsOnStartup, value, v => _settings.Properties.RestoreSettingsOnStartup = v);
}
public bool ShowSystemTrayIcon
{
get => _settings.Properties.ShowSystemTrayIcon;
set
{
if (SetSettingsProperty(_settings.Properties.ShowSystemTrayIcon, value, v => _settings.Properties.ShowSystemTrayIcon = v))
{
// Explicitly signal PowerDisplay to refresh tray icon
// This is needed because set_config() doesn't signal SettingsUpdatedEvent to avoid UI refresh issues
SignalSettingsUpdated();
Logger.LogInfo($"ShowSystemTrayIcon changed to {value}");
}
}
}
public bool ShowProfileSwitcher
{
get => _settings.Properties.ShowProfileSwitcher;
set
{
if (SetSettingsProperty(_settings.Properties.ShowProfileSwitcher, value, v => _settings.Properties.ShowProfileSwitcher = v))
{
SignalSettingsUpdated();
Logger.LogInfo($"ShowProfileSwitcher changed to {value}");
}
}
}
public bool ShowIdentifyMonitorsButton
{
get => _settings.Properties.ShowIdentifyMonitorsButton;
set
{
if (SetSettingsProperty(_settings.Properties.ShowIdentifyMonitorsButton, value, v => _settings.Properties.ShowIdentifyMonitorsButton = v))
{
SignalSettingsUpdated();
Logger.LogInfo($"ShowIdentifyMonitorsButton changed to {value}");
}
}
}
public HotkeySettings ActivationShortcut
{
get => _settings.Properties.ActivationShortcut;
set
{
if (SetSettingsProperty(_settings.Properties.ActivationShortcut, value, v => _settings.Properties.ActivationShortcut = v))
{
// Signal PowerDisplay.exe to re-register the hotkey
EventHelper.SignalEvent(Constants.HotkeyUpdatedPowerDisplayEvent());
Logger.LogInfo($"ActivationShortcut changed, signaled HotkeyUpdatedPowerDisplayEvent");
}
}
}
public override Dictionary<string, HotkeySettings[]> GetAllHotkeySettings()
{
var hotkeysDict = new Dictionary<string, HotkeySettings[]>
{
[ModuleName] = [ActivationShortcut],
};
return hotkeysDict;
}
/// <summary>
/// Gets or sets the delay in seconds before refreshing monitors after display changes.
/// </summary>
public int MonitorRefreshDelay
{
get => _settings.Properties.MonitorRefreshDelay;
set => SetSettingsProperty(_settings.Properties.MonitorRefreshDelay, value, v => _settings.Properties.MonitorRefreshDelay = v);
}
private readonly List<int> _monitorRefreshDelayOptions = new List<int> { 1, 2, 3, 5, 10 };
public List<int> MonitorRefreshDelayOptions => _monitorRefreshDelayOptions;
public ObservableCollection<MonitorInfo> Monitors
{
get => _monitors;
set
{
if (_monitors != null)
{
_monitors.CollectionChanged -= Monitors_CollectionChanged;
UnsubscribeFromItemPropertyChanged(_monitors);
}
_monitors = value;
if (_monitors != null)
{
_monitors.CollectionChanged += Monitors_CollectionChanged;
SubscribeToItemPropertyChanged(_monitors);
}
OnPropertyChanged(nameof(Monitors));
HasMonitors = _monitors?.Count > 0;
// Update TotalMonitorCount for dynamic DisplayName
UpdateTotalMonitorCount();
}
}
public bool HasMonitors
{
get => _hasMonitors;
set
{
if (_hasMonitors != value)
{
_hasMonitors = value;
OnPropertyChanged();
}
}
}
private void Monitors_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
SubscribeToItemPropertyChanged(e.NewItems?.Cast<MonitorInfo>());
UnsubscribeFromItemPropertyChanged(e.OldItems?.Cast<MonitorInfo>());
HasMonitors = _monitors.Count > 0;
_settings.Properties.Monitors = _monitors.ToList();
NotifySettingsChanged();
// Update TotalMonitorCount for dynamic DisplayName
UpdateTotalMonitorCount();
}
/// <summary>
/// Update TotalMonitorCount on all monitors for dynamic DisplayName formatting.
/// When multiple monitors exist, DisplayName shows "Name N" format.
/// </summary>
private void UpdateTotalMonitorCount()
{
if (_monitors == null)
{
return;
}
var count = _monitors.Count;
foreach (var monitor in _monitors)
{
monitor.TotalMonitorCount = count;
}
}
[System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA1816:Dispose methods should call SuppressFinalize", Justification = "Base class PageViewModelBase.Dispose() handles GC.SuppressFinalize")]
public override void Dispose()
{
// Unsubscribe from monitor property changes
UnsubscribeFromItemPropertyChanged(_monitors);
// Unsubscribe from collection changes
if (_monitors != null)
{
_monitors.CollectionChanged -= Monitors_CollectionChanged;
}
base.Dispose();
}
/// <summary>
/// Subscribe to PropertyChanged events for items in the collection
/// </summary>
private void SubscribeToItemPropertyChanged(IEnumerable<MonitorInfo> items)
{
if (items != null)
{
foreach (var item in items)
{
item.PropertyChanged += OnMonitorPropertyChanged;
}
}
}
/// <summary>
/// Unsubscribe from PropertyChanged events for items in the collection
/// </summary>
private void UnsubscribeFromItemPropertyChanged(IEnumerable<MonitorInfo> items)
{
if (items != null)
{
foreach (var item in items)
{
item.PropertyChanged -= OnMonitorPropertyChanged;
}
}
}
/// <summary>
/// Handle PropertyChanged events from MonitorInfo objects
/// </summary>
private void OnMonitorPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
if (sender is MonitorInfo monitor)
{
Logger.LogDebug($"[PowerDisplayViewModel] Monitor {monitor.Name} property {e.PropertyName} changed");
}
// Update the settings object to keep it in sync
_settings.Properties.Monitors = _monitors.ToList();
// Save settings when any monitor property changes
NotifySettingsChanged();
// For feature visibility properties, explicitly signal PowerDisplay to refresh
// This is needed because set_config() doesn't signal SettingsUpdatedEvent to avoid UI refresh issues
if (e.PropertyName == nameof(MonitorInfo.EnableContrast) ||
e.PropertyName == nameof(MonitorInfo.EnableVolume) ||
e.PropertyName == nameof(MonitorInfo.EnableInputSource) ||
e.PropertyName == nameof(MonitorInfo.EnableRotation) ||
e.PropertyName == nameof(MonitorInfo.EnableColorTemperature) ||
e.PropertyName == nameof(MonitorInfo.EnablePowerState) ||
e.PropertyName == nameof(MonitorInfo.IsHidden))
{
SignalSettingsUpdated();
}
}
/// <summary>
/// Signal PowerDisplay.exe that settings have been updated and need to be applied
/// </summary>
private void SignalSettingsUpdated()
{
EventHelper.SignalEvent(Constants.SettingsUpdatedPowerDisplayEvent());
Logger.LogInfo("Signaled SettingsUpdatedPowerDisplayEvent for feature visibility change");
}
public void Launch()
{
var actionMessage = new PowerDisplayActionMessage
{
Action = new PowerDisplayActionMessage.ActionData
{
PowerDisplay = new PowerDisplayActionMessage.PowerDisplayAction
{
ActionName = "Launch",
Value = string.Empty,
},
},
};
SendConfigMSG(JsonSerializer.Serialize(actionMessage, SettingsSerializationContext.Default.PowerDisplayActionMessage));
}
/// <summary>
/// Reload monitor list from settings file (called when PowerDisplay.exe signals monitor changes)
/// </summary>
private void ReloadMonitorsFromSettings()
{
try
{
Logger.LogInfo("Reloading monitors from settings file");
// Read fresh settings from file
var updatedSettings = SettingsUtils.GetSettingsOrDefault<PowerDisplaySettings>(PowerDisplaySettings.ModuleName);
var updatedMonitors = updatedSettings.Properties.Monitors;
Logger.LogInfo($"[ReloadMonitors] Loaded {updatedMonitors.Count} monitors from settings");
// Update existing MonitorInfo objects instead of replacing the collection
// This preserves XAML x:Bind bindings which reference specific object instances
if (Monitors == null)
{
// First time initialization - create new collection
Monitors = new ObservableCollection<MonitorInfo>(updatedMonitors);
}
else
{
// Create a dictionary for quick lookup by Id
var updatedMonitorsDict = updatedMonitors.ToDictionary(m => m.Id, m => m);
// Update existing monitors or remove ones that no longer exist
for (int i = Monitors.Count - 1; i >= 0; i--)
{
var existingMonitor = Monitors[i];
if (updatedMonitorsDict.TryGetValue(existingMonitor.Id, out var updatedMonitor)
&& updatedMonitor != null)
{
// Monitor still exists - update its properties in place
Logger.LogInfo($"[ReloadMonitors] Updating existing monitor: {existingMonitor.Id}");
existingMonitor.UpdateFrom(updatedMonitor);
updatedMonitorsDict.Remove(existingMonitor.Id);
}
else
{
// Monitor no longer exists - remove from collection
Logger.LogInfo($"[ReloadMonitors] Removing monitor: {existingMonitor.Id}");
Monitors.RemoveAt(i);
}
}
// Add any new monitors that weren't in the existing collection
foreach (var newMonitor in updatedMonitorsDict.Values)
{
Logger.LogInfo($"[ReloadMonitors] Adding new monitor: {newMonitor.Id}");
Monitors.Add(newMonitor);
}
}
// Update internal settings reference
_settings.Properties.Monitors = updatedMonitors;
Logger.LogInfo($"Successfully reloaded {updatedMonitors.Count} monitors");
}
catch (Exception ex)
{
Logger.LogError($"Failed to reload monitors from settings: {ex.Message}");
}
}
private Func<string, int> SendConfigMSG { get; }
private bool _isEnabled;
private PowerDisplaySettings _settings;
private ObservableCollection<MonitorInfo> _monitors;
private bool _hasMonitors;
// Profile-related fields
private ObservableCollection<PowerDisplayProfile> _profiles = new ObservableCollection<PowerDisplayProfile>();
/// <summary>
/// Gets or sets collection of available profiles (for button display)
/// </summary>
public ObservableCollection<PowerDisplayProfile> Profiles
{
get => _profiles;
set
{
if (_profiles != value)
{
_profiles = value;
OnPropertyChanged();
}
}
}
public void RefreshEnabledState()
{
InitializeEnabledValue();
OnPropertyChanged(nameof(IsEnabled));
}
private bool SetSettingsProperty<T>(T currentValue, T newValue, Action<T> setter, [CallerMemberName] string propertyName = null)
{
if (EqualityComparer<T>.Default.Equals(currentValue, newValue))
{
return false;
}
setter(newValue);
OnPropertyChanged(propertyName);
NotifySettingsChanged();
return true;
}
/// <summary>
/// Load profiles from disk
/// </summary>
private void LoadProfiles()
{
try
{
var profilesData = ProfileService.LoadProfiles();
// Load profile objects (no Custom - it's not a profile anymore)
Profiles.Clear();
foreach (var profile in profilesData.Profiles)
{
Profiles.Add(profile);
}
Logger.LogInfo($"Loaded {Profiles.Count} profiles");
}
catch (Exception ex)
{
Logger.LogError($"Failed to load profiles: {ex.Message}");
Profiles.Clear();
}
}
/// <summary>
/// Apply a profile to monitors
/// </summary>
public void ApplyProfile(PowerDisplayProfile profile)
{
try
{
if (profile == null || !profile.IsValid())
{
Logger.LogWarning("Invalid profile");
return;
}
Logger.LogInfo($"Applying profile: {profile.Name}");
// Send custom action to trigger profile application
// The profile name is passed via Named Pipe IPC to PowerDisplay.exe
var actionMessage = new PowerDisplayActionMessage
{
Action = new PowerDisplayActionMessage.ActionData
{
PowerDisplay = new PowerDisplayActionMessage.PowerDisplayAction
{
ActionName = "ApplyProfile",
Value = profile.Name,
},
},
};
SendConfigMSG(JsonSerializer.Serialize(actionMessage, SettingsSerializationContext.Default.PowerDisplayActionMessage));
Logger.LogInfo($"Profile '{profile.Name}' applied successfully");
}
catch (Exception ex)
{
Logger.LogError($"Failed to apply profile: {ex.Message}");
}
}
/// <summary>
/// Create a new profile
/// </summary>
public void CreateProfile(PowerDisplayProfile profile)
{
try
{
if (profile == null || !profile.IsValid())
{
Logger.LogWarning("Invalid profile");
return;
}
Logger.LogInfo($"Creating profile: {profile.Name}");
var profilesData = ProfileService.LoadProfiles();
profilesData.SetProfile(profile);
ProfileService.SaveProfiles(profilesData);
// Reload profile list
LoadProfiles();
// Signal PowerDisplay to reload profiles
SignalSettingsUpdated();
Logger.LogInfo($"Profile '{profile.Name}' created successfully");
}
catch (Exception ex)
{
Logger.LogError($"Failed to create profile: {ex.Message}");
}
}
/// <summary>
/// Update an existing profile
/// </summary>
public void UpdateProfile(string oldName, PowerDisplayProfile newProfile)
{
try
{
if (newProfile == null || !newProfile.IsValid())
{
Logger.LogWarning("Invalid profile");
return;
}
Logger.LogInfo($"Updating profile: {oldName} -> {newProfile.Name}");
var profilesData = ProfileService.LoadProfiles();
// Remove old profile and add updated one
profilesData.RemoveProfile(oldName);
profilesData.SetProfile(newProfile);
ProfileService.SaveProfiles(profilesData);
// Reload profile list
LoadProfiles();
// Signal PowerDisplay to reload profiles
SignalSettingsUpdated();
Logger.LogInfo($"Profile updated to '{newProfile.Name}' successfully");
}
catch (Exception ex)
{
Logger.LogError($"Failed to update profile: {ex.Message}");
}
}
/// <summary>
/// Delete a profile
/// </summary>
public void DeleteProfile(string profileName)
{
try
{
if (string.IsNullOrEmpty(profileName))
{
return;
}
Logger.LogInfo($"Deleting profile: {profileName}");
var profilesData = ProfileService.LoadProfiles();
profilesData.RemoveProfile(profileName);
ProfileService.SaveProfiles(profilesData);
// Reload profile list
LoadProfiles();
// Signal PowerDisplay to reload profiles
SignalSettingsUpdated();
Logger.LogInfo($"Profile '{profileName}' deleted successfully");
}
catch (Exception ex)
{
Logger.LogError($"Failed to delete profile: {ex.Message}");
}
}
private void NotifySettingsChanged()
{
// Skip during initialization when SendConfigMSG is not yet set
if (SendConfigMSG == null)
{
return;
}
// Persist locally first so settings survive even if the module DLL isn't loaded yet.
SettingsUtils.SaveSettings(_settings.ToJsonString(), PowerDisplaySettings.ModuleName);
// Using InvariantCulture as this is an IPC message
// This message will be intercepted by the runner, which passes the serialized JSON to
// PowerDisplay Module Interface's set_config() method, which then applies it in-process.
SendConfigMSG(
string.Format(
CultureInfo.InvariantCulture,
"{{ \"powertoys\": {{ \"{0}\": {1} }} }}",
PowerDisplaySettings.ModuleName,
_settings.ToJsonString()));
}
}
}

View File

@@ -0,0 +1,140 @@
// 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.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
using Microsoft.PowerToys.Settings.UI.Library;
using PowerDisplay.Common.Models;
namespace Microsoft.PowerToys.Settings.UI.ViewModels
{
/// <summary>
/// ViewModel for Profile Editor Dialog
/// </summary>
public class ProfileEditorViewModel : INotifyPropertyChanged
{
private string _profileName = string.Empty;
private ObservableCollection<MonitorSelectionItem> _monitors;
public ProfileEditorViewModel(ObservableCollection<MonitorInfo> availableMonitors, string defaultName = "")
{
_profileName = defaultName;
_monitors = new ObservableCollection<MonitorSelectionItem>();
// Set TotalMonitorCount for DisplayName to show monitor numbers when multiple monitors exist
int totalCount = availableMonitors.Count;
foreach (var monitor in availableMonitors)
{
monitor.TotalMonitorCount = totalCount;
}
// Initialize monitor selection items
foreach (var monitor in availableMonitors)
{
var item = new MonitorSelectionItem
{
SuppressAutoSelection = true,
Monitor = monitor,
IsSelected = false,
Brightness = monitor.CurrentBrightness,
Contrast = 50, // Default value (MonitorInfo doesn't store contrast)
Volume = 50, // Default value (MonitorInfo doesn't store volume)
ColorTemperature = monitor.ColorTemperatureVcp,
};
item.SuppressAutoSelection = false;
// Subscribe to selection and checkbox changes
item.PropertyChanged += OnMonitorItemPropertyChanged;
_monitors.Add(item);
}
}
public string ProfileName
{
get => _profileName;
set
{
if (_profileName != value)
{
_profileName = value;
OnPropertyChanged();
OnPropertyChanged(nameof(CanSave));
}
}
}
public ObservableCollection<MonitorSelectionItem> Monitors
{
get => _monitors;
set
{
if (_monitors != value)
{
_monitors = value;
OnPropertyChanged();
}
}
}
public bool HasSelectedMonitors => _monitors?.Any(m => m.IsSelected) ?? false;
public bool HasValidSettings => _monitors != null &&
_monitors.Any(m => m.IsSelected) &&
_monitors.Where(m => m.IsSelected).All(m => m.IncludeBrightness || m.IncludeContrast || m.IncludeVolume || m.IncludeColorTemperature);
public bool CanSave => !string.IsNullOrWhiteSpace(_profileName) && HasSelectedMonitors && HasValidSettings;
public PowerDisplayProfile CreateProfile()
{
var settings = _monitors
.Where(m => m.IsSelected)
.Select(m => new ProfileMonitorSetting(
m.Monitor.Id, // Monitor Id (unique identifier)
m.IncludeBrightness ? (int?)m.Brightness : null,
m.IncludeColorTemperature && m.SupportsColorTemperature ? (int?)m.ColorTemperature : null,
m.IncludeContrast && m.SupportsContrast ? (int?)m.Contrast : null,
m.IncludeVolume && m.SupportsVolume ? (int?)m.Volume : null))
.ToList();
return new PowerDisplayProfile(_profileName, settings);
}
public event PropertyChangedEventHandler? PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
/// <summary>
/// Handle property changes from monitor selection items.
/// Centralizes validation state updates to avoid duplication.
/// </summary>
private void OnMonitorItemPropertyChanged(object? sender, PropertyChangedEventArgs e)
{
// Update selection-dependent properties
if (e.PropertyName == nameof(MonitorSelectionItem.IsSelected))
{
OnPropertyChanged(nameof(HasSelectedMonitors));
}
// Update validation state for relevant property changes
if (e.PropertyName == nameof(MonitorSelectionItem.IsSelected) ||
e.PropertyName == nameof(MonitorSelectionItem.IncludeBrightness) ||
e.PropertyName == nameof(MonitorSelectionItem.IncludeContrast) ||
e.PropertyName == nameof(MonitorSelectionItem.IncludeVolume) ||
e.PropertyName == nameof(MonitorSelectionItem.IncludeColorTemperature))
{
OnPropertyChanged(nameof(CanSave));
OnPropertyChanged(nameof(HasValidSettings));
}
}
}
}