From 404e6f42dad0a6155f185fbabdf8b2e66a965fd4 Mon Sep 17 00:00:00 2001 From: logikonline Date: Thu, 29 Jan 2026 18:14:58 -0500 Subject: [PATCH] refactor(ci): rename DellMonitorControl to MonitorControl Rename project from DellMonitorControl to MonitorControl to reflect broader monitor compatibility beyond Dell hardware. Update all references in solution file, workflow, and project paths. Add DDC/CI rate limiting and throttling logic to prevent command failures: - Minimum 50ms interval between commands to same monitor - 8-second grace period after system resume before sending commands - 3-second cooldown after timeout to allow monitor recovery - Global semaphore to prevent command collisions Replace old icon with new generic monitor icon (ico and png formats). --- .gitea/workflows/release.yaml | 26 ++-- ControlMyMonitorManagement.sln | 2 +- DellMonitorControl/MonitorIcon.ico | Bin 1578 -> 0 bytes Library/Method/CMMCommand.cs | 121 ++++++++++++++++-- .../App.xaml | 4 +- .../App.xaml.cs | 18 ++- .../AssemblyInfo.cs | 0 .../ConfigWindow.xaml | 2 +- .../ConfigWindow.xaml.cs | 71 ++++++++-- .../ControlPanel.xaml | 2 +- .../ControlPanel.xaml.cs | 2 +- .../DebugLogger.cs | 2 +- .../Images/icons.xaml | 0 .../MainWindow.xaml | 2 +- .../MainWindow.xaml.cs | 53 +++++++- .../MonitorControl.csproj | 6 +- .../MonitorControl.iss | 2 +- MonitorControl/MonitorIcon.ico | Bin 0 -> 105356 bytes MonitorControl/MonitorIcon.png | Bin 0 -> 4423 bytes .../Style/Btn.xaml | 0 .../Style/Color.xaml | 0 .../Style/Main.xaml | 0 .../Style/Slider.xaml | 0 .../UpdateChecker.cs | 2 +- 24 files changed, 262 insertions(+), 53 deletions(-) delete mode 100644 DellMonitorControl/MonitorIcon.ico rename {DellMonitorControl => MonitorControl}/App.xaml (83%) rename {DellMonitorControl => MonitorControl}/App.xaml.cs (75%) rename {DellMonitorControl => MonitorControl}/AssemblyInfo.cs (100%) rename {DellMonitorControl => MonitorControl}/ConfigWindow.xaml (99%) rename {DellMonitorControl => MonitorControl}/ConfigWindow.xaml.cs (77%) rename {DellMonitorControl => MonitorControl}/ControlPanel.xaml (92%) rename {DellMonitorControl => MonitorControl}/ControlPanel.xaml.cs (99%) rename {DellMonitorControl => MonitorControl}/DebugLogger.cs (98%) rename {DellMonitorControl => MonitorControl}/Images/icons.xaml (100%) rename {DellMonitorControl => MonitorControl}/MainWindow.xaml (99%) rename {DellMonitorControl => MonitorControl}/MainWindow.xaml.cs (92%) rename DellMonitorControl/DellMonitorControl.csproj => MonitorControl/MonitorControl.csproj (90%) rename {DellMonitorControl => MonitorControl}/MonitorControl.iss (97%) create mode 100644 MonitorControl/MonitorIcon.ico create mode 100644 MonitorControl/MonitorIcon.png rename {DellMonitorControl => MonitorControl}/Style/Btn.xaml (100%) rename {DellMonitorControl => MonitorControl}/Style/Color.xaml (100%) rename {DellMonitorControl => MonitorControl}/Style/Main.xaml (100%) rename {DellMonitorControl => MonitorControl}/Style/Slider.xaml (100%) rename {DellMonitorControl => MonitorControl}/UpdateChecker.cs (99%) diff --git a/.gitea/workflows/release.yaml b/.gitea/workflows/release.yaml index 45eb60c..baa09ac 100644 --- a/.gitea/workflows/release.yaml +++ b/.gitea/workflows/release.yaml @@ -24,7 +24,7 @@ jobs: Write-Host "Setting version to: $version" # Update csproj (preserve UTF-8 BOM encoding) - $csprojPath = "DellMonitorControl/DellMonitorControl.csproj" + $csprojPath = "MonitorControl/MonitorControl.csproj" $content = [System.IO.File]::ReadAllText($csprojPath) $content = $content -replace '.*', "$version" $content = $content -replace '.*', "$version.0" @@ -32,7 +32,7 @@ jobs: [System.IO.File]::WriteAllText($csprojPath, $content, [System.Text.UTF8Encoding]::new($true)) # Update ISS - $issPath = "DellMonitorControl/MonitorControl.iss" + $issPath = "MonitorControl/MonitorControl.iss" $issContent = [System.IO.File]::ReadAllText($issPath) $issContent = $issContent -replace '#define MyAppVersion ".*"', "#define MyAppVersion `"$version`"" [System.IO.File]::WriteAllText($issPath, $issContent, [System.Text.UTF8Encoding]::new($true)) @@ -48,21 +48,21 @@ jobs: run: | echo "Current directory:" cd - echo "DellMonitorControl contents:" - dir DellMonitorControl - echo "DellMonitorControl\bin contents:" - dir DellMonitorControl\bin - echo "DellMonitorControl\bin\Release contents:" - dir DellMonitorControl\bin\Release - echo "DellMonitorControl\bin\Release\net9.0-windows contents:" - dir DellMonitorControl\bin\Release\net9.0-windows + echo "MonitorControl contents:" + dir MonitorControl + echo "MonitorControl\bin contents:" + dir MonitorControl\bin + echo "MonitorControl\bin\Release contents:" + dir MonitorControl\bin\Release + echo "MonitorControl\bin\Release\net9.0-windows contents:" + dir MonitorControl\bin\Release\net9.0-windows - name: Build Installer shell: cmd run: | echo Copying to short path to avoid path length issues... mkdir C:\build 2>nul - xcopy /E /I /Y DellMonitorControl C:\build\app + xcopy /E /I /Y MonitorControl C:\build\app echo. echo Working from C:\build\app cd /d C:\build\app @@ -86,7 +86,7 @@ jobs: shell: powershell run: | $version = "${{ gitea.ref_name }}".TrimStart("v") - $buildDir = "DellMonitorControl\bin\Release\net9.0-windows" + $buildDir = "MonitorControl\bin\Release\net9.0-windows" $zipName = "MonitorControl-Portable-$version.zip" Write-Host "Creating portable zip: $zipName" @@ -115,7 +115,7 @@ jobs: $body = @{ tag_name = $tag name = "Monitor Control $tag" - body = "## Monitor Control $tag`n`n### Installation`n- **Installer**: Download and run the setup exe`n- **Portable**: Download the zip, extract anywhere, and run DellMonitorControl.exe`n`n### Features`n- System tray monitor control`n- Brightness/Contrast adjustment`n- Input source switching`n- Quick-switch toolbar`n`n### Requirements`n- Windows 10/11`n- .NET 9.0 Runtime" + body = "## Monitor Control $tag`n`n### Installation`n- **Installer**: Download and run the setup exe`n- **Portable**: Download the zip, extract anywhere, and run MonitorControl.exe`n`n### Features`n- System tray monitor control`n- Brightness/Contrast adjustment`n- Input source switching`n- Quick-switch toolbar`n`n### Requirements`n- Windows 10/11`n- .NET 9.0 Runtime" } | ConvertTo-Json $release = Invoke-RestMethod -Uri "$baseUrl/repos/$repo/releases" -Method Post -Headers @{"Authorization"="token $env:GITEA_TOKEN"; "Content-Type"="application/json"} -Body $body diff --git a/ControlMyMonitorManagement.sln b/ControlMyMonitorManagement.sln index 822f1a8..4467b03 100644 --- a/ControlMyMonitorManagement.sln +++ b/ControlMyMonitorManagement.sln @@ -11,7 +11,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Language", "Language\Langua EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tester", "Tester\Tester.csproj", "{0D34DD73-3282-40EB-8F59-DF190944BF12}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DellMonitorControl", "DellMonitorControl\DellMonitorControl.csproj", "{64E96610-D431-40B9-A00B-55CE195B4B58}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MonitorControl", "MonitorControl\MonitorControl.csproj", "{64E96610-D431-40B9-A00B-55CE195B4B58}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CMMService", "CMMService\CMMService.csproj", "{FEA2B019-32BC-4704-939F-1CD26F373F55}" EndProject diff --git a/DellMonitorControl/MonitorIcon.ico b/DellMonitorControl/MonitorIcon.ico deleted file mode 100644 index 232686e0a6e020222b9456c2734ae18b39983a5f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1578 zcmZQzU}Run5D;Jh(h3Zl7#JAbfLK8R!oLe-{{UhG0|+0)zRCzBfB>XEmYIR!EE5Am zXMmqOFP9V-P?*=#!zBnv15IQQ;9vui@}K`F0x8DgAa^H*b?0PW0y!+{j=qiz3>*8o z|0J>k`J4qFk;M!Qe1}1p@p%4<6riBHr;B5Vg@1B_gvf*c|Nrv=alV3}wSqK*kbwmT zzY{#VbbH?(mwn>ys?X`<#rR@Ei0W3?z`wuM5~NpW{HtXVaA05r5)aPVZ1B7ldt;V; zx#(jXgVU9b>1Q;$&$H|jJGR)qDDJGZo%yfMwb*~JiZ~q?16@*N@ zUi7Gt!9iovq$M&HOUlkoEecNAIk&jA;V(yl@_}9kbEZ2ej9iHgReB{}yR07Xyk}=@ z{Oh^X>QmlTzqDVt9k?I&?aUjw8^8VgYa%`hz86wOCep}h3O_dF-tD&Xx&<2 zeBivK%zTlLA3<)x2mT$kd|r93a^}t4>V5Y&_m|2_>^T4W@4Q+a#)jLR3@xLG{voiI z>%ix(hP%7h7oYn+>F>O+fB$^@ZhQZ|q~Z1U4M*q2>)dc)Q1BrKB{Uv%4?Nx9R3$Q5 zb{+q;>Ywfh`Irox7;ZQ+xN$I8a~$AcOkih}P-J+cz@W_1Aj^`_!my#4p+|tBOrSxK m=>TPl+Yh=sp8l+%fiE@Uk`wvEpfOihLdW*KJ4gwv!~_6Uai +/// /// Control My Monitor Management Command /// public static class CMMCommand @@ -14,6 +15,78 @@ public static class CMMCommand static readonly string CMMexe = Path.Combine(AppContext.BaseDirectory, "ControlMyMonitor.exe"); static readonly string CMMsMonitors = Path.Combine(CMMTmpFolder, "smonitors.tmp"); + // DDC/CI rate limiting: minimum time between commands to the same monitor + private static readonly ConcurrentDictionary _lastCommandTime = new(); + private static readonly SemaphoreSlim _globalDdcLock = new(1, 1); + private const int MinCommandIntervalMs = 50; + + // Post-resume grace period: prevents DDC/CI commands from hitting monitors still waking up + private static DateTime _resumeTime = DateTime.MinValue; + private const int ResumeGracePeriodMs = 8000; + + // Timeout cooldown: when a command times out, the monitor is likely struggling. + // Back off for this long before sending another command to that monitor. + private static readonly ConcurrentDictionary _timeoutCooldowns = new(); + private const int TimeoutCooldownMs = 3000; + + /// + /// Call this when the system resumes from sleep/hibernate to activate the grace period. + /// Monitors need several seconds after resume before their scalers are ready for DDC/CI. + /// + public static void NotifySystemResumed() + { + _resumeTime = DateTime.UtcNow; + } + + /// + /// Waits for the post-resume grace period to expire, respects timeout cooldowns, + /// and enforces per-monitor command spacing. + /// + private static async Task ThrottleDdcCommand(string monitorSN) + { + // Wait for post-resume grace period + var elapsed = (DateTime.UtcNow - _resumeTime).TotalMilliseconds; + if (elapsed < ResumeGracePeriodMs) + { + var waitMs = (int)(ResumeGracePeriodMs - elapsed); + await Task.Delay(waitMs); + } + + // Wait for timeout cooldown — if this monitor recently timed out, give it time to recover + if (_timeoutCooldowns.TryGetValue(monitorSN, out var cooldownUntil)) + { + var remaining = (cooldownUntil - DateTime.UtcNow).TotalMilliseconds; + if (remaining > 0) + await Task.Delay((int)remaining); + _timeoutCooldowns.TryRemove(monitorSN, out _); + } + + // Enforce minimum interval between commands to the same monitor + await _globalDdcLock.WaitAsync(); + try + { + if (_lastCommandTime.TryGetValue(monitorSN, out var lastTime)) + { + var sinceLastMs = (DateTime.UtcNow - lastTime).TotalMilliseconds; + if (sinceLastMs < MinCommandIntervalMs) + await Task.Delay((int)(MinCommandIntervalMs - sinceLastMs)); + } + _lastCommandTime[monitorSN] = DateTime.UtcNow; + } + finally + { + _globalDdcLock.Release(); + } + } + + /// + /// Records that a command to this monitor timed out, activating a cooldown period. + /// + private static void RecordTimeout(string monitorSN) + { + _timeoutCooldowns[monitorSN] = DateTime.UtcNow.AddMilliseconds(TimeoutCooldownMs); + } + public static async Task ScanMonitor() { // Ensure temp folder exists for output files @@ -23,26 +96,36 @@ public static class CMMCommand public static async Task PowerOn(string monitorSN) { - await ConsoleHelper.ExecuteExeAsync(CMMexe, $"/SetValue {monitorSN} D6 1"); + await ThrottleDdcCommand(monitorSN); + var (_, exitCode) = await ConsoleHelper.ExecuteExeAsync(CMMexe, $"/SetValue {monitorSN} D6 1"); + if (exitCode == -1) RecordTimeout(monitorSN); } public static async Task Sleep(string monitorSN) { - await ConsoleHelper.ExecuteExeAsync(CMMexe, $"/SetValue {monitorSN} D6 4"); + await ThrottleDdcCommand(monitorSN); + var (_, exitCode) = await ConsoleHelper.ExecuteExeAsync(CMMexe, $"/SetValue {monitorSN} D6 4"); + if (exitCode == -1) RecordTimeout(monitorSN); } private static async Task GetMonitorValue(string monitorSN, string vcpCode = "D6", int maxRetries = 2) { for (int attempt = 0; attempt <= maxRetries; attempt++) { + await ThrottleDdcCommand(monitorSN); + // Execute directly without batch file wrapper var (output, exitCode) = await ConsoleHelper.ExecuteExeAsync( CMMexe, $"/GetValue {monitorSN} {vcpCode}"); - // Timeout + // Timeout — monitor is unresponsive, activate cooldown and stop retrying. + // Continuing to send commands to a timed-out monitor risks scaler lockout. if (exitCode == -1) + { + RecordTimeout(monitorSN); return string.Empty; + } // ControlMyMonitor returns the value as the exit code // Exit code > 0 means success with that value @@ -66,7 +149,9 @@ public static class CMMCommand public static async Task SetBrightness(string monitorSN, int value) { - await ConsoleHelper.ExecuteExeAsync(CMMexe, $"/SetValue {monitorSN} 10 {value}"); + await ThrottleDdcCommand(monitorSN); + var (_, exitCode) = await ConsoleHelper.ExecuteExeAsync(CMMexe, $"/SetValue {monitorSN} 10 {value}"); + if (exitCode == -1) RecordTimeout(monitorSN); } public static async Task GetBrightness(string monitorSN) @@ -81,7 +166,9 @@ public static class CMMCommand public static async Task SetContrast(string monitorSN, int value) { - await ConsoleHelper.ExecuteExeAsync(CMMexe, $"/SetValue {monitorSN} 12 {value}"); + await ThrottleDdcCommand(monitorSN); + var (_, exitCode) = await ConsoleHelper.ExecuteExeAsync(CMMexe, $"/SetValue {monitorSN} 12 {value}"); + if (exitCode == -1) RecordTimeout(monitorSN); } public static async Task GetContrast(string monitorSN) @@ -96,7 +183,9 @@ public static class CMMCommand public static async Task SetInputSource(string monitorSN, int value) { - await ConsoleHelper.ExecuteExeAsync(CMMexe, $"/SetValue {monitorSN} 60 {value}"); + await ThrottleDdcCommand(monitorSN); + var (_, exitCode) = await ConsoleHelper.ExecuteExeAsync(CMMexe, $"/SetValue {monitorSN} 60 {value}"); + if (exitCode == -1) RecordTimeout(monitorSN); } public static async Task GetInputSource(string monitorSN) @@ -110,7 +199,9 @@ public static class CMMCommand var options = new List(); var savePath = Path.Combine(CMMTmpFolder, $"{monitorSN}_vcp.tmp"); - await ConsoleHelper.ExecuteExeAsync(CMMexe, $"/sjson {savePath} {monitorSN}"); + await ThrottleDdcCommand(monitorSN); + var (_, exitCode) = await ConsoleHelper.ExecuteExeAsync(CMMexe, $"/sjson {savePath} {monitorSN}"); + if (exitCode == -1) RecordTimeout(monitorSN); if (!File.Exists(savePath)) return options; @@ -177,17 +268,19 @@ public static class CMMCommand public static async Task ScanMonitorStatus(IEnumerable monitors) { - var taskList = monitors.Select(x => + // Sequential scan to avoid overwhelming DDC/CI bus with parallel commands + foreach (var x in monitors) { - return ScanMonitorStatus($"{CMMTmpFolder}\\{x.SerialNumber}.tmp", x); - }); - - await Task.WhenAll(taskList); + await ScanMonitorStatus($"{CMMTmpFolder}\\{x.SerialNumber}.tmp", x); + } } static async Task ScanMonitorStatus(string savePath, XMonitor mon) { - await ConsoleHelper.ExecuteExeAsync(CMMexe, $"/sjson {savePath} {mon.MonitorID}"); + var sn = mon.SerialNumber ?? mon.MonitorID ?? "unknown"; + await ThrottleDdcCommand(sn); + var (_, exitCode) = await ConsoleHelper.ExecuteExeAsync(CMMexe, $"/sjson {savePath} {mon.MonitorID}"); + if (exitCode == -1) RecordTimeout(sn); var monitorModel = JsonHelper.JsonFormFile>(savePath); var status = monitorModel.ReadMonitorStatus(); diff --git a/DellMonitorControl/App.xaml b/MonitorControl/App.xaml similarity index 83% rename from DellMonitorControl/App.xaml rename to MonitorControl/App.xaml index cf6ba55..1d9126a 100644 --- a/DellMonitorControl/App.xaml +++ b/MonitorControl/App.xaml @@ -1,8 +1,8 @@ - _availablePorts; private readonly List _portRows = new(); + private CancellationTokenSource? _detectCts; public bool ConfigChanged { get; private set; } @@ -205,13 +207,35 @@ public partial class ConfigWindow : Window private async void DetectButton_Click(object sender, RoutedEventArgs e) { - btnDetect.IsEnabled = false; - btnDetect.Content = "..."; + // If detection is already running, cancel it + if (_detectCts != null) + { + _detectCts.Cancel(); + return; + } + + // Warn the user about what this does + var confirm = MessageBox.Show( + "Port detection will temporarily switch your monitor's input source to probe for available ports. " + + "Your screen may go dark briefly during each probe.\n\n" + + "This is safe for most monitors, but avoid running it repeatedly in a short time.\n\n" + + "You can click the Cancel button to stop detection at any time.\n\n" + + "Continue?", + "Port Detection", MessageBoxButton.YesNo, MessageBoxImage.Warning); + + if (confirm != MessageBoxResult.Yes) + return; + + _detectCts = new CancellationTokenSource(); + var ct = _detectCts.Token; + + btnDetect.Content = "Cancel"; try { - // Common VCP 60 values: 15=DP1, 16=DP2, 17=HDMI1, 18=HDMI2, 3=DVI1, 4=DVI2, 1=VGA1, 2=VGA2 - var commonPorts = new[] { 15, 16, 17, 18, 3, 4, 1, 2 }; + // Only probe modern ports (DP/HDMI) by default — legacy ports (VGA/DVI) are rarely used + // and probing them on monitors without those connectors wastes time and stresses the scaler + var commonPorts = new[] { 15, 16, 17, 18 }; var detectedPorts = new List(); // Get current input so we can restore it @@ -219,6 +243,8 @@ public partial class ConfigWindow : Window foreach (var vcpValue in commonPorts) { + ct.ThrowIfCancellationRequested(); + // Skip ports we already know about if (_availablePorts.Any(p => p.Value == vcpValue)) continue; @@ -227,7 +253,10 @@ public partial class ConfigWindow : Window { // Try to set the input - if it succeeds, the port exists await CMMCommand.SetInputSource(_serialNumber, vcpValue); - await Task.Delay(500); // Give monitor time to respond + + // Give the monitor's scaler time to attempt signal lock on the new input. + // 3 seconds is the minimum safe settle time for most scalers. + await Task.Delay(3000, ct); // Check if the input actually changed var newInput = await CMMCommand.GetInputSource(_serialNumber); @@ -236,21 +265,26 @@ public partial class ConfigWindow : Window detectedPorts.Add(vcpValue); } } + catch (OperationCanceledException) { throw; } catch { - // Port doesn't exist or isn't supported + // Port doesn't exist or isn't supported — continue to next } + + // Extra settle time between probes to let the scaler fully recover + try { await Task.Delay(1000, ct); } + catch (OperationCanceledException) { throw; } } - // Restore original input if we have one + // Restore original input — give it extra time to settle after all the switching if (currentInput.HasValue) { await CMMCommand.SetInputSource(_serialNumber, currentInput.Value); + await Task.Delay(2000); } if (detectedPorts.Count > 0) { - // Add detected ports to the available list and config foreach (var vcpValue in detectedPorts) { var name = CMMCommand.GetInputSourceName(vcpValue); @@ -258,7 +292,6 @@ public partial class ConfigWindow : Window MonitorConfigManager.AddDiscoveredPort(_serialNumber, _monitorName, vcpValue, name); } - // Reload to show new ports MonitorConfigManager.ClearCache(); LoadPortConfiguration(); @@ -272,6 +305,21 @@ public partial class ConfigWindow : Window MessageBoxButton.OK, MessageBoxImage.Information); } } + catch (OperationCanceledException) + { + // User cancelled — try to restore original input before exiting + try + { + var currentInput = await CMMCommand.GetInputSource(_serialNumber); + // Only restore if we can still talk to the monitor + if (currentInput.HasValue) + await CMMCommand.SetInputSource(_serialNumber, currentInput.Value); + } + catch { } + + MessageBox.Show("Port detection was cancelled.", "Detection Cancelled", + MessageBoxButton.OK, MessageBoxImage.Information); + } catch (Exception ex) { MessageBox.Show($"Detection failed: {ex.Message}", "Error", @@ -279,8 +327,9 @@ public partial class ConfigWindow : Window } finally { + _detectCts?.Dispose(); + _detectCts = null; btnDetect.Content = "Detect"; - btnDetect.IsEnabled = true; } } diff --git a/DellMonitorControl/ControlPanel.xaml b/MonitorControl/ControlPanel.xaml similarity index 92% rename from DellMonitorControl/ControlPanel.xaml rename to MonitorControl/ControlPanel.xaml index e9c4317..bd89825 100644 --- a/DellMonitorControl/ControlPanel.xaml +++ b/MonitorControl/ControlPanel.xaml @@ -1,4 +1,4 @@ - /// Interaction logic for ControlPanel.xaml diff --git a/DellMonitorControl/DebugLogger.cs b/MonitorControl/DebugLogger.cs similarity index 98% rename from DellMonitorControl/DebugLogger.cs rename to MonitorControl/DebugLogger.cs index cf2f070..d464dee 100644 --- a/DellMonitorControl/DebugLogger.cs +++ b/MonitorControl/DebugLogger.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; using System.IO; using System.Text; -namespace DellMonitorControl; +namespace MonitorControl; public static class DebugLogger { diff --git a/DellMonitorControl/Images/icons.xaml b/MonitorControl/Images/icons.xaml similarity index 100% rename from DellMonitorControl/Images/icons.xaml rename to MonitorControl/Images/icons.xaml diff --git a/DellMonitorControl/MainWindow.xaml b/MonitorControl/MainWindow.xaml similarity index 99% rename from DellMonitorControl/MainWindow.xaml rename to MonitorControl/MainWindow.xaml index 9bdd6ac..a226f30 100644 --- a/DellMonitorControl/MainWindow.xaml +++ b/MonitorControl/MainWindow.xaml @@ -1,4 +1,4 @@ - + { + timer.Stop(); + _displayChangeReloadPending = false; + + if (IsVisible) + { + DebugLogger.Log("Reloading monitors after display change"); + await LoadMonitors(); + } + }; + timer.Start(); + } + + return IntPtr.Zero; + } + public void ShowNearTray() { DebugLogger.Log("ShowNearTray called"); @@ -119,7 +158,7 @@ public partial class MainWindow : Window { Title = "About Monitor Control", Width = 300, - Height = 180, + Height = 240, WindowStartupLocation = WindowStartupLocation.CenterScreen, ResizeMode = ResizeMode.NoResize, WindowStyle = WindowStyle.None, @@ -138,6 +177,16 @@ public partial class MainWindow : Window var stack = new StackPanel { VerticalAlignment = VerticalAlignment.Center }; + var logo = new System.Windows.Controls.Image + { + Source = new System.Windows.Media.Imaging.BitmapImage(new Uri("pack://application:,,,/MonitorIcon.png")), + Width = 48, + Height = 48, + HorizontalAlignment = HorizontalAlignment.Center, + Margin = new Thickness(0, 0, 0, 10) + }; + stack.Children.Add(logo); + stack.Children.Add(new TextBlock { Text = "Monitor Control", diff --git a/DellMonitorControl/DellMonitorControl.csproj b/MonitorControl/MonitorControl.csproj similarity index 90% rename from DellMonitorControl/DellMonitorControl.csproj rename to MonitorControl/MonitorControl.csproj index 76c7305..9b7dbcc 100644 --- a/DellMonitorControl/DellMonitorControl.csproj +++ b/MonitorControl/MonitorControl.csproj @@ -4,8 +4,8 @@ WinExe net9.0-windows enable - DellMonitorControl - DellMonitorControl + MonitorControl + MonitorControl ControlMyMonitorManagement true MarketAlly @@ -22,10 +22,12 @@ + + diff --git a/DellMonitorControl/MonitorControl.iss b/MonitorControl/MonitorControl.iss similarity index 97% rename from DellMonitorControl/MonitorControl.iss rename to MonitorControl/MonitorControl.iss index bb28c58..d5b779a 100644 --- a/DellMonitorControl/MonitorControl.iss +++ b/MonitorControl/MonitorControl.iss @@ -1,7 +1,7 @@ #define MyAppName "Monitor Control" #define MyAppVersion "1.1.5" #define MyAppPublisher "MarketAlly" -#define MyAppExeName "DellMonitorControl.exe" +#define MyAppExeName "MonitorControl.exe" #define MyAppIcon "MonitorIcon.ico" #define SourcePath "bin\Release\net9.0-windows" diff --git a/MonitorControl/MonitorIcon.ico b/MonitorControl/MonitorIcon.ico new file mode 100644 index 0000000000000000000000000000000000000000..078b0f0da99967625ff53f66c3cae25a73e91feb GIT binary patch literal 105356 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 T=?VW>;#X_;^A!mKQ3UuO%o#W2 literal 0 HcmV?d00001 diff --git a/MonitorControl/MonitorIcon.png b/MonitorControl/MonitorIcon.png new file mode 100644 index 0000000000000000000000000000000000000000..07ee8c3fcc25cda07c437b21a04f956e0d848d03 GIT binary patch literal 4423 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