diff --git a/DellMonitorControl/ConfigWindow.xaml b/DellMonitorControl/ConfigWindow.xaml
new file mode 100644
index 0000000..296cf0b
--- /dev/null
+++ b/DellMonitorControl/ConfigWindow.xaml
@@ -0,0 +1,50 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/DellMonitorControl/ConfigWindow.xaml.cs b/DellMonitorControl/ConfigWindow.xaml.cs
new file mode 100644
index 0000000..e4e417b
--- /dev/null
+++ b/DellMonitorControl/ConfigWindow.xaml.cs
@@ -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 _availablePorts;
+ private readonly List _portRows = new();
+
+ public bool ConfigChanged { get; private set; }
+
+ public ConfigWindow(string serialNumber, string monitorName, List 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; }
+ }
+}
diff --git a/DellMonitorControl/MainWindow.xaml b/DellMonitorControl/MainWindow.xaml
index 7e69143..e827b64 100644
--- a/DellMonitorControl/MainWindow.xaml
+++ b/DellMonitorControl/MainWindow.xaml
@@ -1,4 +1,4 @@
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -19,20 +179,43 @@
+
-
+
+
+
+
+
+
+
-
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/DellMonitorControl/MainWindow.xaml.cs b/DellMonitorControl/MainWindow.xaml.cs
index 91d87f5..6e5fad1 100644
--- a/DellMonitorControl/MainWindow.xaml.cs
+++ b/DellMonitorControl/MainWindow.xaml.cs
@@ -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 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();
+ _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;
+ if (options != null)
+ {
+ var index = options.FindIndex(o => o.Value == newValue);
+ if (index >= 0)
+ cb.SelectedIndex = index;
+ }
+ return;
+ }
+ }
+ }
+ }
+ }
+
+ private StackPanel CreateMonitorHeader(XMonitor monitor, List 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> 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 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)
diff --git a/Library/Config/MonitorConfigManager.cs b/Library/Config/MonitorConfigManager.cs
new file mode 100644
index 0000000..5fdbb54
--- /dev/null
+++ b/Library/Config/MonitorConfigManager.cs
@@ -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() ?? 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 GetQuickSwitchItems()
+ {
+ var config = Load();
+ var items = new List();
+
+ 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 ApplyConfigToOptions(
+ string serialNumber,
+ List options)
+ {
+ var monitorConfig = GetMonitorConfig(serialNumber);
+
+ if (monitorConfig.Ports.Count == 0)
+ return options;
+
+ var result = new List();
+
+ 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;
+ }
+}
diff --git a/Library/ViewModel/MonitorConfig.cs b/Library/ViewModel/MonitorConfig.cs
new file mode 100644
index 0000000..1211654
--- /dev/null
+++ b/Library/ViewModel/MonitorConfig.cs
@@ -0,0 +1,44 @@
+namespace CMM.Library.ViewModel;
+
+///
+/// Configuration for a single monitor's ports
+///
+public class MonitorConfig
+{
+ public string SerialNumber { get; set; } = string.Empty;
+ public string MonitorName { get; set; } = string.Empty;
+ public List Ports { get; set; } = new();
+}
+
+///
+/// Configuration for a single input port
+///
+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;
+}
+
+///
+/// Quick switch item shown in toolbar
+///
+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;
+}
+
+///
+/// Root configuration object
+///
+public class AppConfig
+{
+ public List Monitors { get; set; } = new();
+}