2
0

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:
2026-01-04 00:25:01 -05:00
parent f23f26d809
commit 2a3a502567
6 changed files with 756 additions and 28 deletions

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)