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).
This commit is contained in:
@@ -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>.*</Version>', "<Version>$version</Version>"
|
||||
$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))
|
||||
|
||||
# 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
|
||||
|
||||
@@ -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
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 1.5 KiB |
@@ -1,6 +1,7 @@
|
||||
using CMM.Library.Base;
|
||||
using CMM.Library.Helpers;
|
||||
using CMM.Library.ViewModel;
|
||||
using System.Collections.Concurrent;
|
||||
using System.IO;
|
||||
|
||||
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 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()
|
||||
{
|
||||
// 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<string> 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<int?> 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<int?> 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<int?> GetInputSource(string monitorSN)
|
||||
@@ -110,7 +199,9 @@ public static class CMMCommand
|
||||
var options = new List<InputSourceOption>();
|
||||
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<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 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<IEnumerable<SMonitorModel>>(savePath);
|
||||
|
||||
var status = monitorModel.ReadMonitorStatus();
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<Application x:Class="DellMonitorControl.App"
|
||||
<Application x:Class="MonitorControl.App"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
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">
|
||||
<Application.Resources>
|
||||
<tb:TaskbarIcon x:Key="TrayIcon"
|
||||
@@ -1,7 +1,9 @@
|
||||
using CMM.Library.Method;
|
||||
using Hardcodet.Wpf.TaskbarNotification;
|
||||
using Microsoft.Win32;
|
||||
using System.Windows;
|
||||
|
||||
namespace DellMonitorControl
|
||||
namespace MonitorControl
|
||||
{
|
||||
public partial class App : Application
|
||||
{
|
||||
@@ -17,10 +19,23 @@ namespace DellMonitorControl
|
||||
|
||||
_mainWindow = new MainWindow();
|
||||
|
||||
// Listen for system sleep/resume to protect monitors during wake
|
||||
SystemEvents.PowerModeChanged += OnPowerModeChanged;
|
||||
|
||||
// Check for updates in background
|
||||
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()
|
||||
{
|
||||
try
|
||||
@@ -62,6 +77,7 @@ namespace DellMonitorControl
|
||||
|
||||
protected override void OnExit(ExitEventArgs e)
|
||||
{
|
||||
SystemEvents.PowerModeChanged -= OnPowerModeChanged;
|
||||
_trayIcon?.Dispose();
|
||||
base.OnExit(e);
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
<Window x:Class="DellMonitorControl.ConfigWindow"
|
||||
<Window x:Class="MonitorControl.ConfigWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
Title="Port Configuration"
|
||||
@@ -4,12 +4,13 @@ using CMM.Library.ViewModel;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace DellMonitorControl;
|
||||
namespace MonitorControl;
|
||||
|
||||
public partial class ConfigWindow : Window
|
||||
{
|
||||
@@ -17,6 +18,7 @@ public partial class ConfigWindow : Window
|
||||
private readonly string _monitorName;
|
||||
private readonly List<InputSourceOption> _availablePorts;
|
||||
private readonly List<PortConfigRow> _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<int>();
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// Restore original input if we have one
|
||||
// Extra settle time between probes to let the scaler fully recover
|
||||
try { await Task.Delay(1000, ct); }
|
||||
catch (OperationCanceledException) { throw; }
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<UserControl x:Class="DellMonitorControl.ControlPanel"
|
||||
<UserControl x:Class="MonitorControl.ControlPanel"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
mc:Ignorable="d"
|
||||
@@ -9,7 +9,7 @@ using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace DellMonitorControl;
|
||||
namespace MonitorControl;
|
||||
|
||||
/// <summary>
|
||||
/// Interaction logic for ControlPanel.xaml
|
||||
@@ -3,7 +3,7 @@ using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
|
||||
namespace DellMonitorControl;
|
||||
namespace MonitorControl;
|
||||
|
||||
public static class DebugLogger
|
||||
{
|
||||
@@ -1,4 +1,4 @@
|
||||
<Window x:Class="DellMonitorControl.MainWindow"
|
||||
<Window x:Class="MonitorControl.MainWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
Title="Monitor Control"
|
||||
@@ -7,11 +7,12 @@ using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Interop;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Animation;
|
||||
using System.Windows.Threading;
|
||||
|
||||
namespace DellMonitorControl;
|
||||
namespace MonitorControl;
|
||||
|
||||
public partial class MainWindow : Window
|
||||
{
|
||||
@@ -21,13 +22,51 @@ public partial class MainWindow : Window
|
||||
private UpdateInfo? _pendingUpdate;
|
||||
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()
|
||||
{
|
||||
InitializeComponent();
|
||||
_spinnerStoryboard = (Storyboard)FindResource("SpinnerAnimation");
|
||||
SourceInitialized += MainWindow_SourceInitialized;
|
||||
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()
|
||||
{
|
||||
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",
|
||||
@@ -4,8 +4,8 @@
|
||||
<OutputType>WinExe</OutputType>
|
||||
<TargetFramework>net9.0-windows</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<AssemblyName>DellMonitorControl</AssemblyName>
|
||||
<RootNamespace>DellMonitorControl</RootNamespace>
|
||||
<AssemblyName>MonitorControl</AssemblyName>
|
||||
<RootNamespace>MonitorControl</RootNamespace>
|
||||
<Product>ControlMyMonitorManagement</Product>
|
||||
<UseWPF>true</UseWPF>
|
||||
<Company>MarketAlly</Company>
|
||||
@@ -22,10 +22,12 @@
|
||||
|
||||
<ItemGroup>
|
||||
<None Remove="MonitorIcon.ico" />
|
||||
<None Remove="MonitorIcon.png" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Resource Include="MonitorIcon.ico" />
|
||||
<Resource Include="MonitorIcon.png" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@@ -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"
|
||||
|
||||
BIN
MonitorControl/MonitorIcon.ico
Normal file
BIN
MonitorControl/MonitorIcon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 103 KiB |
BIN
MonitorControl/MonitorIcon.png
Normal file
BIN
MonitorControl/MonitorIcon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.3 KiB |
@@ -5,7 +5,7 @@ using System.Reflection;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace DellMonitorControl;
|
||||
namespace MonitorControl;
|
||||
|
||||
public class UpdateChecker
|
||||
{
|
||||
Reference in New Issue
Block a user