diff --git a/SentryReplay/CameraLayoutPanel.cs b/SentryReplay/CameraLayoutPanel.cs new file mode 100644 index 0000000..023db36 --- /dev/null +++ b/SentryReplay/CameraLayoutPanel.cs @@ -0,0 +1,117 @@ +using System.Windows; +using System.Windows.Controls; + +namespace SentryReplay; + +/// +/// Arranges the four camera hosts without ever reparenting them: a 2x2 grid, or one primary view filling +/// the top with the other three as a strip of tiles along the bottom. Switching views only re-arranges, +/// so each Flyleaf surface resizes in place (no reparent flash). Identify each child with the attached +/// ; drive the layout with . +/// +public sealed class CameraLayoutPanel : Panel +{ + private const double Gap = 2; + private const double TileStripFraction = 0.22; + + private static readonly string[] CameraOrder = + [ + MainWindowViewModel.FrontCameraView, + MainWindowViewModel.RearCameraView, + MainWindowViewModel.LeftCameraView, + MainWindowViewModel.RightCameraView, + ]; + + public static readonly DependencyProperty CameraProperty = DependencyProperty.RegisterAttached( + "Camera", + typeof(string), + typeof(CameraLayoutPanel), + new PropertyMetadata(null)); + + public static string GetCamera(DependencyObject element) => (string)element.GetValue(CameraProperty); + + public static void SetCamera(DependencyObject element, string value) => element.SetValue(CameraProperty, value); + + public static readonly DependencyProperty SelectedCameraViewProperty = DependencyProperty.Register( + nameof(SelectedCameraView), + typeof(string), + typeof(CameraLayoutPanel), + new FrameworkPropertyMetadata(MainWindowViewModel.FrontCameraView, FrameworkPropertyMetadataOptions.AffectsArrange)); + + public string SelectedCameraView + { + get => (string)GetValue(SelectedCameraViewProperty); + set => SetValue(SelectedCameraViewProperty, value); + } + + protected override Size MeasureOverride(Size availableSize) + { + foreach (UIElement child in InternalChildren) + { + child.Measure(availableSize); + } + + return availableSize; + } + + protected override Size ArrangeOverride(Size finalSize) + { + if (SelectedCameraView == MainWindowViewModel.GridCameraView) + { + ArrangeGrid(finalSize); + } + else + { + ArrangeSingle(finalSize); + } + + return finalSize; + } + + private void ArrangeGrid(Size size) + { + var cellWidth = (size.Width - Gap) / 2; + var cellHeight = (size.Height - Gap) / 2; + var right = cellWidth + Gap; + var bottom = cellHeight + Gap; + + ArrangeCamera(MainWindowViewModel.FrontCameraView, new Rect(0, 0, cellWidth, cellHeight)); + ArrangeCamera(MainWindowViewModel.RearCameraView, new Rect(right, 0, cellWidth, cellHeight)); + ArrangeCamera(MainWindowViewModel.LeftCameraView, new Rect(0, bottom, cellWidth, cellHeight)); + ArrangeCamera(MainWindowViewModel.RightCameraView, new Rect(right, bottom, cellWidth, cellHeight)); + } + + private void ArrangeSingle(Size size) + { + var stripHeight = size.Height * TileStripFraction; + var primaryHeight = Math.Max(0, size.Height - stripHeight - Gap); + + ArrangeCamera(SelectedCameraView, new Rect(0, 0, size.Width, primaryHeight)); + + var tiles = CameraOrder.Where(camera => camera != SelectedCameraView).ToArray(); + if (tiles.Length == 0) + { + return; + } + + var tileWidth = (size.Width - (Gap * (tiles.Length - 1))) / tiles.Length; + var tileTop = primaryHeight + Gap; + + for (var i = 0; i < tiles.Length; i++) + { + ArrangeCamera(tiles[i], new Rect(i * (tileWidth + Gap), tileTop, tileWidth, stripHeight)); + } + } + + private void ArrangeCamera(string camera, Rect rect) + { + foreach (UIElement child in InternalChildren) + { + if (GetCamera(child) == camera) + { + child.Arrange(rect); + return; + } + } + } +} diff --git a/SentryReplay/MainWindow.xaml b/SentryReplay/MainWindow.xaml index bb1dad9..1fbbb39 100644 --- a/SentryReplay/MainWindow.xaml +++ b/SentryReplay/MainWindow.xaml @@ -417,14 +417,6 @@ - - - - - - - @@ -439,113 +431,16 @@ BorderThickness="1" ClipToBounds="True" CornerRadius="6"> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + - + + CornerRadius="3" /> - + + CornerRadius="3" /> - + + CornerRadius="3" /> - + + CornerRadius="3" /> -/// Main WPF window. Owns only view concerns: window lifecycle, Flyleaf host layout, the seek -/// slider input plumbing, and search-box focus. All state, commands, and orchestration live in -/// . +/// Main WPF window. Owns only view concerns: window lifecycle, the seek-slider input plumbing, and +/// search-box focus. The camera layout is handled by (no reparenting); +/// all state, commands, and orchestration live in . /// public partial class MainWindow : Window { @@ -27,10 +27,8 @@ public MainWindow() _viewModel = new MainWindowViewModel( () => VideoPlayerController.Create(FrontFlyleafHost, BackFlyleafHost, LeftFlyleafHost, RightFlyleafHost)); _viewModel.SearchBoxFocusRequested += OnSearchBoxFocusRequested; - _viewModel.PropertyChanged += ViewModelOnPropertyChanged; DataContext = _viewModel; - UpdateCameraHostLayout(); } private async void Window_ContentRendered(object sender, EventArgs e) @@ -99,102 +97,12 @@ private void SeekSlider_ValueChanged(object sender, RoutedPropertyChangedEventAr _viewModel.OnSeekSliderValueChanged(); } - private void ViewModelOnPropertyChanged(object sender, PropertyChangedEventArgs e) - { - if (e.PropertyName == nameof(MainWindowViewModel.SelectedCameraView)) - { - UpdateCameraHostLayout(); - } - } - private void OnSearchBoxFocusRequested(object sender, EventArgs e) { SearchBox.Focus(); SearchBox.SelectAll(); } - private void UpdateCameraHostLayout() - { - if (PrimaryCameraHostSlot is null) - return; - - foreach (var (host, slot) in GetCameraHostLayout(_viewModel.SelectedCameraView)) - { - MoveHostToSlot(host, slot); - } - - // Force a synchronous layout pass so each moved host reaches its final bounds promptly. Note: a - // brief flash of the reparented Flyleaf surface at its old size is a known limitation here and is - // accepted (eliminating it would require not reparenting the hosts at all). - UpdateLayout(); - } - - private IReadOnlyList<(FlyleafHost Host, ContentControl Slot)> GetCameraHostLayout(string view) => view switch - { - MainWindowViewModel.GridCameraView => - [ - (FrontFlyleafHost, GridFrontHostSlot), - (BackFlyleafHost, GridRearHostSlot), - (LeftFlyleafHost, GridLeftHostSlot), - (RightFlyleafHost, GridRightHostSlot), - ], - MainWindowViewModel.RearCameraView => - [ - (BackFlyleafHost, PrimaryCameraHostSlot), - (FrontFlyleafHost, FrontTileHostSlot), - (LeftFlyleafHost, LeftTileHostSlot), - (RightFlyleafHost, RightTileHostSlot), - ], - MainWindowViewModel.LeftCameraView => - [ - (LeftFlyleafHost, PrimaryCameraHostSlot), - (FrontFlyleafHost, FrontTileHostSlot), - (BackFlyleafHost, RearTileHostSlot), - (RightFlyleafHost, RightTileHostSlot), - ], - MainWindowViewModel.RightCameraView => - [ - (RightFlyleafHost, PrimaryCameraHostSlot), - (FrontFlyleafHost, FrontTileHostSlot), - (BackFlyleafHost, RearTileHostSlot), - (LeftFlyleafHost, LeftTileHostSlot), - ], - _ => - [ - (FrontFlyleafHost, PrimaryCameraHostSlot), - (BackFlyleafHost, RearTileHostSlot), - (LeftFlyleafHost, LeftTileHostSlot), - (RightFlyleafHost, RightTileHostSlot), - ], - }; - - private static void MoveHostToSlot(FlyleafHost host, ContentControl slot) - { - if (ReferenceEquals(slot.Content, host)) - return; - - RemoveHostFromParent(host); - slot.Content = host; - } - - private static void RemoveHostFromParent(FlyleafHost host) - { - switch (host.Parent) - { - case ContentControl contentControl when ReferenceEquals(contentControl.Content, host): - contentControl.Content = null; - break; - - case Panel panel: - panel.Children.Remove(host); - break; - - case Decorator decorator when ReferenceEquals(decorator.Child, host): - decorator.Child = null; - break; - } - } - private void Hyperlink_RequestNavigate(object sender, RequestNavigateEventArgs e) { if (e.Uri is null)