2
0

refactor(ci): rename DellMonitorControl to MonitorControl
All checks were successful
Build / build (push) Successful in 9h0m26s
Build and Release / build (push) Successful in 8h0m15s

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).
This commit is contained in:
2026-01-29 18:14:58 -05:00
parent 3c6cc15281
commit 404e6f42da
24 changed files with 262 additions and 53 deletions

View File

@@ -24,7 +24,7 @@ jobs:
Write-Host "Setting version to: $version" Write-Host "Setting version to: $version"
# Update csproj (preserve UTF-8 BOM encoding) # Update csproj (preserve UTF-8 BOM encoding)
$csprojPath = "DellMonitorControl/DellMonitorControl.csproj" $csprojPath = "MonitorControl/MonitorControl.csproj"
$content = [System.IO.File]::ReadAllText($csprojPath) $content = [System.IO.File]::ReadAllText($csprojPath)
$content = $content -replace '<Version>.*</Version>', "<Version>$version</Version>" $content = $content -replace '<Version>.*</Version>', "<Version>$version</Version>"
$content = $content -replace '<AssemblyVersion>.*</AssemblyVersion>', "<AssemblyVersion>$version.0</AssemblyVersion>" $content = $content -replace '<AssemblyVersion>.*</AssemblyVersion>', "<AssemblyVersion>$version.0</AssemblyVersion>"
@@ -32,7 +32,7 @@ jobs:
[System.IO.File]::WriteAllText($csprojPath, $content, [System.Text.UTF8Encoding]::new($true)) [System.IO.File]::WriteAllText($csprojPath, $content, [System.Text.UTF8Encoding]::new($true))
# Update ISS # Update ISS
$issPath = "DellMonitorControl/MonitorControl.iss" $issPath = "MonitorControl/MonitorControl.iss"
$issContent = [System.IO.File]::ReadAllText($issPath) $issContent = [System.IO.File]::ReadAllText($issPath)
$issContent = $issContent -replace '#define MyAppVersion ".*"', "#define MyAppVersion `"$version`"" $issContent = $issContent -replace '#define MyAppVersion ".*"', "#define MyAppVersion `"$version`""
[System.IO.File]::WriteAllText($issPath, $issContent, [System.Text.UTF8Encoding]::new($true)) [System.IO.File]::WriteAllText($issPath, $issContent, [System.Text.UTF8Encoding]::new($true))
@@ -48,21 +48,21 @@ jobs:
run: | run: |
echo "Current directory:" echo "Current directory:"
cd cd
echo "DellMonitorControl contents:" echo "MonitorControl contents:"
dir DellMonitorControl dir MonitorControl
echo "DellMonitorControl\bin contents:" echo "MonitorControl\bin contents:"
dir DellMonitorControl\bin dir MonitorControl\bin
echo "DellMonitorControl\bin\Release contents:" echo "MonitorControl\bin\Release contents:"
dir DellMonitorControl\bin\Release dir MonitorControl\bin\Release
echo "DellMonitorControl\bin\Release\net9.0-windows contents:" echo "MonitorControl\bin\Release\net9.0-windows contents:"
dir DellMonitorControl\bin\Release\net9.0-windows dir MonitorControl\bin\Release\net9.0-windows
- name: Build Installer - name: Build Installer
shell: cmd shell: cmd
run: | run: |
echo Copying to short path to avoid path length issues... echo Copying to short path to avoid path length issues...
mkdir C:\build 2>nul mkdir C:\build 2>nul
xcopy /E /I /Y DellMonitorControl C:\build\app xcopy /E /I /Y MonitorControl C:\build\app
echo. echo.
echo Working from C:\build\app echo Working from C:\build\app
cd /d C:\build\app cd /d C:\build\app
@@ -86,7 +86,7 @@ jobs:
shell: powershell shell: powershell
run: | run: |
$version = "${{ gitea.ref_name }}".TrimStart("v") $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" $zipName = "MonitorControl-Portable-$version.zip"
Write-Host "Creating portable zip: $zipName" Write-Host "Creating portable zip: $zipName"
@@ -115,7 +115,7 @@ jobs:
$body = @{ $body = @{
tag_name = $tag tag_name = $tag
name = "Monitor Control $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 } | ConvertTo-Json
$release = Invoke-RestMethod -Uri "$baseUrl/repos/$repo/releases" -Method Post -Headers @{"Authorization"="token $env:GITEA_TOKEN"; "Content-Type"="application/json"} -Body $body $release = Invoke-RestMethod -Uri "$baseUrl/repos/$repo/releases" -Method Post -Headers @{"Authorization"="token $env:GITEA_TOKEN"; "Content-Type"="application/json"} -Body $body

View File

@@ -11,7 +11,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Language", "Language\Langua
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tester", "Tester\Tester.csproj", "{0D34DD73-3282-40EB-8F59-DF190944BF12}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tester", "Tester\Tester.csproj", "{0D34DD73-3282-40EB-8F59-DF190944BF12}"
EndProject 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 EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CMMService", "CMMService\CMMService.csproj", "{FEA2B019-32BC-4704-939F-1CD26F373F55}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CMMService", "CMMService\CMMService.csproj", "{FEA2B019-32BC-4704-939F-1CD26F373F55}"
EndProject EndProject

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -1,6 +1,7 @@
using CMM.Library.Base; using CMM.Library.Base;
using CMM.Library.Helpers; using CMM.Library.Helpers;
using CMM.Library.ViewModel; using CMM.Library.ViewModel;
using System.Collections.Concurrent;
using System.IO; using System.IO;
namespace CMM.Library.Method; namespace CMM.Library.Method;
@@ -14,6 +15,78 @@ public static class CMMCommand
static readonly string CMMexe = Path.Combine(AppContext.BaseDirectory, "ControlMyMonitor.exe"); static readonly string CMMexe = Path.Combine(AppContext.BaseDirectory, "ControlMyMonitor.exe");
static readonly string CMMsMonitors = Path.Combine(CMMTmpFolder, "smonitors.tmp"); 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<string, DateTime> _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<string, DateTime> _timeoutCooldowns = new();
private const int TimeoutCooldownMs = 3000;
/// <summary>
/// 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.
/// </summary>
public static void NotifySystemResumed()
{
_resumeTime = DateTime.UtcNow;
}
/// <summary>
/// Waits for the post-resume grace period to expire, respects timeout cooldowns,
/// and enforces per-monitor command spacing.
/// </summary>
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();
}
}
/// <summary>
/// Records that a command to this monitor timed out, activating a cooldown period.
/// </summary>
private static void RecordTimeout(string monitorSN)
{
_timeoutCooldowns[monitorSN] = DateTime.UtcNow.AddMilliseconds(TimeoutCooldownMs);
}
public static async Task ScanMonitor() public static async Task ScanMonitor()
{ {
// Ensure temp folder exists for output files // Ensure temp folder exists for output files
@@ -23,26 +96,36 @@ public static class CMMCommand
public static async Task PowerOn(string monitorSN) 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) 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<string> GetMonitorValue(string monitorSN, string vcpCode = "D6", int maxRetries = 2) private static async Task<string> GetMonitorValue(string monitorSN, string vcpCode = "D6", int maxRetries = 2)
{ {
for (int attempt = 0; attempt <= maxRetries; attempt++) for (int attempt = 0; attempt <= maxRetries; attempt++)
{ {
await ThrottleDdcCommand(monitorSN);
// Execute directly without batch file wrapper // Execute directly without batch file wrapper
var (output, exitCode) = await ConsoleHelper.ExecuteExeAsync( var (output, exitCode) = await ConsoleHelper.ExecuteExeAsync(
CMMexe, CMMexe,
$"/GetValue {monitorSN} {vcpCode}"); $"/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) if (exitCode == -1)
{
RecordTimeout(monitorSN);
return string.Empty; return string.Empty;
}
// ControlMyMonitor returns the value as the exit code // ControlMyMonitor returns the value as the exit code
// Exit code > 0 means success with that value // 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) 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<int?> GetBrightness(string monitorSN) public static async Task<int?> GetBrightness(string monitorSN)
@@ -81,7 +166,9 @@ public static class CMMCommand
public static async Task SetContrast(string monitorSN, int value) 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<int?> GetContrast(string monitorSN) public static async Task<int?> GetContrast(string monitorSN)
@@ -96,7 +183,9 @@ public static class CMMCommand
public static async Task SetInputSource(string monitorSN, int value) 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<int?> GetInputSource(string monitorSN) public static async Task<int?> GetInputSource(string monitorSN)
@@ -110,7 +199,9 @@ public static class CMMCommand
var options = new List<InputSourceOption>(); var options = new List<InputSourceOption>();
var savePath = Path.Combine(CMMTmpFolder, $"{monitorSN}_vcp.tmp"); 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; if (!File.Exists(savePath)) return options;
@@ -177,17 +268,19 @@ public static class CMMCommand
public static async Task ScanMonitorStatus(IEnumerable<XMonitor> monitors) public static async Task ScanMonitorStatus(IEnumerable<XMonitor> 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 ScanMonitorStatus($"{CMMTmpFolder}\\{x.SerialNumber}.tmp", x);
}); }
await Task.WhenAll(taskList);
} }
static async Task ScanMonitorStatus(string savePath, XMonitor mon) 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<IEnumerable<SMonitorModel>>(savePath); var monitorModel = JsonHelper.JsonFormFile<IEnumerable<SMonitorModel>>(savePath);
var status = monitorModel.ReadMonitorStatus(); var status = monitorModel.ReadMonitorStatus();

View File

@@ -1,8 +1,8 @@
<Application x:Class="DellMonitorControl.App" <Application x:Class="MonitorControl.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:tb="clr-namespace:Hardcodet.Wpf.TaskbarNotification;assembly=Hardcodet.Wpf.TaskbarNotification.Net6" xmlns:tb="clr-namespace:Hardcodet.Wpf.TaskbarNotification;assembly=Hardcodet.Wpf.TaskbarNotification.Net6"
xmlns:local="clr-namespace:DellMonitorControl" xmlns:local="clr-namespace:MonitorControl"
Startup="Application_Startup"> Startup="Application_Startup">
<Application.Resources> <Application.Resources>
<tb:TaskbarIcon x:Key="TrayIcon" <tb:TaskbarIcon x:Key="TrayIcon"

View File

@@ -1,7 +1,9 @@
using CMM.Library.Method;
using Hardcodet.Wpf.TaskbarNotification; using Hardcodet.Wpf.TaskbarNotification;
using Microsoft.Win32;
using System.Windows; using System.Windows;
namespace DellMonitorControl namespace MonitorControl
{ {
public partial class App : Application public partial class App : Application
{ {
@@ -17,10 +19,23 @@ namespace DellMonitorControl
_mainWindow = new MainWindow(); _mainWindow = new MainWindow();
// Listen for system sleep/resume to protect monitors during wake
SystemEvents.PowerModeChanged += OnPowerModeChanged;
// Check for updates in background // Check for updates in background
await CheckForUpdatesAsync(); await CheckForUpdatesAsync();
} }
private void OnPowerModeChanged(object sender, PowerModeChangedEventArgs e)
{
if (e.Mode == PowerModes.Resume)
{
// Notify the DDC/CI layer that monitors need time to wake up
CMMCommand.NotifySystemResumed();
DebugLogger.Log("System resumed from sleep - DDC/CI grace period activated");
}
}
private async System.Threading.Tasks.Task CheckForUpdatesAsync() private async System.Threading.Tasks.Task CheckForUpdatesAsync()
{ {
try try
@@ -62,6 +77,7 @@ namespace DellMonitorControl
protected override void OnExit(ExitEventArgs e) protected override void OnExit(ExitEventArgs e)
{ {
SystemEvents.PowerModeChanged -= OnPowerModeChanged;
_trayIcon?.Dispose(); _trayIcon?.Dispose();
base.OnExit(e); base.OnExit(e);
} }

View File

@@ -1,4 +1,4 @@
<Window x:Class="DellMonitorControl.ConfigWindow" <Window x:Class="MonitorControl.ConfigWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Port Configuration" Title="Port Configuration"

View File

@@ -4,12 +4,13 @@ using CMM.Library.ViewModel;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Windows; using System.Windows;
using System.Windows.Controls; using System.Windows.Controls;
using System.Windows.Media; using System.Windows.Media;
namespace DellMonitorControl; namespace MonitorControl;
public partial class ConfigWindow : Window public partial class ConfigWindow : Window
{ {
@@ -17,6 +18,7 @@ public partial class ConfigWindow : Window
private readonly string _monitorName; private readonly string _monitorName;
private readonly List<InputSourceOption> _availablePorts; private readonly List<InputSourceOption> _availablePorts;
private readonly List<PortConfigRow> _portRows = new(); private readonly List<PortConfigRow> _portRows = new();
private CancellationTokenSource? _detectCts;
public bool ConfigChanged { get; private set; } public bool ConfigChanged { get; private set; }
@@ -205,13 +207,35 @@ public partial class ConfigWindow : Window
private async void DetectButton_Click(object sender, RoutedEventArgs e) private async void DetectButton_Click(object sender, RoutedEventArgs e)
{ {
btnDetect.IsEnabled = false; // If detection is already running, cancel it
btnDetect.Content = "..."; 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 try
{ {
// Common VCP 60 values: 15=DP1, 16=DP2, 17=HDMI1, 18=HDMI2, 3=DVI1, 4=DVI2, 1=VGA1, 2=VGA2 // Only probe modern ports (DP/HDMI) by default — legacy ports (VGA/DVI) are rarely used
var commonPorts = new[] { 15, 16, 17, 18, 3, 4, 1, 2 }; // 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<int>(); var detectedPorts = new List<int>();
// Get current input so we can restore it // Get current input so we can restore it
@@ -219,6 +243,8 @@ public partial class ConfigWindow : Window
foreach (var vcpValue in commonPorts) foreach (var vcpValue in commonPorts)
{ {
ct.ThrowIfCancellationRequested();
// Skip ports we already know about // Skip ports we already know about
if (_availablePorts.Any(p => p.Value == vcpValue)) if (_availablePorts.Any(p => p.Value == vcpValue))
continue; continue;
@@ -227,7 +253,10 @@ public partial class ConfigWindow : Window
{ {
// Try to set the input - if it succeeds, the port exists // Try to set the input - if it succeeds, the port exists
await CMMCommand.SetInputSource(_serialNumber, vcpValue); 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 // Check if the input actually changed
var newInput = await CMMCommand.GetInputSource(_serialNumber); var newInput = await CMMCommand.GetInputSource(_serialNumber);
@@ -236,21 +265,26 @@ public partial class ConfigWindow : Window
detectedPorts.Add(vcpValue); detectedPorts.Add(vcpValue);
} }
} }
catch (OperationCanceledException) { throw; }
catch 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) if (currentInput.HasValue)
{ {
await CMMCommand.SetInputSource(_serialNumber, currentInput.Value); await CMMCommand.SetInputSource(_serialNumber, currentInput.Value);
await Task.Delay(2000);
} }
if (detectedPorts.Count > 0) if (detectedPorts.Count > 0)
{ {
// Add detected ports to the available list and config
foreach (var vcpValue in detectedPorts) foreach (var vcpValue in detectedPorts)
{ {
var name = CMMCommand.GetInputSourceName(vcpValue); var name = CMMCommand.GetInputSourceName(vcpValue);
@@ -258,7 +292,6 @@ public partial class ConfigWindow : Window
MonitorConfigManager.AddDiscoveredPort(_serialNumber, _monitorName, vcpValue, name); MonitorConfigManager.AddDiscoveredPort(_serialNumber, _monitorName, vcpValue, name);
} }
// Reload to show new ports
MonitorConfigManager.ClearCache(); MonitorConfigManager.ClearCache();
LoadPortConfiguration(); LoadPortConfiguration();
@@ -272,6 +305,21 @@ public partial class ConfigWindow : Window
MessageBoxButton.OK, MessageBoxImage.Information); 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) catch (Exception ex)
{ {
MessageBox.Show($"Detection failed: {ex.Message}", "Error", MessageBox.Show($"Detection failed: {ex.Message}", "Error",
@@ -279,8 +327,9 @@ public partial class ConfigWindow : Window
} }
finally finally
{ {
_detectCts?.Dispose();
_detectCts = null;
btnDetect.Content = "Detect"; btnDetect.Content = "Detect";
btnDetect.IsEnabled = true;
} }
} }

View File

@@ -1,4 +1,4 @@
<UserControl x:Class="DellMonitorControl.ControlPanel" <UserControl x:Class="MonitorControl.ControlPanel"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
mc:Ignorable="d" mc:Ignorable="d"

View File

@@ -9,7 +9,7 @@ using System.Windows;
using System.Windows.Controls; using System.Windows.Controls;
using System.Windows.Input; using System.Windows.Input;
namespace DellMonitorControl; namespace MonitorControl;
/// <summary> /// <summary>
/// Interaction logic for ControlPanel.xaml /// Interaction logic for ControlPanel.xaml

View File

@@ -3,7 +3,7 @@ using System.Collections.Generic;
using System.IO; using System.IO;
using System.Text; using System.Text;
namespace DellMonitorControl; namespace MonitorControl;
public static class DebugLogger public static class DebugLogger
{ {

View File

@@ -1,4 +1,4 @@
<Window x:Class="DellMonitorControl.MainWindow" <Window x:Class="MonitorControl.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Monitor Control" Title="Monitor Control"

View File

@@ -7,11 +7,12 @@ using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Windows; using System.Windows;
using System.Windows.Controls; using System.Windows.Controls;
using System.Windows.Interop;
using System.Windows.Media; using System.Windows.Media;
using System.Windows.Media.Animation; using System.Windows.Media.Animation;
using System.Windows.Threading; using System.Windows.Threading;
namespace DellMonitorControl; namespace MonitorControl;
public partial class MainWindow : Window public partial class MainWindow : Window
{ {
@@ -21,13 +22,51 @@ public partial class MainWindow : Window
private UpdateInfo? _pendingUpdate; private UpdateInfo? _pendingUpdate;
private DateTime _lastUpdateCheck = DateTime.MinValue; private DateTime _lastUpdateCheck = DateTime.MinValue;
// WM_DISPLAYCHANGE: sent when display resolution changes or monitors are connected/disconnected
private const int WM_DISPLAYCHANGE = 0x007E;
private bool _displayChangeReloadPending;
public MainWindow() public MainWindow()
{ {
InitializeComponent(); InitializeComponent();
_spinnerStoryboard = (Storyboard)FindResource("SpinnerAnimation"); _spinnerStoryboard = (Storyboard)FindResource("SpinnerAnimation");
SourceInitialized += MainWindow_SourceInitialized;
DebugLogger.Log("MainWindow initialized"); DebugLogger.Log("MainWindow initialized");
} }
private void MainWindow_SourceInitialized(object? sender, EventArgs e)
{
var source = HwndSource.FromHwnd(new WindowInteropHelper(this).Handle);
source?.AddHook(WndProc);
}
private IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
{
if (msg == WM_DISPLAYCHANGE && !_displayChangeReloadPending)
{
_displayChangeReloadPending = true;
DebugLogger.Log("WM_DISPLAYCHANGE received - scheduling monitor refresh");
// Debounce: display changes often fire multiple messages in quick succession.
// Wait 2 seconds for things to settle, then reload if window is visible.
var timer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(2) };
timer.Tick += async (s, args) =>
{
timer.Stop();
_displayChangeReloadPending = false;
if (IsVisible)
{
DebugLogger.Log("Reloading monitors after display change");
await LoadMonitors();
}
};
timer.Start();
}
return IntPtr.Zero;
}
public void ShowNearTray() public void ShowNearTray()
{ {
DebugLogger.Log("ShowNearTray called"); DebugLogger.Log("ShowNearTray called");
@@ -119,7 +158,7 @@ public partial class MainWindow : Window
{ {
Title = "About Monitor Control", Title = "About Monitor Control",
Width = 300, Width = 300,
Height = 180, Height = 240,
WindowStartupLocation = WindowStartupLocation.CenterScreen, WindowStartupLocation = WindowStartupLocation.CenterScreen,
ResizeMode = ResizeMode.NoResize, ResizeMode = ResizeMode.NoResize,
WindowStyle = WindowStyle.None, WindowStyle = WindowStyle.None,
@@ -138,6 +177,16 @@ public partial class MainWindow : Window
var stack = new StackPanel { VerticalAlignment = VerticalAlignment.Center }; 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 stack.Children.Add(new TextBlock
{ {
Text = "Monitor Control", Text = "Monitor Control",

View File

@@ -4,8 +4,8 @@
<OutputType>WinExe</OutputType> <OutputType>WinExe</OutputType>
<TargetFramework>net9.0-windows</TargetFramework> <TargetFramework>net9.0-windows</TargetFramework>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<AssemblyName>DellMonitorControl</AssemblyName> <AssemblyName>MonitorControl</AssemblyName>
<RootNamespace>DellMonitorControl</RootNamespace> <RootNamespace>MonitorControl</RootNamespace>
<Product>ControlMyMonitorManagement</Product> <Product>ControlMyMonitorManagement</Product>
<UseWPF>true</UseWPF> <UseWPF>true</UseWPF>
<Company>MarketAlly</Company> <Company>MarketAlly</Company>
@@ -22,10 +22,12 @@
<ItemGroup> <ItemGroup>
<None Remove="MonitorIcon.ico" /> <None Remove="MonitorIcon.ico" />
<None Remove="MonitorIcon.png" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Resource Include="MonitorIcon.ico" /> <Resource Include="MonitorIcon.ico" />
<Resource Include="MonitorIcon.png" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@@ -1,7 +1,7 @@
#define MyAppName "Monitor Control" #define MyAppName "Monitor Control"
#define MyAppVersion "1.1.5" #define MyAppVersion "1.1.5"
#define MyAppPublisher "MarketAlly" #define MyAppPublisher "MarketAlly"
#define MyAppExeName "DellMonitorControl.exe" #define MyAppExeName "MonitorControl.exe"
#define MyAppIcon "MonitorIcon.ico" #define MyAppIcon "MonitorIcon.ico"
#define SourcePath "bin\Release\net9.0-windows" #define SourcePath "bin\Release\net9.0-windows"

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

View File

@@ -5,7 +5,7 @@ using System.Reflection;
using System.Text.Json; using System.Text.Json;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace DellMonitorControl; namespace MonitorControl;
public class UpdateChecker public class UpdateChecker
{ {