using System; using System.Collections; using System.ComponentModel; using System.IO; using System.Net.Http; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Effects; using System.Windows.Threading; using System.Windows.Media.Animation; using System.Windows.Media.Imaging; using System.Windows.Shapes; using ytLive.Helpers; using ytLive.Models; using ytLive.ViewModels; namespace ytLive; public partial class MainWindow : Window { private readonly MainViewModel _viewModel; private readonly Dictionary _activeBorderAnimations = new(); public MainWindow() { AppLog.Write("MainWindow ctor: before InitializeComponent"); InitializeComponent(); AppLog.Write("MainWindow ctor: after InitializeComponent"); _viewModel = (MainViewModel)DataContext; _viewModel.InitWebView2(WebViewHostPanel); _viewModel.PropertyChanged += OnViewModelPropertyChanged; _viewModel.ChatMessages.CollectionChanged += OnChatMessagesChanged; UpdateTaskbarOverlay(); UpdateSelectionOverlay(); AvatarImage.Source = null; AvatarInitial.Visibility = Visibility.Collapsed; RestoreWindowGeometry(); AppLog.Write("MainWindow ctor: end"); } private void MainWindow_Loaded(object sender, RoutedEventArgs e) { AppLog.Write("MainWindow loaded"); } // ─── Window geometry persistence (launch size/position) ─── // Saved on close (RestoreBounds so a maximized window remembers its // un-maximize rect); restored in the ctor before the window is shown. // Values clamp to the minimums and to the primary work area — a position // saved on a since-disconnected secondary monitor falls back on-screen. private void RestoreWindowGeometry() { try { var saved = _viewModel.RestoreWindowState(); if (saved == null) return; var (left, top, width, height, state) = saved.Value; var work = SystemParameters.WorkArea; width = Math.Clamp(width, MinWidth, work.Width); height = Math.Clamp(height, MinHeight, work.Height); left = Math.Clamp(left, work.Left - width + 120, work.Right - 120); top = Math.Clamp(top, work.Top, work.Bottom - 80); Left = left; Top = top; Width = width; Height = height; if (state == "Maximized") WindowState = WindowState.Maximized; } catch (Exception ex) { AppLog.Write($"Window geometry restore failed: {ex.Message}"); } } private void SaveWindowGeometry() { try { var bounds = RestoreBounds; var state = WindowState == WindowState.Maximized ? "Maximized" : "Normal"; _viewModel.PersistWindowState(bounds.Left, bounds.Top, bounds.Width, bounds.Height, state); } catch (Exception ex) { AppLog.Write($"Window geometry save failed: {ex.Message}"); } } private ytLive.Services.GlobalHotkeyManager? _hotkeys; protected override void OnSourceInitialized(EventArgs e) { base.OnSourceInitialized(e); _hotkeys = new ytLive.Services.GlobalHotkeyManager(); _hotkeys.Activated += id => _viewModel.HandleHotkey(id); _hotkeys.Attach(this, _viewModel.CurrentHotkeyBindings); _viewModel.HotkeyBindingsChanged = bindings => _hotkeys.Reattach(this, bindings); } private void Window_ContextMenuOpening(object sender, ContextMenuEventArgs e) { if (!_viewModel.IsEditMode) e.Handled = true; } private void OnViewModelPropertyChanged(object? sender, PropertyChangedEventArgs e) { if (e.PropertyName == nameof(MainViewModel.IsLive)) UpdateTaskbarOverlay(); else if (e.PropertyName == nameof(MainViewModel.SelectedElement)) UpdateSelectionOverlay(); else if (e.PropertyName == nameof(MainViewModel.AccountAvatarUrl) || e.PropertyName == nameof(MainViewModel.IsConnected)) LoadAvatarImage(); } private void MainWindow_Closing(object? sender, CancelEventArgs e) { SaveWindowGeometry(); _viewModel.Shutdown(); } private void MainWindow_Activated(object? sender, EventArgs e) { _viewModel.RefreshBackgroundAutoCapture(); } private void MainWindow_Deactivated(object? sender, EventArgs e) { } private void UpdateTaskbarOverlay() { TaskbarInfo.Overlay = _viewModel.IsLive ? (ImageSource)FindResource("LiveOverlay") : null; } private static readonly HttpClient AvatarHttp = new() { Timeout = TimeSpan.FromSeconds(15) }; private string? _loadedAvatarUrl; private async void LoadAvatarImage() { var url = _viewModel.AccountAvatarUrl; if (!_viewModel.IsConnected) { ShowAvatarFallback(null); return; } if (string.IsNullOrWhiteSpace(url)) { AppLog.Write("Avatar: connected but no profile image URL on the session"); ShowAvatarFallback(null); return; } if (url == _loadedAvatarUrl && AvatarImage.Source != null) return; try { var bytes = await AvatarHttp.GetByteArrayAsync(url); using var ms = new MemoryStream(bytes); var bitmap = new BitmapImage(); bitmap.BeginInit(); bitmap.CacheOption = BitmapCacheOption.OnLoad; bitmap.StreamSource = ms; bitmap.EndInit(); bitmap.Freeze(); _loadedAvatarUrl = url; AvatarImage.Source = bitmap; AvatarInitial.Visibility = Visibility.Collapsed; } catch (Exception ex) { AppLog.Write($"Avatar load failed ({url}): {ex.Message}"); ShowAvatarFallback(_viewModel.AccountDisplayName); } } private void ShowAvatarFallback(string? name) { _loadedAvatarUrl = null; AvatarImage.Source = null; AvatarInitial.Text = string.IsNullOrWhiteSpace(name) ? "?" : name.Substring(0, 1).ToUpperInvariant(); AvatarInitial.Visibility = Visibility.Visible; } private void SourceNameBox_IsVisibleChanged(object sender, DependencyPropertyChangedEventArgs e) { if (sender is TextBox { IsVisible: true } box) { box.Focus(); box.SelectAll(); } } 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 } private void BackToAbout_Click(object sender, RoutedEventArgs e) { if (DataContext is ViewModels.MainViewModel vm) { vm.IsLicensesOpen = false; vm.IsLicenseEntryOpen = false; } } 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 void Thumbnail_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) { if (sender is Border { DataContext: Scene scene }) _viewModel.TransitionToScene(scene); } 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; var form = _viewModel.BroadcastForm; if (form.IsDrawerOpen && !IsDescendantOf(original, TextPullOutHost)) form.CloseDrawerCommand.Execute(null); if (IsDescendantOf(original, PreviewGrid)) return; if (IsDescendantOf(original, SourceList)) return; if (IsDescendantOf(original, ElementsScrollViewer)) return; _viewModel.SelectedElement = null; } private static bool IsDescendantOf(DependencyObject? child, DependencyObject ancestor) { while (child != null && !ReferenceEquals(child, ancestor)) { child = child is System.Windows.Media.Visual or System.Windows.Media.Media3D.Visual3D ? VisualTreeHelper.GetParent(child) : LogicalTreeHelper.GetParent(child); } return child != null; } private void UpdateSelectionOverlay() { if (!_viewModel.IsEditMode) { SelectionOverlay.Visibility = Visibility.Collapsed; return; } var selected = _viewModel.SelectedElement is { } s && IsDraggableElement(s); SelectionOverlay.Visibility = selected ? Visibility.Visible : Visibility.Collapsed; if (selected) OpacityValueText.Text = $"{Math.Round(_viewModel.SelectedElement!.Opacity * 100)}%"; TakeSnapshot(); } private void OnChatMessagesChanged(object? sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e) { if (e.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Add) { ChatListBox.ScrollIntoView(ChatListBox.Items[^1]); } } // Static images and the webcam share the move/resize/selection behavior. private static bool IsDraggableElement(SceneElement element) => element.IsDraggable; private void MirrorButton_Click(object sender, RoutedEventArgs e) { if (_viewModel.SelectedElement is WebcamSceneConfig webcam) webcam.IsMirrored = !webcam.IsMirrored; } private void ShapeButton_Click(object sender, RoutedEventArgs e) { if (_viewModel.SelectedElement is WebcamSceneConfig webcam) webcam.ToggleClipShape(); } private void WebcamMenu_ChangeWebcam(object sender, RoutedEventArgs e) => _viewModel.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 }) _viewModel.RemoveSourceCommand.Execute(element); } private void BackgroundMenu_ChangeCapture(object sender, RoutedEventArgs e) { _ = _viewModel.ChangeBackgroundCaptureAsync(); } private void BackgroundMenu_RefreshCapture(object sender, RoutedEventArgs e) { _viewModel.RefreshBackgroundAutoCapture(); } private void SourceMenu_Remove(object sender, RoutedEventArgs e) { if (sender is MenuItem { DataContext: SceneElement element }) _viewModel.RemoveSourceCommand.Execute(element); } private void OpacitySlider_ValueChanged(object sender, RoutedPropertyChangedEventArgs e) => OpacityValueText.Text = $"{Math.Round(e.NewValue * 100)}%"; // ─── 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) { _viewModel.ToggleSocialBarPosition(); e.Handled = true; } private DispatcherTimer? _socialGlowTimer; private double _socialGlowAngle; 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; } private void Preview_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) { if (!_viewModel.IsEditMode) return; var grid = (Grid)sender; var toCanvas = CanvasGrid.TransformToVisual(grid).Inverse; var p = toCanvas.Transform(e.GetPosition(grid)); var selected = _viewModel.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(p); if (hit != null) { _viewModel.SelectedElement = hit; _isDraggingOverlay = true; _grabOffset = new Point(p.X - hit.X, p.Y - hit.Y); grid.CaptureMouse(); e.Handled = true; return; } _viewModel.SelectedElement = null; } private void Preview_MouseMove(object sender, MouseEventArgs e) { if (!_viewModel.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 = _viewModel.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(_viewModel.StagedScene?.Name) : 1920; var maxH = isWebcam ? MainViewModel.MaxWebcamHeightFor(_viewModel.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(Point p) { var scene = _viewModel.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; } private void SourceNameBox_KeyDown(object sender, KeyEventArgs e) { if (e.Key is Key.Enter or Key.Escape) { CommitSourceEdit(sender); e.Handled = true; } } private void SourceNameBox_LostFocus(object sender, RoutedEventArgs e) { CommitSourceEdit(sender); } private static void CommitSourceEdit(object sender) { if (sender is FrameworkElement { DataContext: SceneElement element }) element.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.SelectedElement = 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; } private Models.SceneElement? _snapshotElement; private string _snapshotBorderColor = string.Empty; private int _snapshotBorderWidth; private int _snapshotTimerMinutes = 15; private string? _snapshotWebUri; private void TakeSnapshot() { if (_viewModel.SelectedElement is Models.Source src) { _snapshotElement = src; _snapshotBorderColor = src.BorderColor; _snapshotBorderWidth = src.BorderWidth; _snapshotTimerMinutes = src.TimerMinutes; _snapshotWebUri = src.WebUri; } } private void WebcamBorderColorPicker_Click(object sender, RoutedEventArgs e) { if (_viewModel.SelectedElement is not SceneElement src) return; if (sender is FrameworkElement fe && fe.ContextMenu == null) { var menu = new System.Windows.Controls.ContextMenu(); string[] colors = ["#FFFFFF", "#000000", "#FF0000", "#00FF00", "#0000FF", "#FFFF00", "#FF00FF", "#00FFFF", "#FF8800", "#8800FF", "#e94560", "#22c55e", "#3b82f6", "#f59e0b"]; foreach (var c in colors) { var item = new System.Windows.Controls.MenuItem { Header = c, ToolTip = c }; var brush = new System.Windows.Media.SolidColorBrush( (System.Windows.Media.Color)System.Windows.Media.ColorConverter.ConvertFromString(c)); item.Icon = new System.Windows.Controls.Border { Width = 16, Height = 16, Background = brush, CornerRadius = new System.Windows.CornerRadius(2) }; var color = c; item.Click += (_, _) => src.BorderColor = color; menu.Items.Add(item); } fe.ContextMenu = menu; } if (sender is FrameworkElement fe2) fe2.ContextMenu.IsOpen = true; } private void WebcamBorderRevert_Click(object sender, RoutedEventArgs e) { if (_snapshotElement is Models.Source src) { src.BorderColor = _snapshotBorderColor; src.BorderWidth = _snapshotBorderWidth; } } private void WebcamBorderAccept_Click(object sender, RoutedEventArgs e) { if (_snapshotElement is Models.Source src) { _snapshotBorderColor = src.BorderColor; _snapshotBorderWidth = src.BorderWidth; } } private void CountdownRevert_Click(object sender, RoutedEventArgs e) { if (_snapshotElement is Models.Source src) src.TimerMinutes = _snapshotTimerMinutes; } private void CountdownAccept_Click(object sender, RoutedEventArgs e) { if (_snapshotElement is Models.Source src) _snapshotTimerMinutes = src.TimerMinutes; } private void WebRevert_Click(object sender, RoutedEventArgs e) { if (_snapshotElement is Models.Source src) src.WebUri = string.Empty; _snapshotWebUri = string.Empty; } private void WebAccept_Click(object sender, RoutedEventArgs e) { if (_snapshotElement is Models.Source src) _snapshotWebUri = src.WebUri; } }