Add port config, quick-switch toolbar, and dark mode styling

- Add Config button to each monitor for hiding ports, custom labels, and quick-switch selection
- Add quick-switch toolbar at top of popup for one-click input switching
- Add dark mode styling for all controls (buttons, combobox, dropdown items)
- Add loading spinner animation
- Add Exit button in header
- Fix dropdown selection to correctly show current input

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
David H. Friedel Jr. 2026-01-04 00:25:01 -05:00
parent f23f26d809
commit 2a3a502567
6 changed files with 756 additions and 28 deletions

View File

@ -0,0 +1,50 @@
<Window x:Class="DellMonitorControl.ConfigWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Port Configuration"
Width="400" Height="450"
WindowStartupLocation="CenterScreen"
WindowStyle="None"
AllowsTransparency="True"
Background="Transparent"
ResizeMode="NoResize">
<Border Background="#F0333333" CornerRadius="8" BorderBrush="#555" BorderThickness="1" Margin="5">
<Border.Effect>
<DropShadowEffect BlurRadius="10" ShadowDepth="2" Opacity="0.5"/>
</Border.Effect>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- Header -->
<Border Grid.Row="0" Background="#444" CornerRadius="8,8,0,0" Padding="12,10">
<Grid>
<TextBlock Name="tbHeader" Text="Configure Ports" Foreground="White" FontSize="14" FontWeight="SemiBold"/>
<Button Content="X" HorizontalAlignment="Right" Width="24" Height="24"
Background="Transparent" Foreground="White" BorderThickness="0"
Click="CloseButton_Click" Cursor="Hand"/>
</Grid>
</Border>
<!-- Port List -->
<ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto" Padding="12">
<StackPanel Name="spPorts"/>
</ScrollViewer>
<!-- Footer Buttons -->
<Border Grid.Row="2" Background="#3A3A3A" CornerRadius="0,0,8,8" Padding="12,10">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
<Button Content="Cancel" Width="80" Margin="0,0,8,0" Click="CancelButton_Click"
Background="#555" Foreground="White" BorderThickness="0" Padding="8,6"/>
<Button Content="Save" Width="80" Click="SaveButton_Click"
Background="#0078D4" Foreground="White" BorderThickness="0" Padding="8,6"/>
</StackPanel>
</Border>
</Grid>
</Border>
</Window>

View File

@ -0,0 +1,180 @@
using CMM.Library.Config;
using CMM.Library.ViewModel;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
namespace DellMonitorControl;
public partial class ConfigWindow : Window
{
private readonly string _serialNumber;
private readonly string _monitorName;
private readonly List<InputSourceOption> _availablePorts;
private readonly List<PortConfigRow> _portRows = new();
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();
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 CloseButton_Click(object sender, RoutedEventArgs e)
{
Close();
}
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; }
}
}

View File

