using System.Collections.Specialized; using System.Linq; using System.Text; using System.Windows; using System.Windows.Controls; using System.Windows.Controls.Primitives; using System.Windows.Input; using System.Windows.Media; using AmebaPro3_ControlPanel.ViewModels; namespace AmebaPro3_ControlPanel.Views.Controls; public partial class ConsolePanel : UserControl { public static readonly DependencyProperty HeaderContentProperty = DependencyProperty.Register(nameof(HeaderContent), typeof(UIElement), typeof(ConsolePanel), new PropertyMetadata(null)); public static readonly DependencyProperty CommandButtonsContentProperty = DependencyProperty.Register(nameof(CommandButtonsContent), typeof(UIElement), typeof(ConsolePanel), new PropertyMetadata(null)); public UIElement? HeaderContent { get => (UIElement?)GetValue(HeaderContentProperty); set => SetValue(HeaderContentProperty, value); } public UIElement? CommandButtonsContent { get => (UIElement?)GetValue(CommandButtonsContentProperty); set => SetValue(CommandButtonsContentProperty, value); } private bool _isAutoScrolling = true; private ScrollViewer? _logScrollViewer; private bool _isProgrammaticScroll = false; private bool _userScrollIntent = false; private bool _isHistoryAutoScrolling = true; private ScrollViewer? _historyScrollViewer; private bool _isHistoryProgrammaticScroll = false; private bool _historyUserScrollIntent = false; public ConsolePanel() { InitializeComponent(); Loaded += ConsolePanel_Loaded; } private void ConsolePanel_Loaded(object sender, RoutedEventArgs e) { if (DataContext is ConsolePanelViewModel vm) { vm.LogLines.CollectionChanged += LogLines_CollectionChanged; vm.History.CollectionChanged += History_CollectionChanged; // Scroll to bottom if there are already items if (vm.LogLines.Count > 0) { Dispatcher.BeginInvoke(new System.Action(() => { if (_logScrollViewer != null) { _isProgrammaticScroll = true; _logScrollViewer.ScrollToEnd(); _isProgrammaticScroll = false; } else if (LogListBox.Items.Count > 0) { LogListBox.ScrollIntoView(LogListBox.Items[LogListBox.Items.Count - 1]); } }), System.Windows.Threading.DispatcherPriority.Loaded); } if (vm.History.Count > 0) { Dispatcher.BeginInvoke(new System.Action(() => { if (_historyScrollViewer != null) { _isHistoryProgrammaticScroll = true; _historyScrollViewer.ScrollToEnd(); _isHistoryProgrammaticScroll = false; } }), System.Windows.Threading.DispatcherPriority.Loaded); } } // Find the ScrollViewer in the LogListBox _logScrollViewer = FindScrollViewer(LogListBox); if (_logScrollViewer != null) { _logScrollViewer.ScrollChanged += LogScrollViewer_ScrollChanged; // Initialize auto-scroll state _isAutoScrolling = IsAtBottom(_logScrollViewer); } _historyScrollViewer = FindScrollViewer(HistoryListBox); if (_historyScrollViewer != null) { _historyScrollViewer.ScrollChanged += HistoryScrollViewer_ScrollChanged; _isHistoryAutoScrolling = IsAtBottom(_historyScrollViewer); } } private void LogLines_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) { if (e.Action == NotifyCollectionChangedAction.Add) { // Ensure ScrollViewer is found (might not be available immediately) if (_logScrollViewer == null) { _logScrollViewer = FindScrollViewer(LogListBox); if (_logScrollViewer != null) { _logScrollViewer.ScrollChanged += LogScrollViewer_ScrollChanged; } } // Check if we should auto-scroll (verify current position before scrolling) bool shouldAutoScroll = _isAutoScrolling && !_userScrollIntent; if (_logScrollViewer != null && !_userScrollIntent) { shouldAutoScroll = IsAtBottom(_logScrollViewer); } if (shouldAutoScroll) { // Scroll to the bottom after UI updates Dispatcher.BeginInvoke(new System.Action(() => { if (_logScrollViewer != null) { _isProgrammaticScroll = true; _logScrollViewer.ScrollToEnd(); _isProgrammaticScroll = false; } else { // Fallback: try to find ScrollViewer again and scroll _logScrollViewer = FindScrollViewer(LogListBox); if (_logScrollViewer != null) { _logScrollViewer.ScrollChanged += LogScrollViewer_ScrollChanged; _isProgrammaticScroll = true; _logScrollViewer.ScrollToEnd(); _isProgrammaticScroll = false; } else if (LogListBox.Items.Count > 0) { // Last resort: use ScrollIntoView LogListBox.ScrollIntoView(LogListBox.Items[LogListBox.Items.Count - 1]); } } }), System.Windows.Threading.DispatcherPriority.Loaded); } } } private void LogScrollViewer_ScrollChanged(object sender, ScrollChangedEventArgs e) { if (_logScrollViewer == null) return; // Ignore scroll changes caused by programmatic scrolling if (_isProgrammaticScroll) { return; } if (e.VerticalChange != 0) { _userScrollIntent = false; } if (!_userScrollIntent) { _isAutoScrolling = IsAtBottom(_logScrollViewer); } } private void History_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) { if (e.Action == NotifyCollectionChangedAction.Add) { if (_historyScrollViewer == null) { _historyScrollViewer = FindScrollViewer(HistoryListBox); if (_historyScrollViewer != null) { _historyScrollViewer.ScrollChanged += HistoryScrollViewer_ScrollChanged; } } bool shouldAutoScroll = _isHistoryAutoScrolling && !_historyUserScrollIntent; if (_historyScrollViewer != null && !_historyUserScrollIntent) { shouldAutoScroll = IsAtBottom(_historyScrollViewer); } if (shouldAutoScroll) { Dispatcher.BeginInvoke(new System.Action(() => { if (_historyScrollViewer != null) { _isHistoryProgrammaticScroll = true; _historyScrollViewer.ScrollToEnd(); _isHistoryProgrammaticScroll = false; } }), System.Windows.Threading.DispatcherPriority.Loaded); } } } private void HistoryScrollViewer_ScrollChanged(object sender, ScrollChangedEventArgs e) { if (_historyScrollViewer == null) return; if (_isHistoryProgrammaticScroll) { return; } if (e.VerticalChange != 0) { _historyUserScrollIntent = false; } if (!_historyUserScrollIntent) { _isHistoryAutoScrolling = IsAtBottom(_historyScrollViewer); } } private static bool IsAtBottom(ScrollViewer scrollViewer) { return scrollViewer.ExtentHeight <= scrollViewer.ViewportHeight || (scrollViewer.VerticalOffset + scrollViewer.ViewportHeight >= scrollViewer.ExtentHeight - 5); } private static ScrollViewer? FindScrollViewer(DependencyObject parent) { for (int i = 0; i < VisualTreeHelper.GetChildrenCount(parent); i++) { var child = VisualTreeHelper.GetChild(parent, i); if (child is ScrollViewer scrollViewer) { return scrollViewer; } var result = FindScrollViewer(child); if (result != null) { return result; } } return null; } private void LogListBox_OnPreviewMouseWheel(object sender, MouseWheelEventArgs e) { MarkUserScrollIntent(); } private void LogListBox_OnPreviewMouseDown(object sender, MouseButtonEventArgs e) { if (e.ChangedButton == MouseButton.Left || e.ChangedButton == MouseButton.Middle || e.ChangedButton == MouseButton.Right) { MarkUserScrollIntent(); } } private void LogListBox_OnPreviewKeyDown(object sender, KeyEventArgs e) { if (e.Key is Key.Up or Key.Down or Key.PageUp or Key.PageDown or Key.Home or Key.End) { MarkUserScrollIntent(); } } private void MarkUserScrollIntent() { _userScrollIntent = true; if (_logScrollViewer == null) { _logScrollViewer = FindScrollViewer(LogListBox); if (_logScrollViewer != null) { _logScrollViewer.ScrollChanged += LogScrollViewer_ScrollChanged; } } Dispatcher.BeginInvoke(new System.Action(() => { if (_logScrollViewer == null) { return; } if (_userScrollIntent) { _userScrollIntent = false; _isAutoScrolling = IsAtBottom(_logScrollViewer); } }), System.Windows.Threading.DispatcherPriority.Background); } private void HistoryListBox_OnPreviewMouseWheel(object sender, MouseWheelEventArgs e) { MarkHistoryUserScrollIntent(); } private void HistoryListBox_OnPreviewMouseDown(object sender, MouseButtonEventArgs e) { if (e.ChangedButton == MouseButton.Left || e.ChangedButton == MouseButton.Middle || e.ChangedButton == MouseButton.Right) { MarkHistoryUserScrollIntent(); } } private void HistoryListBox_OnPreviewKeyDown(object sender, KeyEventArgs e) { if (e.Key is Key.Up or Key.Down or Key.PageUp or Key.PageDown or Key.Home or Key.End) { MarkHistoryUserScrollIntent(); } } private void MarkHistoryUserScrollIntent() { _historyUserScrollIntent = true; if (_historyScrollViewer == null) { _historyScrollViewer = FindScrollViewer(HistoryListBox); if (_historyScrollViewer != null) { _historyScrollViewer.ScrollChanged += HistoryScrollViewer_ScrollChanged; } } Dispatcher.BeginInvoke(new System.Action(() => { if (_historyScrollViewer == null) { return; } if (_historyUserScrollIntent) { _historyUserScrollIntent = false; _isHistoryAutoScrolling = IsAtBottom(_historyScrollViewer); } }), System.Windows.Threading.DispatcherPriority.Background); } private void HistoryListBox_OnSelectionChanged(object sender, SelectionChangedEventArgs e) { if (DataContext is ConsolePanelViewModel vm) { // Only update CurrentCommand if single item is selected if (HistoryListBox.SelectedItems.Count == 1 && HistoryListBox.SelectedItem is string selected) { vm.CurrentCommand = selected; } else if (HistoryListBox.SelectedItems.Count == 0) { vm.CurrentCommand = string.Empty; } // For multi-select, don't update CurrentCommand } } private void HistoryListBox_OnMouseDoubleClick(object sender, MouseButtonEventArgs e) { if (DataContext is ConsolePanelViewModel vm && HistoryListBox.SelectedItem is string selected) { vm.SendFromHistory(selected); } } private void CommandInput_OnKeyDown(object sender, KeyEventArgs e) { if (DataContext is not ConsolePanelViewModel vm) { return; } if (e.Key == Key.Enter) { if (vm.SendCommand.CanExecute(null)) { vm.SendCommand.Execute(null); HistoryListBox.SelectedIndex = -1; } e.Handled = true; } else if (e.Key == Key.Up) { if (HistoryListBox.Items.Count == 0) { return; } if (HistoryListBox.SelectedIndex < 0) { HistoryListBox.SelectedIndex = HistoryListBox.Items.Count - 1; } else if (HistoryListBox.SelectedIndex > 0) { HistoryListBox.SelectedIndex -= 1; } e.Handled = true; } else if (e.Key == Key.Down) { if (HistoryListBox.Items.Count == 0) { return; } if (HistoryListBox.SelectedIndex >= 0 && HistoryListBox.SelectedIndex < HistoryListBox.Items.Count - 1) { HistoryListBox.SelectedIndex += 1; } else { HistoryListBox.SelectedIndex = -1; vm.CurrentCommand = string.Empty; } e.Handled = true; } } private void HistoryListBox_OnKeyDown(object sender, KeyEventArgs e) { if (e.Key == Key.Delete && DataContext is ConsolePanelViewModel vm) { var selectedItems = HistoryListBox.SelectedItems.Cast().ToList(); if (selectedItems.Count > 0) { vm.DeleteHistoryItems(selectedItems); // Clear selection after deletion HistoryListBox.SelectedIndex = -1; e.Handled = true; } } } private void LogListBox_OnKeyDown(object sender, KeyEventArgs e) { if (DataContext is not ConsolePanelViewModel vm) { return; } if (e.Key == Key.Delete) { var selectedItems = LogListBox.SelectedItems.Cast().ToList(); if (selectedItems.Count > 0) { vm.DeleteLogLines(selectedItems); // Clear selection after deletion LogListBox.SelectedIndex = -1; e.Handled = true; } } else if (e.Key == Key.C && (Keyboard.Modifiers & ModifierKeys.Control) == ModifierKeys.Control) { // Copy selected items to clipboard var selectedItems = LogListBox.SelectedItems.Cast().ToList(); if (selectedItems.Count > 0) { var clipboardText = string.Join("\r\n", selectedItems); Clipboard.SetText(clipboardText); e.Handled = true; } } } }