using System; 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; private Controls.TopBar _topBar = null!; private Controls.PreviewPane _previewPane = null!; 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; _topBar = (Controls.TopBar)FindName("TopBar")!; _previewPane = (Controls.PreviewPane)FindName("PreviewPane")!; UpdateTaskbarOverlay(); UpdateSelectionOverlay(); _topBar.RefreshAvatar(); 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)) _topBar.RefreshAvatar(); } 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 void SourceNameBox_IsVisibleChanged(object sender, DependencyPropertyChangedEventArgs e) { if (sender is TextBox { IsVisible: true } box) { box.Focus(); box.SelectAll(); } } 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; } } private void Window_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e) { if (e.OriginalSource is not DependencyObject original) return; var form = _viewModel.BroadcastForm; if (form.IsDrawerOpen && !_previewPane.IsClickInsideDrawer(original)) form.CloseDrawerCommand.Execute(null); if (_previewPane.IsClickInsidePreview(original)) 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) { _previewPane.UpdateSelectionOverlay(false); return; } var selected = _viewModel.SelectedElement is { } s && Controls.PreviewPane.IsDraggableElement(s); _previewPane.UpdateSelectionOverlay(selected); 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]); } } 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 SourceMenu_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 OpacitySlider_ValueChanged(object sender, RoutedPropertyChangedEventArgs e) => OpacityValueText.Text = $"{Math.Round(e.NewValue * 100)}%"; 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; } }