@ -1,4 +1,4 @@
<Window x:Class="DellMonitorControl.MainWindow"
<Window x:Class="DellMonitorControl.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Monitor Control"
@ -12,6 +12,166 @@
Topmost="True"
Deactivated="Window_Deactivated">
<Window.Resources>
<!-- Dark Button Style with proper hover -->
<Style x:Key="DarkButton" TargetType="Button">
<Setter Property="Background" Value="#464646"/>
<Setter Property="Foreground" Value="White"/>
<Setter Property="BorderBrush" Value="#646464"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="Padding" Value="8,4"/>
<Setter Property="Cursor" Value="Hand"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
Padding="{TemplateBinding Padding}"
CornerRadius="3">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="#0056A0"/>
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter Property="Background" Value="#004080"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- Dark ComboBox Style -->
<Style x:Key="DarkComboBox" TargetType="ComboBox">
<Setter Property="Background" Value="#3A3A3A"/>
<Setter Property="Foreground" Value="White"/>
<Setter Property="BorderBrush" Value="#555"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="Padding" Value="6,4"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ComboBox">
<Grid>
<ToggleButton x:Name="ToggleButton"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
IsChecked="{Binding IsDropDownOpen, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}"
ClickMode="Press">
<ToggleButton.Template>
<ControlTemplate TargetType="ToggleButton">
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="3">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition Width="20"/>
</Grid.ColumnDefinitions>
<ContentPresenter Grid.Column="0"/>
<Path Grid.Column="1" Data="M0,0 L4,4 L8,0" Stroke="White" StrokeThickness="1.5"
HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Grid>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="#4A4A4A"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</ToggleButton.Template>
</ToggleButton>
<ContentPresenter x:Name="ContentSite"
Content="{TemplateBinding SelectionBoxItem}"
ContentTemplate="{TemplateBinding SelectionBoxItemTemplate}"
Margin="8,4,28,4"
VerticalAlignment="Center"
HorizontalAlignment="Left"
IsHitTestVisible="False"/>
<Popup x:Name="Popup" Placement="Bottom" IsOpen="{TemplateBinding IsDropDownOpen}"
AllowsTransparency="True" Focusable="False" PopupAnimation="Slide">
<Border x:Name="DropDown" Background="#3A3A3A" BorderBrush="#555" BorderThickness="1"
MinWidth="{TemplateBinding ActualWidth}" MaxHeight="{TemplateBinding MaxDropDownHeight}"
CornerRadius="3" Margin="0,2,0,0">
<ScrollViewer SnapsToDevicePixels="True">
<StackPanel IsItemsHost="True"/>
</ScrollViewer>
</Border>
</Popup>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- Dark ComboBoxItem Style -->
<Style x:Key="DarkComboBoxItem" TargetType="ComboBoxItem">
<Setter Property="Background" Value="#3A3A3A"/>
<Setter Property="Foreground" Value="White"/>
<Setter Property="Padding" Value="8,4"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ComboBoxItem">
<Border Background="{TemplateBinding Background}" Padding="{TemplateBinding Padding}">
<ContentPresenter/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsHighlighted" Value="True">
<Setter Property="Background" Value="#0056A0"/>
</Trigger>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="#4A4A4A"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- Quick Switch Button Style -->
<Style x:Key="QuickSwitchButton" TargetType="Button">
<Setter Property="Background" Value="#3C3C3C"/>
<Setter Property="Foreground" Value="White"/>
<Setter Property="BorderBrush" Value="#0078D4"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="Padding" Value="10,5"/>
<Setter Property="Cursor" Value="Hand"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
Padding="{TemplateBinding Padding}"
CornerRadius="3">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="#0056A0"/>
<Setter Property="BorderBrush" Value="#0078D4"/>
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter Property="Background" Value="#004080"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- Spinner Animation -->
<Storyboard x:Key="SpinnerAnimation" RepeatBehavior="Forever">
<DoubleAnimation Storyboard.TargetName="SpinnerRotate"
Storyboard.TargetProperty="Angle"
From="0" To="360" Duration="0:0:1"/>
</Storyboard>
</Window.Resources>
<Border Background="#F0333333" CornerRadius="8" BorderBrush="#555" BorderThickness="1" Margin="5">
<Border.Effect>
<DropShadowEffect BlurRadius="10" ShadowDepth="2" Opacity="0.5"/>
@ -19,20 +179,43 @@
<Grid MinHeight="180">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<!-- Header -->
<Border Grid.Row="0" Background="#444" CornerRadius="8,8,0,0" Padding="12,10">
<TextBlock Text="Monitor Control" Foreground="White" FontSize="14" FontWeight="SemiBold"/>
<Grid>
<TextBlock Text="Monitor Control" Foreground="White" FontSize="14" FontWeight="SemiBold" VerticalAlignment="Center"/>
<Button Content="Exit" HorizontalAlignment="Right" Click="ExitButton_Click"
Style="{StaticResource DarkButton}" FontSize="11"/>
</Grid>
</Border>
<!-- Quick Switch Toolbar -->
<WrapPanel Name="quickSwitchPanel" Grid.Row="1" Margin="8,8,8,0" Visibility="Collapsed"/>
<!-- Content -->
<ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto" Padding="12">
<ScrollViewer Grid.Row="2" VerticalScrollBarVisibility="Auto" Padding="12">
<StackPanel Name="sp" VerticalAlignment="Center">
<TextBlock Name="loadingText" Text="Loading..." Foreground="LightGray" FontSize="12"
HorizontalAlignment="Center" Margin="0,40,0,40"/>
<!-- Loading indicator with spinner -->
<StackPanel Name="loadingPanel" HorizontalAlignment="Center" Margin="0,30,0,30">
<Ellipse Width="24" Height="24" StrokeThickness="3" HorizontalAlignment="Center" Margin="0,0,0,10"
RenderTransformOrigin="0.5,0.5">
<Ellipse.Stroke>
<LinearGradientBrush>
<GradientStop Color="#0078D4" Offset="0"/>
<GradientStop Color="Transparent" Offset="1"/>
</LinearGradientBrush>
</Ellipse.Stroke>
<Ellipse.RenderTransform>
<RotateTransform x:Name="SpinnerRotate" Angle="0"/>
</Ellipse.RenderTransform>
</Ellipse>
<TextBlock Name="loadingText" Text="Loading..." Foreground="LightGray" FontSize="12"
HorizontalAlignment="Center"/>
</StackPanel>
</StackPanel>
</ScrollViewer>
</Grid>

