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:
344
MonitorControl/ConfigWindow.xaml.cs
Normal file
344
MonitorControl/ConfigWindow.xaml.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user