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:
parent
f23f26d809
commit
2a3a502567
50
DellMonitorControl/ConfigWindow.xaml
Normal file
50
DellMonitorControl/ConfigWindow.xaml
Normal 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>
|
||||
180
DellMonitorControl/ConfigWindow.xaml.cs
Normal file
180
DellMonitorControl/ConfigWindow.xaml.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
|
||||
@ -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)
|
||||
|
||||
125
Library/Config/MonitorConfigManager.cs
Normal file
125
Library/Config/MonitorConfigManager.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
44
Library/ViewModel/MonitorConfig.cs
Normal file
44
Library/ViewModel/MonitorConfig.cs
Normal 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();
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user