Add brightness/contrast sliders, input source switching, and 9-language localization
- Add VCP commands for brightness (10), contrast (12), input source (60) - Fix UTF-16 encoding for monitor data parsing - Add system tray app with monitor controls - Add localization for en, es, fr, de, zh, ja, pt, it, hi - Update to .NET 9.0 - Add LICENSE and README 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,10 +1,6 @@
|
||||
using CMM.Library.Config;
|
||||
using CMM.Language;
|
||||
using CMM.Library.Config;
|
||||
using CMM.Library.Method;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Configuration;
|
||||
using System.Data;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
|
||||
@@ -17,6 +13,7 @@ namespace CMM.Management
|
||||
{
|
||||
internal static XConfig cfg { get; private set; }
|
||||
internal static CMMMgr CMMMgr { get; private set; }
|
||||
private readonly CulturesHelper _culturesHelper = new();
|
||||
|
||||
public App()
|
||||
{
|
||||
@@ -28,6 +25,7 @@ namespace CMM.Management
|
||||
|
||||
private async Task App_Startup(object sender, StartupEventArgs e)
|
||||
{
|
||||
_culturesHelper.ApplySystemLanguage();
|
||||
await CMMMgr.Init();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows;
|
||||
using CMM.Library.ViewModel;
|
||||
using CMM.Library.Base;
|
||||
using CMM.Library.Method;
|
||||
using System.Windows.Data;
|
||||
using CMM.Language;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CMM.Management.Control
|
||||
{
|
||||
/// <summary>
|
||||
/// 單一顆螢幕
|
||||
/// Single monitor control
|
||||
/// </summary>
|
||||
internal class MonCtrl : System.Windows.Controls.Control
|
||||
{
|
||||
public readonly static DependencyProperty MonProperty;
|
||||
public static readonly DependencyProperty MonProperty;
|
||||
private StackPanel _sp;
|
||||
|
||||
static MonCtrl()
|
||||
@@ -30,6 +31,10 @@ namespace CMM.Management.Control
|
||||
public override void OnApplyTemplate()
|
||||
{
|
||||
_sp = Template.FindName("sp", this) as StackPanel;
|
||||
if (_sp != null && Mon != null)
|
||||
{
|
||||
_ = LoadControlsAsync();
|
||||
}
|
||||
}
|
||||
|
||||
public XMonitor Mon
|
||||
@@ -49,7 +54,236 @@ namespace CMM.Management.Control
|
||||
|
||||
public virtual void OnMonChanged(XMonitor value)
|
||||
{
|
||||
|
||||
if (_sp != null && value != null)
|
||||
{
|
||||
_ = LoadControlsAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LoadControlsAsync()
|
||||
{
|
||||
if (_sp == null || Mon == null) return;
|
||||
|
||||
_sp.Children.Clear();
|
||||
|
||||
var brightness = await CMMCommand.GetBrightness(Mon.SerialNumber);
|
||||
var contrast = await CMMCommand.GetContrast(Mon.SerialNumber);
|
||||
var inputSource = await CMMCommand.GetInputSource(Mon.SerialNumber);
|
||||
var inputOptions = await CMMCommand.GetInputSourceOptions(Mon.SerialNumber);
|
||||
var powerStatus = await CMMCommand.GetMonPowerStatus(Mon.SerialNumber);
|
||||
|
||||
// Brightness slider
|
||||
var brightnessPanel = CreateSliderControl(
|
||||
Lang.Find("Brightness"),
|
||||
brightness ?? 50,
|
||||
Mon.SerialNumber,
|
||||
async (sn, value) => await CMMCommand.SetBrightness(sn, value));
|
||||
_sp.Children.Add(brightnessPanel);
|
||||
|
||||
// Contrast slider
|
||||
var contrastPanel = CreateSliderControl(
|
||||
Lang.Find("Contrast"),
|
||||
contrast ?? 50,
|
||||
Mon.SerialNumber,
|
||||
async (sn, value) => await CMMCommand.SetContrast(sn, value));
|
||||
_sp.Children.Add(contrastPanel);
|
||||
|
||||
// Input source dropdown
|
||||
if (inputOptions.Count > 0)
|
||||
{
|
||||
var inputPanel = CreateInputSourceControl(
|
||||
Mon.SerialNumber,
|
||||
inputSource,
|
||||
inputOptions);
|
||||
_sp.Children.Add(inputPanel);
|
||||
}
|
||||
|
||||
// Power button
|
||||
var powerPanel = CreatePowerControl(Mon.SerialNumber, powerStatus);
|
||||
_sp.Children.Add(powerPanel);
|
||||
}
|
||||
|
||||
private StackPanel CreateSliderControl(
|
||||
string label,
|
||||
int currentValue,
|
||||
string monitorSN,
|
||||
Func<string, int, Task> onValueChanged)
|
||||
{
|
||||
var panel = new StackPanel
|
||||
{
|
||||
Orientation = Orientation.Horizontal,
|
||||
Margin = new Thickness(0, 5, 0, 5)
|
||||
};
|
||||
|
||||
var labelBlock = new TextBlock
|
||||
{
|
||||
Text = label,
|
||||
Width = 100,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Foreground = System.Windows.Media.Brushes.White
|
||||
};
|
||||
|
||||
var slider = new Slider
|
||||
{
|
||||
Style = (Style)FindResource("Horizontal_Slider"),
|
||||
Minimum = 0,
|
||||
Maximum = 100,
|
||||
Value = currentValue,
|
||||
Tag = monitorSN,
|
||||
Width = 200
|
||||
};
|
||||
|
||||
var valueBlock = new TextBlock
|
||||
{
|
||||
Text = currentValue.ToString(),
|
||||
Width = 40,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Foreground = System.Windows.Media.Brushes.White,
|
||||
TextAlignment = TextAlignment.Right
|
||||
};
|
||||
|
||||
// Update value display while dragging
|
||||
slider.ValueChanged += (s, e) =>
|
||||
{
|
||||
valueBlock.Text = ((int)e.NewValue).ToString();
|
||||
};
|
||||
|
||||
// Only send command on release (PreviewMouseUp)
|
||||
slider.PreviewMouseUp += async (s, e) =>
|
||||
{
|
||||
var sld = s as Slider;
|
||||
var sn = sld?.Tag?.ToString();
|
||||
if (!string.IsNullOrEmpty(sn))
|
||||
{
|
||||
await onValueChanged(sn, (int)sld.Value);
|
||||
}
|
||||
};
|
||||
|
||||
panel.Children.Add(labelBlock);
|
||||
panel.Children.Add(slider);
|
||||
panel.Children.Add(valueBlock);
|
||||
|
||||
return panel;
|
||||
}
|
||||
|
||||
private StackPanel CreateInputSourceControl(
|
||||
string monitorSN,
|
||||
int? currentInputSource,
|
||||
List<InputSourceOption> options)
|
||||
{
|
||||
var panel = new StackPanel
|
||||
{
|
||||
Orientation = Orientation.Horizontal,
|
||||
Margin = new Thickness(0, 5, 0, 5)
|
||||
};
|
||||
|
||||
var labelBlock = new TextBlock
|
||||
{
|
||||
Text = Lang.Find("InputSource"),
|
||||
Width = 100,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Foreground = System.Windows.Media.Brushes.White
|
||||
};
|
||||
|
||||
var comboBox = new ComboBox
|
||||
{
|
||||
Width = 200,
|
||||
Tag = monitorSN,
|
||||
ItemsSource = options,
|
||||
DisplayMemberPath = "Name"
|
||||
};
|
||||
|
||||
// Set current selection
|
||||
if (currentInputSource.HasValue)
|
||||
{
|
||||
var currentOption = options.Find(o => o.Value == currentInputSource.Value);
|
||||
if (currentOption != null)
|
||||
{
|
||||
comboBox.SelectedItem = currentOption;
|
||||
}
|
||||
}
|
||||
|
||||
comboBox.SelectionChanged += async (s, e) =>
|
||||
{
|
||||
var cb = s as ComboBox;
|
||||
var sn = cb?.Tag?.ToString();
|
||||
var selectedOption = cb?.SelectedItem as InputSourceOption;
|
||||
|
||||
if (!string.IsNullOrEmpty(sn) && selectedOption != null)
|
||||
{
|
||||
await CMMCommand.SetInputSource(sn, selectedOption.Value);
|
||||
}
|
||||
};
|
||||
|
||||
panel.Children.Add(labelBlock);
|
||||
panel.Children.Add(comboBox);
|
||||
|
||||
return panel;
|
||||
}
|
||||
|
||||
private StackPanel CreatePowerControl(string monitorSN, string powerStatus)
|
||||
{
|
||||
var panel = new StackPanel
|
||||
{
|
||||
Orientation = Orientation.Horizontal,
|
||||
Margin = new Thickness(0, 10, 0, 0)
|
||||
};
|
||||
|
||||
var labelBlock = new TextBlock
|
||||
{
|
||||
Text = Lang.Find("Power"),
|
||||
Width = 100,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Foreground = System.Windows.Media.Brushes.White
|
||||
};
|
||||
|
||||
var btn = new Button
|
||||
{
|
||||
Tag = monitorSN,
|
||||
Content = GetLocalizedPowerStatus(powerStatus),
|
||||
Width = 200,
|
||||
Height = 30
|
||||
};
|
||||
|
||||
btn.Click += async (s, e) => await TogglePower(s, e);
|
||||
|
||||
panel.Children.Add(labelBlock);
|
||||
panel.Children.Add(btn);
|
||||
|
||||
return panel;
|
||||
}
|
||||
|
||||
private string GetLocalizedPowerStatus(string status)
|
||||
{
|
||||
return status switch
|
||||
{
|
||||
"PowerOn" => Lang.Find("PowerOn"),
|
||||
"Sleep" => Lang.Find("Sleep"),
|
||||
"PowerOff" => Lang.Find("PowerOff"),
|
||||
_ => status
|
||||
};
|
||||
}
|
||||
|
||||
private async Task TogglePower(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var btn = sender as Button;
|
||||
var tag = btn?.Tag?.ToString();
|
||||
if (string.IsNullOrEmpty(tag)) return;
|
||||
|
||||
var currentStatus = await CMMCommand.GetMonPowerStatus(tag);
|
||||
|
||||
if (currentStatus == "Sleep" || currentStatus == "PowerOff")
|
||||
{
|
||||
await CMMCommand.PowerOn(tag);
|
||||
}
|
||||
else
|
||||
{
|
||||
await CMMCommand.Sleep(tag);
|
||||
}
|
||||
|
||||
await Task.Delay(1000);
|
||||
var newStatus = await CMMCommand.GetMonPowerStatus(tag);
|
||||
btn!.Content = GetLocalizedPowerStatus(newStatus);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<TargetFramework>net7.0-windows</TargetFramework>
|
||||
<TargetFramework>net9.0-windows</TargetFramework>
|
||||
<RootNamespace>CMM.Management</RootNamespace>
|
||||
<UseWPF>true</UseWPF>
|
||||
<Copyright>Copyright © DangWang $([System.DateTime]::Now.ToString(yyyy))</Copyright>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
xmlns:ctrl="clr-namespace:CMM.Management.Control"
|
||||
xmlns:local="clr-namespace:CMM.Management"
|
||||
mc:Ignorable="d"
|
||||
Title="ControlMyMonitor Management"
|
||||
Title="{DynamicResource AppTitle}"
|
||||
Background="#666C6161"
|
||||
AllowsTransparency="True"
|
||||
ResizeMode="CanResizeWithGrip"
|
||||
|
||||
@@ -1,18 +1,33 @@
|
||||
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:local="clr-namespace:CMM.Management.Control">
|
||||
|
||||
<Style TargetType="{x:Type local:MonCtrl}">
|
||||
<Setter Property="Margin" Value="10"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="{x:Type local:MonCtrl}">
|
||||
<StackPanel x:Name="sp" Orientation="Horizontal" HorizontalAlignment="Left">
|
||||
<TextBlock Text="{Binding Mon.MonitorName, Mode=OneWay, NotifyOnSourceUpdated=True, NotifyOnValidationError=True,
|
||||
RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type local:MonCtrl}}}"/>
|
||||
</StackPanel>
|
||||
<Border Background="#44000000" CornerRadius="8" Padding="15">
|
||||
<StackPanel Orientation="Vertical">
|
||||
<!-- Monitor Name Header -->
|
||||
<TextBlock Text="{Binding Mon.MonitorName, Mode=OneWay,
|
||||
RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type local:MonCtrl}}}"
|
||||
FontSize="16"
|
||||
FontWeight="Bold"
|
||||
Foreground="White"
|
||||
Margin="0,0,0,10"/>
|
||||
<TextBlock Text="{Binding Mon.SerialNumber, Mode=OneWay,
|
||||
RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type local:MonCtrl}}}"
|
||||
FontSize="11"
|
||||
Foreground="#AAAAAA"
|
||||
Margin="0,0,0,15"/>
|
||||
<!-- Dynamic controls container -->
|
||||
<StackPanel x:Name="sp" Orientation="Vertical"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
</ResourceDictionary>
|
||||
</ResourceDictionary>
|
||||
|
||||
Reference in New Issue
Block a user