using CMM.Library.Config; using CMM.Library.Method; using CMM.Library.ViewModel; using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; using System.Windows.Media; using System.Windows.Media.Animation; using System.Windows.Threading; namespace DellMonitorControl; public partial class MainWindow : Window { private List<(XMonitor Monitor, List Options)> _loadedMonitors = new(); private Storyboard? _spinnerStoryboard; private DispatcherTimer? _showLogButtonTimer; private UpdateInfo? _pendingUpdate; private DateTime _lastUpdateCheck = DateTime.MinValue; public MainWindow() { InitializeComponent(); _spinnerStoryboard = (Storyboard)FindResource("SpinnerAnimation"); DebugLogger.Log("MainWindow initialized"); } public void ShowNearTray() { DebugLogger.Log("ShowNearTray called"); var workArea = SystemParameters.WorkArea; Left = workArea.Right - Width - 10; Top = workArea.Bottom - 350; Show(); Activate(); // Start spinner _spinnerStoryboard?.Begin(this, true); // Show log button after 3 seconds if still loading showLogButton.Visibility = Visibility.Collapsed; _showLogButtonTimer?.Stop(); _showLogButtonTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(3) }; _showLogButtonTimer.Tick += (s, e) => { _showLogButtonTimer.Stop(); if (loadingPanel.Visibility == Visibility.Visible || sp.Children.Contains(loadingPanel)) { showLogButton.Visibility = Visibility.Visible; DebugLogger.Log("Show Log button displayed (loading took > 3s)"); } }; _showLogButtonTimer.Start(); // Check for updates in background (max once per hour) if ((DateTime.Now - _lastUpdateCheck).TotalMinutes > 60) { _lastUpdateCheck = DateTime.Now; _ = CheckForUpdatesAsync(); } Dispatcher.BeginInvoke(new Action(() => { Top = workArea.Bottom - ActualHeight - 10; }), DispatcherPriority.Loaded); } private async Task CheckForUpdatesAsync() { try { var update = await UpdateChecker.CheckForUpdateAsync(); if (update != null) { _pendingUpdate = update; await Dispatcher.InvokeAsync(() => ShowUpdateBanner(update)); } } catch { // Silently ignore update check failures } } private void ShowUpdateBanner(UpdateInfo update) { updateBanner.Visibility = Visibility.Visible; updateText.Text = $"v{update.LatestVersion} available - Click to update"; } private void UpdateBanner_Click(object sender, System.Windows.Input.MouseButtonEventArgs e) { if (_pendingUpdate != null && !string.IsNullOrEmpty(_pendingUpdate.DownloadUrl)) { UpdateChecker.OpenDownloadPage(_pendingUpdate.DownloadUrl); } } private void Window_Deactivated(object sender, EventArgs e) { Hide(); _spinnerStoryboard?.Stop(this); } private void ExitButton_Click(object sender, RoutedEventArgs e) { Application.Current.Shutdown(); } private void AboutButton_Click(object sender, RoutedEventArgs e) { var version = System.Reflection.Assembly.GetExecutingAssembly().GetName().Version; var versionStr = $"{version?.Major}.{version?.Minor}.{version?.Build}"; var aboutWindow = new Window { Title = "About Monitor Control", Width = 300, Height = 180, WindowStartupLocation = WindowStartupLocation.CenterScreen, ResizeMode = ResizeMode.NoResize, WindowStyle = WindowStyle.None, AllowsTransparency = true, Background = Brushes.Transparent }; var border = new Border { Background = new SolidColorBrush(Color.FromArgb(0xF0, 0x33, 0x33, 0x33)), CornerRadius = new CornerRadius(8), BorderBrush = new SolidColorBrush(Color.FromRgb(0x55, 0x55, 0x55)), BorderThickness = new Thickness(1), Padding = new Thickness(20) }; var stack = new StackPanel { VerticalAlignment = VerticalAlignment.Center }; stack.Children.Add(new TextBlock { Text = "Monitor Control", Foreground = Brushes.White, FontSize = 18, FontWeight = FontWeights.SemiBold, HorizontalAlignment = HorizontalAlignment.Center, Margin = new Thickness(0, 0, 0, 5) }); stack.Children.Add(new TextBlock { Text = $"Version {versionStr}", Foreground = Brushes.LightGray, FontSize = 12, HorizontalAlignment = HorizontalAlignment.Center, Margin = new Thickness(0, 0, 0, 10) }); stack.Children.Add(new TextBlock { Text = "by David H. Friedel Jr", Foreground = Brushes.Gray, FontSize = 11, HorizontalAlignment = HorizontalAlignment.Center }); stack.Children.Add(new TextBlock { Text = "MarketAlly", Foreground = Brushes.Gray, FontSize = 11, HorizontalAlignment = HorizontalAlignment.Center, Margin = new Thickness(0, 0, 0, 15) }); var closeBtn = new Button { Content = "OK", Width = 80, HorizontalAlignment = HorizontalAlignment.Center, Style = (Style)FindResource("DarkButton") }; closeBtn.Click += (s, args) => aboutWindow.Close(); stack.Children.Add(closeBtn); border.Child = stack; aboutWindow.Content = border; aboutWindow.ShowDialog(); } private void ShowLogButton_Click(object sender, RoutedEventArgs e) { var logWindow = new Window { Title = "Debug Log", Width = 600, Height = 400, WindowStartupLocation = WindowStartupLocation.CenterScreen, Background = new SolidColorBrush(Color.FromRgb(45, 45, 45)) }; var grid = new Grid(); grid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Star) }); grid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto }); var textBox = new TextBox { Text = DebugLogger.GetLogs(), IsReadOnly = true, Background = new SolidColorBrush(Color.FromRgb(30, 30, 30)), Foreground = Brushes.LightGray, FontFamily = new FontFamily("Consolas"), FontSize = 11, VerticalScrollBarVisibility = ScrollBarVisibility.Auto, HorizontalScrollBarVisibility = ScrollBarVisibility.Auto, TextWrapping = TextWrapping.NoWrap, Margin = new Thickness(10) }; Grid.SetRow(textBox, 0); grid.Children.Add(textBox); var buttonPanel = new StackPanel { Orientation = Orientation.Horizontal, HorizontalAlignment = HorizontalAlignment.Right, Margin = new Thickness(10) }; var copyBtn = new Button { Content = "Copy to Clipboard", Padding = new Thickness(10, 5, 10, 5), Margin = new Thickness(0, 0, 10, 0) }; copyBtn.Click += (s, args) => { Clipboard.SetText(DebugLogger.GetLogs()); MessageBox.Show("Log copied to clipboard!", "Debug Log", MessageBoxButton.OK, MessageBoxImage.Information); }; buttonPanel.Children.Add(copyBtn); var openFileBtn = new Button { Content = "Open Log File", Padding = new Thickness(10, 5, 10, 5), Margin = new Thickness(0, 0, 10, 0) }; openFileBtn.Click += (s, args) => { try { System.Diagnostics.Process.Start("explorer.exe", $"/select,\"{DebugLogger.LogFilePath}\""); } catch { } }; buttonPanel.Children.Add(openFileBtn); var closeBtn = new Button { Content = "Close", Padding = new Thickness(10, 5, 10, 5) }; closeBtn.Click += (s, args) => logWindow.Close(); buttonPanel.Children.Add(closeBtn); Grid.SetRow(buttonPanel, 1); grid.Children.Add(buttonPanel); logWindow.Content = grid; logWindow.Show(); } public async Task LoadMonitors() { DebugLogger.Log("LoadMonitors started"); var newChildren = new List(); _loadedMonitors.Clear(); try { DebugLogger.Log("Scanning for monitors..."); await CMMCommand.ScanMonitor(); DebugLogger.Log("Scan complete, reading monitor data..."); var monitors = (await CMMCommand.ReadMonitorsData()).ToList(); DebugLogger.Log($"Found {monitors.Count} monitor(s)"); if (!monitors.Any()) { DebugLogger.Log("No DDC/CI monitors detected"); newChildren.Add(new TextBlock { Text = "No DDC/CI monitors detected", Foreground = Brushes.LightGray, FontSize = 12, HorizontalAlignment = HorizontalAlignment.Center, Margin = new Thickness(0, 30, 0, 30) }); } else { foreach (var m in monitors) { DebugLogger.Log($"Processing monitor: {m.MonitorName} (SN: {m.SerialNumber})"); DebugLogger.Log($" Getting brightness..."); var brightness = await CMMCommand.GetBrightness(m.SerialNumber) ?? 50; DebugLogger.Log($" Brightness: {brightness}"); DebugLogger.Log($" Getting contrast..."); var contrast = await CMMCommand.GetContrast(m.SerialNumber) ?? 50; DebugLogger.Log($" Contrast: {contrast}"); DebugLogger.Log($" Getting input source..."); var inputSource = await CMMCommand.GetInputSource(m.SerialNumber); DebugLogger.Log($" Input source: {inputSource}"); DebugLogger.Log($" Getting input options..."); var inputOptions = await CMMCommand.GetInputSourceOptions(m.SerialNumber); DebugLogger.Log($" Input options count: {inputOptions.Count}"); // Add any previously discovered ports from config var discoveredPorts = MonitorConfigManager.GetDiscoveredPorts(m.SerialNumber, inputOptions); foreach (var port in discoveredPorts) { inputOptions.Insert(0, port); DebugLogger.Log($" Added discovered port from config: {port.Value} ({port.Name})"); } // Some monitors don't report current input in their possible values list // Add it if missing and save to config for future if (inputSource.HasValue && !inputOptions.Any(o => o.Value == inputSource.Value)) { var currentInputName = CMMCommand.GetInputSourceName(inputSource.Value); inputOptions.Insert(0, new InputSourceOption(inputSource.Value, currentInputName)); DebugLogger.Log($" Added missing current input: {inputSource.Value} ({currentInputName})"); // Save this discovered port so it appears in future even when not current MonitorConfigManager.AddDiscoveredPort(m.SerialNumber, m.MonitorName, inputSource.Value, currentInputName); DebugLogger.Log($" Saved discovered port to config"); } DebugLogger.Log($" Getting power status..."); var powerStatus = await CMMCommand.GetMonPowerStatus(m.SerialNumber) ?? "Unknown"; DebugLogger.Log($" Power status: {powerStatus}"); _loadedMonitors.Add((m, inputOptions)); // Apply config to filter hidden ports and use custom labels // Pass currentInput so we never hide the currently active port var filteredOptions = MonitorConfigManager.ApplyConfigToOptions(m.SerialNumber, inputOptions, inputSource); DebugLogger.Log($" Filtered options count: {filteredOptions.Count}"); // 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 (filteredOptions.Count > 0) newChildren.Add(CreateInputRow(inputSource, filteredOptions, m.SerialNumber)); newChildren.Add(CreatePowerRow(powerStatus, m.SerialNumber)); // Separator newChildren.Add(new Border { Height = 1, Background = new SolidColorBrush(Color.FromRgb(80, 80, 80)), Margin = new Thickness(0, 10, 0, 2) }); DebugLogger.Log($" Monitor {m.MonitorName} processing complete"); } // Remove last separator if (newChildren.Count > 0 && newChildren.Last() is Border) newChildren.RemoveAt(newChildren.Count - 1); } // Load quick-switch toolbar DebugLogger.Log("Loading quick-switch toolbar..."); LoadQuickSwitchToolbar(); DebugLogger.Log("Quick-switch toolbar loaded"); } catch (Exception ex) { DebugLogger.LogError("LoadMonitors failed", ex); newChildren.Clear(); newChildren.Add(new TextBlock { Text = $"Error: {ex.Message}", Foreground = Brushes.OrangeRed, FontSize = 11, TextWrapping = TextWrapping.Wrap }); } // Stop spinner and timer _spinnerStoryboard?.Stop(this); _showLogButtonTimer?.Stop(); sp.VerticalAlignment = VerticalAlignment.Top; sp.Children.Clear(); foreach (var child in newChildren) sp.Children.Add(child); DebugLogger.Log("LoadMonitors complete, UI updated"); Dispatcher.BeginInvoke(new Action(() => { var workArea = SystemParameters.WorkArea; Top = workArea.Bottom - ActualHeight - 10; }), 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) }; row.Children.Add(new TextBlock { Text = label, Foreground = Brushes.LightGray, Width = 70, FontSize = 12, VerticalAlignment = VerticalAlignment.Center }); var slider = new Slider { Minimum = 0, Maximum = 100, Value = value, Width = 140, Tag = serialNumber }; var valueText = new TextBlock { Text = value.ToString(), Foreground = Brushes.White, Width = 30, FontSize = 12, Margin = new Thickness(5, 0, 0, 0) }; slider.ValueChanged += (s, e) => valueText.Text = ((int)e.NewValue).ToString(); slider.PreviewMouseUp += async (s, e) => { if (s is Slider sl && sl.Tag is string sn) await setCommand(sn, (int)sl.Value); }; row.Children.Add(slider); row.Children.Add(valueText); return row; } private StackPanel CreateInputRow(int? currentInput, List options, string serialNumber) { DebugLogger.Log($" CreateInputRow: currentInput={currentInput}, optionCount={options.Count}"); foreach (var opt in options) DebugLogger.Log($" Option: Value={opt.Value}, Name={opt.Name}"); 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, 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); DebugLogger.Log($" Selection: Looking for Value={currentInput.Value}, found at index={index}"); if (index >= 0) combo.SelectedIndex = index; } else { DebugLogger.Log($" Selection: currentInput is null, no selection"); } // 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; } private StackPanel CreatePowerRow(string status, string serialNumber) { 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 isUnsupported = string.IsNullOrEmpty(status) || status == "Unknown"; var btn = new Button { Content = isUnsupported ? "Power Unsupported" : status, Width = 170, Tag = serialNumber, Style = (Style)FindResource("DarkButton"), IsEnabled = !isUnsupported }; if (!isUnsupported) { btn.Click += async (s, e) => { if (s is Button b && b.Tag is string sn) { var current = await CMMCommand.GetMonPowerStatus(sn); if (current == "Sleep" || current == "PowerOff") await CMMCommand.PowerOn(sn); else await CMMCommand.Sleep(sn); await Task.Delay(1000); b.Content = await CMMCommand.GetMonPowerStatus(sn); } }; } row.Children.Add(btn); return row; } }