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

@@ -0,0 +1,344 @@
using CMM.Library.Config;
using CMM.Library.Method;
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 MonitorControl;
public partial class ConfigWindow : Window
{
private readonly string _serialNumber;
private readonly string _monitorName;
private readonly List<InputSourceOption> _availablePorts;
private readonly List<PortConfigRow> _portRows = new();
private CancellationTokenSource? _detectCts;
public bool ConfigChanged { get; private set; }
public ConfigWindow(string serialNumber, string monitorName, List<InputSourceOption> availablePorts)
{
InitializeComponent();
_serialNumber = serialNumber;
_monitorName = monitorName;
_availablePorts = availablePorts;
tbHeader.Text = $"Configure: {monitorName}";
LoadPortConfiguration();
}
private void LoadPortConfiguration()
{
var config = MonitorConfigManager.GetMonitorConfig(_serialNumber);
spPorts.Children.Clear();
_portRows.Clear();
// Show unsupported message if no ports available
if (_availablePorts == null || _availablePorts.Count == 0)
{
spPorts.Children.Add(new TextBlock
{
Text = "Unsupported",
Foreground = Brushes.Gray,
FontSize = 16,
FontStyle = FontStyles.Italic,
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center,
Margin = new Thickness(0, 40, 0, 40)
});
return;
}
foreach (var port in _availablePorts)
{
var existingPortConfig = config.Ports.FirstOrDefault(p => p.VcpValue == port.Value);
var row = new PortConfigRow
{
VcpValue = port.Value,
DefaultName = port.Name,
CustomLabel = existingPortConfig?.CustomLabel ?? "",
IsHidden = existingPortConfig?.IsHidden ?? false,
ShowInQuickSwitch = existingPortConfig?.ShowInQuickSwitch ?? false
};
_portRows.Add(row);
spPorts.Children.Add(CreatePortRow(row));
}
}
private UIElement CreatePortRow(PortConfigRow row)
{
var container = new Border
{
Background = new SolidColorBrush(Color.FromRgb(60, 60, 60)),
CornerRadius = new CornerRadius(4),
Padding = new Thickness(10),
Margin = new Thickness(0, 0, 0, 8)
};
var grid = new Grid();
grid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
grid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
grid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
// Row 1: Port name and Hide checkbox
var row1 = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(0, 0, 0, 6) };
row1.Children.Add(new TextBlock
{
Text = row.DefaultName,
Foreground = Brushes.White,
FontWeight = FontWeights.SemiBold,
FontSize = 13,
Width = 120,
VerticalAlignment = VerticalAlignment.Center
});
var hideCheck = new CheckBox
{
Content = "Hide",
IsChecked = row.IsHidden,
Foreground = Brushes.LightGray,
VerticalAlignment = VerticalAlignment.Center,
Margin = new Thickness(10, 0, 0, 0)
};
hideCheck.Checked += (s, e) => row.IsHidden = true;
hideCheck.Unchecked += (s, e) => row.IsHidden = false;
row1.Children.Add(hideCheck);
Grid.SetRow(row1, 0);
grid.Children.Add(row1);
// Row 2: Custom label
var row2 = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(0, 0, 0, 6) };
row2.Children.Add(new TextBlock
{
Text = "Label:",
Foreground = Brushes.LightGray,
FontSize = 12,
Width = 50,
VerticalAlignment = VerticalAlignment.Center
});
var labelBox = new TextBox
{
Text = row.CustomLabel,
Width = 200,
Background = new SolidColorBrush(Color.FromRgb(50, 50, 50)),
Foreground = Brushes.White,
BorderBrush = new SolidColorBrush(Color.FromRgb(80, 80, 80)),
Padding = new Thickness(6, 4, 6, 4)
};
labelBox.TextChanged += (s, e) => row.CustomLabel = labelBox.Text;
row2.Children.Add(labelBox);
Grid.SetRow(row2, 1);
grid.Children.Add(row2);
// Row 3: Quick switch checkbox
var quickSwitchCheck = new CheckBox
{
Content = "Show in quick-switch toolbar",
IsChecked = row.ShowInQuickSwitch,
Foreground = Brushes.LightGray,
FontSize = 12
};
quickSwitchCheck.Checked += (s, e) => row.ShowInQuickSwitch = true;
quickSwitchCheck.Unchecked += (s, e) => row.ShowInQuickSwitch = false;
Grid.SetRow(quickSwitchCheck, 2);
grid.Children.Add(quickSwitchCheck);
container.Child = grid;
return container;
}
private void SaveButton_Click(object sender, RoutedEventArgs e)
{
var config = new MonitorConfig
{
SerialNumber = _serialNumber,
MonitorName = _monitorName,
Ports = _portRows.Select(r => new PortConfig
{
VcpValue = r.VcpValue,
DefaultName = r.DefaultName,
CustomLabel = r.CustomLabel,
IsHidden = r.IsHidden,
ShowInQuickSwitch = r.ShowInQuickSwitch
}).ToList()
};
MonitorConfigManager.SaveMonitorConfig(config);
ConfigChanged = true;
Close();
}
private void CancelButton_Click(object sender, RoutedEventArgs e)
{
Close();
}
private void ResetButton_Click(object sender, RoutedEventArgs e)
{
// Reset all port rows to default values
foreach (var row in _portRows)
{
row.CustomLabel = "";
row.IsHidden = false;
row.ShowInQuickSwitch = false;
}
// Reload the UI to reflect changes
LoadPortConfiguration();
// Clear any discovered ports from config
var config = MonitorConfigManager.GetMonitorConfig(_serialNumber);
config.Ports.Clear();
MonitorConfigManager.SaveMonitorConfig(config);
MonitorConfigManager.ClearCache();
}
private async void DetectButton_Click(object sender, RoutedEventArgs e)
{
// 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
{
// 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
var currentInput = await CMMCommand.GetInputSource(_serialNumber);
foreach (var vcpValue in commonPorts)
{
ct.ThrowIfCancellationRequested();
// Skip ports we already know about
if (_availablePorts.Any(p => p.Value == vcpValue))
continue;
try
{
// Try to set the input - if it succeeds, the port exists
await CMMCommand.SetInputSource(_serialNumber, vcpValue);
// 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);
if (newInput == vcpValue)
{
detectedPorts.Add(vcpValue);
}
}
catch (OperationCanceledException) { throw; }
catch
{
// 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 — 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)
{
foreach (var vcpValue in detectedPorts)
{
var name = CMMCommand.GetInputSourceName(vcpValue);
_availablePorts.Add(new InputSourceOption(vcpValue, name));
MonitorConfigManager.AddDiscoveredPort(_serialNumber, _monitorName, vcpValue, name);
}
MonitorConfigManager.ClearCache();
LoadPortConfiguration();
MessageBox.Show($"Detected {detectedPorts.Count} new port(s):\n" +
string.Join("\n", detectedPorts.Select(v => $" • {CMMCommand.GetInputSourceName(v)}")),
"Detection Complete", MessageBoxButton.OK, MessageBoxImage.Information);
}
else
{
MessageBox.Show("No additional ports were detected.", "Detection Complete",
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",
MessageBoxButton.OK, MessageBoxImage.Error);
}
finally
{
_detectCts?.Dispose();
_detectCts = null;
btnDetect.Content = "Detect";
}
}
private class PortConfigRow
{
public int VcpValue { get; set; }
public string DefaultName { get; set; } = "";
public string CustomLabel { get; set; } = "";
public bool IsHidden { get; set; }
public bool ShowInQuickSwitch { get; set; }
}
}