using System.Collections; using System.ComponentModel; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; using ytLive.Helpers; using ytLive.Models; using ytLive.ViewModels; namespace ytLive; public partial class MainWindow : Window { private readonly MainViewModel _viewModel; public MainWindow() { AppLog.Write("MainWindow ctor: before InitializeComponent"); InitializeComponent(); AppLog.Write("MainWindow ctor: after InitializeComponent"); _viewModel = (MainViewModel)DataContext; _viewModel.PropertyChanged += OnViewModelPropertyChanged; // When a focused scene is deleted, never land focus on a hidden scene. SceneList.IsSelectable = item => item is Scene { IsHidden: false }; UpdateTaskbarOverlay(); UpdateSelectionOverlay(); AppLog.Write("MainWindow ctor: end"); } private void MainWindow_Loaded(object sender, RoutedEventArgs e) { AppLog.Write("MainWindow loaded"); } private void OnViewModelPropertyChanged(object? sender, PropertyChangedEventArgs e) { if (e.PropertyName == nameof(MainViewModel.IsLive)) UpdateTaskbarOverlay(); else if (e.PropertyName == nameof(MainViewModel.SelectedSource)) UpdateSelectionOverlay(); } private void MainWindow_Closing(object? sender, CancelEventArgs e) { _viewModel.Shutdown(); } private void UpdateTaskbarOverlay() { TaskbarInfo.Overlay = _viewModel.IsLive ? (ImageSource)FindResource("LiveOverlay") : null; } private void SceneNameBox_IsVisibleChanged(object sender, DependencyPropertyChangedEventArgs e) { if (sender is TextBox { IsVisible: true } box) { box.Focus(); box.SelectAll(); } } private void SceneList_SelectionChanged(object sender, SelectionChangedEventArgs e) { if (e.AddedItems.Count > 0 && e.AddedItems[0] is Scene { IsHidden: true } hidden) { var list = (ListBox)sender; list.SelectedItem = _viewModel.ActiveScene; } } private void GearButton_Click(object sender, RoutedEventArgs e) { if (sender is Button { ContextMenu: { } menu } button) { menu.PlacementTarget = button; menu.Placement = System.Windows.Controls.Primitives.PlacementMode.Bottom; menu.IsOpen = true; } } private void AddSourceButton_Click(object sender, RoutedEventArgs e) { if (sender is Button { ContextMenu: { } menu } button) { menu.PlacementTarget = button; menu.Placement = System.Windows.Controls.Primitives.PlacementMode.Bottom; menu.IsOpen = true; } } // ─── Preview image selection / move / resize ─── private bool _isDraggingOverlay; private bool _isResizing; private Point _grabOffset; private double _resizeAspect; private void Window_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e) { if (e.OriginalSource is not DependencyObject original) return; if (IsDescendantOf(original, PreviewGrid)) return; if (IsDescendantOf(original, SourceList)) return; if (IsDescendantOf(original, OpacityChip)) return; _viewModel.SelectedSource = null; } private static bool IsDescendantOf(DependencyObject? child, DependencyObject ancestor) { while (child != null && !ReferenceEquals(child, ancestor)) child = VisualTreeHelper.GetParent(child); return child != null; } private void UpdateSelectionOverlay() { var selected = _viewModel.SelectedSource is { } s && IsDraggableSource(s); SelectionOverlay.Visibility = selected ? Visibility.Visible : Visibility.Collapsed; OpacityChip.Visibility = selected ? Visibility.Visible : Visibility.Collapsed; if (selected) OpacityValueText.Text = $"{Math.Round(_viewModel.SelectedSource!.Opacity * 100)}%"; } // Static images and the webcam share the move/resize/selection behavior. private static bool IsDraggableSource(Source source) => source.Type is SourceType.Image or SourceType.Webcam; private void MirrorButton_Click(object sender, RoutedEventArgs e) { if (_viewModel.SelectedSource is { Type: SourceType.Webcam } source) source.IsMirrored = !source.IsMirrored; } private void ShapeButton_Click(object sender, RoutedEventArgs e) { if (_viewModel.SelectedSource is { Type: SourceType.Webcam } source) source.ClipShape = source.ClipShape == ClipShape.Traditional ? ClipShape.Round : ClipShape.Traditional; } private void OpacitySlider_ValueChanged(object sender, RoutedPropertyChangedEventArgs e) => OpacityValueText.Text = $"{Math.Round(e.NewValue * 100)}%"; private void Preview_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) { if (OpacityChip.IsMouseOver) return; var grid = (Grid)sender; var toCanvas = CanvasGrid.TransformToVisual(grid).Inverse; var p = toCanvas.Transform(e.GetPosition(grid)); var selected = _viewModel.SelectedSource; if (selected is { } sel && IsDraggableSource(sel) && HitHandle(e.GetPosition(grid), sel)) { _isResizing = true; _resizeAspect = sel.Width / Math.Max(1, sel.Height); grid.CaptureMouse(); e.Handled = true; return; } var hit = HitImage(p); if (hit != null) { _viewModel.SelectedSource = hit; _isDraggingOverlay = true; _grabOffset = new Point(p.X - hit.X, p.Y - hit.Y); grid.CaptureMouse(); e.Handled = true; return; } _viewModel.SelectedSource = null; } private void Preview_MouseMove(object sender, MouseEventArgs e) { if (!_isDraggingOverlay && !_isResizing) return; var grid = (Grid)sender; var toCanvas = CanvasGrid.TransformToVisual(grid).Inverse; var p = toCanvas.Transform(e.GetPosition(grid)); var selected = _viewModel.SelectedSource; if (selected == null) { EndPreviewDrag(grid); return; } if (_isDraggingOverlay) { var minX = -(selected.Width - 20); var maxX = 1920 - 20; var minY = -(selected.Height - 20); var maxY = 1080 - 20; selected.X = Math.Clamp(p.X - _grabOffset.X, Math.Min(minX, maxX), Math.Max(minX, maxX)); selected.Y = Math.Clamp(p.Y - _grabOffset.Y, Math.Min(minY, maxY), Math.Max(minY, maxY)); } else if (_isResizing) { var newW = Math.Clamp(p.X - selected.X, 32, 1920); var newH = newW / _resizeAspect; if (newH > 1080) { newH = 1080; newW = newH * _resizeAspect; } selected.Width = newW; selected.Height = newH; } } private void Preview_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) => EndPreviewDrag((Grid)sender); private void EndPreviewDrag(Grid grid) { if (_isDraggingOverlay || _isResizing) grid.ReleaseMouseCapture(); _isDraggingOverlay = false; _isResizing = false; } private bool HitHandle(Point mouseScreen, Source source) { var corner = CanvasGrid.TransformToVisual(PreviewGrid).Transform(new Point(source.X + source.Width, source.Y + source.Height)); return Math.Abs(mouseScreen.X - corner.X) <= 20 && Math.Abs(mouseScreen.Y - corner.Y) <= 20; } private Source? HitImage(Point p) { var scene = _viewModel.ActiveScene; if (scene == null) return null; for (var i = scene.Sources.Count - 1; i >= 0; i--) { var source = scene.Sources[i]; if (!IsDraggableSource(source) || !source.IsEnabled) continue; if (p.X >= source.X && p.X <= source.X + source.Width && p.Y >= source.Y && p.Y <= source.Y + source.Height) return source; } return null; } private void SceneNameBox_KeyDown(object sender, KeyEventArgs e) { if (e.Key is Key.Enter or Key.Escape) { CommitSceneEdit(sender); e.Handled = true; } } private void SceneNameBox_LostFocus(object sender, RoutedEventArgs e) { CommitSceneEdit(sender); } private static void CommitSceneEdit(object sender) { if (sender is FrameworkElement { DataContext: Scene scene }) scene.IsEditing = false; } // ─── List drag-to-reorder (scenes list + sources list) ─── private Point _dragStartPoint; private int _dragIndex = -1; private bool _isDragging; private object? _lastHoveredItem; private void List_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e) { var listBox = (ListBox)sender; var item = FindItemContainerAt(listBox, e.GetPosition(listBox)); if (item == null) { if (ReferenceEquals(listBox, SourceList)) _viewModel.SelectedSource = null; _dragIndex = -1; return; } if (listBox.ItemsSource is not IList items || items.Count == 0) { _dragIndex = -1; return; } _dragIndex = listBox.Items.IndexOf(item.DataContext); _dragStartPoint = e.GetPosition(listBox); _isDragging = false; _lastHoveredItem = null; } private void List_PreviewMouseMove(object sender, MouseEventArgs e) { if (_dragIndex < 0) return; var listBox = (ListBox)sender; if (e.LeftButton != MouseButtonState.Pressed || listBox.ItemsSource is not IList items) { EndListDrag(listBox); return; } var position = e.GetPosition(listBox); if (!_isDragging) { if (Math.Abs(position.X - _dragStartPoint.X) < SystemParameters.MinimumHorizontalDragDistance && Math.Abs(position.Y - _dragStartPoint.Y) < SystemParameters.MinimumVerticalDragDistance) return; _isDragging = true; listBox.CaptureMouse(); } var item = FindItemContainerAt(listBox, position); if (item == null) return; var targetItem = item.DataContext; if (ReferenceEquals(targetItem, items[_dragIndex]) || ReferenceEquals(targetItem, _lastHoveredItem)) return; var targetIndex = listBox.Items.IndexOf(targetItem); if (targetIndex < 0) return; var dragged = items[_dragIndex]; items.RemoveAt(_dragIndex); items.Insert(targetIndex, dragged); _dragIndex = targetIndex; _lastHoveredItem = targetItem; } private void List_PreviewMouseLeftButtonUp(object sender, MouseButtonEventArgs e) { EndListDrag((ListBox)sender); } private void EndListDrag(ListBox listBox) { if (_isDragging) listBox.ReleaseMouseCapture(); _dragIndex = -1; _isDragging = false; _lastHoveredItem = null; } private static ListBoxItem? FindItemContainerAt(ListBox listBox, Point position) { var hit = listBox.InputHitTest(position) as DependencyObject; while (hit != null && hit != listBox) { if (hit is ListBoxItem item) return item; hit = VisualTreeHelper.GetParent(hit); } return null; } }