using System; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Effects; using System.Windows.Media.Animation; using System.Windows.Shapes; using System.Windows.Threading; using ytLive.Helpers; using ytLive.Models; using ytLive.ViewModels; namespace ytLive.Controls; public partial class PreviewPane : UserControl { private bool _isDraggingOverlay; private bool _isResizing; private Point _grabOffset; private double _resizeAspect; private readonly System.Collections.Generic.Dictionary _activeBorderAnimations = new(); private DispatcherTimer? _socialGlowTimer; private double _socialGlowAngle; public PreviewPane() { InitializeComponent(); } // Static images and the webcam share the move/resize/selection behavior. public static bool IsDraggableElement(SceneElement element) => element.IsDraggable; public void UpdateSelectionOverlay(bool selected) { SelectionOverlay.Visibility = selected ? Visibility.Visible : Visibility.Collapsed; } // ─── Clicking inside the preview / pull-out drawer (used by the window's // Window_PreviewMouseLeftButtonDown deselection logic) ─── public bool IsClickInsidePreview(DependencyObject original) => IsDescendantOf(original, PreviewGrid); public bool IsClickInsideDrawer(DependencyObject original) => IsDescendantOf(original, TextPullOutHost); private static bool IsDescendantOf(DependencyObject? child, DependencyObject ancestor) { while (child != null && !ReferenceEquals(child, ancestor)) { child = child is Visual or System.Windows.Media.Media3D.Visual3D ? VisualTreeHelper.GetParent(child) : LogicalTreeHelper.GetParent(child); } return child != null; } // ─── Audio meters + TRAX + socials (ROW 2 of the preview pane) ─── private void VolumeSlider_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e) { if (sender is Slider slider) SetSliderValueFromClick(slider, e); if (sender is FrameworkElement { DataContext: MainViewModel vm }) vm.SetVolumeAdjusting(true); } private void VolumeSlider_PreviewMouseLeftButtonUp(object sender, MouseButtonEventArgs e) { if (sender is FrameworkElement { DataContext: MainViewModel vm }) vm.SetVolumeAdjusting(false); } private void VolumeSlider_LostMouseCapture(object sender, MouseEventArgs e) { if (sender is FrameworkElement { DataContext: MainViewModel vm }) vm.SetVolumeAdjusting(false); } private void GameVolumeSlider_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e) { if (sender is Slider slider) SetSliderValueFromClick(slider, e); if (sender is FrameworkElement { DataContext: MainViewModel vm }) vm.SetGameVolumeAdjusting(true); } private void GameVolumeSlider_PreviewMouseLeftButtonUp(object sender, MouseButtonEventArgs e) { if (sender is FrameworkElement { DataContext: MainViewModel vm }) vm.SetGameVolumeAdjusting(false); } private void GameVolumeSlider_LostMouseCapture(object sender, MouseEventArgs e) { if (sender is FrameworkElement { DataContext: MainViewModel vm }) vm.SetGameVolumeAdjusting(false); } private static void SetSliderValueFromClick(Slider slider, MouseButtonEventArgs e) { if (FindParent(e.OriginalSource as Visual) != null) return; var track = slider.Template.FindName("PART_Track", slider) as System.Windows.Controls.Primitives.Track; if (track == null) return; var pos = e.GetPosition(track); var fraction = Math.Clamp(pos.X / track.RenderSize.Width, 0, 1); slider.Value = slider.Minimum + fraction * (slider.Maximum - slider.Minimum); e.Handled = true; } private static T? FindParent(Visual? visual) where T : Visual { while (visual != null) { if (visual is T match) return match; visual = VisualTreeHelper.GetParent(visual) as Visual; } return null; } private void MicSpeaker_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) { if (sender is FrameworkElement { DataContext: MainViewModel vm }) { vm.ToggleMicMuteCommand.Execute(null); } } private void GameSpeaker_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) { if (sender is FrameworkElement { DataContext: MainViewModel vm }) { vm.ToggleGameMuteCommand.Execute(null); } } // TRAX (TASK 9): left-click toggles play/pause (picks a track when none is // loaded); right-click always opens the picker. The picker is the OS file // dialog — the "never the OS viewer" rule covers the preview, not pickers. private void TraxButton_Click(object sender, RoutedEventArgs e) { if (DataContext is ViewModels.MainViewModel vm) vm.TraxLeftClick(); } private void TraxButton_PreviewMouseRightButtonUp(object sender, MouseButtonEventArgs e) { if (DataContext is ViewModels.MainViewModel vm) vm.TraxRightClick(); e.Handled = true; // the button's right-click context menu must not fire } // ─── Webcam / background context menus (right-click on overlay elements) ─── private void WebcamMenu_ChangeWebcam(object sender, RoutedEventArgs e) => (DataContext as MainViewModel)?.ChangeWebcamCommand.Execute(null); private void WebcamMenu_SetBorderAnimation(object sender, RoutedEventArgs e) { if (sender is not MenuItem { DataContext: SceneElement element, Tag: string tag } || !Enum.TryParse(tag, out var animation)) return; element.BorderAnimation = animation; StopBorderAnimation(element.Id); if (animation == BorderAnimation.None) return; var shape = FindBorderShape(sender as DependencyObject); if (shape == null) return; var sb = CreateBorderAnimation(animation, shape); if (sb == null) return; sb.Freeze(); _activeBorderAnimations[element.Id] = sb; shape.BeginAnimation(Shape.OpacityProperty, null); Storyboard.SetTarget(sb, shape); sb.Begin(); } private void StopBorderAnimation(string elementId) { if (_activeBorderAnimations.TryGetValue(elementId, out var old)) { old.Stop(); _activeBorderAnimations.Remove(elementId); } } private static Shape? FindBorderShape(DependencyObject? start) { var current = start; while (current != null) { if (current is ContextMenu cm && cm.PlacementTarget is FrameworkElement target) { for (var i = 0; i < VisualTreeHelper.GetChildrenCount(target); i++) { var child = VisualTreeHelper.GetChild(target, i); if (child is Shape s && (s is Rectangle or Ellipse) && !s.IsHitTestVisible) return s; } return null; } current = VisualTreeHelper.GetParent(current); } return null; } private static Storyboard? CreateBorderAnimation(BorderAnimation animation, Shape shape) { var sb = new Storyboard(); switch (animation) { case BorderAnimation.Pulse: AddDoubleAnimation(sb, Shape.OpacityProperty, 1.0, 0.3, TimeSpan.FromSeconds(0.8), true); break; case BorderAnimation.Glow: AddDoubleAnimation(sb, Shape.OpacityProperty, 1.0, 0.5, TimeSpan.FromSeconds(1.2), true); break; case BorderAnimation.Shimmer: AddDoubleAnimation(sb, Shape.OpacityProperty, 1.0, 0.15, TimeSpan.FromSeconds(0.5), true); break; case BorderAnimation.Electricity: AddDoubleAnimation(sb, Shape.OpacityProperty, 1.0, 0.2, TimeSpan.FromSeconds(0.1), true); break; case BorderAnimation.Sparkles: AddDoubleAnimation(sb, Shape.OpacityProperty, 1.0, 0.4, TimeSpan.FromSeconds(0.3), true); break; case BorderAnimation.MarchingAnts: shape.StrokeDashArray = new DoubleCollection { 4, 2 }; AddDoubleAnimation(sb, Shape.StrokeDashOffsetProperty, 0, -20, TimeSpan.FromSeconds(0.8), false); break; case BorderAnimation.Chase: shape.StrokeDashArray = new DoubleCollection { 8, 4 }; AddDoubleAnimation(sb, Shape.StrokeDashOffsetProperty, 0, -30, TimeSpan.FromSeconds(1.5), false); break; case BorderAnimation.Rainbow: if (shape.Stroke is SolidColorBrush brush) { var frozen = brush.IsFrozen; if (frozen) brush = brush.Clone(); shape.Stroke = brush; var colorAnim = new ColorAnimationUsingKeyFrames { Duration = new Duration(TimeSpan.FromSeconds(3)), RepeatBehavior = RepeatBehavior.Forever }; colorAnim.KeyFrames.Add(new LinearColorKeyFrame(Colors.Red, KeyTime.FromTimeSpan(TimeSpan.Zero))); colorAnim.KeyFrames.Add(new LinearColorKeyFrame(Colors.Yellow, KeyTime.FromTimeSpan(TimeSpan.FromSeconds(0.5)))); colorAnim.KeyFrames.Add(new LinearColorKeyFrame(Colors.Green, KeyTime.FromTimeSpan(TimeSpan.FromSeconds(1)))); colorAnim.KeyFrames.Add(new LinearColorKeyFrame(Colors.DodgerBlue, KeyTime.FromTimeSpan(TimeSpan.FromSeconds(1.5)))); colorAnim.KeyFrames.Add(new LinearColorKeyFrame(Colors.MediumPurple, KeyTime.FromTimeSpan(TimeSpan.FromSeconds(2)))); colorAnim.KeyFrames.Add(new LinearColorKeyFrame(Colors.Red, KeyTime.FromTimeSpan(TimeSpan.FromSeconds(2.5)))); Storyboard.SetTargetProperty(colorAnim, new PropertyPath("(Shape.Stroke).(SolidColorBrush.Color)")); sb.Children.Add(colorAnim); } break; default: return null; } return sb.Children.Count > 0 ? sb : null; } private static void AddDoubleAnimation(Storyboard sb, DependencyProperty prop, double from, double to, Duration duration, bool autoReverse) { var anim = new DoubleAnimation(from, to, duration) { AutoReverse = autoReverse, RepeatBehavior = RepeatBehavior.Forever }; Storyboard.SetTargetProperty(anim, new PropertyPath(prop)); sb.Children.Add(anim); } private void WebcamMenu_SetBorderColor(object sender, RoutedEventArgs e) { if (sender is MenuItem { DataContext: SceneElement element, Tag: string color }) element.BorderColor = color; } private void WebcamMenu_HideInScene(object sender, RoutedEventArgs e) { if (sender is MenuItem { DataContext: SceneElement element }) element.IsVisible = false; } private void WebcamMenu_Remove(object sender, RoutedEventArgs e) { if (sender is MenuItem { DataContext: SceneElement element }) (DataContext as MainViewModel)?.RemoveSourceCommand.Execute(element); } // ─── Social bar: click toggles top ⇄ bottom. KISS — the drag variant was // unusable for shaky hands (jitter around the direction deadzone), so the bar // rides on {Binding SocialBarTop} alone and a click flips it. ─── private void SocialBar_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) { (DataContext as MainViewModel)?.ToggleSocialBarPosition(); e.Handled = true; } private void SocialsButton_MouseEnter(object sender, MouseEventArgs e) { if (SocialBarElement == null) return; // Kick the social bar to full brightness so it's immediately visible var effect = SocialBarElement.Effect as DropShadowEffect; if (effect != null) { effect.BlurRadius = 40; effect.Opacity = 1.0; } // Pulse the glow so it catches the eye _socialGlowAngle = 0; _socialGlowTimer?.Stop(); _socialGlowTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(30) }; _socialGlowTimer.Tick += SocialGlowTick; _socialGlowTimer.Start(); } private void SocialsButton_MouseLeave(object sender, MouseEventArgs e) { _socialGlowTimer?.Stop(); _socialGlowTimer = null; if (SocialBarElement?.Effect is DropShadowEffect effect) { effect.BlurRadius = 18; effect.Opacity = 0.9; } } private void SocialGlowTick(object? sender, EventArgs e) { if (SocialBarElement?.Effect is not DropShadowEffect effect) return; _socialGlowAngle += 0.18; if (_socialGlowAngle > Math.PI * 2) _socialGlowAngle -= Math.PI * 2; // Smooth sine pulse: BlurRadius 24→48, Opacity 0.7→1.0 double t = (Math.Sin(_socialGlowAngle) + 1.0) / 2.0; // 0..1 effect.BlurRadius = 24 + t * 24; effect.Opacity = 0.7 + t * 0.3; } // ─── Preview: element selection / move / resize / hit-testing ─── private void Preview_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) { var vm = DataContext as MainViewModel; if (vm is null || !vm.IsEditMode) return; var grid = (Grid)sender; var toCanvas = CanvasGrid.TransformToVisual(grid).Inverse; var p = toCanvas.Transform(e.GetPosition(grid)); var selected = vm.SelectedElement; if (selected is { } sel && IsDraggableElement(sel) && HitHandle(e.GetPosition(grid), sel)) { _isResizing = true; _resizeAspect = sel.ClipShape == ClipShape.Round ? 1 : sel.Width / Math.Max(1, sel.Height); grid.CaptureMouse(); e.Handled = true; return; } var hit = HitElement(vm, p); if (hit != null) { vm.SelectedElement = hit; _isDraggingOverlay = true; _grabOffset = new Point(p.X - hit.X, p.Y - hit.Y); grid.CaptureMouse(); e.Handled = true; return; } vm.SelectedElement = null; } private void Preview_MouseMove(object sender, MouseEventArgs e) { var vm = DataContext as MainViewModel; if (vm is null || !vm.IsEditMode) return; if (!_isDraggingOverlay && !_isResizing) return; var grid = (Grid)sender; var toCanvas = CanvasGrid.TransformToVisual(grid).Inverse; var p = toCanvas.Transform(e.GetPosition(grid)); var selected = vm.SelectedElement; 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 isWebcam = selected is WebcamSceneConfig; var isRound = selected.ClipShape == ClipShape.Round; var proportional = isRound || Keyboard.Modifiers.HasFlag(ModifierKeys.Shift); var maxW = isWebcam ? MainViewModel.MaxWebcamWidthFor(vm.StagedScene?.Name) : 1920; var maxH = isWebcam ? MainViewModel.MaxWebcamHeightFor(vm.StagedScene?.Name) : 1080; var newW = Math.Clamp(p.X - selected.X, 32, maxW); var newH = proportional ? newW / _resizeAspect : Math.Clamp(p.Y - selected.Y, 32, maxH); if (proportional && newH > maxH) { newH = maxH; 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, SceneElement element) { var corner = CanvasGrid.TransformToVisual(PreviewGrid).Transform(new Point(element.X + element.Width, element.Y + element.Height)); return Math.Abs(mouseScreen.X - corner.X) <= 20 && Math.Abs(mouseScreen.Y - corner.Y) <= 20; } private SceneElement? HitElement(MainViewModel vm, Point p) { var scene = vm.StagedScene; if (scene == null) return null; for (var i = scene.Elements.Count - 1; i >= 0; i--) { var element = scene.Elements[i]; if (!IsDraggableElement(element)) continue; if (element is Source { IsEnabled: false }) continue; if (element is WebcamSceneConfig { IsVisible: false }) continue; if (p.X >= element.X && p.X <= element.X + element.Width && p.Y >= element.Y && p.Y <= element.Y + element.Height) return element; } return null; } }