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(); SizeChanged += (_, e) => Helpers.AppLog.Write($"WIDTHS window={e.NewSize.Width:F0} " + $"layers={LeftPanel.ActualWidth:F0} strip={SceneStrip.ActualWidth:F0} preview={PreviewPane.ActualWidth:F0}"); AppLog.Write("MainWindow ctor: after InitializeComponent"); _viewModel = (MainViewModel)DataContext; _viewModel.PropertyChanged += OnViewModelPropertyChanged; // Don't steal focus from an exclusive-fullscreen game the creator is already // playing (e.g. while starting a gameplay stream): activating our window would // drop that game to the taskbar and make the desktop look "locked". ShowActivated = !_viewModel.FullscreenGameRunningNow; _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 bool _shutdownComplete; private async void MainWindow_Closing(object? sender, CancelEventArgs e) { if (_shutdownComplete) { SaveWindowGeometry(); _viewModel.Shutdown(); return; } // A session must END before anything is disposed (2026-11-12: closing mid-recording used to // pull the pump out from under itself). Hold the window open, stop properly, then close again. if (_viewModel.IsLive || _viewModel.IsRecording) { e.Cancel = true; await _viewModel.StopBeforeShutdownAsync(); _shutdownComplete = true; Close(); return; } SaveWindowGeometry(); _viewModel.Shutdown(); } private void MainWindow_Activated(object? sender, EventArgs e) { _viewModel.RefreshBackgroundAutoCapture(); } private void MainWindow_Deactivated(object? sender, EventArgs e) { } /// /// Ctrl released/late-pressed while the magnifier is open → the box closes (creator ruling /// 2026-11-01). This is the "let go and it's gone" half of the zoom gesture; it also means one /// hand can dismiss without reaching for the mouse. Creating the box still takes Ctrl+right /// (edit) or a left click (preview), so holding Ctrl to *make* it never cancels it: the key is /// already down before the box exists. /// private void MainWindow_PreviewKeyDown(object sender, System.Windows.Input.KeyEventArgs e) { // ⛔ e.IsRepeat matters: Windows auto-repeats a held modifier. Creation holds Ctrl (the // chord is Ctrl+right-click), so without this guard the repeat tick ~500 ms later saw an // open box and closed it — the box appeared and vanished as it was created. if (e.IsRepeat) return; if (e.Key is System.Windows.Input.Key.LeftCtrl or System.Windows.Input.Key.RightCtrl && _viewModel.IsZoomActive) { _viewModel.DismissZoomIfActive(); } // Esc leaves a sub-menu (creator ruling 2026-11-12): the Output world is a sub-menu, and its // back chevron is gone in favour of this. The bar says so under the switches. if (e.Key == System.Windows.Input.Key.Escape && _viewModel.Activity != ViewModels.TopActivity.Build) { _viewModel.SelectBuildActivityCommand.Execute(null); e.Handled = true; } } 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); // The Settings (gear) and Socials drawers live in the same host but were left out of this // guard, so clicking anywhere else left them open (reported 2026-11-05). _viewModel.CloseSettingsDrawerIfOpen(); _viewModel.CloseSocialsDrawer(); } 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); } }