mirror of
https://github.com/microsoft/PowerToys.git
synced 2025-12-16 11:48:06 +01:00
Refactor logging, state management, and IPC
Improved logging consistency by replacing verbose debug logs with concise warnings and errors where appropriate. Introduced a debounced-save strategy in `MonitorStateManager` to optimize disk I/O during rapid updates. Removed the `PowerDisplayProcessManager` class and named pipe-based IPC, indicating a significant architectural shift. Translated all comments from Chinese to English for better readability. Simplified and refactored initialization logic in `MainWindow.xaml.cs` for better maintainability. Removed unused code, including system tray-related structures and imports, and improved overall code clarity and consistency.
This commit is contained in:
@@ -63,7 +63,7 @@ namespace PowerDisplay.Core
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogInfo("WMI brightness control not available on this system");
|
||||
Logger.LogWarning("WMI brightness control not available on this system");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -199,31 +199,23 @@ namespace PowerDisplay.Core
|
||||
/// </summary>
|
||||
public async Task<MonitorOperationResult> SetBrightnessAsync(string monitorId, int brightness, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Logger.LogDebug($"[MonitorManager] SetBrightnessAsync called for {monitorId}, brightness={brightness}");
|
||||
|
||||
var monitor = GetMonitor(monitorId);
|
||||
if (monitor == null)
|
||||
{
|
||||
Logger.LogError($"[MonitorManager] Monitor not found: {monitorId}");
|
||||
Logger.LogError($"Monitor not found: {monitorId}");
|
||||
return MonitorOperationResult.Failure("Monitor not found");
|
||||
}
|
||||
|
||||
Logger.LogDebug($"[MonitorManager] Monitor found: {monitor.Id}, Type={monitor.Type}, Handle=0x{monitor.Handle:X}, DeviceKey={monitor.DeviceKey}");
|
||||
|
||||
var controller = GetControllerForMonitor(monitor);
|
||||
if (controller == null)
|
||||
{
|
||||
Logger.LogError($"[MonitorManager] No controller available for monitor {monitorId}, Type={monitor.Type}");
|
||||
Logger.LogError($"No controller available for monitor {monitorId}, Type={monitor.Type}");
|
||||
return MonitorOperationResult.Failure("No controller available for this monitor");
|
||||
}
|
||||
|
||||
Logger.LogDebug($"[MonitorManager] Controller found: {controller.GetType().Name}");
|
||||
|
||||
try
|
||||
{
|
||||
Logger.LogDebug($"[MonitorManager] Calling controller.SetBrightnessAsync for {monitor.Id}");
|
||||
var result = await controller.SetBrightnessAsync(monitor, brightness, cancellationToken);
|
||||
Logger.LogDebug($"[MonitorManager] controller.SetBrightnessAsync returned: IsSuccess={result.IsSuccess}, ErrorMessage={result.ErrorMessage}");
|
||||
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
@@ -344,8 +336,6 @@ namespace PowerDisplay.Core
|
||||
// This is a rough mapping - actual values depend on monitor implementation
|
||||
var kelvin = ConvertVcpValueToKelvin(tempInfo.Current, tempInfo.Maximum);
|
||||
monitor.CurrentColorTemperature = kelvin;
|
||||
|
||||
Logger.LogInfo($"Initialized color temperature for {monitorId}: {kelvin}K (VCP: {tempInfo.Current}/{tempInfo.Maximum})");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,8 +25,11 @@ namespace PowerDisplay.Helpers
|
||||
private readonly string _stateFilePath;
|
||||
private readonly Dictionary<string, MonitorState> _states = new();
|
||||
private readonly object _lock = new object();
|
||||
private readonly Timer _saveTimer;
|
||||
|
||||
private bool _disposed;
|
||||
private bool _isDirty;
|
||||
private const int SaveDebounceMs = 2000; // Save 2 seconds after last update
|
||||
|
||||
/// <summary>
|
||||
/// Monitor state data (internal tracking, not serialized)
|
||||
@@ -55,16 +58,40 @@ namespace PowerDisplay.Helpers
|
||||
|
||||
_stateFilePath = Path.Combine(powerToysPath, AppConstants.State.StateFileName);
|
||||
|
||||
// Initialize debounce timer (disabled initially)
|
||||
_saveTimer = new Timer(OnSaveTimerElapsed, null, Timeout.Infinite, Timeout.Infinite);
|
||||
|
||||
// Load existing state if available
|
||||
LoadStateFromDisk();
|
||||
|
||||
Logger.LogInfo($"MonitorStateManager initialized with direct-save strategy, state file: {_stateFilePath}");
|
||||
Logger.LogInfo($"MonitorStateManager initialized with debounced-save strategy (debounce: {SaveDebounceMs}ms), state file: {_stateFilePath}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update monitor parameter and save immediately to disk.
|
||||
/// Timer callback to save state when dirty
|
||||
/// </summary>
|
||||
private void OnSaveTimerElapsed(object? state)
|
||||
{
|
||||
bool shouldSave = false;
|
||||
lock (_lock)
|
||||
{
|
||||
if (_isDirty && !_disposed)
|
||||
{
|
||||
shouldSave = true;
|
||||
_isDirty = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldSave)
|
||||
{
|
||||
SaveStateToDisk();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update monitor parameter and schedule debounced save to disk.
|
||||
/// Uses HardwareId as the stable key.
|
||||
/// Direct-save strategy ensures no data loss and simplifies code (KISS principle).
|
||||
/// Debounced-save strategy reduces disk I/O by batching rapid updates (e.g., during slider drag).
|
||||
/// </summary>
|
||||
public void UpdateMonitorParameter(string hardwareId, string property, int value)
|
||||
{
|
||||
@@ -104,12 +131,15 @@ namespace PowerDisplay.Helpers
|
||||
Logger.LogWarning($"Unknown property: {property}");
|
||||
return;
|
||||
}
|
||||
|
||||
// Mark dirty and schedule debounced save
|
||||
_isDirty = true;
|
||||
}
|
||||
|
||||
// Save immediately after update - simple and reliable!
|
||||
SaveStateToDisk();
|
||||
// Reset timer to debounce rapid updates (e.g., during slider drag)
|
||||
_saveTimer.Change(SaveDebounceMs, Timeout.Infinite);
|
||||
|
||||
Logger.LogTrace($"[State] Updated and saved {property}={value} for monitor HardwareId='{hardwareId}'");
|
||||
Logger.LogTrace($"[State] Updated {property}={value} for monitor HardwareId='{hardwareId}', save scheduled");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -201,7 +231,7 @@ namespace PowerDisplay.Helpers
|
||||
|
||||
/// <summary>
|
||||
/// Save current state to disk immediately.
|
||||
/// Simplified direct-save approach - no timer, no dirty flags, just save!
|
||||
/// Called by timer after debounce period or on dispose to flush pending changes.
|
||||
/// </summary>
|
||||
private void SaveStateToDisk()
|
||||
{
|
||||
@@ -257,12 +287,26 @@ namespace PowerDisplay.Helpers
|
||||
return;
|
||||
}
|
||||
|
||||
// Stop the timer first
|
||||
_saveTimer?.Change(Timeout.Infinite, Timeout.Infinite);
|
||||
|
||||
bool wasDirty = false;
|
||||
lock (_lock)
|
||||
{
|
||||
wasDirty = _isDirty;
|
||||
_disposed = true;
|
||||
_isDirty = false;
|
||||
}
|
||||
|
||||
// State is already saved with each update, no need for final flush!
|
||||
// Flush any pending changes before disposing
|
||||
if (wasDirty)
|
||||
{
|
||||
Logger.LogInfo("Flushing pending state changes before dispose");
|
||||
SaveStateToDisk();
|
||||
}
|
||||
|
||||
_saveTimer?.Dispose();
|
||||
|
||||
Logger.LogInfo("MonitorStateManager disposed");
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.IO.Pipes;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace PowerDisplay.Helpers;
|
||||
|
||||
public static class NamedPipeProcessor
|
||||
{
|
||||
public static async Task ProcessNamedPipeAsync(string pipeName, TimeSpan connectTimeout, Action<string> messageHandler, CancellationToken cancellationToken)
|
||||
{
|
||||
using NamedPipeClientStream pipeClient = new(".", pipeName, PipeDirection.In);
|
||||
|
||||
await pipeClient.ConnectAsync(connectTimeout, cancellationToken);
|
||||
|
||||
using StreamReader streamReader = new(pipeClient, Encoding.Unicode);
|
||||
|
||||
while (true)
|
||||
{
|
||||
var message = await streamReader.ReadLineAsync(cancellationToken);
|
||||
|
||||
if (message != null)
|
||||
{
|
||||
messageHandler(message);
|
||||
}
|
||||
|
||||
var intraMessageDelay = TimeSpan.FromMilliseconds(10);
|
||||
await Task.Delay(intraMessageDelay, cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -224,7 +224,6 @@ namespace PowerDisplay.Native.DDC
|
||||
var vcpCode = _vcpResolver.GetColorTemperatureVcpCode(monitor.Id, monitor.Handle);
|
||||
if (vcpCode.HasValue && DdcCiNative.TrySetVCPFeature(monitor.Handle, vcpCode.Value, targetValue))
|
||||
{
|
||||
Logger.LogInfo($"Successfully set color temperature to {colorTemperature}K via DDC/CI (VCP 0x{vcpCode.Value:X2})");
|
||||
return MonitorOperationResult.Success();
|
||||
}
|
||||
|
||||
@@ -326,25 +325,20 @@ namespace PowerDisplay.Native.DDC
|
||||
{
|
||||
// Get all display devices with stable device IDs (Twinkle Tray style)
|
||||
var displayDevices = DdcCiNative.GetAllDisplayDevices();
|
||||
Logger.LogInfo($"DDC: Found {displayDevices.Count} display devices via EnumDisplayDevices");
|
||||
|
||||
// Also get hardware info for friendly names
|
||||
var monitorDisplayInfo = DdcCiNative.GetAllMonitorDisplayInfo();
|
||||
Logger.LogDebug($"DDC: GetAllMonitorDisplayInfo returned {monitorDisplayInfo.Count} items");
|
||||
|
||||
// Enumerate all monitors
|
||||
var monitorHandles = new List<IntPtr>();
|
||||
Logger.LogDebug($"DDC: About to call EnumDisplayMonitors...");
|
||||
|
||||
bool EnumProc(IntPtr hMonitor, IntPtr hdcMonitor, IntPtr lprcMonitor, IntPtr dwData)
|
||||
{
|
||||
Logger.LogDebug($"DDC: EnumProc callback - hMonitor=0x{hMonitor:X}");
|
||||
monitorHandles.Add(hMonitor);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool enumResult = EnumDisplayMonitors(IntPtr.Zero, IntPtr.Zero, EnumProc, IntPtr.Zero);
|
||||
Logger.LogDebug($"DDC: EnumDisplayMonitors returned {enumResult}, found {monitorHandles.Count} monitor handles");
|
||||
|
||||
if (!enumResult)
|
||||
{
|
||||
@@ -371,7 +365,6 @@ namespace PowerDisplay.Native.DDC
|
||||
{
|
||||
if (attempt > 0)
|
||||
{
|
||||
Logger.LogInfo($"DDC: Retry attempt {attempt}/{maxRetries - 1} for hMonitor 0x{hMonitor:X} after {retryDelayMs}ms delay");
|
||||
await Task.Delay(retryDelayMs, cancellationToken);
|
||||
}
|
||||
|
||||
@@ -402,11 +395,6 @@ namespace PowerDisplay.Native.DDC
|
||||
if (!hasNullHandle)
|
||||
{
|
||||
// Success! All handles are valid
|
||||
if (attempt > 0)
|
||||
{
|
||||
Logger.LogInfo($"DDC: Successfully obtained valid handles on attempt {attempt + 1}");
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
else if (attempt < maxRetries - 1)
|
||||
@@ -474,7 +462,6 @@ namespace PowerDisplay.Native.DDC
|
||||
var monitor = _discoveryHelper.CreateMonitorFromPhysical(monitorToCreate, adapterName, i, monitorDisplayInfo, matchedDevice);
|
||||
if (monitor != null)
|
||||
{
|
||||
Logger.LogInfo($"DDC: Created monitor {monitor.Id} with handle 0x{monitor.Handle:X} (reused: {reusingOldHandle}), deviceKey: {monitor.DeviceKey}");
|
||||
monitors.Add(monitor);
|
||||
|
||||
// Store in new map for cleanup
|
||||
@@ -492,7 +479,6 @@ namespace PowerDisplay.Native.DDC
|
||||
}
|
||||
finally
|
||||
{
|
||||
Logger.LogDebug($"DDC: DiscoverMonitorsAsync returning {monitors.Count} monitors");
|
||||
}
|
||||
|
||||
return monitors;
|
||||
|
||||
@@ -10,7 +10,7 @@ using static PowerDisplay.Native.NativeConstants;
|
||||
using static PowerDisplay.Native.NativeDelegates;
|
||||
using static PowerDisplay.Native.PInvoke;
|
||||
|
||||
// 类型别名,兼容 Windows API 命名约定
|
||||
// Type aliases for Windows API naming conventions compatibility
|
||||
using DISPLAY_DEVICE = PowerDisplay.Native.DisplayDevice;
|
||||
using DISPLAYCONFIG_DEVICE_INFO_HEADER = PowerDisplay.Native.DISPLAYCONFIG_DEVICE_INFO_HEADER;
|
||||
using DISPLAYCONFIG_MODE_INFO = PowerDisplay.Native.DISPLAYCONFIG_MODE_INFO;
|
||||
@@ -27,7 +27,7 @@ using RECT = PowerDisplay.Native.Rect;
|
||||
namespace PowerDisplay.Native.DDC
|
||||
{
|
||||
/// <summary>
|
||||
/// 显示设备信息类
|
||||
/// Display device information class
|
||||
/// </summary>
|
||||
public class DisplayDeviceInfo
|
||||
{
|
||||
@@ -41,11 +41,11 @@ namespace PowerDisplay.Native.DDC
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// DDC/CI 原生 API 封装
|
||||
/// DDC/CI native API wrapper
|
||||
/// </summary>
|
||||
public static class DdcCiNative
|
||||
{
|
||||
// Display Configuration 常量
|
||||
// Display Configuration constants
|
||||
public const uint QdcAllPaths = 0x00000001;
|
||||
|
||||
public const uint QdcOnlyActivePaths = 0x00000002;
|
||||
@@ -55,13 +55,13 @@ namespace PowerDisplay.Native.DDC
|
||||
// Helper Methods
|
||||
|
||||
/// <summary>
|
||||
/// 获取 VCP 功能值的安全包装
|
||||
/// Safe wrapper for getting VCP feature value
|
||||
/// </summary>
|
||||
/// <param name="hPhysicalMonitor">物理显示器句柄</param>
|
||||
/// <param name="vcpCode">VCP 代码</param>
|
||||
/// <param name="currentValue">当前值</param>
|
||||
/// <param name="maxValue">最大值</param>
|
||||
/// <returns>是否成功</returns>
|
||||
/// <param name="hPhysicalMonitor">Physical monitor handle</param>
|
||||
/// <param name="vcpCode">VCP code</param>
|
||||
/// <param name="currentValue">Current value</param>
|
||||
/// <param name="maxValue">Maximum value</param>
|
||||
/// <returns>True if successful</returns>
|
||||
public static bool TryGetVCPFeature(IntPtr hPhysicalMonitor, byte vcpCode, out uint currentValue, out uint maxValue)
|
||||
{
|
||||
currentValue = 0;
|
||||
@@ -83,12 +83,12 @@ namespace PowerDisplay.Native.DDC
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置 VCP 功能值的安全包装
|
||||
/// Safe wrapper for setting VCP feature value
|
||||
/// </summary>
|
||||
/// <param name="hPhysicalMonitor">物理显示器句柄</param>
|
||||
/// <param name="vcpCode">VCP 代码</param>
|
||||
/// <param name="value">新值</param>
|
||||
/// <returns>是否成功</returns>
|
||||
/// <param name="hPhysicalMonitor">Physical monitor handle</param>
|
||||
/// <param name="vcpCode">VCP code</param>
|
||||
/// <param name="value">New value</param>
|
||||
/// <returns>True if successful</returns>
|
||||
public static bool TrySetVCPFeature(IntPtr hPhysicalMonitor, byte vcpCode, uint value)
|
||||
{
|
||||
if (hPhysicalMonitor == IntPtr.Zero)
|
||||
@@ -107,13 +107,13 @@ namespace PowerDisplay.Native.DDC
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取高级亮度信息的安全包装
|
||||
/// Safe wrapper for getting advanced brightness information
|
||||
/// </summary>
|
||||
/// <param name="hPhysicalMonitor">物理显示器句柄</param>
|
||||
/// <param name="minBrightness">最小亮度</param>
|
||||
/// <param name="currentBrightness">当前亮度</param>
|
||||
/// <param name="maxBrightness">最大亮度</param>
|
||||
/// <returns>是否成功</returns>
|
||||
/// <param name="hPhysicalMonitor">Physical monitor handle</param>
|
||||
/// <param name="minBrightness">Minimum brightness</param>
|
||||
/// <param name="currentBrightness">Current brightness</param>
|
||||
/// <param name="maxBrightness">Maximum brightness</param>
|
||||
/// <returns>True if successful</returns>
|
||||
public static bool TryGetMonitorBrightness(IntPtr hPhysicalMonitor, out uint minBrightness, out uint currentBrightness, out uint maxBrightness)
|
||||
{
|
||||
minBrightness = 0;
|
||||
@@ -394,10 +394,10 @@ namespace PowerDisplay.Native.DDC
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取所有显示设备信息(使用 EnumDisplayDevices API)
|
||||
/// 与 Twinkle Tray 实现保持一致
|
||||
/// Get all display device information (using EnumDisplayDevices API)
|
||||
/// Implementation consistent with Twinkle Tray
|
||||
/// </summary>
|
||||
/// <returns>显示设备信息列表</returns>
|
||||
/// <returns>List of display device information</returns>
|
||||
public static unsafe List<DisplayDeviceInfo> GetAllDisplayDevices()
|
||||
{
|
||||
var devices = new List<DisplayDeviceInfo>();
|
||||
|
||||
@@ -42,22 +42,22 @@ namespace PowerDisplay.Native
|
||||
public const byte VcpCodeMute = 0x8D;
|
||||
|
||||
/// <summary>
|
||||
/// VCP code: Color temperature request (主要色温控制)
|
||||
/// VCP code: Color temperature request (primary color temperature control)
|
||||
/// </summary>
|
||||
public const byte VcpCodeColorTemperature = 0x0C;
|
||||
|
||||
/// <summary>
|
||||
/// VCP code: Color temperature increment (色温增量调节)
|
||||
/// VCP code: Color temperature increment (incremental color temperature adjustment)
|
||||
/// </summary>
|
||||
public const byte VcpCodeColorTemperatureIncrement = 0x0B;
|
||||
|
||||
/// <summary>
|
||||
/// VCP code: Gamma correction (Gamma调节)
|
||||
/// VCP code: Gamma correction (gamma adjustment)
|
||||
/// </summary>
|
||||
public const byte VcpCodeGamma = 0x72;
|
||||
|
||||
/// <summary>
|
||||
/// VCP code: Select color preset (颜色预设选择)
|
||||
/// VCP code: Select color preset (color preset selection)
|
||||
/// </summary>
|
||||
public const byte VcpCodeSelectColorPreset = 0x14;
|
||||
|
||||
|
||||
@@ -5,31 +5,31 @@
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
// 类型别名,兼容 Windows API 命名约定
|
||||
// Type aliases for Windows API naming conventions compatibility
|
||||
using RECT = PowerDisplay.Native.Rect;
|
||||
|
||||
namespace PowerDisplay.Native;
|
||||
|
||||
/// <summary>
|
||||
/// 委托类型定义
|
||||
/// Native delegate type definitions
|
||||
/// </summary>
|
||||
public static class NativeDelegates
|
||||
{
|
||||
/// <summary>
|
||||
/// 显示器枚举过程委托
|
||||
/// Monitor enumeration procedure delegate
|
||||
/// </summary>
|
||||
/// <param name="hMonitor">显示器句柄</param>
|
||||
/// <param name="hdcMonitor">显示器 DC</param>
|
||||
/// <param name="lprcMonitor">显示器矩形指针</param>
|
||||
/// <param name="dwData">用户数据</param>
|
||||
/// <returns>继续枚举返回 true</returns>
|
||||
/// <param name="hMonitor">Monitor handle</param>
|
||||
/// <param name="hdcMonitor">Monitor device context</param>
|
||||
/// <param name="lprcMonitor">Pointer to monitor rectangle</param>
|
||||
/// <param name="dwData">User data</param>
|
||||
/// <returns>True to continue enumeration</returns>
|
||||
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
|
||||
public delegate bool MonitorEnumProc(IntPtr hMonitor, IntPtr hdcMonitor, IntPtr lprcMonitor, IntPtr dwData);
|
||||
|
||||
/// <summary>
|
||||
/// 线程启动例程委托
|
||||
/// Thread start routine delegate
|
||||
/// </summary>
|
||||
/// <param name="lpParameter">线程参数</param>
|
||||
/// <returns>线程退出代码</returns>
|
||||
/// <param name="lpParameter">Thread parameter</param>
|
||||
/// <returns>Thread exit code</returns>
|
||||
public delegate uint ThreadStartRoutine(IntPtr lpParameter);
|
||||
}
|
||||
|
||||
@@ -442,91 +442,4 @@ namespace PowerDisplay.Native
|
||||
this.Y = y;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Notify icon data structure (for system tray)
|
||||
/// </summary>
|
||||
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
|
||||
public unsafe struct NOTIFYICONDATA
|
||||
{
|
||||
public uint CbSize;
|
||||
public IntPtr HWnd;
|
||||
public uint UID;
|
||||
public uint UFlags;
|
||||
public uint UCallbackMessage;
|
||||
public IntPtr HIcon;
|
||||
|
||||
/// <summary>
|
||||
/// Tooltip text - fixed buffer for LibraryImport compatibility
|
||||
/// </summary>
|
||||
public fixed ushort SzTip[128];
|
||||
|
||||
public uint DwState;
|
||||
public uint DwStateMask;
|
||||
|
||||
/// <summary>
|
||||
/// Info balloon text - fixed buffer for LibraryImport compatibility
|
||||
/// </summary>
|
||||
public fixed ushort SzInfo[256];
|
||||
|
||||
public uint UTimeout;
|
||||
|
||||
/// <summary>
|
||||
/// Info balloon title - fixed buffer for LibraryImport compatibility
|
||||
/// </summary>
|
||||
public fixed ushort SzInfoTitle[64];
|
||||
|
||||
public uint DwInfoFlags;
|
||||
|
||||
/// <summary>
|
||||
/// Helper method to set tooltip text
|
||||
/// </summary>
|
||||
public void SetTip(string tip)
|
||||
{
|
||||
fixed (ushort* ptr = SzTip)
|
||||
{
|
||||
int length = Math.Min(tip.Length, 127);
|
||||
for (int i = 0; i < length; i++)
|
||||
{
|
||||
ptr[i] = tip[i];
|
||||
}
|
||||
|
||||
ptr[length] = 0; // Null terminator
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper method to set info balloon text
|
||||
/// </summary>
|
||||
public void SetInfo(string info)
|
||||
{
|
||||
fixed (ushort* ptr = SzInfo)
|
||||
{
|
||||
int length = Math.Min(info.Length, 255);
|
||||
for (int i = 0; i < length; i++)
|
||||
{
|
||||
ptr[i] = info[i];
|
||||
}
|
||||
|
||||
ptr[length] = 0; // Null terminator
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper method to set info balloon title
|
||||
/// </summary>
|
||||
public void SetInfoTitle(string title)
|
||||
{
|
||||
fixed (ushort* ptr = SzInfoTitle)
|
||||
{
|
||||
int length = Math.Min(title.Length, 63);
|
||||
for (int i = 0; i < length; i++)
|
||||
{
|
||||
ptr[i] = title[i];
|
||||
}
|
||||
|
||||
ptr[length] = 0; // Null terminator
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -171,13 +171,6 @@ namespace PowerDisplay.Native
|
||||
POINT pt,
|
||||
uint dwFlags);
|
||||
|
||||
// ==================== Shell32.dll - Tray Icon ====================
|
||||
[LibraryImport("shell32.dll", EntryPoint = "Shell_NotifyIconW", StringMarshalling = StringMarshalling.Utf16)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
internal static partial bool Shell_NotifyIcon(
|
||||
uint dwMessage,
|
||||
ref NOTIFYICONDATA lpData);
|
||||
|
||||
// ==================== Dxva2.dll - DDC/CI Monitor Control ====================
|
||||
[LibraryImport("Dxva2.dll")]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
|
||||
@@ -43,32 +43,32 @@ namespace PowerDisplay.Native
|
||||
private const int SwRestore = 9;
|
||||
|
||||
/// <summary>
|
||||
/// 禁用窗口的拖动和缩放功能
|
||||
/// Disable window moving and resizing functionality
|
||||
/// </summary>
|
||||
public static void DisableWindowMovingAndResizing(IntPtr hWnd)
|
||||
{
|
||||
// 获取当前窗口样式
|
||||
// Get current window style
|
||||
#if WIN64
|
||||
int style = (int)GetWindowLong(hWnd, GwlStyle);
|
||||
#else
|
||||
int style = GetWindowLong(hWnd, GwlStyle);
|
||||
#endif
|
||||
|
||||
// 移除可调整大小的边框、标题栏和系统菜单
|
||||
// Remove resizable borders, title bar, and system menu
|
||||
style &= ~WsThickframe;
|
||||
style &= ~WsMaximizebox;
|
||||
style &= ~WsMinimizebox;
|
||||
style &= ~WsCaption; // 移除整个标题栏
|
||||
style &= ~WsSysmenu; // 移除系统菜单
|
||||
style &= ~WsCaption; // Remove entire title bar
|
||||
style &= ~WsSysmenu; // Remove system menu
|
||||
|
||||
// 设置新的窗口样式
|
||||
// Set new window style
|
||||
#if WIN64
|
||||
_ = SetWindowLong(hWnd, GwlStyle, new IntPtr(style));
|
||||
#else
|
||||
_ = SetWindowLong(hWnd, GwlStyle, style);
|
||||
#endif
|
||||
|
||||
// 获取扩展样式并移除相关边框
|
||||
// Get extended style and remove related borders
|
||||
#if WIN64
|
||||
int exStyle = (int)GetWindowLong(hWnd, GwlExstyle);
|
||||
#else
|
||||
@@ -84,7 +84,7 @@ namespace PowerDisplay.Native
|
||||
_ = SetWindowLong(hWnd, GwlExstyle, exStyle);
|
||||
#endif
|
||||
|
||||
// 刷新窗口框架
|
||||
// Refresh window frame
|
||||
SetWindowPos(
|
||||
hWnd,
|
||||
IntPtr.Zero,
|
||||
@@ -96,7 +96,7 @@ namespace PowerDisplay.Native
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置窗口是否置顶
|
||||
/// Set whether window is topmost
|
||||
/// </summary>
|
||||
public static void SetWindowTopmost(IntPtr hWnd, bool topmost)
|
||||
{
|
||||
@@ -111,7 +111,7 @@ namespace PowerDisplay.Native
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 显示或隐藏窗口
|
||||
/// Show or hide window
|
||||
/// </summary>
|
||||
public static void ShowWindow(IntPtr hWnd, bool show)
|
||||
{
|
||||
@@ -119,7 +119,7 @@ namespace PowerDisplay.Native
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 最小化窗口
|
||||
/// Minimize window
|
||||
/// </summary>
|
||||
public static void MinimizeWindow(IntPtr hWnd)
|
||||
{
|
||||
@@ -127,7 +127,7 @@ namespace PowerDisplay.Native
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 恢复窗口
|
||||
/// Restore window
|
||||
/// </summary>
|
||||
public static void RestoreWindow(IntPtr hWnd)
|
||||
{
|
||||
@@ -135,28 +135,28 @@ namespace PowerDisplay.Native
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置窗口不在任务栏显示
|
||||
/// Hide window from taskbar
|
||||
/// </summary>
|
||||
public static void HideFromTaskbar(IntPtr hWnd)
|
||||
{
|
||||
// 获取当前扩展样式
|
||||
// Get current extended style
|
||||
#if WIN64
|
||||
int exStyle = (int)GetWindowLong(hWnd, GwlExstyle);
|
||||
#else
|
||||
int exStyle = GetWindowLong(hWnd, GwlExstyle);
|
||||
#endif
|
||||
|
||||
// 添加 WS_EX_TOOLWINDOW 样式,这会让窗口不在任务栏显示
|
||||
// Add WS_EX_TOOLWINDOW style to hide window from taskbar
|
||||
exStyle |= WsExToolwindow;
|
||||
|
||||
// 设置新的扩展样式
|
||||
// Set new extended style
|
||||
#if WIN64
|
||||
_ = SetWindowLong(hWnd, GwlExstyle, new IntPtr(exStyle));
|
||||
#else
|
||||
_ = SetWindowLong(hWnd, GwlExstyle, exStyle);
|
||||
#endif
|
||||
|
||||
// 刷新窗口框架
|
||||
// Refresh window frame
|
||||
SetWindowPos(
|
||||
hWnd,
|
||||
IntPtr.Zero,
|
||||
|
||||
@@ -51,21 +51,34 @@ namespace PowerDisplay
|
||||
{
|
||||
this.InitializeComponent();
|
||||
|
||||
// Lightweight initialization - no heavy operations in constructor
|
||||
// Setup window properties
|
||||
SetupWindow();
|
||||
// 1. Configure window immediately (synchronous, no data dependency)
|
||||
ConfigureWindow();
|
||||
|
||||
// Initialize UI text
|
||||
// 2. Initialize UI text (synchronous, lightweight)
|
||||
InitializeUIText();
|
||||
|
||||
// Clean up resources on window close
|
||||
this.Closed += OnWindowClosed;
|
||||
// 3. Create ViewModel immediately (lightweight object, no scanning yet)
|
||||
_viewModel = new MainViewModel();
|
||||
RootGrid.DataContext = _viewModel;
|
||||
Bindings.Update();
|
||||
|
||||
// Auto-hide window when it loses focus (click outside)
|
||||
this.Activated += OnWindowActivated;
|
||||
// 4. Register event handlers
|
||||
RegisterEventHandlers();
|
||||
|
||||
// Delay ViewModel creation until first activation (async)
|
||||
this.Activated += OnFirstActivated;
|
||||
// 5. Start background initialization (don't wait)
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await InitializeAsync();
|
||||
_hasInitialized = true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Background initialization failed: {ex.Message}");
|
||||
DispatcherQueue.TryEnqueue(() => ShowError($"Initialization failed: {ex.Message}"));
|
||||
}
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -74,22 +87,31 @@ namespace PowerDisplay
|
||||
}
|
||||
}
|
||||
|
||||
private bool _hasInitialized;
|
||||
|
||||
private async void OnFirstActivated(object sender, WindowActivatedEventArgs args)
|
||||
/// <summary>
|
||||
/// Register all event handlers for window and ViewModel
|
||||
/// </summary>
|
||||
private void RegisterEventHandlers()
|
||||
{
|
||||
// Only initialize once on first activation
|
||||
if (_hasInitialized)
|
||||
{
|
||||
return;
|
||||
}
|
||||
// Window events
|
||||
this.Closed += OnWindowClosed;
|
||||
this.Activated += OnWindowActivated;
|
||||
|
||||
await EnsureInitializedAsync();
|
||||
// ViewModel events
|
||||
_viewModel.UIRefreshRequested += OnUIRefreshRequested;
|
||||
_viewModel.Monitors.CollectionChanged += OnMonitorsCollectionChanged;
|
||||
_viewModel.PropertyChanged += OnViewModelPropertyChanged;
|
||||
|
||||
// Button events
|
||||
LinkButton.Click += OnLinkClick;
|
||||
DisableButton.Click += OnDisableClick;
|
||||
RefreshButton.Click += OnRefreshClick;
|
||||
}
|
||||
|
||||
private bool _hasInitialized;
|
||||
|
||||
/// <summary>
|
||||
/// Ensures the window is properly initialized with ViewModel and data
|
||||
/// Can be called from external code (e.g., App startup) to initialize in background
|
||||
/// Can be called from external code (e.g., App startup) to pre-initialize in background
|
||||
/// </summary>
|
||||
public async Task EnsureInitializedAsync()
|
||||
{
|
||||
@@ -98,57 +120,32 @@ namespace PowerDisplay
|
||||
return;
|
||||
}
|
||||
|
||||
_hasInitialized = true;
|
||||
this.Activated -= OnFirstActivated; // Unsubscribe after first run
|
||||
|
||||
// Create and initialize ViewModel asynchronously
|
||||
// This will trigger Loading UI (IsScanning) during monitor discovery
|
||||
_viewModel = new MainViewModel();
|
||||
RootGrid.DataContext = _viewModel;
|
||||
|
||||
// Notify bindings that ViewModel is now available (for x:Bind)
|
||||
Bindings.Update();
|
||||
|
||||
// Initialize ViewModel event handlers
|
||||
_viewModel.UIRefreshRequested += OnUIRefreshRequested;
|
||||
_viewModel.Monitors.CollectionChanged += OnMonitorsCollectionChanged;
|
||||
_viewModel.PropertyChanged += OnViewModelPropertyChanged;
|
||||
|
||||
// Bind button events
|
||||
LinkButton.Click += OnLinkClick;
|
||||
DisableButton.Click += OnDisableClick;
|
||||
RefreshButton.Click += OnRefreshClick;
|
||||
|
||||
// Start async initialization (monitor scanning happens here)
|
||||
// Wait for background initialization to complete
|
||||
// This is a no-op if initialization already completed
|
||||
await InitializeAsync();
|
||||
|
||||
// FIX BUG #4: Don't auto-hide window after initialization
|
||||
// Window visibility should be controlled by IPC commands (show_window/toggle_window)
|
||||
// If launched via PowerToys Runner, window should start hidden and wait for IPC show command
|
||||
// If launched standalone, window should stay visible
|
||||
// HideWindow(); // REMOVED - controlled by IPC or standalone mode
|
||||
_hasInitialized = true;
|
||||
}
|
||||
|
||||
private async Task InitializeAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
// No delays! Direct async operation
|
||||
// Perform monitor scanning and settings reload
|
||||
await _viewModel.RefreshMonitorsAsync();
|
||||
await _viewModel.ReloadMonitorSettingsAsync();
|
||||
|
||||
// Adjust window size after data is loaded (event-driven)
|
||||
AdjustWindowSizeToContent();
|
||||
// Adjust window size after data is loaded (must run on UI thread)
|
||||
DispatcherQueue.TryEnqueue(() => AdjustWindowSizeToContent());
|
||||
}
|
||||
catch (WmiLight.WmiException ex)
|
||||
{
|
||||
Logger.LogError($"WMI access failed: {ex.Message}");
|
||||
ShowError("Unable to access internal display control, administrator privileges may be required.");
|
||||
DispatcherQueue.TryEnqueue(() => ShowError("Unable to access internal display control, administrator privileges may be required."));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Initialization failed: {ex.Message}");
|
||||
ShowError($"Initialization failed: {ex.Message}");
|
||||
DispatcherQueue.TryEnqueue(() => ShowError($"Initialization failed: {ex.Message}"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -199,7 +196,6 @@ namespace PowerDisplay
|
||||
// Auto-hide window when it loses focus (deactivated)
|
||||
if (args.WindowActivationState == WindowActivationState.Deactivated)
|
||||
{
|
||||
Logger.LogInfo("[DEACTIVATED] Window lost focus, hiding");
|
||||
HideWindow();
|
||||
}
|
||||
}
|
||||
@@ -228,80 +224,54 @@ namespace PowerDisplay
|
||||
|
||||
public void ShowWindow()
|
||||
{
|
||||
Logger.LogInfo("[SHOWWINDOW] Method entry");
|
||||
Logger.LogInfo($"[SHOWWINDOW] _hasInitialized: {_hasInitialized}");
|
||||
Logger.LogInfo($"[SHOWWINDOW] Current thread ID: {Environment.CurrentManagedThreadId}");
|
||||
|
||||
try
|
||||
{
|
||||
// If not initialized, log warning but continue showing
|
||||
if (!_hasInitialized)
|
||||
{
|
||||
Logger.LogWarning("[SHOWWINDOW] Window not fully initialized yet, showing anyway");
|
||||
Logger.LogWarning("Window not fully initialized yet, showing anyway");
|
||||
}
|
||||
|
||||
Logger.LogInfo("[SHOWWINDOW] Getting window handle");
|
||||
var hWnd = WinRT.Interop.WindowNative.GetWindowHandle(this);
|
||||
Logger.LogInfo($"[SHOWWINDOW] Window handle: 0x{hWnd:X}");
|
||||
|
||||
Logger.LogInfo("[SHOWWINDOW] Adjusting window size");
|
||||
AdjustWindowSizeToContent();
|
||||
|
||||
Logger.LogInfo("[SHOWWINDOW] Repositioning window");
|
||||
if (_appWindow != null)
|
||||
{
|
||||
PositionWindowAtBottomRight(_appWindow);
|
||||
Logger.LogInfo("[SHOWWINDOW] Window repositioned");
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogWarning("[SHOWWINDOW] _appWindow is null, skipping reposition");
|
||||
Logger.LogWarning("AppWindow is null, skipping window repositioning");
|
||||
}
|
||||
|
||||
Logger.LogInfo("[SHOWWINDOW] Setting opacity to 0 for animation");
|
||||
RootGrid.Opacity = 0;
|
||||
|
||||
Logger.LogInfo("[SHOWWINDOW] Calling this.Activate()");
|
||||
this.Activate();
|
||||
|
||||
Logger.LogInfo("[SHOWWINDOW] Calling WindowHelper.ShowWindow");
|
||||
WindowHelper.ShowWindow(hWnd, true);
|
||||
|
||||
Logger.LogInfo("[SHOWWINDOW] Calling WindowHelpers.BringToForeground");
|
||||
WindowHelpers.BringToForeground(hWnd);
|
||||
|
||||
Logger.LogInfo("[SHOWWINDOW] Checking for animation storyboard");
|
||||
if (RootGrid.Resources.ContainsKey("SlideInStoryboard"))
|
||||
{
|
||||
Logger.LogInfo("[SHOWWINDOW] Starting SlideInStoryboard animation");
|
||||
var slideInStoryboard = RootGrid.Resources["SlideInStoryboard"] as Storyboard;
|
||||
slideInStoryboard?.Begin();
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogWarning("[SHOWWINDOW] SlideInStoryboard not found, setting opacity=1");
|
||||
Logger.LogWarning("SlideInStoryboard not found, window will appear without animation");
|
||||
RootGrid.Opacity = 1;
|
||||
}
|
||||
|
||||
Logger.LogInfo("[SHOWWINDOW] Verifying window visibility");
|
||||
bool isVisible = IsWindowVisible();
|
||||
Logger.LogInfo($"[SHOWWINDOW] IsWindowVisible result: {isVisible}");
|
||||
|
||||
if (!isVisible)
|
||||
{
|
||||
Logger.LogError("[SHOWWINDOW] Window not visible after show, forcing visibility");
|
||||
Logger.LogError("Window not visible after show attempt, forcing visibility");
|
||||
RootGrid.Opacity = 1;
|
||||
this.Activate();
|
||||
WindowHelpers.BringToForeground(hWnd);
|
||||
}
|
||||
|
||||
Logger.LogInfo("[SHOWWINDOW] Method completed successfully");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"[SHOWWINDOW] Exception: {ex.GetType().Name}");
|
||||
Logger.LogError($"[SHOWWINDOW] Exception message: {ex.Message}");
|
||||
Logger.LogError($"[SHOWWINDOW] Stack trace: {ex.StackTrace}");
|
||||
Logger.LogError($"Failed to show window: {ex.Message}");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
@@ -346,35 +316,28 @@ namespace PowerDisplay
|
||||
/// </summary>
|
||||
public void ToggleWindow()
|
||||
{
|
||||
Logger.LogInfo("[TOGGLEWINDOW] Method entry");
|
||||
try
|
||||
{
|
||||
bool isVisible = IsWindowVisible();
|
||||
Logger.LogInfo($"[TOGGLEWINDOW] Current visibility: {isVisible}");
|
||||
|
||||
if (isVisible)
|
||||
{
|
||||
Logger.LogInfo("[TOGGLEWINDOW] Window is visible, hiding");
|
||||
HideWindow();
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogInfo("[TOGGLEWINDOW] Window is hidden, showing");
|
||||
ShowWindow();
|
||||
}
|
||||
|
||||
Logger.LogInfo("[TOGGLEWINDOW] Method completed");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"[TOGGLEWINDOW] Exception: {ex.Message}");
|
||||
Logger.LogError($"Failed to toggle window: {ex.Message}");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private async void OnUIRefreshRequested(object? sender, EventArgs e)
|
||||
{
|
||||
Logger.LogInfo("UI refresh requested due to settings change");
|
||||
await _viewModel.ReloadMonitorSettingsAsync();
|
||||
|
||||
// Adjust window size after settings are reloaded (no delay needed!)
|
||||
@@ -415,7 +378,7 @@ namespace PowerDisplay
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 快速关闭窗口,跳过动画和复杂清理
|
||||
/// Fast shutdown: skip animations and complex cleanup
|
||||
/// </summary>
|
||||
public void FastShutdown()
|
||||
{
|
||||
@@ -423,25 +386,25 @@ namespace PowerDisplay
|
||||
{
|
||||
_isExiting = true;
|
||||
|
||||
// 快速清理 ViewModel
|
||||
// Quick cleanup of ViewModel
|
||||
if (_viewModel != null)
|
||||
{
|
||||
// 取消事件订阅
|
||||
// Unsubscribe from events
|
||||
_viewModel.UIRefreshRequested -= OnUIRefreshRequested;
|
||||
_viewModel.Monitors.CollectionChanged -= OnMonitorsCollectionChanged;
|
||||
_viewModel.PropertyChanged -= OnViewModelPropertyChanged;
|
||||
|
||||
// 立即释放
|
||||
// Dispose immediately
|
||||
_viewModel.Dispose();
|
||||
}
|
||||
|
||||
// 直接关闭窗口,不等待动画
|
||||
// Close window directly without animations
|
||||
var hWnd = WinRT.Interop.WindowNative.GetWindowHandle(this);
|
||||
WindowHelper.ShowWindow(hWnd, false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// 忽略清理错误,确保能够关闭
|
||||
// Ignore cleanup errors to ensure shutdown
|
||||
Logger.LogWarning($"FastShutdown error: {ex.Message}");
|
||||
}
|
||||
}
|
||||
@@ -450,21 +413,21 @@ namespace PowerDisplay
|
||||
{
|
||||
try
|
||||
{
|
||||
// 使用快速关闭
|
||||
// Use fast shutdown
|
||||
FastShutdown();
|
||||
|
||||
// 直接调用应用程序快速退出
|
||||
// Call application shutdown directly
|
||||
if (Application.Current is App app)
|
||||
{
|
||||
app.Shutdown();
|
||||
}
|
||||
|
||||
// 确保立即退出
|
||||
// Ensure immediate exit
|
||||
Environment.Exit(0);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// 确保能够退出
|
||||
// Ensure exit even on error
|
||||
Logger.LogError($"ExitApplication error: {ex.Message}");
|
||||
Environment.Exit(0);
|
||||
}
|
||||
@@ -618,7 +581,10 @@ namespace PowerDisplay
|
||||
}
|
||||
}
|
||||
|
||||
private void SetupWindow()
|
||||
/// <summary>
|
||||
/// Configure window properties (synchronous, no data dependency)
|
||||
/// </summary>
|
||||
private void ConfigureWindow()
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -712,7 +678,7 @@ namespace PowerDisplay
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Ignore window setup errors
|
||||
Logger.LogWarning($"Window setup error: {ex.Message}");
|
||||
Logger.LogWarning($"Window configuration error: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -743,7 +709,6 @@ namespace PowerDisplay
|
||||
var currentSize = _appWindow.Size;
|
||||
if (Math.Abs(currentSize.Height - scaledHeight) > 1)
|
||||
{
|
||||
Logger.LogInfo($"Adjusting window height from {currentSize.Height} to {scaledHeight} (content: {contentHeight})");
|
||||
_appWindow.Resize(new SizeInt32 { Width = 640, Height = scaledHeight });
|
||||
|
||||
// Update clip region to match new window size
|
||||
@@ -800,10 +765,9 @@ namespace PowerDisplay
|
||||
appWindow.Move(new PointInt32 { X = x, Y = y });
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
catch (Exception)
|
||||
{
|
||||
// Ignore errors when positioning window
|
||||
Logger.LogDebug($"Failed to position window: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -857,19 +821,15 @@ namespace PowerDisplay
|
||||
{
|
||||
case "Brightness":
|
||||
monitorVm.Brightness = finalValue;
|
||||
Logger.LogDebug($"[UI] Brightness drag completed: {finalValue}");
|
||||
break;
|
||||
case "ColorTemperature":
|
||||
monitorVm.ColorTemperaturePercent = finalValue;
|
||||
Logger.LogDebug($"[UI] ColorTemperature drag completed: {finalValue}%");
|
||||
break;
|
||||
case "Contrast":
|
||||
monitorVm.ContrastPercent = finalValue;
|
||||
Logger.LogDebug($"[UI] Contrast drag completed: {finalValue}%");
|
||||
break;
|
||||
case "Volume":
|
||||
monitorVm.Volume = finalValue;
|
||||
Logger.LogDebug($"[UI] Volume drag completed: {finalValue}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,343 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
#include "pch.h"
|
||||
#include "PowerDisplayProcessManager.h"
|
||||
|
||||
#include <common/logger/logger.h>
|
||||
#include <common/utils/winapi_error.h>
|
||||
#include <common/interop/shared_constants.h>
|
||||
#include <atlstr.h>
|
||||
#include <format>
|
||||
|
||||
namespace
|
||||
{
|
||||
/// <summary>
|
||||
/// Generate a pipe name with UUID suffix
|
||||
/// </summary>
|
||||
std::optional<std::wstring> get_pipe_uuid()
|
||||
{
|
||||
UUID temp_uuid;
|
||||
wchar_t* uuid_chars = nullptr;
|
||||
|
||||
if (UuidCreate(&temp_uuid) == RPC_S_UUID_NO_ADDRESS)
|
||||
{
|
||||
const auto val = get_last_error_message(GetLastError());
|
||||
Logger::error(L"UuidCreate cannot create guid. {}", val.has_value() ? val.value() : L"");
|
||||
return std::nullopt;
|
||||
}
|
||||
else if (UuidToString(&temp_uuid, reinterpret_cast<RPC_WSTR*>(&uuid_chars)) != RPC_S_OK)
|
||||
{
|
||||
const auto val = get_last_error_message(GetLastError());
|
||||
Logger::error(L"UuidToString cannot convert to string. {}", val.has_value() ? val.value() : L"");
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const auto uuid_str = std::wstring(uuid_chars);
|
||||
RpcStringFree(reinterpret_cast<RPC_WSTR*>(&uuid_chars));
|
||||
|
||||
return uuid_str;
|
||||
}
|
||||
}
|
||||
|
||||
PowerDisplayProcessManager::~PowerDisplayProcessManager()
|
||||
{
|
||||
stop();
|
||||
}
|
||||
|
||||
void PowerDisplayProcessManager::start()
|
||||
{
|
||||
m_enabled = true;
|
||||
submit_task([this]() { refresh(); });
|
||||
}
|
||||
|
||||
void PowerDisplayProcessManager::stop()
|
||||
{
|
||||
m_enabled = false;
|
||||
submit_task([this]() { refresh(); });
|
||||
}
|
||||
|
||||
void PowerDisplayProcessManager::send_message_to_powerdisplay(const std::wstring& message)
|
||||
{
|
||||
submit_task([this, message]() {
|
||||
if (m_write_pipe)
|
||||
{
|
||||
try
|
||||
{
|
||||
const auto formatted = std::format(L"{}\r\n", message);
|
||||
|
||||
// Match WinUI side which reads the pipe using UTF-16 (Encoding.Unicode)
|
||||
const CString payload(formatted.c_str());
|
||||
const DWORD bytes_to_write = static_cast<DWORD>(payload.GetLength() * sizeof(wchar_t));
|
||||
DWORD bytes_written = 0;
|
||||
|
||||
if (FAILED(m_write_pipe->Write(payload, bytes_to_write, &bytes_written)))
|
||||
{
|
||||
Logger::error(L"Failed to write message to PowerDisplay pipe");
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger::trace(L"Sent message to PowerDisplay: {}", message);
|
||||
}
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
Logger::error(L"Exception while sending message to PowerDisplay");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger::warn(L"Cannot send message to PowerDisplay: pipe not connected");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void PowerDisplayProcessManager::submit_task(std::function<void()> task)
|
||||
{
|
||||
m_thread_executor.submit(OnThreadExecutor::task_t{ task });
|
||||
}
|
||||
|
||||
bool PowerDisplayProcessManager::is_process_running() const
|
||||
{
|
||||
return m_hProcess != nullptr && WaitForSingleObject(m_hProcess, 0) == WAIT_TIMEOUT;
|
||||
}
|
||||
|
||||
void PowerDisplayProcessManager::terminate_process()
|
||||
{
|
||||
// Terminate process if still running
|
||||
if (m_hProcess != nullptr)
|
||||
{
|
||||
// Check if process is still running
|
||||
if (WaitForSingleObject(m_hProcess, 0) == WAIT_TIMEOUT)
|
||||
{
|
||||
Logger::trace(L"Process still running, calling TerminateProcess");
|
||||
|
||||
// Force terminate the process
|
||||
if (TerminateProcess(m_hProcess, 1))
|
||||
{
|
||||
// Wait a bit to ensure process is terminated
|
||||
DWORD wait_result = WaitForSingleObject(m_hProcess, 1000);
|
||||
if (wait_result == WAIT_OBJECT_0)
|
||||
{
|
||||
Logger::trace(L"PowerDisplay process successfully terminated");
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger::error(L"TerminateProcess succeeded but process did not exit within timeout");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger::error(L"TerminateProcess failed: {}", get_last_error_or_default(GetLastError()));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger::trace(L"PowerDisplay process already exited gracefully");
|
||||
}
|
||||
|
||||
// Clean up process handle
|
||||
CloseHandle(m_hProcess);
|
||||
m_hProcess = nullptr;
|
||||
}
|
||||
|
||||
// Close pipe after process is terminated
|
||||
m_write_pipe.reset();
|
||||
Logger::trace(L"PowerDisplay process cleanup complete");
|
||||
}
|
||||
|
||||
HRESULT PowerDisplayProcessManager::start_process(const std::wstring& pipe_uuid)
|
||||
{
|
||||
const unsigned long powertoys_pid = GetCurrentProcessId();
|
||||
|
||||
// Pass both runner PID and pipe UUID to PowerDisplay.exe
|
||||
const auto executable_args = std::format(L"{} {}", std::to_wstring(powertoys_pid), pipe_uuid);
|
||||
|
||||
SHELLEXECUTEINFOW sei{ sizeof(sei) };
|
||||
sei.fMask = SEE_MASK_NOCLOSEPROCESS | SEE_MASK_FLAG_NO_UI;
|
||||
sei.lpFile = L"WinUI3Apps\\PowerToys.PowerDisplay.exe";
|
||||
sei.nShow = SW_SHOWNORMAL;
|
||||
sei.lpParameters = executable_args.data();
|
||||
|
||||
if (ShellExecuteExW(&sei))
|
||||
{
|
||||
Logger::trace(L"Successfully started PowerDisplay process with UUID: {}", pipe_uuid);
|
||||
terminate_process(); // Clean up old process if any
|
||||
m_hProcess = sei.hProcess;
|
||||
return S_OK;
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger::error(L"PowerDisplay process failed to start. {}", get_last_error_or_default(GetLastError()));
|
||||
return E_FAIL;
|
||||
}
|
||||
}
|
||||
|
||||
HRESULT PowerDisplayProcessManager::start_command_pipe(const std::wstring& pipe_uuid)
|
||||
{
|
||||
const constexpr DWORD BUFSIZE = 4096 * 4;
|
||||
const constexpr DWORD client_timeout_millis = 5000;
|
||||
|
||||
// Create single-direction pipe: ModuleInterface writes commands to PowerDisplay
|
||||
const std::wstring pipe_name = std::format(L"\\\\.\\pipe\\powertoys_powerdisplay_{}", pipe_uuid);
|
||||
|
||||
HANDLE hWritePipe = CreateNamedPipe(
|
||||
pipe_name.c_str(),
|
||||
PIPE_ACCESS_OUTBOUND | FILE_FLAG_OVERLAPPED,
|
||||
PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT,
|
||||
1, // max instances
|
||||
BUFSIZE, // out buffer size
|
||||
0, // in buffer size (not used for outbound)
|
||||
0, // client timeout
|
||||
NULL // default security
|
||||
);
|
||||
|
||||
if (hWritePipe == NULL || hWritePipe == INVALID_HANDLE_VALUE)
|
||||
{
|
||||
Logger::error(L"Error creating pipe for PowerDisplay");
|
||||
return E_FAIL;
|
||||
}
|
||||
|
||||
// Create overlapped event for waiting for client to connect
|
||||
OVERLAPPED overlapped = { 0 };
|
||||
overlapped.hEvent = CreateEvent(nullptr, TRUE, FALSE, nullptr);
|
||||
|
||||
if (!overlapped.hEvent)
|
||||
{
|
||||
Logger::error(L"Error creating overlapped event for PowerDisplay pipe");
|
||||
CloseHandle(hWritePipe);
|
||||
return E_FAIL;
|
||||
}
|
||||
|
||||
// Connect pipe
|
||||
if (!ConnectNamedPipe(hWritePipe, &overlapped))
|
||||
{
|
||||
const auto lastError = GetLastError();
|
||||
if (lastError != ERROR_IO_PENDING && lastError != ERROR_PIPE_CONNECTED)
|
||||
{
|
||||
Logger::error(L"Error connecting pipe");
|
||||
CloseHandle(overlapped.hEvent);
|
||||
CloseHandle(hWritePipe);
|
||||
return E_FAIL;
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for pipe to connect (with timeout)
|
||||
DWORD wait_result = WaitForSingleObject(overlapped.hEvent, client_timeout_millis);
|
||||
CloseHandle(overlapped.hEvent);
|
||||
|
||||
if (wait_result == WAIT_OBJECT_0 || wait_result == WAIT_TIMEOUT)
|
||||
{
|
||||
// Check if actually connected
|
||||
DWORD bytes_transferred = 0;
|
||||
if (GetOverlappedResult(hWritePipe, &overlapped, &bytes_transferred, FALSE) || GetLastError() == ERROR_PIPE_CONNECTED)
|
||||
{
|
||||
m_write_pipe = std::make_unique<CAtlFile>(hWritePipe);
|
||||
Logger::trace(L"PowerDisplay pipe connected successfully: {}", pipe_name);
|
||||
return S_OK;
|
||||
}
|
||||
}
|
||||
|
||||
Logger::error(L"Timeout waiting for PowerDisplay to connect to pipe");
|
||||
CloseHandle(hWritePipe);
|
||||
return E_FAIL;
|
||||
}
|
||||
|
||||
void PowerDisplayProcessManager::refresh()
|
||||
{
|
||||
if (m_enabled == is_process_running())
|
||||
{
|
||||
// Already in correct state
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_enabled)
|
||||
{
|
||||
// Start PowerDisplay process
|
||||
Logger::trace(L"Starting PowerDisplay process");
|
||||
|
||||
const auto pipe_uuid = get_pipe_uuid();
|
||||
if (!pipe_uuid)
|
||||
{
|
||||
Logger::error(L"Failed to generate pipe UUID");
|
||||
return;
|
||||
}
|
||||
|
||||
// FIX BUG #1: Start process FIRST, then create pipes
|
||||
// This ensures PowerDisplay.exe is running when pipes try to connect
|
||||
if (start_process(pipe_uuid.value()) != S_OK)
|
||||
{
|
||||
Logger::error(L"Failed to start PowerDisplay process");
|
||||
return;
|
||||
}
|
||||
|
||||
// Now create pipes and wait for PowerDisplay to connect
|
||||
if (start_command_pipe(pipe_uuid.value()) != S_OK)
|
||||
{
|
||||
Logger::error(L"Failed to initialize command pipes, terminating process");
|
||||
terminate_process();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Stop PowerDisplay process
|
||||
Logger::trace(L"Stopping PowerDisplay process");
|
||||
|
||||
// Send terminate message synchronously (not through thread executor)
|
||||
// This ensures the message is sent before we wait for process exit
|
||||
if (m_write_pipe)
|
||||
{
|
||||
try
|
||||
{
|
||||
const auto message = L"{\"action\":\"terminate\"}";
|
||||
const auto formatted = std::format(L"{}\r\n", message);
|
||||
|
||||
// Match WinUI side which reads the pipe using UTF-16 (Encoding.Unicode)
|
||||
const CString payload(formatted.c_str());
|
||||
const DWORD bytes_to_write = static_cast<DWORD>(payload.GetLength() * sizeof(wchar_t));
|
||||
DWORD bytes_written = 0;
|
||||
|
||||
if (SUCCEEDED(m_write_pipe->Write(payload, bytes_to_write, &bytes_written)))
|
||||
{
|
||||
Logger::trace(L"Sent terminate message to PowerDisplay");
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger::warn(L"Failed to send terminate message to PowerDisplay");
|
||||
}
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
Logger::warn(L"Exception while sending terminate message to PowerDisplay");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger::warn(L"Cannot send terminate message: pipe not connected");
|
||||
}
|
||||
|
||||
// Wait for graceful exit (use longer timeout like AdvancedPaste)
|
||||
if (m_hProcess != nullptr)
|
||||
{
|
||||
Logger::trace(L"Waiting for PowerDisplay process to exit gracefully");
|
||||
DWORD wait_result = WaitForSingleObject(m_hProcess, 5000);
|
||||
|
||||
if (wait_result == WAIT_OBJECT_0)
|
||||
{
|
||||
Logger::trace(L"PowerDisplay process exited gracefully");
|
||||
}
|
||||
else if (wait_result == WAIT_TIMEOUT)
|
||||
{
|
||||
Logger::warn(L"PowerDisplay process failed to exit within timeout, will force terminate");
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger::error(L"WaitForSingleObject failed with error: {}", get_last_error_or_default(GetLastError()));
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up (will force terminate if still running)
|
||||
terminate_process();
|
||||
}
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <thread>
|
||||
#include <atlfile.h>
|
||||
#include <common/utils/OnThreadExecutor.h>
|
||||
|
||||
/// <summary>
|
||||
/// Manages PowerDisplay.exe process lifecycle and IPC communication
|
||||
/// </summary>
|
||||
class PowerDisplayProcessManager
|
||||
{
|
||||
private:
|
||||
HANDLE m_hProcess = nullptr;
|
||||
std::unique_ptr<CAtlFile> m_write_pipe;
|
||||
OnThreadExecutor m_thread_executor;
|
||||
bool m_enabled = false;
|
||||
|
||||
public:
|
||||
PowerDisplayProcessManager() = default;
|
||||
~PowerDisplayProcessManager();
|
||||
|
||||
/// <summary>
|
||||
/// Start PowerDisplay.exe process
|
||||
/// </summary>
|
||||
void start();
|
||||
|
||||
/// <summary>
|
||||
/// Stop PowerDisplay.exe process
|
||||
/// </summary>
|
||||
void stop();
|
||||
|
||||
/// <summary>
|
||||
/// Send message to PowerDisplay.exe
|
||||
/// </summary>
|
||||
void send_message_to_powerdisplay(const std::wstring& message);
|
||||
|
||||
private:
|
||||
/// <summary>
|
||||
/// Submit task to thread executor
|
||||
/// </summary>
|
||||
void submit_task(std::function<void()> task);
|
||||
|
||||
/// <summary>
|
||||
/// Check if PowerDisplay.exe is running
|
||||
/// </summary>
|
||||
bool is_process_running() const;
|
||||
|
||||
/// <summary>
|
||||
/// Terminate PowerDisplay.exe process
|
||||
/// </summary>
|
||||
void terminate_process();
|
||||
|
||||
/// <summary>
|
||||
/// Start PowerDisplay.exe with command line arguments
|
||||
/// </summary>
|
||||
HRESULT start_process(const std::wstring& pipe_uuid);
|
||||
|
||||
/// <summary>
|
||||
/// Create named pipe for sending commands to PowerDisplay
|
||||
/// </summary>
|
||||
HRESULT start_command_pipe(const std::wstring& pipe_uuid);
|
||||
|
||||
/// <summary>
|
||||
/// Refresh - start or stop process based on m_enabled state
|
||||
/// </summary>
|
||||
void refresh();
|
||||
};
|
||||
Reference in New Issue
Block a user