View File

@ -1,4 +1,5 @@
using CMM.Library.Method;
using CMM.Library.Config;
using CMM.Library.Method;
using CMM.Library.ViewModel;
using System;
using System.Collections.Generic;
@ -7,26 +8,32 @@ using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Animation;
namespace DellMonitorControl;
public partial class MainWindow : Window
{
private List<(XMonitor Monitor, List<InputSourceOption> Options)> _loadedMonitors = new();
private Storyboard? _spinnerStoryboard;
public MainWindow()
{
InitializeComponent();
_spinnerStoryboard = (Storyboard)FindResource("SpinnerAnimation");
}
public void ShowNearTray()
{
var workArea = SystemParameters.WorkArea;
Left = workArea.Right - Width - 10;
// Use estimated height since ActualHeight is 0 before render
Top = workArea.Bottom - 350;
Show();
Activate();
// Reposition after layout is complete
// Start spinner
_spinnerStoryboard?.Begin(this, true);
Dispatcher.BeginInvoke(new Action(() =>
{
Top = workArea.Bottom - ActualHeight - 10;
@ -36,11 +43,18 @@ public partial class MainWindow : Window
private void Window_Deactivated(object sender, EventArgs e)
{
Hide();
_spinnerStoryboard?.Stop(this);
}
private void ExitButton_Click(object sender, RoutedEventArgs e)
{
Application.Current.Shutdown();
}
public async Task LoadMonitors()
{
var newChildren = new List<UIElement>();
_loadedMonitors.Clear();
try
{
@ -68,21 +82,19 @@ public partial class MainWindow : Window
var inputOptions = await CMMCommand.GetInputSourceOptions(m.SerialNumber);
var powerStatus = await CMMCommand.GetMonPowerStatus(m.SerialNumber) ?? "Unknown";
// Monitor name header
newChildren.Add(new TextBlock
{
Text = m.MonitorName,
Foreground = Brushes.White,
FontSize = 13,
FontWeight = FontWeights.SemiBold,
Margin = new Thickness(0, 8, 0, 6)
});
_loadedMonitors.Add((m, inputOptions));
// Apply config to filter hidden ports and use custom labels
var filteredOptions = MonitorConfigManager.ApplyConfigToOptions(m.SerialNumber, inputOptions);
// Monitor name header with Config button
newChildren.Add(CreateMonitorHeader(m, inputOptions));
newChildren.Add(CreateSliderRow("Brightness", brightness, m.SerialNumber, CMMCommand.SetBrightness));
newChildren.Add(CreateSliderRow("Contrast", contrast, m.SerialNumber, CMMCommand.SetContrast));
if (inputOptions.Count > 0)
newChildren.Add(CreateInputRow(inputSource, inputOptions, m.SerialNumber));
if (filteredOptions.Count > 0)
newChildren.Add(CreateInputRow(inputSource, filteredOptions, m.SerialNumber));
newChildren.Add(CreatePowerRow(powerStatus, m.SerialNumber));
@ -94,6 +106,9 @@ public partial class MainWindow : Window
if (newChildren.Count > 0 && newChildren.Last() is Border)
newChildren.RemoveAt(newChildren.Count - 1);
}
// Load quick-switch toolbar
LoadQuickSwitchToolbar();
}
catch (Exception ex)
{
@ -101,12 +116,14 @@ public partial class MainWindow : Window
newChildren.Add(new TextBlock { Text = $"Error: {ex.Message}", Foreground = Brushes.OrangeRed, FontSize = 11, TextWrapping = TextWrapping.Wrap });
}
// Stop spinner
_spinnerStoryboard?.Stop(this);
sp.VerticalAlignment = VerticalAlignment.Top;
sp.Children.Clear();
foreach (var child in newChildren)
sp.Children.Add(child);
// Reposition after content changes
Dispatcher.BeginInvoke(new Action(() =>
{
var workArea = SystemParameters.WorkArea;
@ -114,6 +131,112 @@ public partial class MainWindow : Window
}), System.Windows.Threading.DispatcherPriority.Loaded);
}
private void LoadQuickSwitchToolbar()
{
quickSwitchPanel.Children.Clear();
var quickItems = MonitorConfigManager.GetQuickSwitchItems();
if (quickItems.Count == 0)
{
quickSwitchPanel.Visibility = Visibility.Collapsed;
return;
}
quickSwitchPanel.Visibility = Visibility.Visible;
foreach (var item in quickItems)
{
var btn = new Button
{
Content = $"{item.PortLabel}",
ToolTip = $"{item.MonitorName}: {item.PortLabel}",
Margin = new Thickness(0, 0, 6, 6),
Style = (Style)FindResource("QuickSwitchButton"),
Tag = item
};
btn.Click += async (s, e) =>
{
if (s is Button b && b.Tag is QuickSwitchItem qsi)
{
await CMMCommand.SetInputSource(qsi.MonitorSerialNumber, qsi.PortVcpValue);
UpdateInputDropdown(qsi.MonitorSerialNumber, qsi.PortVcpValue);
}
};
quickSwitchPanel.Children.Add(btn);
}
}
private void UpdateInputDropdown(string serialNumber, int newValue)
{
foreach (var child in sp.Children)
{
if (child is StackPanel row)
{
foreach (var element in row.Children)
{
if (element is ComboBox cb && cb.Tag is string sn && sn == serialNumber)
{
var options = cb.ItemsSource as List<InputSourceOption>;
if (options != null)
{
var index = options.FindIndex(o => o.Value == newValue);
if (index >= 0)
cb.SelectedIndex = index;
}
return;
}
}
}
}
}
private StackPanel CreateMonitorHeader(XMonitor monitor, List<InputSourceOption> allOptions)
{
var row = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(0, 8, 0, 6) };
row.Children.Add(new TextBlock
{
Text = monitor.MonitorName,
Foreground = Brushes.White,
FontSize = 13,
FontWeight = FontWeights.SemiBold,
VerticalAlignment = VerticalAlignment.Center
});
var configBtn = new Button
{
Content = "Config",
Margin = new Thickness(10, 0, 0, 0),
FontSize = 11,
Style = (Style)FindResource("DarkButton"),
Tag = (monitor, allOptions)
};
configBtn.Click += ConfigButton_Click;
row.Children.Add(configBtn);
return row;
}
private async void ConfigButton_Click(object sender, RoutedEventArgs e)
{
if (sender is Button btn && btn.Tag is ValueTuple<XMonitor, List<InputSourceOption>> data)
{
var (monitor, options) = data;
var configWindow = new ConfigWindow(monitor.SerialNumber, monitor.MonitorName, options);
configWindow.Owner = this;
configWindow.ShowDialog();
if (configWindow.ConfigChanged)
{
MonitorConfigManager.ClearCache();
await LoadMonitors();
}
}
}
private StackPanel CreateSliderRow(string label, int value, string serialNumber, Func<string, int, Task> setCommand)
{
var row = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(0, 3, 0, 3) };
@ -139,17 +262,34 @@ public partial class MainWindow : Window
var row = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(0, 3, 0, 3) };
row.Children.Add(new TextBlock { Text = "Input", Foreground = Brushes.LightGray, Width = 70, FontSize = 12, VerticalAlignment = VerticalAlignment.Center });
var combo = new ComboBox { Width = 170, ItemsSource = options, DisplayMemberPath = "Name", Tag = serialNumber };
if (currentInput.HasValue)
combo.SelectedItem = options.Find(o => o.Value == currentInput.Value);
combo.SelectionChanged += async (s, e) =>
var combo = new ComboBox
{
if (s is ComboBox cb && cb.Tag is string sn && cb.SelectedItem is InputSourceOption opt)
await CMMCommand.SetInputSource(sn, opt.Value);
Width = 170,
ItemsSource = options,
DisplayMemberPath = "Name",
Tag = serialNumber,
Style = (Style)FindResource("DarkComboBox"),
ItemContainerStyle = (Style)FindResource("DarkComboBoxItem")
};
row.Children.Add(combo);
// Set selection AFTER adding to visual tree, BEFORE adding event handler
if (currentInput.HasValue)
{
var index = options.FindIndex(o => o.Value == currentInput.Value);
if (index >= 0)
combo.SelectedIndex = index;
}
// Add event handler AFTER setting the initial selection
combo.SelectionChanged += async (s, e) =>
{
// Only trigger if user actually changed the selection (not initial load)
if (s is ComboBox cb && cb.Tag is string sn && cb.SelectedItem is InputSourceOption opt && e.AddedItems.Count > 0 && e.RemovedItems.Count > 0)
await CMMCommand.SetInputSource(sn, opt.Value);
};
return row;
}
@ -158,7 +298,13 @@ public partial class MainWindow : Window
var row = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(0, 3, 0, 3) };
row.Children.Add(new TextBlock { Text = "Power", Foreground = Brushes.LightGray, Width = 70, FontSize = 12, VerticalAlignment = VerticalAlignment.Center });
var btn = new Button { Content = status, Width = 170, Tag = serialNumber };
var btn = new Button
{
Content = status,
Width = 170,
Tag = serialNumber,
Style = (Style)FindResource("DarkButton")
};
btn.Click += async (s, e) =>
{
if (s is Button b && b.Tag is string sn)

View File

@ -0,0 +1,125 @@
using CMM.Library.Helpers;
using CMM.Library.ViewModel;
using System.IO;
namespace CMM.Library.Config;
public static class MonitorConfigManager
{
private static readonly string ConfigFolder = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"MonitorControl");
private static readonly string ConfigFile = Path.Combine(ConfigFolder, "config.json");
private static AppConfig? _cachedConfig;
public static AppConfig Load()
{
if (_cachedConfig != null)
return _cachedConfig;
if (!File.Exists(ConfigFile))
{
_cachedConfig = new AppConfig();
return _cachedConfig;
}
try
{
_cachedConfig = ConfigFile.JsonFormFile<AppConfig>() ?? new AppConfig();
}
catch
{
_cachedConfig = new AppConfig();
}
return _cachedConfig;
}
public static void Save(AppConfig config)
{
_cachedConfig = config;
config.FileToJson(ConfigFile);
}
public static MonitorConfig GetMonitorConfig(string serialNumber)
{
var config = Load();
return config.Monitors.FirstOrDefault(m => m.SerialNumber == serialNumber)
?? new MonitorConfig { SerialNumber = serialNumber };
}
public static void SaveMonitorConfig(MonitorConfig monitorConfig)
{
var config = Load();
var existing = config.Monitors.FirstOrDefault(m => m.SerialNumber == monitorConfig.SerialNumber);
if (existing != null)
config.Monitors.Remove(existing);
config.Monitors.Add(monitorConfig);
Save(config);
}
public static List<QuickSwitchItem> GetQuickSwitchItems()
{
var config = Load();
var items = new List<QuickSwitchItem>();
foreach (var monitor in config.Monitors)
{
foreach (var port in monitor.Ports.Where(p => p.ShowInQuickSwitch && !p.IsHidden))
{
items.Add(new QuickSwitchItem
{
MonitorSerialNumber = monitor.SerialNumber,
MonitorName = monitor.MonitorName,
PortVcpValue = port.VcpValue,
PortLabel = port.DisplayName
});
}
}
return items;
}
public static List<InputSourceOption> ApplyConfigToOptions(
string serialNumber,
List<InputSourceOption> options)
{
var monitorConfig = GetMonitorConfig(serialNumber);
if (monitorConfig.Ports.Count == 0)
return options;
var result = new List<InputSourceOption>();
foreach (var option in options)
{
var portConfig = monitorConfig.Ports.FirstOrDefault(p => p.VcpValue == option.Value);
if (portConfig != null)
{
// Respect hidden setting - don't show hidden ports
if (portConfig.IsHidden)
continue;
if (!string.IsNullOrWhiteSpace(portConfig.CustomLabel))
{
result.Add(new InputSourceOption(option.Value, portConfig.CustomLabel));
continue;
}
}
result.Add(option);
}
return result;
}
public static void ClearCache()
{
_cachedConfig = null;
}
}

View File

@ -0,0 +1,44 @@
namespace CMM.Library.ViewModel;
/// <summary>
/// Configuration for a single monitor's ports
/// </summary>
public class MonitorConfig
{
public string SerialNumber { get; set; } = string.Empty;
public string MonitorName { get; set; } = string.Empty;
public List<PortConfig> Ports { get; set; } = new();
}
/// <summary>
/// Configuration for a single input port
/// </summary>
public class PortConfig
{
public int VcpValue { get; set; }
public string DefaultName { get; set; } = string.Empty;
public string CustomLabel { get; set; } = string.Empty;
public bool IsHidden { get; set; }
public bool ShowInQuickSwitch { get; set; }
public string DisplayName => string.IsNullOrWhiteSpace(CustomLabel) ? DefaultName : CustomLabel;
}
/// <summary>
/// Quick switch item shown in toolbar
/// </summary>
public class QuickSwitchItem
{
public string MonitorSerialNumber { get; set; } = string.Empty;
public string MonitorName { get; set; } = string.Empty;
public int PortVcpValue { get; set; }
public string PortLabel { get; set; } = string.Empty;
}
/// <summary>
/// Root configuration object
/// </summary>
public class AppConfig
{
public List<MonitorConfig> Monitors { get; set; } = new();
}