using System; using System.ComponentModel; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; using ytLive.Helpers; using ytLive.ViewModels; namespace ytLive; public partial class MainWindow : Window { private readonly MainViewModel _viewModel; private Controls.TopBar _topBar = null!; private Controls.PreviewPane _previewPane = null!; private Controls.LeftPanel _leftPanel = null!; public MainWindow() { AppLog.Write("MainWindow ctor: before InitializeComponent"); InitializeComponent(); AppLog.Write("MainWindow ctor: after InitializeComponent"); _viewModel = (MainViewModel)DataContext; _viewModel.PropertyChanged += OnViewModelPropertyChanged; _topBar = (Controls.TopBar)FindName("TopBar")!; _previewPane = (Controls.PreviewPane)FindName("PreviewPane")!; _leftPanel = (Controls.LeftPanel)FindName("LeftPanel")!; UpdateTaskbarOverlay(); UpdateSelectionOverlay(); _topBar.RefreshAvatar(); RestoreWindowGeometry(); AppLog.Write("MainWindow ctor: end"); } private void MainWindow_Loaded(object sender, RoutedEventArgs e) { AppLog.Write("MainWindow loaded"); // Web sources need the window HWND as the composition controller's parent, // which does not exist in the ctor — InitWebView2 runs here (Loaded), after // SourceInitialized. Nothing renders from web frames before this. _viewModel.InitWebView2(); } // ─── Window geometry (launch position/size) ─── // Launch POSITION is derived, never restored: upper-left of the work area plus // padding (creator ruling 2026-10-04). The old restore clamped the saved position // to [work.Left - width + 120, work.Right - 120] horizontally and // [work.Top, work.Bottom - 80] vertically — ranges wide enough to leave the window // almost entirely off-screen, which is what the creator was seeing ("below the // fold"). Size and maximized state are still persisted. Placement math is pure and // tested (WindowPlacementTests); changing it back to restore position is reverting // this block only, since the saved values are still written. /// Launch inset from the work-area corner, in device-independent units — /// so it scales with DPI rather than being a fixed physical pixel count. internal const double LaunchPadding = 75; /// Upper-left of plus padding, clamped so the /// window is never pushed off-screen. The clamp is the point: a window that fits /// stays wholly visible, and one too big to fit falls back to the work-area origin /// rather than hanging off an edge. internal static Point TopLeftPlacement(Rect workArea, double width, double height, double padding) { var left = workArea.Left + padding; var top = workArea.Top + padding; if (width <= workArea.Width) left = Math.Min(left, workArea.Right - width); else left = workArea.Left; if (height <= workArea.Height) top = Math.Min(top, workArea.Bottom - height); else top = workArea.Top; return new Point(left, top); } private void RestoreWindowGeometry() { try { var work = SystemParameters.WorkArea; var saved = _viewModel.RestoreWindowState(); // No saved state: keep the XAML-declared size rather than collapsing to Min*. var width = Math.Clamp(saved?.Width ?? Width, MinWidth, work.Width); var height = Math.Clamp(saved?.Height ?? Height, MinHeight, work.Height); Width = width; Height = height; var place = TopLeftPlacement(work, width, height, LaunchPadding); Left = place.X; Top = place.Y; if (saved?.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 Window_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e) { if (e.OriginalSource is not DependencyObject original) return; var form = _viewModel.BroadcastForm; var ypp = _viewModel.Ypp; var test = _viewModel.TestSession; if (!_previewPane.IsClickInsideDrawer(original)) { if (form.IsDrawerOpen) form.CloseDrawerCommand.Execute(null); if (ypp.IsDrawerOpen) ypp.CloseDrawerCommand.Execute(null); if (test.IsDrawerOpen) test.CloseDrawerCommand.Execute(null); } if (_previewPane.IsClickInsidePreview(original)) return; if (_leftPanel.IsClickInside(original)) return; _viewModel.SelectedElement = null; } private void UpdateSelectionOverlay() { if (!_viewModel.IsEditMode) { _previewPane.UpdateSelectionOverlay(false); return; } var selected = _viewModel.SelectedElement is { } s && Controls.PreviewPane.IsDraggableElement(s); _previewPane.UpdateSelectionOverlay(selected); _leftPanel.OnSelectionChanged(_viewModel); } }