diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 9a1a435..927d2bb 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -8,10 +8,10 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install .NET - uses: actions/setup-dotnet@v3 + uses: actions/setup-dotnet@v4 with: dotnet-version: 7.0.x @@ -27,10 +27,10 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install .NET - uses: actions/setup-dotnet@v3 + uses: actions/setup-dotnet@v4 with: dotnet-version: 7.0.x @@ -48,13 +48,13 @@ jobs: & "c:\Program Files (x86)\Inno Setup 6\ISCC.exe" Installer/Installer.iss - name: Upload artifacts (portable) - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: NightGlow-Portable path: NightGlow/bin/publish/ - name: Upload artifacts (installer) - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: NightGlow-Installer path: Installer/bin/NightGlow-Installer.exe @@ -67,13 +67,13 @@ jobs: steps: - name: Download artifacts (portable) - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: NightGlow-Portable path: NightGlow - name: Download artifacts (installer) - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: NightGlow-Installer diff --git a/.gitignore b/.gitignore index 8a30d25..c946808 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,8 @@ ## ## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore +Installer/Source/ + # User-specific files *.rsuser *.suo diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b07e69..bb68b40 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## v3.0.0 (14-Sep-2024) +#### Breaking changes +- Switched to using display IDs for detection rather than the display name and index. **All settings in the DDC tab will be reset** + - This makes remembering displays and configured settings in the DDC tab more reliable + +#### Non-breaking changes +- Added option in General to show small popup when changing brightness/temperature +- Made changing DDC brightness more reliable on some monitors + ## v2.0.0 (13-Dec-2023) - Add DDC support (native monitor brightness) diff --git a/Installer/Installer.iss b/Installer/Installer.iss index 3b67c75..843cec5 100644 --- a/Installer/Installer.iss +++ b/Installer/Installer.iss @@ -1,6 +1,6 @@ #define AppName "Night Glow" #define AppExe "NightGlow.exe" -#define AppVersion "2.0.0" +#define AppVersion "3.0.0" [Setup] AppId={{8B87A5F9-0C03-4B92-9080-DDCF75DC18A4} diff --git a/NightGlow.MonitorConfig/Monitors.cs b/NightGlow.MonitorConfig/Monitors.cs deleted file mode 100644 index ceaae7a..0000000 --- a/NightGlow.MonitorConfig/Monitors.cs +++ /dev/null @@ -1,21 +0,0 @@ -namespace NightGlow.MonitorConfig; - -public class Monitors -{ - - public List VirtualMonitors = new List(); - - public bool Scan() - { - VirtualMonitors.Clear(); - bool success = WinApi.EnumDisplayMonitors(IntPtr.Zero, IntPtr.Zero, MonitorEnumCallback, IntPtr.Zero); - return success; - } - - private bool MonitorEnumCallback(IntPtr hMonitor, IntPtr hdcMonitor, ref WinApi.RECT lprcMonitor, IntPtr dwData) - { - VirtualMonitors.Add(new VirtualMonitor(hMonitor, VirtualMonitors.Count)); - return true; - } - -} diff --git a/NightGlow.MonitorConfig/NightGlow.MonitorConfig.csproj b/NightGlow.MonitorConfig/NightGlow.MonitorConfig.csproj deleted file mode 100644 index cfadb03..0000000 --- a/NightGlow.MonitorConfig/NightGlow.MonitorConfig.csproj +++ /dev/null @@ -1,9 +0,0 @@ - - - - net7.0 - enable - enable - - - diff --git a/NightGlow.MonitorConfig/PhysicalMonitor.cs b/NightGlow.MonitorConfig/PhysicalMonitor.cs deleted file mode 100644 index a1715af..0000000 --- a/NightGlow.MonitorConfig/PhysicalMonitor.cs +++ /dev/null @@ -1,77 +0,0 @@ -using System.Diagnostics; -using static NightGlow.MonitorConfig.WinApi; - -namespace NightGlow.MonitorConfig; - -public class PhysicalMonitor -{ - - // Sometimes setting/getting monitor settings can fail. Retry this many times before assuming failure. - private const int DDC_ATTEMPTS = 3; - - PHYSICAL_MONITOR Monitor; - - public string Description; - - private readonly Setting Brightness = new(); - private readonly Setting Contrast = new(); - - public PhysicalMonitor(PHYSICAL_MONITOR monitor) - { - Monitor = monitor; - Description = new string(Monitor.szPhysicalMonitorDescription); - } - - public Setting GetBrightness() - { - bool success = RetryGet((monitor) => GetMonitorBrightness( - monitor, out Brightness.Min, out Brightness.Current, out Brightness.Max), Monitor.hPhysicalMonitor - ); - return Brightness; - } - - public void SetBrightness(uint value) - { - bool success = RetrySet(SetMonitorBrightness, Monitor.hPhysicalMonitor, value); - if (success) - Brightness.Current = value; - } - - public Setting GetContrast() - { - bool success = RetryGet((monitor) => GetMonitorContrast( - monitor, out Contrast.Min, out Contrast.Current, out Contrast.Max), Monitor.hPhysicalMonitor - ); - return Contrast; - } - - public void SetContrast(uint value) - { - bool success = RetrySet(SetMonitorContrast, Monitor.hPhysicalMonitor, value); - if (success) - Contrast.Current = value; - } - - private bool RetrySet(Func function, nint monitor, uint value) - { - int attempt = 1; - while (attempt <= DDC_ATTEMPTS) - { - if (function(monitor, value)) return true; - attempt++; - } - return false; - } - - private bool RetryGet(Func function, nint monitor) - { - int attempt = 1; - while (attempt <= DDC_ATTEMPTS) - { - if (function(monitor)) return true; - attempt++; - } - return false; - } - -} diff --git a/NightGlow.MonitorConfig/VirtualMonitor.cs b/NightGlow.MonitorConfig/VirtualMonitor.cs deleted file mode 100644 index 2b40344..0000000 --- a/NightGlow.MonitorConfig/VirtualMonitor.cs +++ /dev/null @@ -1,106 +0,0 @@ -using System.Diagnostics; -using System.Runtime.InteropServices; -using static NightGlow.MonitorConfig.WinApi; - -namespace NightGlow.MonitorConfig; - -public class VirtualMonitor -{ - - public string DeviceName = ""; - public string FriendlyName = ""; - - public List PhysicalMonitors = new(); - - private MONITORINFOEX MonitorInfo; - - public VirtualMonitor(IntPtr hMonitor, int index) - { - MonitorInfo = new MONITORINFOEX { cbSize = (uint)Marshal.SizeOf() }; - - if (!GetMonitorInfo(hMonitor, ref MonitorInfo)) - { - // TODO throw error or log - Debug.WriteLine("Error: GetMonitorInfo"); - } - - DeviceName = MonitorInfo.szDevice; - - LoadFriendlyName(index); - - GetNumberOfPhysicalMonitorsFromHMONITOR(hMonitor, out uint physicalMonitorCount); - if (physicalMonitorCount == 0) - return; - - PHYSICAL_MONITOR[] physicalMonitorArray = new PHYSICAL_MONITOR[physicalMonitorCount]; - GetPhysicalMonitorsFromHMONITOR(hMonitor, physicalMonitorCount, physicalMonitorArray); - - for (int i = 0; i < physicalMonitorCount; i++) - { - PhysicalMonitors.Add(new PhysicalMonitor(physicalMonitorArray[i])); - } - } - - public bool IsPrimary() - { - return (MonitorInfo.dwFlags & MONITORINFOF.MONITORINFOF_PRIMARY) == MONITORINFOF.MONITORINFOF_PRIMARY; - } - - private void LoadFriendlyName(int index) - { - FriendlyName = ""; - - uint pathCount = 0, modeCount = 0; - - long error = GetDisplayConfigBufferSizes(QUERY_DEVICE_CONFIG_FLAGS.QDC_ONLY_ACTIVE_PATHS, - ref pathCount, ref modeCount); - if (error != ERROR_SUCCESS) - { - // TODO throw error or log - Debug.WriteLine("Error: GetDisplayConfigBufferSizes"); - return; - } - - DISPLAYCONFIG_PATH_INFO[] displayPaths = new DISPLAYCONFIG_PATH_INFO[pathCount]; - DISPLAYCONFIG_MODE_INFO[] displayModes = new DISPLAYCONFIG_MODE_INFO[modeCount]; - - error = QueryDisplayConfig(QUERY_DEVICE_CONFIG_FLAGS.QDC_ONLY_ACTIVE_PATHS, - ref pathCount, displayPaths, ref modeCount, displayModes, IntPtr.Zero); - if (error != ERROR_SUCCESS) - { - // TODO throw error or log - Debug.WriteLine("Error: QueryDisplayConfig"); - return; - } - - int modeTargetCount = 0; - for (int i = 0; i < modeCount; i++) - { - if (displayModes[i].infoType != DISPLAYCONFIG_MODE_INFO_TYPE.DISPLAYCONFIG_MODE_INFO_TYPE_TARGET) - continue; - - if (modeTargetCount != index) - { - modeTargetCount++; - continue; - } - - DISPLAYCONFIG_TARGET_DEVICE_NAME deviceName = new DISPLAYCONFIG_TARGET_DEVICE_NAME(); - deviceName.header.size = (uint)Marshal.SizeOf(typeof(DISPLAYCONFIG_TARGET_DEVICE_NAME)); - deviceName.header.adapterId = displayModes[i].adapterId; - deviceName.header.id = displayModes[i].id; - deviceName.header.type = DISPLAYCONFIG_DEVICE_INFO_TYPE.DISPLAYCONFIG_DEVICE_INFO_GET_TARGET_NAME; - - error = DisplayConfigGetDeviceInfo(ref deviceName); - if (error != ERROR_SUCCESS) - { - // TODO throw error or log - Debug.WriteLine("Error: DisplayConfigGetDeviceInfo"); - return; - } - FriendlyName = deviceName.monitorFriendlyDeviceName; - return; - } - } - -} diff --git a/NightGlow.sln b/NightGlow.sln index e8431ba..b008e91 100644 --- a/NightGlow.sln +++ b/NightGlow.sln @@ -4,14 +4,9 @@ Microsoft Visual Studio Solution File, Format Version 12.00 VisualStudioVersion = 17.7.34009.444 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NightGlow", "NightGlow\NightGlow.csproj", "{8E4BBB59-4D6D-48CF-9E04-CB3D7CA97401}" - ProjectSection(ProjectDependencies) = postProject - {5D7C7E03-E446-49ED-977A-2EA6824FE29A} = {5D7C7E03-E446-49ED-977A-2EA6824FE29A} - EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NightGlow.WindowsApi", "NightGlow.WindowsApi\NightGlow.WindowsApi.csproj", "{961F5C16-4446-40EC-9A72-1C352920335D}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NightGlow.MonitorConfig", "NightGlow.MonitorConfig\NightGlow.MonitorConfig.csproj", "{5D7C7E03-E446-49ED-977A-2EA6824FE29A}" -EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -46,18 +41,6 @@ Global {961F5C16-4446-40EC-9A72-1C352920335D}.Release|x64.Build.0 = Release|Any CPU {961F5C16-4446-40EC-9A72-1C352920335D}.Release|x86.ActiveCfg = Release|Any CPU {961F5C16-4446-40EC-9A72-1C352920335D}.Release|x86.Build.0 = Release|Any CPU - {5D7C7E03-E446-49ED-977A-2EA6824FE29A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {5D7C7E03-E446-49ED-977A-2EA6824FE29A}.Debug|Any CPU.Build.0 = Debug|Any CPU - {5D7C7E03-E446-49ED-977A-2EA6824FE29A}.Debug|x64.ActiveCfg = Debug|Any CPU - {5D7C7E03-E446-49ED-977A-2EA6824FE29A}.Debug|x64.Build.0 = Debug|Any CPU - {5D7C7E03-E446-49ED-977A-2EA6824FE29A}.Debug|x86.ActiveCfg = Debug|Any CPU - {5D7C7E03-E446-49ED-977A-2EA6824FE29A}.Debug|x86.Build.0 = Debug|Any CPU - {5D7C7E03-E446-49ED-977A-2EA6824FE29A}.Release|Any CPU.ActiveCfg = Release|Any CPU - {5D7C7E03-E446-49ED-977A-2EA6824FE29A}.Release|Any CPU.Build.0 = Release|Any CPU - {5D7C7E03-E446-49ED-977A-2EA6824FE29A}.Release|x64.ActiveCfg = Release|Any CPU - {5D7C7E03-E446-49ED-977A-2EA6824FE29A}.Release|x64.Build.0 = Release|Any CPU - {5D7C7E03-E446-49ED-977A-2EA6824FE29A}.Release|x86.ActiveCfg = Release|Any CPU - {5D7C7E03-E446-49ED-977A-2EA6824FE29A}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/NightGlow/App.config b/NightGlow/App.config index a2985f4..6f31d3e 100644 --- a/NightGlow/App.config +++ b/NightGlow/App.config @@ -52,6 +52,12 @@ + + False + + + + \ No newline at end of file diff --git a/NightGlow/App.xaml b/NightGlow/App.xaml index 7587f70..bd19b08 100644 --- a/NightGlow/App.xaml +++ b/NightGlow/App.xaml @@ -29,6 +29,13 @@ + + + + + + + + + + \ No newline at end of file diff --git a/NightGlow/Assets/popup-temperature.svg b/NightGlow/Assets/popup-temperature.svg new file mode 100644 index 0000000..fa291e5 --- /dev/null +++ b/NightGlow/Assets/popup-temperature.svg @@ -0,0 +1,12 @@ + + + + + + + \ No newline at end of file diff --git a/NightGlow/Data/DdcConfig.cs b/NightGlow/Data/DdcConfig.cs index eaadd52..1e17de3 100644 --- a/NightGlow/Data/DdcConfig.cs +++ b/NightGlow/Data/DdcConfig.cs @@ -1,4 +1,4 @@ -using NightGlow.MonitorConfig; +using NightGlow.Models; using System; using System.Collections.Generic; @@ -8,56 +8,56 @@ namespace NightGlow.Data; public class DdcConfig { - public IList DdcMonitorItems; + public IList DdcConfigMonitorItems; public DdcConfig() { - DdcMonitorItems = new List(); + DdcConfigMonitorItems = new List(); } - public DdcMonitorItem GetOrCreateDdcMonitorItem(VirtualMonitor vm) + public DdcConfigMonitor GetOrCreateDdcConfigMonitor(DdcMonitor monitor) { - foreach (var item in DdcMonitorItems) - if (item.Name.Equals(vm.FriendlyName) && item.DeviceName.Equals(vm.DeviceName)) + foreach (var item in DdcConfigMonitorItems) + if (item.DeviceInstanceId.Equals(monitor.DeviceInstanceId)) return item; - DdcMonitorItem newItem = new DdcMonitorItem + DdcConfigMonitor newItem = new DdcConfigMonitor { - Name = vm.FriendlyName, - DeviceName = vm.DeviceName, + Description = monitor.Description, + DeviceInstanceId = monitor.DeviceInstanceId, EnableDdc = false, MinBrightnessPct = 0, MaxBrightness = 100 }; - DdcMonitorItems.Add(newItem); + DdcConfigMonitorItems.Add(newItem); return newItem; } - public void SetEnableDdc(VirtualMonitor vm, bool value) + public void SetEnableDdc(DdcMonitor monitor, bool value) { - DdcMonitorItem item = GetOrCreateDdcMonitorItem(vm); + DdcConfigMonitor item = GetOrCreateDdcConfigMonitor(monitor); item.EnableDdc = value; } - public void SetMinBrightnessPct(VirtualMonitor vm, int value) + public void SetMinBrightnessPct(DdcMonitor monitor, int value) { - DdcMonitorItem item = GetOrCreateDdcMonitorItem(vm); + DdcConfigMonitor item = GetOrCreateDdcConfigMonitor(monitor); item.MinBrightnessPct = value; } - public void SetMaxBrightness(VirtualMonitor vm, int value) + public void SetMaxBrightness(DdcMonitor monitor, int value) { - DdcMonitorItem item = GetOrCreateDdcMonitorItem(vm); + DdcConfigMonitor item = GetOrCreateDdcConfigMonitor(monitor); item.MaxBrightness = value; } } [Serializable] -public class DdcMonitorItem +public class DdcConfigMonitor { - public string Name; - public string DeviceName; + public string Description; + public string DeviceInstanceId; public bool EnableDdc; diff --git a/NightGlow.MonitorConfig/WinApi.cs b/NightGlow/Helper/WinApi.cs similarity index 69% rename from NightGlow.MonitorConfig/WinApi.cs rename to NightGlow/Helper/WinApi.cs index 570944b..6f7a4bb 100644 --- a/NightGlow.MonitorConfig/WinApi.cs +++ b/NightGlow/Helper/WinApi.cs @@ -1,6 +1,8 @@ -using System.Runtime.InteropServices; +using NightGlow.Models; +using System; +using System.Runtime.InteropServices; -namespace NightGlow.MonitorConfig; +namespace NightGlow.Helper; public class WinApi { @@ -8,24 +10,44 @@ public class WinApi public const int ERROR_SUCCESS = 0; [DllImport("user32.dll")] - public static extern bool EnumDisplayMonitors(IntPtr hdc, IntPtr lprcClip, MonitorEnumProc lpfnEnum, IntPtr dwData); + public static extern bool EnumDisplayMonitors( + IntPtr hdc, + IntPtr lprcClip, + MonitorEnumProc lpfnEnum, + IntPtr dwData); + + public delegate bool MonitorEnumProc( + IntPtr hMonitor, + IntPtr hdcMonitor, + IntPtr lprcMonitor, + IntPtr dwData); + + [DllImport("user32.dll", EntryPoint = "GetMonitorInfoW")] + public static extern bool GetMonitorInfo( + IntPtr hMonitor, + ref MONITORINFOEX lpmi); - public delegate bool MonitorEnumProc(IntPtr hMonitor, IntPtr hdcMonitor, ref RECT lprcMonitor, IntPtr dwData); - - [DllImport("User32.dll", EntryPoint = "GetMonitorInfoW")] - public static extern bool GetMonitorInfo(IntPtr hMonitor, ref MONITORINFOEX lpmi); - - [DllImport("User32.dll")] - public static extern int GetDisplayConfigBufferSizes(QUERY_DEVICE_CONFIG_FLAGS Flags, ref uint numPathArrayElements, ref uint numModeInfoArrayElements); + [DllImport("user32.dll")] + public static extern int GetDisplayConfigBufferSizes( + uint flags, + out uint numPathArrayElements, + out uint numModeInfoArrayElements); + + [DllImport("user32.dll", CharSet = CharSet.Ansi)] + public static extern bool EnumDisplayDevices( + string lpDevice, + uint iDevNum, + ref DISPLAY_DEVICE lpDisplayDevice, + uint dwFlags); [DllImport("user32.dll")] public static extern int QueryDisplayConfig( - QUERY_DEVICE_CONFIG_FLAGS Flags, - ref uint NumPathArrayElements, - [Out] DISPLAYCONFIG_PATH_INFO[] PathInfoArray, - ref uint NumModeInfoArrayElements, - [Out] DISPLAYCONFIG_MODE_INFO[] ModeInfoArray, - IntPtr CurrentTopologyId + uint flags, + ref uint numPathArrayElements, + [Out] DISPLAYCONFIG_PATH_INFO[] pathInfoArray, + ref uint numModeInfoArrayElements, + [Out] DISPLAYCONFIG_MODE_INFO[] modeInfoArray, + IntPtr currentTopologyId ); [DllImport("user32.dll")] @@ -34,35 +56,84 @@ ref DISPLAYCONFIG_TARGET_DEVICE_NAME deviceName ); [DllImport("dxva2.dll", SetLastError = true)] - public extern static bool GetNumberOfPhysicalMonitorsFromHMONITOR(IntPtr hMonitor, out uint pdwNumberOfPhysicalMonitors); + public extern static bool GetNumberOfPhysicalMonitorsFromHMONITOR( + IntPtr hMonitor, + out uint pdwNumberOfPhysicalMonitors); [DllImport("dxva2.dll", SetLastError = true)] - public extern static bool GetPhysicalMonitorsFromHMONITOR(IntPtr hMonitor, uint dwPhysicalMonitorArraySize, [Out] PHYSICAL_MONITOR[] pPhysicalMonitorArray); + public extern static bool GetPhysicalMonitorsFromHMONITOR( + IntPtr hMonitor, + uint dwPhysicalMonitorArraySize, + [Out] PHYSICAL_MONITOR[] pPhysicalMonitorArray); [DllImport("dxva2.dll", SetLastError = true)] - public extern static bool GetMonitorCapabilities(IntPtr hMonitor, out uint pdwMonitorCapabilities, out uint pdwSupportedColorTemperatures); + public extern static bool GetMonitorBrightness( + SafePhysicalMonitorHandle hMonitor, + out uint pdwMinimumBrightness, + out uint pdwCurrentBrightness, + out uint pdwMaximumBrightness); [DllImport("dxva2.dll", SetLastError = true)] - public extern static bool GetMonitorBrightness(IntPtr hMonitor, out uint pdwMinimumBrightness, out uint pdwCurrentBrightness, out uint pdwMaximumBrightness); + public extern static bool SetMonitorBrightness( + SafePhysicalMonitorHandle hMonitor, + uint dwNewBrightness); [DllImport("dxva2.dll", SetLastError = true)] - public extern static bool SetMonitorBrightness(IntPtr hMonitor, uint dwNewBrightness); + public extern static bool GetMonitorContrast( + SafePhysicalMonitorHandle hMonitor, + out uint pdwMinimumContrast, + out uint pdwCurrentContrast, + out uint pdwMaximumContrast); [DllImport("dxva2.dll", SetLastError = true)] - public extern static bool GetMonitorContrast(IntPtr hMonitor, out uint pdwMinimumContrast, out uint pdwCurrentContrast, out uint pdwMaximumContrast); + public extern static bool SetMonitorContrast( + SafePhysicalMonitorHandle hMonitor, + uint dwNewContrast); [DllImport("dxva2.dll", SetLastError = true)] - public extern static bool SetMonitorContrast(IntPtr hMonitor, uint dwNewContrast); + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool DestroyPhysicalMonitor(IntPtr hMonitor); + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)] + public struct DISPLAY_DEVICE + { + public uint cb; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)] + public string DeviceName; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)] + public string DeviceString; + public DISPLAY_DEVICE_FLAG StateFlags; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)] + public string DeviceID; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)] + public string DeviceKey; + } - private const int PHYSICAL_MONITOR_DESCRIPTION_SIZE = 128; + [Flags] + public enum DISPLAY_DEVICE_FLAG : uint + { + DISPLAY_DEVICE_ATTACHED_TO_DESKTOP = 0x00000001, + DISPLAY_DEVICE_MULTI_DRIVER = 0x00000002, + DISPLAY_DEVICE_PRIMARY_DEVICE = 0x00000004, + DISPLAY_DEVICE_MIRRORING_DRIVER = 0x00000008, + DISPLAY_DEVICE_VGA_COMPATIBLE = 0x00000010, + DISPLAY_DEVICE_REMOVABLE = 0x00000020, + DISPLAY_DEVICE_ACC_DRIVER = 0x00000040, + DISPLAY_DEVICE_RDPUDD = 0x01000000, + DISPLAY_DEVICE_DISCONNECT = 0x02000000, + DISPLAY_DEVICE_REMOTE = 0x04000000, + DISPLAY_DEVICE_MODESPRUNED = 0x08000000, + + DISPLAY_DEVICE_ACTIVE = 0x00000001, + DISPLAY_DEVICE_ATTACHED = 0x00000002, + } - [StructLayout(LayoutKind.Sequential)] + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)] public struct PHYSICAL_MONITOR { public IntPtr hPhysicalMonitor; - [MarshalAs(UnmanagedType.ByValArray, ArraySubType = UnmanagedType.U2, SizeConst = PHYSICAL_MONITOR_DESCRIPTION_SIZE)] - public char[] szPhysicalMonitorDescription; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)] + public string szPhysicalMonitorDescription; } [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] @@ -94,6 +165,7 @@ public struct DISPLAYCONFIG_TARGET_DEVICE_NAME_FLAGS { public uint value; } + public enum DISPLAYCONFIG_DEVICE_INFO_TYPE : uint { DISPLAYCONFIG_DEVICE_INFO_GET_SOURCE_NAME = 1, @@ -105,20 +177,13 @@ public enum DISPLAYCONFIG_DEVICE_INFO_TYPE : uint DISPLAYCONFIG_DEVICE_INFO_FORCE_UINT32 = 0xFFFFFFFF } + public const uint EDD_GET_DEVICE_INTERFACE_NAME = 0x00000001; - - public enum QUERY_DEVICE_CONFIG_FLAGS : uint - { - QDC_ALL_PATHS = 0x00000001, - QDC_ONLY_ACTIVE_PATHS = 0x00000002, - QDC_DATABASE_CURRENT = 0x00000004 - } - + public const uint QDC_ONLY_ACTIVE_PATHS = 2; [StructLayout(LayoutKind.Sequential)] public struct RECT { public int left, top, right, bottom; } - [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)] public struct MONITORINFOEX { @@ -126,7 +191,6 @@ public struct MONITORINFOEX public RECT rcMonitor; public RECT rcWork; public MONITORINFOF dwFlags; - [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)] public string szDevice; } @@ -137,9 +201,6 @@ public enum MONITORINFOF : uint MONITORINFOF_PRIMARY = 0x00000001, } - - - [StructLayout(LayoutKind.Sequential)] public struct DISPLAYCONFIG_PATH_INFO { @@ -249,7 +310,7 @@ public struct DISPLAYCONFIG_PATH_TARGET_INFO DISPLAYCONFIG_VIDEO_OUTPUT_TECHNOLOGY outputTechnology; DISPLAYCONFIG_ROTATION rotation; DISPLAYCONFIG_SCALING scaling; - DISPLAYCONFIG_RATIONAL refreshRate; + public DISPLAYCONFIG_RATIONAL refreshRate; DISPLAYCONFIG_SCANLINE_ORDERING scanLineOrdering; public bool targetAvailable; public uint statusFlags; @@ -314,5 +375,4 @@ public enum DISPLAYCONFIG_SCANLINE_ORDERING : uint DISPLAYCONFIG_SCANLINE_ORDERING_FORCE_UINT32 = 0xFFFFFFFF } - } diff --git a/NightGlow/Models/DdcMonitor.cs b/NightGlow/Models/DdcMonitor.cs new file mode 100644 index 0000000..8939769 --- /dev/null +++ b/NightGlow/Models/DdcMonitor.cs @@ -0,0 +1,147 @@ +using System; +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; +using static NightGlow.Helper.WinApi; + +namespace NightGlow.Models; + +public class DdcMonitor +{ + // Sometimes setting/getting monitor settings can fail. Retry this many times before assuming failure. + private const int DDC_ATTEMPTS = 3; + + public string DeviceInstanceId { get; } + public string Description { get; } + public byte DisplayIndex { get; } + public byte MonitorIndex { get; } + public RECT MonitorRect { get; } + public string DeviceName { get; } + + private readonly SafePhysicalMonitorHandle _handle; + + private readonly Setting Brightness = new(); + private readonly Setting Contrast = new(); + + // Brightness + private CancellationTokenSource _cancelTokenB = new CancellationTokenSource(); + private readonly object _lockB = new object(); + + public DdcMonitor( + string deviceInstanceId, + string description, + byte displayIndex, + byte monitorIndex, + RECT monitorRect, + SafePhysicalMonitorHandle handle) + { + if (string.IsNullOrWhiteSpace(deviceInstanceId)) + throw new ArgumentNullException(nameof(deviceInstanceId)); + if (string.IsNullOrWhiteSpace(description)) + throw new ArgumentNullException(nameof(description)); + + this.DeviceInstanceId = deviceInstanceId; + this.Description = description; + this.DisplayIndex = displayIndex; + this.MonitorIndex = monitorIndex; + this.MonitorRect = monitorRect; + this.DeviceName = $"\\\\.\\DISPLAY{displayIndex}"; + + + this._handle = handle ?? throw new ArgumentNullException(nameof(handle)); + } + + private bool RetryGet(Func getFunction, SafePhysicalMonitorHandle hMonitor) + { + for (int attempt = 1; attempt <= DDC_ATTEMPTS; attempt++) + { + if (getFunction(hMonitor)) + return true; + } + return false; + } + + private static void RetrySet( + Func setFunction, + Func getFunction, + SafePhysicalMonitorHandle hMonitor, + uint value, + Setting setting, + CancellationToken cancelToken + ) + { + try + { + for (int attempt = 1; attempt <= DDC_ATTEMPTS; attempt++) + { + if (cancelToken.IsCancellationRequested) + return; + + // Sometimes setting a monitor value (e.g. brightness) will report as success, + // but monitor does not change brightness. + // Confirm value has been set by getting current value after doing the set. + if (setFunction(hMonitor, value) && getFunction().Current == value) + { + setting.Current = value; + return; + } + } + } + catch (TaskCanceledException) + { + Debug.WriteLine("Operation cancelled."); + } + catch (Exception ex) + { + Debug.WriteLine($"An error occurred: {ex.Message}"); + } + } + + public Setting GetBrightness() + { + bool success = RetryGet((handle) => GetMonitorBrightness( + _handle, out Brightness.Min, out Brightness.Current, out Brightness.Max), _handle + ); + return Brightness; + } + + public void SetBrightness(uint value) + { + lock (_lockB) + { + _cancelTokenB.Cancel(); + _cancelTokenB.Dispose(); + _cancelTokenB = new CancellationTokenSource(); + var cancelToken = _cancelTokenB.Token; + + Task.Run(() => RetrySet(SetMonitorBrightness, GetBrightness, _handle, value, Brightness, cancelToken), cancelToken); + } + } + + public void Dispose() + { + _handle.Dispose(); + } +} + +public class SafePhysicalMonitorHandle : SafeHandle +{ + public SafePhysicalMonitorHandle(IntPtr handle) : base(IntPtr.Zero, true) + { + this.handle = handle; // IntPtr.Zero may be a valid handle. + } + + public override bool IsInvalid => false; // The validity cannot be checked by the handle. + + protected override bool ReleaseHandle() + { + return DestroyPhysicalMonitor(handle); + } + + [DllImport("Dxva2.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool DestroyPhysicalMonitor( + IntPtr hMonitor); + +} diff --git a/NightGlow/Models/Monitors.cs b/NightGlow/Models/Monitors.cs new file mode 100644 index 0000000..59e262d --- /dev/null +++ b/NightGlow/Models/Monitors.cs @@ -0,0 +1,472 @@ +using NightGlow.Data; +using NightGlow.Services; +using NightGlow.ViewModels; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Diagnostics; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; +using System.Windows.Data; +using static NightGlow.Helper.WinApi; + +namespace NightGlow.Models; + +public class Monitors +{ + + public ObservableCollection MonitorItems { get; set; } + = new ObservableCollection(); + + protected readonly object _monitorsLock = new(); + + private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1); + private CancellationTokenSource _cts = new CancellationTokenSource(); + + public Monitors() + { + BindingOperations.EnableCollectionSynchronization(MonitorItems, _monitorsLock); + } + + public Task Scan(SettingsService _settingsService) => ScanAsync(_settingsService); + + private async Task ScanAsync(SettingsService _settingsService) + { + var newCts = new CancellationTokenSource(); + + CancellationTokenSource prevCts = Interlocked.Exchange(ref _cts, newCts); + prevCts.Cancel(); + prevCts.Dispose(); + + await _semaphore.WaitAsync(); + try + { + await Task.Run(async () => + { + await ScanAsync(_settingsService, newCts.Token); + }); + } + finally + { + _semaphore.Release(); + } + } + + private async Task ScanAsync(SettingsService settingsService, CancellationToken cts) + { + if (cts.IsCancellationRequested) + return; + + lock (_monitorsLock) + { + foreach (var monitor in MonitorItems) + { + monitor.Dispose(); + } + MonitorItems.Clear(); + } + + foreach (var ddcMonitor in await EnumerateMonitorsAsync()) + { + if (cts.IsCancellationRequested) + return; + + Debug.WriteLine($"{ddcMonitor.DisplayIndex}, {ddcMonitor.DeviceInstanceId}, {ddcMonitor.Description}"); + + DdcConfigMonitor ddcConfigMonitor = settingsService.DdcConfig.GetOrCreateDdcConfigMonitor(ddcMonitor); + + var monitorVm = new MonitorViewModel(ddcMonitor) + { + Brightness = Convert.ToInt32(ddcMonitor.GetBrightness().Current), + EnableDdc = ddcConfigMonitor.EnableDdc, + MaxBrightness = ddcConfigMonitor.MaxBrightness, + MinBrightnessPct = ddcConfigMonitor.MinBrightnessPct, + }; + monitorVm.BrightnessChangeEvent += (s, e) => + { + ddcMonitor.SetBrightness((uint)e.Monitor.Brightness); + }; + monitorVm.ContrastChangeEvent += (s, e) => + { + //item.SetContrast(item, e.Monitor.Contrast); + }; + monitorVm.EnableDdcChangeEvent += (s, e) => + { + settingsService.DdcConfig.SetEnableDdc(ddcMonitor, e.Monitor.EnableDdc); + settingsService.SaveDdcConfig(); + }; + monitorVm.MinBrightnessPctChangeEvent += (s, e) => + { + settingsService.DdcConfig.SetMinBrightnessPct(ddcMonitor, e.Monitor.MinBrightnessPct); + settingsService.SaveDdcConfig(); + }; + monitorVm.MaxBrightnessChangeEvent += (s, e) => + { + settingsService.DdcConfig.SetMaxBrightness(ddcMonitor, e.Monitor.MaxBrightness); + settingsService.SaveDdcConfig(); + }; + + lock (_monitorsLock) + { + MonitorItems.Add(monitorVm); + } + } + } + + private static async Task> EnumerateMonitorsAsync(CancellationToken cancellationToken = default) + { + var deviceItems = EnumerateMonitorDevices().ToArray(); + var displayItems = EnumerateDisplayConfigs().ToArray(); + + IEnumerable EnumerateBasicItems() + { + foreach (var deviceItem in deviceItems) + { + var displayItem = displayItems.FirstOrDefault(x => string.Equals(deviceItem.DeviceInstanceId, x.DeviceInstanceId, StringComparison.OrdinalIgnoreCase)); + if (!string.IsNullOrWhiteSpace(displayItem.DisplayName)) + { + yield return new BasicItem(deviceItem, displayItem.DisplayName); + } + } + } + + var basicItems = EnumerateBasicItems().Where(x => !string.IsNullOrWhiteSpace(x.AlternateDescription)).ToList(); + if (basicItems.Count == 0) + return Enumerable.Empty(); + + var handleItems = GetMonitorHandles(); + + var physicalItemsTasks = handleItems.Select(x => Task.Run(() => (x, physicalItems: EnumeratePhysicalMonitors(x.MonitorHandle)))) + .ToArray(); + await Task.WhenAll(physicalItemsTasks); + cancellationToken.ThrowIfCancellationRequested(); + var physicalItemsPairs = physicalItemsTasks.Where(x => x.Status == TaskStatus.RanToCompletion).Select(x => x.Result); + + IEnumerable EnumerateMonitorItems() + { + foreach ((var handleItem, var physicalItems) in physicalItemsPairs) + { + foreach (var physicalItem in physicalItems) + { + int index = basicItems.FindIndex(x => + (x.DisplayIndex == handleItem.DisplayIndex) && + (x.MonitorIndex == physicalItem.MonitorIndex) && + string.Equals(x.Description, physicalItem.Description, StringComparison.OrdinalIgnoreCase)); + if (index < 0) + { + physicalItem.Handle.Dispose(); + continue; + } + + var basicItem = basicItems[index]; + + yield return new DdcMonitor( + deviceInstanceId: basicItem.DeviceInstanceId, + description: basicItem.AlternateDescription, + displayIndex: basicItem.DisplayIndex, + monitorIndex: basicItem.MonitorIndex, + monitorRect: handleItem.MonitorRect, + handle: physicalItem.Handle); + + basicItems.RemoveAt(index); + if (basicItems.Count == 0) + yield break; + } + } + } + + return EnumerateMonitorItems(); + } + + public static IEnumerable EnumeratePhysicalMonitors(IntPtr monitorHandle) + { + if (!GetNumberOfPhysicalMonitorsFromHMONITOR(monitorHandle, out uint count)) + { + Debug.WriteLine($"Failed to get the number of physical monitors."); + yield break; + } + + if (count == 0) + yield break; + + var physicalMonitors = new PHYSICAL_MONITOR[count]; + + try + { + if (!GetPhysicalMonitorsFromHMONITOR(monitorHandle, count, physicalMonitors)) + { + Debug.WriteLine($"Failed to get an array of physical monitors."); + yield break; + } + + int monitorIndex = 0; + + foreach (var physicalMonitor in physicalMonitors) + { + var handle = new SafePhysicalMonitorHandle(physicalMonitor.hPhysicalMonitor); + + yield return new PhysicalItem( + description: physicalMonitor.szPhysicalMonitorDescription, + monitorIndex: monitorIndex, + handle: handle); + + monitorIndex++; + } + } + finally + { + // The physical monitor handles should be destroyed at a later stage. + } + } + + public static HandleItem[] GetMonitorHandles() + { + var handleItems = new List(); + + if (EnumDisplayMonitors(IntPtr.Zero, IntPtr.Zero, Proc, IntPtr.Zero)) + return handleItems.ToArray(); + + return Array.Empty(); + + bool Proc(IntPtr monitorHandle, IntPtr hdcMonitor, IntPtr lprcMonitor, IntPtr dwData) + { + var monitorInfo = new MONITORINFOEX { cbSize = (uint)Marshal.SizeOf() }; + + if (GetMonitorInfo(monitorHandle, ref monitorInfo)) + { + if (TryGetDisplayIndex(monitorInfo.szDevice, out byte displayIndex)) + { + handleItems.Add(new HandleItem( + displayIndex: displayIndex, + monitorRect: monitorInfo.rcMonitor, + monitorHandle: monitorHandle)); + } + } + return true; + } + } + + public static IEnumerable EnumerateMonitorDevices() + { + foreach (var (_, displayIndex, monitor, monitorIndex) in EnumerateDevices()) + { + var deviceInstanceId = ConvertToDeviceInstanceId(monitor.DeviceID); + if (string.IsNullOrEmpty(deviceInstanceId)) + continue; + + yield return new DeviceItem( + deviceInstanceId: deviceInstanceId, + description: monitor.DeviceString, + displayIndex: displayIndex, + monitorIndex: monitorIndex); + } + } + + private static IEnumerable<(DISPLAY_DEVICE display, byte displayIndex, DISPLAY_DEVICE monitor, byte monitorIndex)> EnumerateDevices() + { + var size = (uint)Marshal.SizeOf(); + var display = new DISPLAY_DEVICE { cb = size }; + var monitor = new DISPLAY_DEVICE { cb = size }; + + for (uint i = 0; EnumDisplayDevices(null, i, ref display, EDD_GET_DEVICE_INTERFACE_NAME); i++) + { + if (!TryGetDisplayIndex(display.DeviceName, out byte displayIndex)) + continue; + + byte monitorIndex = 0; + + for (uint j = 0; EnumDisplayDevices(display.DeviceName, j, ref monitor, EDD_GET_DEVICE_INTERFACE_NAME); j++) + { + if (!monitor.StateFlags.HasFlag(DISPLAY_DEVICE_FLAG.DISPLAY_DEVICE_ACTIVE)) + continue; + + yield return (display, displayIndex, monitor, monitorIndex); + + monitorIndex++; + } + } + } + + public static IEnumerable EnumerateDisplayConfigs() + { + if (GetDisplayConfigBufferSizes( + QDC_ONLY_ACTIVE_PATHS, + out uint pathCount, + out uint modeCount) != ERROR_SUCCESS) + yield break; + + var displayPaths = new DISPLAYCONFIG_PATH_INFO[pathCount]; + var displayModes = new DISPLAYCONFIG_MODE_INFO[modeCount]; + + if (QueryDisplayConfig( + QDC_ONLY_ACTIVE_PATHS, + ref pathCount, + displayPaths, + ref modeCount, + displayModes, + IntPtr.Zero) != ERROR_SUCCESS) + yield break; + + foreach (var displayPath in displayPaths) + { + var displayMode = displayModes + .Where(x => x.infoType == DISPLAYCONFIG_MODE_INFO_TYPE.DISPLAYCONFIG_MODE_INFO_TYPE_TARGET) + .FirstOrDefault(x => x.id == displayPath.targetInfo.id); + if (displayMode.Equals(default(DISPLAYCONFIG_MODE_INFO))) + continue; + + var deviceName = new DISPLAYCONFIG_TARGET_DEVICE_NAME(); + deviceName.header.size = (uint)Marshal.SizeOf(); + deviceName.header.adapterId = displayMode.adapterId; + deviceName.header.id = displayMode.id; + deviceName.header.type = DISPLAYCONFIG_DEVICE_INFO_TYPE.DISPLAYCONFIG_DEVICE_INFO_GET_TARGET_NAME; + + if (DisplayConfigGetDeviceInfo(ref deviceName) != ERROR_SUCCESS) + continue; + + var deviceInstanceId = ConvertToDeviceInstanceId(deviceName.monitorDevicePath); + + yield return new DisplayItem( + deviceInstanceId: deviceInstanceId, + displayName: deviceName.monitorFriendlyDeviceName, + isInternal: (deviceName.outputTechnology == DISPLAYCONFIG_VIDEO_OUTPUT_TECHNOLOGY.DISPLAYCONFIG_OUTPUT_TECHNOLOGY_INTERNAL), + refreshRate: displayPath.targetInfo.refreshRate.Numerator / (float)displayPath.targetInfo.refreshRate.Denominator, + isAvailable: displayPath.targetInfo.targetAvailable); + } + } + + private static bool TryGetDisplayIndex(string deviceName, out byte index) + { + var match = Regex.Match(deviceName, @"DISPLAY(?\d{1,2})\s*$"); + if (match.Success) + { + index = byte.Parse(match.Groups["index"].Value); + return true; + } + index = 0; + return false; + } + + internal static string ConvertToDeviceInstanceId(string devicePath) + { + // The typical format of device path is as follows: + // \\?\DISPLAY###{e6f07b5f-ee97-4a90-b076-33f57bf4eaa7} + // \\?\ is extended-length path prefix. + // DISPLAY indicates display device. + // {e6f07b5f-ee97-4a90-b076-33f57bf4eaa7} means GUID_DEVINTERFACE_MONITOR. + + int index = devicePath.IndexOf("DISPLAY", StringComparison.Ordinal); + if (index < 0) + return null; + + var fields = devicePath.Substring(index).Split('#'); + if (fields.Length < 3) + return null; + + return string.Join(@"\", fields.Take(3)); + } + + public class PhysicalItem + { + public string Description { get; } + + public int MonitorIndex { get; } + + public SafePhysicalMonitorHandle Handle { get; } + + public PhysicalItem(string description, int monitorIndex, SafePhysicalMonitorHandle handle) + { + this.Description = description; + this.MonitorIndex = monitorIndex; + this.Handle = handle; + } + } + + public class HandleItem + { + public int DisplayIndex { get; } + + public RECT MonitorRect { get; } + private string _monitorRectString; + + public IntPtr MonitorHandle { get; } + + public HandleItem(int displayIndex, RECT monitorRect, IntPtr monitorHandle) + { + this.DisplayIndex = displayIndex; + this.MonitorRect = monitorRect; + this.MonitorHandle = monitorHandle; + } + } + + private class BasicItem + { + private readonly DeviceItem _deviceItem; + + public string DeviceInstanceId => _deviceItem.DeviceInstanceId; + public string Description => _deviceItem.Description; + public string AlternateDescription { get; } + public byte DisplayIndex => _deviceItem.DisplayIndex; + public byte MonitorIndex => _deviceItem.MonitorIndex; + + public BasicItem(DeviceItem deviceItem, string alternateDescription = null) + { + this._deviceItem = deviceItem ?? throw new ArgumentNullException(nameof(deviceItem)); + this.AlternateDescription = alternateDescription ?? deviceItem.Description; + } + } + + public class DeviceItem + { + public string DeviceInstanceId { get; } + + public string Description { get; } + + public byte DisplayIndex { get; } + + public byte MonitorIndex { get; } + + public DeviceItem( + string deviceInstanceId, + string description, + byte displayIndex, + byte monitorIndex) + { + this.DeviceInstanceId = deviceInstanceId; + this.Description = description; + this.DisplayIndex = displayIndex; + this.MonitorIndex = monitorIndex; + } + } + + public class DisplayItem + { + public string DeviceInstanceId { get; } + + public string DisplayName { get; } + + public bool IsInternal { get; } + + public float RefreshRate { get; } + + public bool IsAvailable { get; } + + public DisplayItem( + string deviceInstanceId, + string displayName, + bool isInternal, + float refreshRate, + bool isAvailable) + { + this.DeviceInstanceId = deviceInstanceId; + this.DisplayName = displayName; + this.IsInternal = isInternal; + this.RefreshRate = refreshRate; + this.IsAvailable = isAvailable; + } + } + +} diff --git a/NightGlow.MonitorConfig/Setting.cs b/NightGlow/Models/Setting.cs similarity index 89% rename from NightGlow.MonitorConfig/Setting.cs rename to NightGlow/Models/Setting.cs index c44ff25..027065d 100644 --- a/NightGlow.MonitorConfig/Setting.cs +++ b/NightGlow/Models/Setting.cs @@ -1,4 +1,4 @@ -namespace NightGlow.MonitorConfig; +namespace NightGlow.Models; public class Setting { diff --git a/NightGlow/NightGlow.csproj b/NightGlow/NightGlow.csproj index bc95177..b3b481f 100644 --- a/NightGlow/NightGlow.csproj +++ b/NightGlow/NightGlow.csproj @@ -6,30 +6,33 @@ enable true Assets\app-icon.ico - 2.0.0 + 3.0.0 + + - - + + - + + + - diff --git a/NightGlow/Properties/Settings.Designer.cs b/NightGlow/Properties/Settings.Designer.cs index ddcac2f..bd7bc2e 100644 --- a/NightGlow/Properties/Settings.Designer.cs +++ b/NightGlow/Properties/Settings.Designer.cs @@ -12,7 +12,7 @@ namespace NightGlow.Properties { [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "17.7.0.0")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "17.11.0.0")] internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase { private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); @@ -202,5 +202,29 @@ public string DdcConfigJson { this["DdcConfigJson"] = value; } } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("False")] + public bool ShowBrightTempPopup { + get { + return ((bool)(this["ShowBrightTempPopup"])); + } + set { + this["ShowBrightTempPopup"] = value; + } + } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("")] + public string DdcConfigJsonV2 { + get { + return ((string)(this["DdcConfigJsonV2"])); + } + set { + this["DdcConfigJsonV2"] = value; + } + } } } diff --git a/NightGlow/Properties/Settings.settings b/NightGlow/Properties/Settings.settings index 52d1689..be80695 100644 --- a/NightGlow/Properties/Settings.settings +++ b/NightGlow/Properties/Settings.settings @@ -47,5 +47,11 @@ + + False + + + + \ No newline at end of file diff --git a/NightGlow/Services/DdcService.cs b/NightGlow/Services/DdcService.cs index d2f39f3..05d32d9 100644 --- a/NightGlow/Services/DdcService.cs +++ b/NightGlow/Services/DdcService.cs @@ -1,52 +1,41 @@ -using NightGlow.MonitorConfig; -using System.Threading; +using NightGlow.Models; namespace NightGlow.Services; public class DdcService { - public Monitors Monitors = new Monitors(); + private readonly SettingsService _settingsService; - private Timer timer; - private bool isUpdatingMonitors = false; + public readonly Monitors _monitors; - public DdcService() + public Monitors Monitors { get => _monitors; } + + public DdcService(SettingsService settingsService) { + _settingsService = settingsService; + _monitors = new Monitors(); UpdateMonitors(); } public void UpdateMonitors() { - // UpdateMonitors could be called multiple times in a short time by windows event listeners - // Scanning monitors takes time, so wait for short time while calls come in, and only execute after short time has passed. - if (isUpdatingMonitors) - { - timer.Change(1000, Timeout.Infinite); - } - else - { - isUpdatingMonitors = true; - timer = new Timer(ExecuteUpdateMonitors, null, 1000, Timeout.Infinite); - } + ExecuteUpdateMonitors(); } - private void ExecuteUpdateMonitors(object? state) + private void ExecuteUpdateMonitors() { - Monitors.Scan(); - - isUpdatingMonitors = false; - timer.Dispose(); + Monitors.Scan(_settingsService); } - public void SetBrightness(VirtualMonitor vm, PhysicalMonitor pm, int value) + public void SetBrightness(DdcMonitor monitor, int value) { - pm.SetBrightness((uint)value); + monitor.SetBrightness((uint)value); } - public void SetContrast(VirtualMonitor vm, PhysicalMonitor pm, int value) - { - pm.SetContrast((uint)value); + public void SetContrast(DdcMonitor monitor, int value) + { + //monitor.SetContrast((uint)value); } } diff --git a/NightGlow/Services/GammaService.cs b/NightGlow/Services/GammaService.cs index 22aa71d..a9ad0d3 100644 --- a/NightGlow/Services/GammaService.cs +++ b/NightGlow/Services/GammaService.cs @@ -111,32 +111,6 @@ public void SetDeviceGamma(ColorConfiguration configuration, string deviceName) //Debug.WriteLine($"Updated gamma to {configuration}."); } - //public void SetAllGamma(ColorConfiguration configuration) - //{ - // // Avoid unnecessary changes as updating too often will cause stutters - // if (!IsGammaStale() && !IsSignificantChange(configuration)) - // return; - - // EnsureValidDeviceContexts(); - - // _isUpdatingGamma = true; - - // foreach (var deviceContext in _deviceContexts) - // { - // deviceContext.SetGamma( - // GetRed(configuration) * configuration.Brightness, - // GetGreen(configuration) * configuration.Brightness, - // GetBlue(configuration) * configuration.Brightness - // ); - // } - - // _isUpdatingGamma = false; - - // _lastConfiguration = configuration; - // _lastUpdateTimestamp = DateTimeOffset.Now; - // Debug.WriteLine($"Updated gamma to {configuration}."); - //} - public void Dispose() { // Reset gamma on all contexts diff --git a/NightGlow/Services/NightGlowService.cs b/NightGlow/Services/NightGlowService.cs index 737c287..d1ea016 100644 --- a/NightGlow/Services/NightGlowService.cs +++ b/NightGlow/Services/NightGlowService.cs @@ -1,7 +1,6 @@ using CommunityToolkit.Mvvm.ComponentModel; using NightGlow.Data; using NightGlow.Models; -using NightGlow.MonitorConfig; using NightGlow.WindowsApi; using NightGlow.Utils.Extensions; using System; @@ -9,6 +8,9 @@ using System.Linq; using System.Reactive.Disposables; using System.Threading.Tasks; +using NightGlow.Views; +using Microsoft.Extensions.DependencyInjection; +using NightGlow.ViewModels; namespace NightGlow.Services; @@ -21,6 +23,8 @@ public class NightGlowService : ObservableObject, IDisposable private readonly GammaService _gammaService; private readonly DdcService _ddcService; + private readonly IServiceProvider _serviceProvider; + private readonly IDisposable _eventRegistration; public double Brightness @@ -41,13 +45,15 @@ public NightGlowService( SettingsService settingsService, HotKeyService hotKeyService, GammaService gammaService, - DdcService ddcService + DdcService ddcService, + IServiceProvider serviceProvider ) { _settingsService = settingsService; _hotKeyService = hotKeyService; _gammaService = gammaService; _ddcService = ddcService; + _serviceProvider = serviceProvider; RegisterHotKeys(); @@ -184,6 +190,11 @@ private void ChangeConfig(double brightnessOffset, int temperatureOffset) OnPropertyChanged(nameof(Temperature)); UpdateConfiguration(); + + if (_settingsService.ShowBrightTempPopup) + { + _serviceProvider.GetRequiredService().ShowPopup(); + } } // Using the brightness value, check wheter to use ddc/gamma to set the brightness @@ -197,50 +208,48 @@ private void UpdateConfiguration() { Debug.WriteLine(""); - foreach (VirtualMonitor vm in _ddcService.Monitors.VirtualMonitors) + foreach (MonitorViewModel monitor in _ddcService.Monitors.MonitorItems) { - PhysicalMonitor? pm = vm.PhysicalMonitors.FirstOrDefault(); - if (pm == null) continue; - int configBrightnessPct = Convert.ToInt32(Brightness * 100); - DdcMonitorItem ddcMonitorItem = _settingsService.DdcConfig.GetOrCreateDdcMonitorItem(vm); + var ddcMonitor = monitor.DdcMonitor; + DdcConfigMonitor ddcConfigMonitor = _settingsService.DdcConfig.GetOrCreateDdcConfigMonitor(ddcMonitor); - if (ddcMonitorItem.EnableDdc && configBrightnessPct > ddcMonitorItem.MinBrightnessPct) + if (ddcConfigMonitor.EnableDdc && configBrightnessPct > ddcConfigMonitor.MinBrightnessPct) { // Use DDC to set the brightness // Find the percentage value the brightness is between lower (ddcMonitorItem.MinBrightnessPct) and upper (100) - double ddcRangePct = (double)(configBrightnessPct - ddcMonitorItem.MinBrightnessPct) / (100 - ddcMonitorItem.MinBrightnessPct); - int ddcBrightness = Convert.ToInt32(ddcRangePct * ddcMonitorItem.MaxBrightness); + double ddcRangePct = (double)(configBrightnessPct - ddcConfigMonitor.MinBrightnessPct) / (100 - ddcConfigMonitor.MinBrightnessPct); + int ddcBrightness = Convert.ToInt32(ddcRangePct * ddcConfigMonitor.MaxBrightness); ColorConfiguration gamma = new ColorConfiguration(Temperature, _settingsService.BrightnessMax); - Debug.WriteLine($"Using DDC. DDC: {ddcBrightness}. Gamma: {gamma.Brightness}. Monitor: {ddcMonitorItem.Name}"); - _ddcService.SetBrightness(vm, pm, ddcBrightness); - _gammaService.SetDeviceGamma(gamma, vm.DeviceName); + Debug.WriteLine($"Using DDC. DDC: {ddcBrightness}. Gamma: {gamma.Brightness}. Monitor: {ddcMonitor.Description}"); + _ddcService.SetBrightness(ddcMonitor, ddcBrightness); + _gammaService.SetDeviceGamma(gamma, ddcMonitor.DeviceName); } - else if (ddcMonitorItem.EnableDdc) + else if (ddcConfigMonitor.EnableDdc) { // Use Use gamma to set the brightness when DDC brightness is at minimum - double gammaRangePct = (double)(configBrightnessPct - _settingsService.BrightnessMin) / (ddcMonitorItem.MinBrightnessPct - _settingsService.BrightnessMin); + double gammaRangePct = (double)(configBrightnessPct - _settingsService.BrightnessMin) / (ddcConfigMonitor.MinBrightnessPct - _settingsService.BrightnessMin); double gammaBrightness = gammaRangePct; ColorConfiguration gamma = new ColorConfiguration(Temperature, gammaBrightness); - Debug.WriteLine($"Using Gam. DDC: 0. Gamma: {gammaBrightness}. Monitor: {ddcMonitorItem.Name}"); - _ddcService.SetBrightness(vm, pm, 0); - _gammaService.SetDeviceGamma(gamma, vm.DeviceName); + Debug.WriteLine($"Using Gam. DDC: 0. Gamma: {gammaBrightness}. Monitor: {ddcMonitor.Description}"); + _ddcService.SetBrightness(ddcMonitor, 0); + _gammaService.SetDeviceGamma(gamma, ddcMonitor.DeviceName); } else { // Use Use gamma to set the brightness - Debug.WriteLine($"Using Gam. DDC: x. Gamma: {Brightness}. Monitor: {ddcMonitorItem.Name}"); - _gammaService.SetDeviceGamma(ColorConfig, vm.DeviceName); + Debug.WriteLine($"Using Gam. DDC: x. Gamma: {Brightness}. Monitor: {ddcMonitor.Description}"); + _gammaService.SetDeviceGamma(ColorConfig, ddcMonitor.DeviceName); } } diff --git a/NightGlow/Services/SettingsService.cs b/NightGlow/Services/SettingsService.cs index 9fc9e05..32626e9 100644 --- a/NightGlow/Services/SettingsService.cs +++ b/NightGlow/Services/SettingsService.cs @@ -37,6 +37,17 @@ public bool ExtendedGammaRange set => _extendedGammaRange.Set = value; } + public bool ShowBrightTempPopup + { + get => Settings.Default.ShowBrightTempPopup; + set + { + Settings.Default.ShowBrightTempPopup = value; + Settings.Default.Save(); + OnPropertyChanged(nameof(ShowBrightTempPopup)); + } + } + public bool FirstLaunch { get => Settings.Default.FirstLaunch; @@ -182,7 +193,7 @@ public SettingsService() public void LoadDdcConfig() { - string ddcConfigJson = Settings.Default.DdcConfigJson; + string ddcConfigJson = Settings.Default.DdcConfigJsonV2; try { DdcConfig = JsonConvert.DeserializeObject(ddcConfigJson) ?? new DdcConfig(); @@ -197,7 +208,7 @@ public void LoadDdcConfig() public void SaveDdcConfig() { string ddcConfigJson = JsonConvert.SerializeObject(DdcConfig); - Settings.Default.DdcConfigJson = ddcConfigJson; + Settings.Default.DdcConfigJsonV2 = ddcConfigJson; Settings.Default.Save(); } diff --git a/NightGlow/ViewModels/MainWindowViewModel.cs b/NightGlow/ViewModels/MainWindowViewModel.cs index 28b15d6..982db2c 100644 --- a/NightGlow/ViewModels/MainWindowViewModel.cs +++ b/NightGlow/ViewModels/MainWindowViewModel.cs @@ -1,11 +1,6 @@ using CommunityToolkit.Mvvm.ComponentModel; -using NightGlow.Data; -using NightGlow.MonitorConfig; using NightGlow.Services; -using System; -using System.Collections.ObjectModel; using System.ComponentModel; -using System.Linq; using System.Windows; namespace NightGlow.ViewModels; @@ -18,13 +13,9 @@ public class MainWindowViewModel : ObservableObject private readonly NightGlowService _nightGlowService; public SettingsService SettingsService { get => _settingsService; } + public DdcService DdcService { get => _ddcService; } public NightGlowService NightGlowService { get => _nightGlowService; } - - public ObservableCollection MonitorItems { get; set; } - = new ObservableCollection(); - - private int _selectedTabIndex; public int SelectedTabIndex @@ -58,62 +49,11 @@ NightGlowService nightGlowService _nightGlowService = nightGlowService; UpdateDdcMonitors(); - SetupMonitorItems(); - } - - public void SetupMonitorItems() - { - MonitorItems.Clear(); - - foreach (VirtualMonitor vm in _ddcService.Monitors.VirtualMonitors) - { - PhysicalMonitor? pm = vm.PhysicalMonitors.FirstOrDefault(); - if (pm == null) continue; - - DdcMonitorItem ddcMonitorItem = _settingsService.DdcConfig.GetOrCreateDdcMonitorItem(vm); - - MonitorItemViewModel monitorItemViewModel = new MonitorItemViewModel - { - VirtualName = vm.FriendlyName, - DeviceName = vm.DeviceName, - PhysicalName = pm.Description, - Brightness = Convert.ToInt32(pm.GetBrightness().Current), - Contrast = Convert.ToInt32(pm.GetContrast().Current), - EnableDdc = ddcMonitorItem.EnableDdc, - MaxBrightness = ddcMonitorItem.MaxBrightness, - MinBrightnessPct = ddcMonitorItem.MinBrightnessPct - }; - monitorItemViewModel.BrightnessChangeEvent += (object sender, DdcItemChangeEventArgs e) => - { - _ddcService.SetBrightness(vm, pm, e.MonitorItem.Brightness); - }; - monitorItemViewModel.ContrastChangeEvent += (object sender, DdcItemChangeEventArgs e) => - { - _ddcService.SetContrast(vm, pm, e.MonitorItem.Contrast); - }; - monitorItemViewModel.EnableDdcChangeEvent += (object sender, DdcItemChangeEventArgs e) => - { - _settingsService.DdcConfig.SetEnableDdc(vm, e.MonitorItem.EnableDdc); - _settingsService.SaveDdcConfig(); - }; - monitorItemViewModel.MinBrightnessPctChangeEvent += (object sender, DdcItemChangeEventArgs e) => - { - _settingsService.DdcConfig.SetMinBrightnessPct(vm, e.MonitorItem.MinBrightnessPct); - _settingsService.SaveDdcConfig(); - }; - monitorItemViewModel.MaxBrightnessChangeEvent += (object sender, DdcItemChangeEventArgs e) => - { - _settingsService.DdcConfig.SetMaxBrightness(vm, e.MonitorItem.MaxBrightness); - _settingsService.SaveDdcConfig(); - }; - MonitorItems.Add(monitorItemViewModel); - } } public void UpdateDdcMonitors() { - _ddcService.UpdateMonitors(); - SetupMonitorItems(); + DdcService.UpdateMonitors(); } public void OnWindowLoaded(object sender, RoutedEventArgs e) @@ -129,85 +69,3 @@ public void OnWindowClosing(object sender, CancelEventArgs e) } } - -public class MonitorItemViewModel : ObservableObject -{ - - public string VirtualName { get; set; } - public string DeviceName { get; set; } - public string PhysicalName { get; set; } - public string CombinedName { get => $"{VirtualName} - {DeviceName}"; } - - private int _brightness; - private int _contrast; - private bool _enableDdc; - private int _minBrightnessPct; - private int _maxBrightness; - - public int Brightness - { - get => _brightness; - set - { - _brightness = value; - OnPropertyChanged(nameof(Brightness)); - BrightnessChangeEvent?.Invoke(null, new DdcItemChangeEventArgs { MonitorItem = this }); - } - } - - public int Contrast - { - get => _contrast; - set - { - _contrast = value; - OnPropertyChanged(nameof(Contrast)); - ContrastChangeEvent?.Invoke(null, new DdcItemChangeEventArgs { MonitorItem = this }); - } - } - - public bool EnableDdc - { - get => _enableDdc; - set - { - _enableDdc = value; - OnPropertyChanged(nameof(EnableDdc)); - EnableDdcChangeEvent?.Invoke(null, new DdcItemChangeEventArgs { MonitorItem = this }); - } - } - - public int MinBrightnessPct - { - get => _minBrightnessPct; - set - { - _minBrightnessPct = value; - OnPropertyChanged(nameof(MinBrightnessPct)); - MinBrightnessPctChangeEvent?.Invoke(null, new DdcItemChangeEventArgs { MonitorItem = this }); - } - } - - public int MaxBrightness - { - get => _maxBrightness; - set - { - _maxBrightness = value; - OnPropertyChanged(nameof(MaxBrightness)); - MaxBrightnessChangeEvent?.Invoke(null, new DdcItemChangeEventArgs { MonitorItem = this }); - } - } - - public event EventHandler EnableDdcChangeEvent; - public event EventHandler BrightnessChangeEvent; - public event EventHandler ContrastChangeEvent; - public event EventHandler MinBrightnessPctChangeEvent; - public event EventHandler MaxBrightnessChangeEvent; - -} - -public class DdcItemChangeEventArgs : EventArgs -{ - public MonitorItemViewModel MonitorItem { get; set; } -} diff --git a/NightGlow/ViewModels/MonitorViewModel.cs b/NightGlow/ViewModels/MonitorViewModel.cs new file mode 100644 index 0000000..381cfca --- /dev/null +++ b/NightGlow/ViewModels/MonitorViewModel.cs @@ -0,0 +1,96 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using NightGlow.Models; +using System; + +namespace NightGlow.ViewModels; + +public class MonitorViewModel : ObservableObject, IDisposable +{ + + public DdcMonitor DdcMonitor; + public string CombinedName { get => $"{DdcMonitor.Description} (Display {DdcMonitor.DisplayIndex})"; } + + private int _brightness; + private int _contrast; + + // User config + private bool _enableDdc; + private int _minBrightnessPct; + private int _maxBrightness; + + public int Brightness + { + get => _brightness; + set + { + _brightness = value; + OnPropertyChanged(nameof(Brightness)); + BrightnessChangeEvent?.Invoke(null, new DdcItemChangeEventArgs { Monitor = this }); + } + } + + public int Contrast + { + get => _contrast; + set + { + _contrast = value; + OnPropertyChanged(nameof(Contrast)); + ContrastChangeEvent?.Invoke(null, new DdcItemChangeEventArgs { Monitor = this }); + } + } + + public bool EnableDdc + { + get => _enableDdc; + set + { + _enableDdc = value; + OnPropertyChanged(nameof(EnableDdc)); + EnableDdcChangeEvent?.Invoke(null, new DdcItemChangeEventArgs { Monitor = this }); + } + } + + public int MinBrightnessPct + { + get => _minBrightnessPct; + set + { + _minBrightnessPct = value; + OnPropertyChanged(nameof(MinBrightnessPct)); + MinBrightnessPctChangeEvent?.Invoke(null, new DdcItemChangeEventArgs { Monitor = this }); + } + } + + public int MaxBrightness + { + get => _maxBrightness; + set + { + _maxBrightness = value; + OnPropertyChanged(nameof(MaxBrightness)); + MaxBrightnessChangeEvent?.Invoke(null, new DdcItemChangeEventArgs { Monitor = this }); + } + } + + public event EventHandler EnableDdcChangeEvent; + public event EventHandler BrightnessChangeEvent; + public event EventHandler ContrastChangeEvent; + public event EventHandler MinBrightnessPctChangeEvent; + public event EventHandler MaxBrightnessChangeEvent; + + public MonitorViewModel(DdcMonitor _ddcMonitor) + { + DdcMonitor = _ddcMonitor; + } + + public void Dispose() + { + DdcMonitor.Dispose(); + } +} + +public class DdcItemChangeEventArgs : EventArgs +{ + public MonitorViewModel Monitor { get; set; } +} diff --git a/NightGlow/ViewModels/NotifyIconViewModel.cs b/NightGlow/ViewModels/NotifyIconViewModel.cs index 5356b8d..4b80a2a 100644 --- a/NightGlow/ViewModels/NotifyIconViewModel.cs +++ b/NightGlow/ViewModels/NotifyIconViewModel.cs @@ -35,7 +35,10 @@ NightGlowService nightGlowService public void ShowWindow() { - Application.Current.MainWindow ??= _serviceProvider.GetRequiredService(); + if (!(Application.Current.MainWindow is MainWindow)) + { + Application.Current.MainWindow = _serviceProvider.GetRequiredService(); + } Application.Current.MainWindow.Show(); Application.Current.MainWindow.Activate(); } diff --git a/NightGlow/ViewModels/PopupViewModel.cs b/NightGlow/ViewModels/PopupViewModel.cs new file mode 100644 index 0000000..add5e81 --- /dev/null +++ b/NightGlow/ViewModels/PopupViewModel.cs @@ -0,0 +1,21 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using NightGlow.Services; + +namespace NightGlow.ViewModels; + +public partial class PopupViewModel : ObservableObject +{ + + private readonly SettingsService _settingsService; + private readonly NightGlowService _nightGlowService; + + public SettingsService SettingsService { get => _settingsService; } + public NightGlowService NightGlowService { get => _nightGlowService; } + + public PopupViewModel(SettingsService settingsService, NightGlowService nightGlowService) + { + _settingsService = settingsService; + _nightGlowService = nightGlowService; + } + +} diff --git a/NightGlow/Views/Controls/PopupDisplayInfoView.xaml b/NightGlow/Views/Controls/PopupDisplayInfoView.xaml new file mode 100644 index 0000000..be01ac5 --- /dev/null +++ b/NightGlow/Views/Controls/PopupDisplayInfoView.xaml @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/NightGlow/Views/Controls/PopupDisplayInfoView.xaml.cs b/NightGlow/Views/Controls/PopupDisplayInfoView.xaml.cs new file mode 100644 index 0000000..e94e662 --- /dev/null +++ b/NightGlow/Views/Controls/PopupDisplayInfoView.xaml.cs @@ -0,0 +1,40 @@ +using System.Windows; +using System.Windows.Controls; + +namespace NightGlow.Views.Controls; + +public partial class PopupDisplayInfoView : UserControl +{ + + public static readonly DependencyProperty SliderValueProperty = DependencyProperty.Register(nameof(SliderValue), typeof(double), typeof(PopupDisplayInfoView), null); + + public double SliderValue + { + get => (double)GetValue(SliderValueProperty); + set => SetValue(SliderValueProperty, value); + } + + public static readonly DependencyProperty SliderValueMinProperty = DependencyProperty.Register(nameof(SliderValueMin), typeof(double), typeof(PopupDisplayInfoView), null); + + public double SliderValueMin + { + get => (double)GetValue(SliderValueMinProperty); + set => SetValue(SliderValueMinProperty, value); + } + + public static readonly DependencyProperty SliderValueMaxProperty = DependencyProperty.Register(nameof(SliderValueMax), typeof(double), typeof(PopupDisplayInfoView), null); + + public double SliderValueMax + { + get => (double)GetValue(SliderValueMaxProperty); + set => SetValue(SliderValueMaxProperty, value); + } + + public string Icon { get; set; } + + public PopupDisplayInfoView() + { + InitializeComponent(); + } + +} diff --git a/NightGlow/Views/PopupWindow.xaml b/NightGlow/Views/PopupWindow.xaml new file mode 100644 index 0000000..4618fd7 --- /dev/null +++ b/NightGlow/Views/PopupWindow.xaml @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/NightGlow/Views/PopupWindow.xaml.cs b/NightGlow/Views/PopupWindow.xaml.cs new file mode 100644 index 0000000..ea04591 --- /dev/null +++ b/NightGlow/Views/PopupWindow.xaml.cs @@ -0,0 +1,135 @@ +using NightGlow.ViewModels; +using System; +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Windows; +using System.Windows.Interop; +using System.Windows.Media; +using System.Windows.Media.Animation; +using System.Windows.Threading; + +namespace NightGlow.Views; + +public partial class PopupWindow : Window +{ + + private const double ANIM_TIME = 0.25; + private const double DISPLAY_TIME = 2; + private const int BOTTOM_OFFSET = 12; // Offest above the taskbar / bottom of screen + + private DispatcherTimer hideTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(DISPLAY_TIME) }; + + private DoubleAnimation _slideDownAnimation; + private EventHandler _slideDownCompleteHandler; + + private STATE state = STATE.HIDDEN; + + private enum STATE + { + HIDDEN = 1, + APPEARING = 2, + ACTIVE = 3, + HIDING = 4 + } + + public PopupWindow(PopupViewModel viewModel) + { + DataContext = viewModel; + InitializeComponent(); + + hideTimer.Tick += (s, args) => + { + hideTimer.Stop(); + AnimateOut(); + }; + + this.Loaded += Window_Loaded; + } + + public void ShowPopup() + { + switch (state) + { + case STATE.HIDDEN: + Show(); + AnimateIn(); + break; + case STATE.APPEARING: + case STATE.ACTIVE: + ResetHideTimer(); + break; + case STATE.HIDING: + _slideDownAnimation.Completed -= _slideDownCompleteHandler; + AnimateIn(); + break; + } + } + + private void ResetHideTimer() + { + hideTimer.Interval = TimeSpan.FromSeconds(DISPLAY_TIME); + } + + private void AnimateIn() + { + state = STATE.APPEARING; + ResetHideTimer(); + + var workArea = SystemParameters.WorkArea; + this.Left = (workArea.Width - this.Width) / 2; + this.Top = workArea.Bottom - this.Height - BOTTOM_OFFSET; + + var slideAnimation = new DoubleAnimation + { + From = this.ActualHeight, + To = 0, + Duration = TimeSpan.FromSeconds(ANIM_TIME), + EasingFunction = new CircleEase { EasingMode = EasingMode.EaseOut } + }; + slideAnimation.Completed += (s, args) => state = STATE.ACTIVE; + AnimatedContainerTransform.BeginAnimation(TranslateTransform.YProperty, slideAnimation); + + hideTimer.Start(); + } + + private void AnimateOut() + { + state = STATE.HIDING; + + _slideDownAnimation = new DoubleAnimation + { + From = 0, + To = this.ActualHeight, + Duration = TimeSpan.FromSeconds(ANIM_TIME), + EasingFunction = new CircleEase { EasingMode = EasingMode.EaseIn } + }; + _slideDownCompleteHandler = (s, args) => HideNow(); + _slideDownAnimation.Completed += _slideDownCompleteHandler; + AnimatedContainerTransform.BeginAnimation(TranslateTransform.YProperty, _slideDownAnimation); + } + + private void HideNow() + { + state = STATE.HIDDEN; + this.Hide(); + } + + private const int GWL_EX_STYLE = -20, WS_EX_APPWINDOW = 0x00040000, WS_EX_TOOLWINDOW = 0x00000080, WS_EX_NOACTIVATE = 0x08000000; + + [DllImport("user32.dll", SetLastError = true)] + static extern int GetWindowLong(IntPtr hWnd, int nIndex); + + [DllImport("user32.dll")] + static extern int SetWindowLong(IntPtr hWnd, int nIndex, int dwNewLong); + + private void Window_Loaded(object sender, RoutedEventArgs e) + { + var helper = new WindowInteropHelper(this).Handle; + int currentStyle = GetWindowLong(helper, GWL_EX_STYLE); + // Hide from Alt + Tab (WS_EX_TOOLWINDOW & WS_EX_APPWINDOW) + // Prevent Window from taking focus when showing (WS_EX_NOACTIVATE) + int newStyle = (currentStyle | WS_EX_TOOLWINDOW | WS_EX_NOACTIVATE) & ~WS_EX_APPWINDOW; + SetWindowLong(helper, GWL_EX_STYLE, newStyle); + } + +} diff --git a/NightGlow/Views/SettingsDdcView.xaml b/NightGlow/Views/SettingsDdcView.xaml index 64c999f..900955e 100644 --- a/NightGlow/Views/SettingsDdcView.xaml +++ b/NightGlow/Views/SettingsDdcView.xaml @@ -86,7 +86,7 @@ diff --git a/NightGlow/Views/SettingsGeneralView.xaml b/NightGlow/Views/SettingsGeneralView.xaml index f6cf8bf..b34fa20 100644 --- a/NightGlow/Views/SettingsGeneralView.xaml +++ b/NightGlow/Views/SettingsGeneralView.xaml @@ -26,6 +26,13 @@ Margin="0,0,0,10" /> + + diff --git a/README.md b/README.md index 0c0593a..e26c5be 100644 --- a/README.md +++ b/README.md @@ -52,4 +52,7 @@ The app is built and released automatically from source using GitHub Actions [he

+
+
+

diff --git a/res/popup.png b/res/popup.png new file mode 100644 index 0000000..be992c2 Binary files /dev/null and b/res/popup.png differ