Files
LlamaCasty/MainWindow.xaml
T
gramps 15734a5477 feat(zoom): Ctrl closes the box, and a Follow pill that re-anchors the capture
Two creator directives, 2026-11-01.

**Ctrl-down dismisses an open zoom box.** Window-level PreviewKeyDown, so the same hand that
opened it can put it away. It cannot cancel itself at creation: Ctrl is already held before
the right-click that makes the box, so the keydown fires while nothing is open.

**Follow mode, default Anchor.** A PillToggle in App Settings ("Follow the box"), persisted
as Zoom.FollowBox. Off — the default — keeps the 2026-10-08 model: the capture stays on the
clicked point and dragging only moves the box over it. On, UpdateZoomMove re-anchors the
capture to the box centre (half the box's size, clamped per axis), so the magnifier shows
whatever it hovers. Flipping to Follow while the box is open snaps it immediately.

Three tests: anchor drags leave CaptureX alone, follow moves it to the expected quarter-offset
centre, and the setting round-trips through LayoutStore (tested against the store, not via two
view models — two live VMs fight over the temp DB, which is a fixture problem, not a bug).

Both new test classes share a collection tag: MainViewModel.LayoutPathOverride is static, and
xUnit runs classes in parallel, so without it the two view models collided (one failure in a
combined run, clean when run apart).

Release build 0 warnings; 14 tests pass across zoom, entitlement and preview history. Debug
build is blocked by the running app (PID 47388) — close it and the exe updates.
2026-11-01 08:51:28 -08:00

177 lines
9.5 KiB
XML

<Window x:Class="ytLive.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="clr-namespace:ytLive.ViewModels"
xmlns:models="clr-namespace:ytLive.Models"
xmlns:Helpers="clr-namespace:ytLive.Helpers"
xmlns:controls="clr-namespace:ytLive.Controls"
xmlns:notifications="clr-namespace:Notification.Wpf.Controls;assembly=Notification.Wpf"
mc:Ignorable="d"
Title="{Binding WindowTitle}" Height="1040" Width="1920"
MinWidth="1366" MinHeight="768"
Icon="/LlamaCasty;component/Assets/llamacasty-icon.png"
Background="{StaticResource Brush.Background}"
FontFamily="{StaticResource FontFamily.Sans}"
WindowStartupLocation="Manual"
Closing="MainWindow_Closing"
Loaded="MainWindow_Loaded"
Activated="MainWindow_Activated"
Deactivated="MainWindow_Deactivated"
PreviewKeyDown="MainWindow_PreviewKeyDown"
PreviewMouseLeftButtonDown="Window_PreviewMouseLeftButtonDown"
ContextMenuOpening="Window_ContextMenuOpening">
<!-- Preview undo/redo (creator directive 2026-11-01). Window-level so the keys work wherever
focus sits; the commands themselves refuse unless EditingActive, because undo with the
preview showing program output would rewrite a scene nobody is looking at. -->
<Window.InputBindings>
<KeyBinding Gesture="Ctrl+Z" Command="{Binding UndoPreviewCommand}"/>
<KeyBinding Gesture="Ctrl+Shift+Z" Command="{Binding RedoPreviewCommand}"/>
</Window.InputBindings>
<Window.DataContext>
<vm:MainViewModel/>
</Window.DataContext>
<Window.TaskbarItemInfo>
<TaskbarItemInfo x:Name="TaskbarInfo"/>
</Window.TaskbarItemInfo>
<Window.Resources>
<ResourceDictionary>
<Helpers:DroppedFramesToBrushConverter x:Key="DroppedToBrush"/>
<Helpers:DroppedPercentConverter x:Key="DroppedPercent"/>
<!-- Red dot shown in the taskbar icon while live -->
<DrawingImage x:Key="LiveOverlay">
<DrawingImage.Drawing>
<DrawingGroup>
<GeometryDrawing Brush="{StaticResource Brush.Tertiary}">
<GeometryDrawing.Geometry>
<EllipseGeometry Center="8,8" RadiusX="8" RadiusY="8"/>
</GeometryDrawing.Geometry>
</GeometryDrawing>
</DrawingGroup>
</DrawingImage.Drawing>
</DrawingImage>
<!-- EyeButton/EyeIconStyle MOVED to Themes/Controls.xaml (2026-09-01): Controls/
LeftPanel.xaml consumes them via StaticResource, and window-scope resources
are invisible to a UserControl at parse time — the "known test failure" that
was actually an app-crash regression from the LeftPanel extraction. -->
</ResourceDictionary>
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- ═══ TOP BAR: Stream Controls ═══ -->
<Border Grid.Row="0" Background="{Binding TopBarBackground}" Padding="16,10">
<controls:TopBar x:Name="TopBar"/>
</Border>
<!-- ═══ MIDDLE: Main Content ═══ -->
<controls:HealthBanner Grid.Row="1"/>
<Grid Grid.Row="2" Margin="8">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<!-- the dock lives INSIDE the work area so its bottom edge lines up with the
layers panel (creator, 2026-10-30) -->
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="360"/>
<ColumnDefinition Width="3*"/>
</Grid.ColumnDefinitions>
<!-- STREAM READOUTS (top-left cell, above the layers) + resolution -->
<Border Grid.Row="0" Grid.Column="0" Background="{StaticResource Brush.SurfaceContainerLowest}" CornerRadius="4" Padding="10,8" Margin="0,0,8,0">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Row="0" Grid.Column="0" Text="Bitrate" Foreground="{StaticResource Brush.OnSurfaceVariant}" FontSize="12" Margin="0,0,12,2"/>
<TextBlock Grid.Row="0" Grid.Column="1" Foreground="{StaticResource Brush.OnSurface}" FontSize="13" FontFamily="{StaticResource FontFamily.Mono}" HorizontalAlignment="Right" Margin="0,0,0,2">
<Run Text="{Binding CurrentHealth.CurrentBitrate, Mode=OneWay, StringFormat={}{0:0.0}}"/><Run Text=" Mbps"/>
</TextBlock>
<TextBlock Grid.Row="1" Grid.Column="0" Text="FPS" Foreground="{StaticResource Brush.OnSurfaceVariant}" FontSize="12" Margin="0,0,12,2"/>
<TextBlock Grid.Row="1" Grid.Column="1" Text="{Binding CurrentHealth.FPS, Mode=OneWay, StringFormat={}{0:0}}"
Foreground="{StaticResource Brush.OnSurface}" FontSize="13" FontFamily="{StaticResource FontFamily.Mono}" HorizontalAlignment="Right" Margin="0,0,0,2"/>
<TextBlock Grid.Row="2" Grid.Column="0" Text="Dropped" Foreground="{StaticResource Brush.OnSurfaceVariant}" FontSize="12" Margin="0,0,12,2"/>
<TextBlock Grid.Row="2" Grid.Column="1" FontSize="13" FontFamily="{StaticResource FontFamily.Mono}" HorizontalAlignment="Right" Margin="0,0,0,2"
Foreground="{Binding CurrentHealth.DroppedFrames, Converter={StaticResource DroppedToBrush}}">
<TextBlock.Text>
<MultiBinding Converter="{StaticResource DroppedPercent}">
<Binding Path="CurrentHealth.DroppedFrames" Mode="OneWay"/>
<Binding Path="CurrentHealth.FPS" Mode="OneWay"/>
<Binding Path="CurrentHealth.StreamDuration" Mode="OneWay"/>
</MultiBinding>
</TextBlock.Text>
</TextBlock>
<TextBlock Grid.Row="3" Grid.Column="0" Text="Live:" Foreground="{StaticResource Brush.OnSurfaceVariant}" FontSize="12" Margin="0,0,12,2"/>
<TextBlock Grid.Row="3" Grid.Column="1" Text="{Binding CurrentHealth.StreamDuration, Mode=OneWay, StringFormat={}{0:hh\:mm\:ss}}"
Foreground="{StaticResource Brush.Attention}" FontSize="13" FontFamily="{StaticResource FontFamily.Mono}" FontWeight="SemiBold" HorizontalAlignment="Right" Margin="0,0,0,2"/>
<TextBlock Grid.Row="4" Grid.ColumnSpan="2" Text="{Binding CurrentHealth.HealthMessage}"
Foreground="{StaticResource Brush.Attention}" FontSize="12" TextWrapping="Wrap" Margin="0,0,0,6"/>
<ComboBox Grid.Row="5" Grid.ColumnSpan="2" VerticalAlignment="Bottom" Style="{StaticResource YtComboBox}"
ItemsSource="{Binding QualityOptions}"
SelectedItem="{Binding SelectedQuality}"
DisplayMemberPath="Label"
ToolTip="{Binding ResolutionHelp}"
IsEnabled="{Binding IsOffline}"/>
</Grid>
</Border>
<!-- ═══ THUMBNAIL STRIP (over the preview column; left column stays empty) ═══ -->
<controls:SceneThumbnailStrip x:Name="SceneStrip" Grid.Row="0" Grid.Column="1" Margin="8,0" Height="154"/>
<!-- LEFT PANEL: one panel, two states.
Offline (edit mode): LAYERS + PROPERTIES
Live/Recording: YouTube Live Chat -->
<controls:LeftPanel x:Name="LeftPanel" Grid.Row="1" Grid.Column="0" Grid.RowSpan="2"/>
<!-- CENTER: Preview Area -->
<controls:PreviewPane x:Name="PreviewPane" Grid.Row="1" Grid.Column="1"/>
<!-- The dock starts where the layers panel ends and shares its bottom edge. -->
<controls:AudioDock x:Name="AudioDock" Grid.Row="2" Grid.Column="1"/>
</Grid>
<!-- ═══ OVERLAY: Settings / Bug / Feature / About ═══ -->
<controls:OverlayHost Grid.RowSpan="3"/>
<!-- ═══ TOASTS: non-blocking notifications (bottom-right, above footer) ═══ -->
<notifications:NotificationArea x:Name="ToastArea"
Grid.RowSpan="4"
Position="BottomRight" MaxItems="4"
Margin="0,0,12,96"/>
</Grid>
</Window>