refactor(settings): a pull-out drawer, with Zoom as a category — and the menu moves to the wordmark
Creator directive, 2026-11-01. Settings stopped being a centred modal. - **Drawer** (Controls/SettingsDrawer): hosted in PreviewPane's pull-out stack, 320px, the same animated-width pattern as Stream Settings / YPP / Socials / TEST, opened by the gear rail tab on a single left click. SettingsDrawerOpen is exclusive with the other four, both directions. This also removes the modal's MaxHeight-700 scroll problem for good: the drawer is the height of the preview. - **Categories**: headings with controls indented beneath them. Zoom now owns *Zoom view shape* (Circle / Phone / Tablet) and *Zoom box follows*; Recordings, Brand logo and Scene transition are their own categories. - **The app menu moved to the LlamaCasty wordmark** (right-click → Report Bug / Feature Request / About). Left-click still opens About. The gear tab no longer carries a menu at all, so clicking it does exactly one thing. OverlayHost keeps only the Bug / Feature / About panels; its brand-logo handlers and the dev-panel injection moved with the settings block. BrandLogoPreviewTests follows the control (same assertions, new path). Release 0 warnings; 15 tests pass across brand logo, zoom, entitlement and preview history.
This commit is contained in:
@@ -35,106 +35,6 @@
|
||||
</Button>
|
||||
</Grid>
|
||||
|
||||
<!-- App Settings: broadcast fields live in the preview's
|
||||
Text pull-out now — this panel keeps app-level prefs only.
|
||||
⛔ The ScrollViewer is load-bearing: the shell is MaxHeight 700 and the
|
||||
panel has more sections than that (Recordings, Brand logo, Preview,
|
||||
transitions, Zoom box, Developer), so without it the lower options were
|
||||
clipped off-screen — which is how "Tablet (Horizontal Rectangle)" existed
|
||||
in the XAML yet never appeared in the app (2026-11-01). -->
|
||||
<ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto"
|
||||
Visibility="{Binding IsSettingsOpen, Converter={StaticResource BoolToVis}}">
|
||||
<StackPanel Margin="0,8,0,0">
|
||||
|
||||
<TextBlock Text="Recordings" Foreground="{StaticResource Brush.OnSurfaceVariant}" FontSize="11" FontWeight="SemiBold" Margin="0,8,0,2"/>
|
||||
<TextBlock Text="Default location" Foreground="{StaticResource Brush.OnSurfaceVariant}" FontSize="11" Margin="10,0,0,2"/>
|
||||
<DockPanel Margin="10,0,0,8">
|
||||
<Button DockPanel.Dock="Right" Content="Browse…" Command="{Binding ChooseRecordFolderCommand}"
|
||||
Style="{StaticResource YtButtonSecondary}" Padding="8,2" FontSize="11" Margin="6,0,0,0"/>
|
||||
<Button DockPanel.Dock="Right" Content="Use Downloads" Command="{Binding ResetRecordFolderCommand}"
|
||||
Style="{StaticResource YtButtonSecondary}" Padding="8,2" FontSize="11" Margin="6,0,0,0"/>
|
||||
<TextBox Style="{StaticResource YtTextBoxCompact}" Text="{Binding RecordFolderDisplay, Mode=OneWay}"
|
||||
IsReadOnly="True" Foreground="{StaticResource Brush.OnSurfaceVariant}" VerticalAlignment="Center"/>
|
||||
</DockPanel>
|
||||
|
||||
<TextBlock Text="Brand logo" Foreground="{StaticResource Brush.OnSurfaceVariant}" FontSize="11" FontWeight="SemiBold" Margin="0,8,0,2"/>
|
||||
<TextBlock Text="Image file (shown on every scene)" Foreground="{StaticResource Brush.OnSurfaceVariant}" FontSize="11" Margin="10,0,0,2"/>
|
||||
<DockPanel Margin="10,0,0,8">
|
||||
<Button DockPanel.Dock="Right" Content="Browse…" Command="{Binding BrowseBrandLogoCommand}"
|
||||
Style="{StaticResource YtButtonSecondary}" Padding="8,2" FontSize="11" Margin="6,0,0,0"/>
|
||||
<!-- A file NAME says nothing about the picture. The thumbnail shows what
|
||||
is set; hovering enlarges it, a click collapses it again. -->
|
||||
<Border x:Name="BrandLogoThumbFrame" DockPanel.Dock="Left" Width="28" Height="28"
|
||||
Margin="0,0,6,0" CornerRadius="4" ClipToBounds="True"
|
||||
Background="{StaticResource Brush.SurfaceContainerLowest}"
|
||||
BorderBrush="{StaticResource Brush.Outline}" BorderThickness="1"
|
||||
Cursor="Hand" ToolTip="Hover to enlarge — click to close"
|
||||
Visibility="{Binding BrandLogoPreviewImage, Converter={StaticResource NotNullToVis}}">
|
||||
<Image x:Name="BrandLogoThumbImage" Source="{Binding BrandLogoPreviewImage}"
|
||||
Stretch="Uniform" MouseEnter="BrandLogoThumb_MouseEnter"
|
||||
MouseLeave="BrandLogoThumb_MouseLeave"
|
||||
MouseLeftButtonUp="BrandLogoThumb_Click"/>
|
||||
</Border>
|
||||
<TextBox Style="{StaticResource YtTextBoxCompact}" Text="{Binding BrandLogoFileDisplay, Mode=OneWay}"
|
||||
IsReadOnly="True" Foreground="{StaticResource Brush.OnSurfaceVariant}" VerticalAlignment="Center"/>
|
||||
</DockPanel>
|
||||
<Popup x:Name="BrandLogoPreviewPopup" PlacementTarget="{Binding ElementName=BrandLogoThumbFrame}"
|
||||
Placement="Right" HorizontalOffset="6" StaysOpen="True" AllowsTransparency="True"
|
||||
IsHitTestVisible="True" MouseLeave="BrandLogoPreviewPopup_MouseLeave">
|
||||
<Border Padding="6" CornerRadius="6"
|
||||
Background="{StaticResource Brush.SurfaceContainerHighest}"
|
||||
BorderBrush="{StaticResource Brush.Outline}" BorderThickness="1">
|
||||
<StackPanel>
|
||||
<Image Source="{Binding BrandLogoPreviewImage}" MaxWidth="420" MaxHeight="320"
|
||||
Stretch="Uniform" RenderOptions.BitmapScalingMode="HighQuality"/>
|
||||
<TextBlock Text="{Binding BrandLogoFileDisplay}" FontSize="11" Margin="0,6,0,0"
|
||||
Foreground="{StaticResource Brush.OnSurfaceVariant}"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</Popup>
|
||||
|
||||
<TextBlock Text="Preview" Foreground="{StaticResource Brush.OnSurfaceVariant}" FontSize="11" FontWeight="SemiBold" Margin="0,8,0,2"/>
|
||||
<TextBlock Text="Zoom view shape" Foreground="{StaticResource Brush.OnSurfaceVariant}" FontSize="11" Margin="10,0,0,2"/>
|
||||
<ComboBox Style="{StaticResource YtComboBox}" Margin="10,0,0,8" SelectedValue="{Binding ZoomShape}" SelectedValuePath="Tag">
|
||||
<ComboBoxItem Content="Circle" Tag="{x:Static Models:ZoomShape.Circle}"/>
|
||||
<ComboBoxItem Content="Phone (Vertical Rectangle)" Tag="{x:Static Models:ZoomShape.VerticalRectangle}"/>
|
||||
<ComboBoxItem Content="Tablet (Horizontal Rectangle)" Tag="{x:Static Models:ZoomShape.HorizontalRectangle}"/>
|
||||
</ComboBox>
|
||||
|
||||
<TextBlock Text="Scene transition" Foreground="{StaticResource Brush.OnSurfaceVariant}" FontSize="11" FontWeight="SemiBold" Margin="0,8,0,2"/>
|
||||
<TextBlock Text="Type" Foreground="{StaticResource Brush.OnSurfaceVariant}" FontSize="11" Margin="10,0,0,2"/>
|
||||
<ComboBox Style="{StaticResource YtComboBox}" Margin="10,0,0,8" SelectedValue="{Binding SelectedTransitionType}" SelectedValuePath="Tag">
|
||||
<ComboBoxItem Content="Cut" Tag="{x:Static services:TransitionType.Cut}"/>
|
||||
<ComboBoxItem Content="Fade" Tag="{x:Static services:TransitionType.Fade}"/>
|
||||
<ComboBoxItem Content="Move" Tag="{x:Static services:TransitionType.Move}"/>
|
||||
<ComboBoxItem Content="Wipe" Tag="{x:Static services:TransitionType.Wipe}"/>
|
||||
</ComboBox>
|
||||
<DockPanel Margin="10,0,0,6">
|
||||
<TextBlock DockPanel.Dock="Right" Foreground="{StaticResource Brush.OnSurfaceVariant}" FontSize="11" VerticalAlignment="Center" Margin="8,0,0,0" Width="56">
|
||||
<Run Text="{Binding TransitionDurationMs, StringFormat={}{0:0}}"/><Run Text=" ms"/>
|
||||
</TextBlock>
|
||||
<Slider Minimum="100" Maximum="2000" TickFrequency="50" IsSnapToTickEnabled="True"
|
||||
Value="{Binding TransitionDurationMs, Mode=TwoWay}" VerticalAlignment="Center"/>
|
||||
</DockPanel>
|
||||
|
||||
<!-- Zoom box (creator ruling 2026-11-01): Anchor keeps the magnification on
|
||||
the point that was clicked; Follow re-anchors it to the box as it is
|
||||
dragged. Anchor is the default and the persisted norm. -->
|
||||
<TextBlock Text="Zoom box" Foreground="{StaticResource Brush.OnSurfaceVariant}" FontSize="11" FontWeight="SemiBold" Margin="0,8,0,2"/>
|
||||
<DockPanel Margin="10,0,0,8">
|
||||
<ToggleButton DockPanel.Dock="Right" Style="{StaticResource PillToggle}"
|
||||
IsChecked="{Binding ZoomFollowsBox, Mode=TwoWay}" VerticalAlignment="Center"
|
||||
ToolTip="Off: the box magnifies the area you clicked. On: it magnifies whatever it hovers over."/>
|
||||
<TextBlock Text="Follow the box" Foreground="{StaticResource Brush.OnSurface}" FontSize="11" VerticalAlignment="Center"/>
|
||||
</DockPanel>
|
||||
|
||||
<!-- Development panel host. Empty in a Release build: the panel itself is
|
||||
excluded from the Release compile, and OverlayHost.xaml.cs only fills
|
||||
this inside #if DEBUG. Never put content here directly. -->
|
||||
<StackPanel x:Name="DeveloperPanelHost"/>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
|
||||
<!-- Report Bug -->
|
||||
<StackPanel Grid.Row="1" Margin="0,16,0,0"
|
||||
Visibility="{Binding IsBugOpen, Converter={StaticResource BoolToVis}}">
|
||||
|
||||
@@ -9,44 +9,8 @@ public partial class OverlayHost : UserControl
|
||||
public OverlayHost()
|
||||
{
|
||||
InitializeComponent();
|
||||
#if DEBUG
|
||||
// Development affordances are added at runtime, never declared in XAML: the panel is
|
||||
// excluded from the Release compile (ytLive.csproj), so this line is the only
|
||||
// reference to it and a Release build has none.
|
||||
DeveloperPanelHost.Children.Add(new DeveloperPanel());
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>Is the enlarged brand-logo preview showing? Test seam — a Popup is a separate
|
||||
/// window, so nothing else can observe it.</summary>
|
||||
internal bool IsBrandLogoPreviewOpen => BrandLogoPreviewPopup.IsOpen;
|
||||
|
||||
/// <summary>Hover opens the enlargement. Nothing to show when no image is set, so the
|
||||
/// thumbnail simply does nothing in that case.</summary>
|
||||
private void BrandLogoThumb_MouseEnter(object sender, System.Windows.Input.MouseEventArgs e)
|
||||
=> OpenBrandLogoPreview();
|
||||
|
||||
/// <summary>Leaving the thumbnail closes it — unless the pointer walked onto the popup
|
||||
/// itself (which sits beside it), where it stays put so the creator can look closely.</summary>
|
||||
private void BrandLogoThumb_MouseLeave(object sender, System.Windows.Input.MouseEventArgs e)
|
||||
{
|
||||
if (BrandLogoPreviewPopup.Child?.IsMouseOver == true) return;
|
||||
CloseBrandLogoPreview();
|
||||
}
|
||||
|
||||
/// <summary>A click collapses the enlargement — the explicit "enough, thanks" gesture.</summary>
|
||||
private void BrandLogoThumb_Click(object sender, System.Windows.Input.MouseButtonEventArgs e)
|
||||
=> CloseBrandLogoPreview();
|
||||
|
||||
private void BrandLogoPreviewPopup_MouseLeave(object sender, System.Windows.Input.MouseEventArgs e)
|
||||
=> CloseBrandLogoPreview();
|
||||
|
||||
/// <summary>The thumbnail is hidden when no image is set (see the XAML), so there is nothing
|
||||
/// to open in that case — no empty frame can appear.</summary>
|
||||
internal void OpenBrandLogoPreview() => BrandLogoPreviewPopup.IsOpen = true;
|
||||
|
||||
internal void CloseBrandLogoPreview() => BrandLogoPreviewPopup.IsOpen = false;
|
||||
|
||||
private void BackToAbout_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (DataContext is MainViewModel vm)
|
||||
|
||||
+67
-17
@@ -2,7 +2,7 @@
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:Helpers="clr-namespace:ytLive.Helpers"
|
||||
xmlns:local="clr-namespace:ytLive">
|
||||
xmlns:local="clr-namespace:ytLive" xmlns:controls="clr-namespace:ytLive.Controls">
|
||||
<Border Background="{StaticResource Brush.SurfaceContainerLowest}" CornerRadius="6" Margin="8,0"
|
||||
BorderBrush="{Binding PreviewGlowBrush}" BorderThickness="{Binding PreviewGlowThickness}">
|
||||
<Grid>
|
||||
@@ -881,26 +881,76 @@
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
<!-- The Settings drawer: same animated-width pattern as the other
|
||||
pull-outs, opened by the gear rail tab (creator ruling 2026-11-01).
|
||||
Settings used to be a centred modal; it is a drawer now. -->
|
||||
<Border Background="{StaticResource Brush.SurfaceContainer}" ClipToBounds="True">
|
||||
<Border.Style>
|
||||
<Style TargetType="Border">
|
||||
<Setter Property="Width" Value="0"/>
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding SettingsDrawerOpen}" Value="True">
|
||||
<DataTrigger.EnterActions>
|
||||
<BeginStoryboard>
|
||||
<Storyboard>
|
||||
<DoubleAnimation Storyboard.TargetProperty="Width"
|
||||
To="320" Duration="0:0:0.2">
|
||||
<DoubleAnimation.EasingFunction>
|
||||
<CubicEase EasingMode="EaseOut"/>
|
||||
</DoubleAnimation.EasingFunction>
|
||||
</DoubleAnimation>
|
||||
</Storyboard>
|
||||
</BeginStoryboard>
|
||||
</DataTrigger.EnterActions>
|
||||
<DataTrigger.ExitActions>
|
||||
<BeginStoryboard>
|
||||
<Storyboard>
|
||||
<DoubleAnimation Storyboard.TargetProperty="Width"
|
||||
To="0" Duration="0:0:0.2">
|
||||
<DoubleAnimation.EasingFunction>
|
||||
<CubicEase EasingMode="EaseOut"/>
|
||||
</DoubleAnimation.EasingFunction>
|
||||
</DoubleAnimation>
|
||||
</Storyboard>
|
||||
</BeginStoryboard>
|
||||
</DataTrigger.ExitActions>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Border.Style>
|
||||
<Grid Width="320" Margin="14,10,14,12">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
</Grid.RowDefinitions>
|
||||
<Grid Grid.Row="0" Margin="0,0,0,8">
|
||||
<TextBlock Text="SETTINGS" Style="{StaticResource SectionHeader}" HorizontalAlignment="Left"/>
|
||||
<Button HorizontalAlignment="Right" VerticalAlignment="Top"
|
||||
Background="Transparent" BorderThickness="0" Cursor="Hand"
|
||||
Command="{Binding OpenSettingsCommand}"
|
||||
ToolTip="Close">
|
||||
<Button.Template>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<ContentPresenter/>
|
||||
</ControlTemplate>
|
||||
</Button.Template>
|
||||
<TextBlock Text="✕" Foreground="{StaticResource Brush.OnSurfaceVariant}" FontSize="14" FontWeight="Bold"/>
|
||||
</Button>
|
||||
</Grid>
|
||||
<controls:SettingsDrawer Grid.Row="1"/>
|
||||
</Grid>
|
||||
</Border>
|
||||
<!-- The stacked white tabs -->
|
||||
<StackPanel Orientation="Vertical" HorizontalAlignment="Right" VerticalAlignment="Stretch">
|
||||
<!-- Settings: the first tab, orange, gear only (creator ruling 2026-11-01 —
|
||||
moved off the top bar). Left-click opens the same app menu
|
||||
(Settings / Bug / Feature / About); right-click goes straight
|
||||
to App Settings, matching the old gear's behaviour exactly. -->
|
||||
<!-- Settings: the first tab, orange, gear only (creator ruling
|
||||
2026-11-01). A left click opens the Settings DRAWER immediately
|
||||
— same behaviour as the other rail tabs. The app menu (Report
|
||||
Bug / Feature Request / About) moved to a right-click on the
|
||||
LlamaCasty wordmark in the top bar. -->
|
||||
<Border Width="30" VerticalAlignment="Center" Background="{StaticResource Brush.Attention}"
|
||||
CornerRadius="4,0,0,4" Padding="0,10" Cursor="Hand" Margin="0,4,0,0"
|
||||
ToolTip="App menu — right-click for Settings"
|
||||
MouseLeftButtonUp="SettingsTab_Click"
|
||||
PreviewMouseRightButtonDown="SettingsTab_PreviewMouseRightButtonDown">
|
||||
<Border.ContextMenu>
|
||||
<ContextMenu DataContext="{Binding PlacementTarget.DataContext, RelativeSource={RelativeSource Self}}">
|
||||
<MenuItem Header="App Settings" Command="{Binding OpenSettingsCommand}"/>
|
||||
<Separator/>
|
||||
<MenuItem Header="Report Bug" Command="{Binding OpenBugCommand}"/>
|
||||
<MenuItem Header="Feature Request" Command="{Binding OpenFeatureCommand}"/>
|
||||
<MenuItem Header="About" Command="{Binding OpenAboutCommand}"/>
|
||||
</ContextMenu>
|
||||
</Border.ContextMenu>
|
||||
ToolTip="Settings"
|
||||
MouseLeftButtonUp="SettingsTab_Click">
|
||||
<Path Data="M19.43,12.98c0.04,-0.32 0.07,-0.65 0.07,-0.98c0,-0.33 -0.03,-0.66 -0.07,-0.98l2.11,-1.65c0.19,-0.15 0.24,-0.42 0.12,-0.64l-2,-3.46c-0.12,-0.22 -0.38,-0.31 -0.61,-0.22l-2.49,1c-0.52,-0.4 -1.08,-0.73 -1.69,-0.98l-0.38,-2.65C14.46,2.18 14.25,2 14,2h-4c-0.25,0 -0.46,0.18 -0.49,0.42l-0.38,2.65c-0.61,0.25 -1.17,0.59 -1.69,0.98l-2.49,-1c-0.23,-0.09 -0.49,0 -0.61,0.22l-2,3.46c-0.13,0.22 -0.07,0.49 0.12,0.64l2.11,1.65c-0.04,0.32 -0.07,0.65 -0.07,0.98c0,0.33 0.03,0.66 0.07,0.98l-2.11,1.65c-0.19,0.15 -0.24,0.42 -0.12,0.64l2,3.46c0.12,0.22 0.38,0.31 0.61,0.22l2.49,-1c0.52,0.4 1.08,0.73 1.69,0.98l0.38,2.65c0.03,0.24 0.24,0.42 0.49,0.42h4c0.25,0 0.46,-0.18 0.49,-0.42l0.38,-2.65c0.61,-0.25 1.17,-0.59 1.69,-0.98l2.49,1c0.23,0.09 0.49,0 0.61,-0.22l2,-3.46c0.12,-0.22 0.07,-0.49 -0.12,-0.64L19.43,12.98zM12,15.5c-1.93,0 -3.5,-1.57 -3.5,-3.5c0,-1.93 1.57,-3.5 3.5,-3.5c1.93,0 3.5,1.57 3.5,3.5C15.5,13.93 13.93,15.5 12,15.5z"
|
||||
Fill="{StaticResource Brush.Background}" Width="18" Height="18" Stretch="Uniform"
|
||||
HorizontalAlignment="Center"/>
|
||||
|
||||
@@ -730,22 +730,11 @@ public partial class PreviewPane : UserControl
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Rail settings tab (creator ruling 2026-11-01): left-click opens the same app menu
|
||||
/// the top-bar gear used to own, so Bug / Feature / About keep their only entry point.</summary>
|
||||
/// <summary>Rail settings tab (creator ruling 2026-11-01): a left click opens the Settings
|
||||
/// DRAWER immediately, exactly like the other rail tabs — no menu, no modal.</summary>
|
||||
private void SettingsTab_Click(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (sender is not FrameworkElement { ContextMenu: { } menu } anchor) return;
|
||||
menu.PlacementTarget = anchor;
|
||||
menu.Placement = System.Windows.Controls.Primitives.PlacementMode.Left;
|
||||
menu.IsOpen = true;
|
||||
if (DataContext is MainViewModel vm) vm.ToggleSettingsDrawer();
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
/// <summary>Right-click goes straight to App Settings — the same shortcut the old gear had.</summary>
|
||||
private void SettingsTab_PreviewMouseRightButtonDown(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
e.Handled = true;
|
||||
if (DataContext is MainViewModel { OpenSettingsCommand: { } open } && open.CanExecute(null))
|
||||
open.Execute(null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
<UserControl x:Class="ytLive.Controls.SettingsDrawer"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:models="clr-namespace:ytLive.Models"
|
||||
xmlns:services="clr-namespace:ytLive.Services">
|
||||
<!-- App Settings, as a RIGHT-EDGE PULL-OUT (creator ruling 2026-11-01): the gear tab opens
|
||||
this drawer immediately, in the same animated-width pattern as Stream Settings / YPP /
|
||||
Socials / TEST. It used to be a centred modal overlay, which then needed its own scroll
|
||||
viewer once the section list outgrew the shell — a drawer has the height of the preview
|
||||
and that problem disappears.
|
||||
|
||||
Categories are headings; their controls are indented under them (Zoom is one). -->
|
||||
<ScrollViewer VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled">
|
||||
<StackPanel Margin="0,0,4,0">
|
||||
|
||||
<!-- ═══ RECORDINGS ═══ -->
|
||||
<TextBlock Text="Recordings" Style="{StaticResource SectionHeader}" Margin="0,0,0,4"/>
|
||||
<TextBlock Text="Default location" Foreground="{StaticResource Brush.OnSurfaceVariant}" FontSize="11" Margin="10,0,0,2"/>
|
||||
<DockPanel Margin="10,0,0,10">
|
||||
<Button DockPanel.Dock="Right" Content="Browse…" Command="{Binding ChooseRecordFolderCommand}"
|
||||
Style="{StaticResource YtButtonSecondary}" Padding="8,2" FontSize="11" Margin="6,0,0,0"/>
|
||||
<Button DockPanel.Dock="Right" Content="Use Downloads" Command="{Binding ResetRecordFolderCommand}"
|
||||
Style="{StaticResource YtButtonSecondary}" Padding="8,2" FontSize="11" Margin="6,0,0,0"/>
|
||||
<TextBox Style="{StaticResource YtTextBoxCompact}" Text="{Binding RecordFolderDisplay, Mode=OneWay}"
|
||||
IsReadOnly="True" Foreground="{StaticResource Brush.OnSurfaceVariant}" VerticalAlignment="Center"/>
|
||||
</DockPanel>
|
||||
|
||||
<!-- ═══ BRAND ═══ -->
|
||||
<TextBlock Text="Brand logo" Style="{StaticResource SectionHeader}" Margin="0,6,0,4"/>
|
||||
<TextBlock Text="Image file (shown on every scene)" Foreground="{StaticResource Brush.OnSurfaceVariant}" FontSize="11" Margin="10,0,0,2"/>
|
||||
<DockPanel Margin="10,0,0,10">
|
||||
<Button DockPanel.Dock="Right" Content="Browse…" Command="{Binding BrowseBrandLogoCommand}"
|
||||
Style="{StaticResource YtButtonSecondary}" Padding="8,2" FontSize="11" Margin="6,0,0,0"/>
|
||||
<!-- A file NAME says nothing about the picture. The thumbnail shows what is set;
|
||||
hovering enlarges it, a click collapses it again. -->
|
||||
<Border x:Name="BrandLogoThumbFrame" DockPanel.Dock="Left" Width="28" Height="28"
|
||||
Margin="0,0,6,0" CornerRadius="4" ClipToBounds="True"
|
||||
Background="{StaticResource Brush.SurfaceContainerLowest}"
|
||||
BorderBrush="{StaticResource Brush.Outline}" BorderThickness="1"
|
||||
Cursor="Hand" ToolTip="Hover to enlarge — click to close"
|
||||
Visibility="{Binding BrandLogoPreviewImage, Converter={StaticResource NotNullToVis}}">
|
||||
<Image x:Name="BrandLogoThumbImage" Source="{Binding BrandLogoPreviewImage}"
|
||||
Stretch="Uniform" MouseEnter="BrandLogoThumb_MouseEnter"
|
||||
MouseLeave="BrandLogoThumb_MouseLeave"
|
||||
MouseLeftButtonUp="BrandLogoThumb_Click"/>
|
||||
</Border>
|
||||
<TextBox Style="{StaticResource YtTextBoxCompact}" Text="{Binding BrandLogoFileDisplay, Mode=OneWay}"
|
||||
IsReadOnly="True" Foreground="{StaticResource Brush.OnSurfaceVariant}" VerticalAlignment="Center"/>
|
||||
</DockPanel>
|
||||
<Popup x:Name="BrandLogoPreviewPopup" PlacementTarget="{Binding ElementName=BrandLogoThumbFrame}"
|
||||
Placement="Left" HorizontalOffset="-6" StaysOpen="True" AllowsTransparency="True"
|
||||
IsHitTestVisible="True" MouseLeave="BrandLogoPreviewPopup_MouseLeave">
|
||||
<Border Padding="6" CornerRadius="6"
|
||||
Background="{StaticResource Brush.SurfaceContainerHighest}"
|
||||
BorderBrush="{StaticResource Brush.Outline}" BorderThickness="1">
|
||||
<StackPanel>
|
||||
<Image Source="{Binding BrandLogoPreviewImage}" MaxWidth="420" MaxHeight="320"
|
||||
Stretch="Uniform" RenderOptions.BitmapScalingMode="HighQuality"/>
|
||||
<TextBlock Text="{Binding BrandLogoFileDisplay}" FontSize="11" Margin="0,6,0,0"
|
||||
Foreground="{StaticResource Brush.OnSurfaceVariant}"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</Popup>
|
||||
|
||||
<!-- ═══ ZOOM ═══ -->
|
||||
<TextBlock Text="Zoom" Style="{StaticResource SectionHeader}" Margin="0,6,0,4"/>
|
||||
|
||||
<TextBlock Text="Zoom view shape" Foreground="{StaticResource Brush.OnSurfaceVariant}" FontSize="11" Margin="10,0,0,2"/>
|
||||
<ComboBox Style="{StaticResource YtComboBox}" Margin="10,0,0,8" SelectedValue="{Binding ZoomShape}" SelectedValuePath="Tag">
|
||||
<ComboBoxItem Content="Circle" Tag="{x:Static models:ZoomShape.Circle}"/>
|
||||
<ComboBoxItem Content="Phone (Vertical Rectangle)" Tag="{x:Static models:ZoomShape.VerticalRectangle}"/>
|
||||
<ComboBoxItem Content="Tablet (Horizontal Rectangle)" Tag="{x:Static models:ZoomShape.HorizontalRectangle}"/>
|
||||
</ComboBox>
|
||||
|
||||
<DockPanel Margin="10,0,0,10">
|
||||
<ToggleButton DockPanel.Dock="Right" Style="{StaticResource PillToggle}"
|
||||
IsChecked="{Binding ZoomFollowsBox, Mode=TwoWay}" VerticalAlignment="Center"
|
||||
ToolTip="Off: the box magnifies the area you clicked. On: it magnifies whatever it hovers over."/>
|
||||
<TextBlock Text="Zoom box follows" Foreground="{StaticResource Brush.OnSurface}" FontSize="11" VerticalAlignment="Center"/>
|
||||
</DockPanel>
|
||||
|
||||
<!-- ═══ SCENE TRANSITIONS ═══ -->
|
||||
<TextBlock Text="Scene transition" Style="{StaticResource SectionHeader}" Margin="0,6,0,4"/>
|
||||
<TextBlock Text="Type" Foreground="{StaticResource Brush.OnSurfaceVariant}" FontSize="11" Margin="10,0,0,2"/>
|
||||
<ComboBox Style="{StaticResource YtComboBox}" Margin="10,0,0,8" SelectedValue="{Binding SelectedTransitionType}" SelectedValuePath="Tag">
|
||||
<ComboBoxItem Content="Cut" Tag="{x:Static services:TransitionType.Cut}"/>
|
||||
<ComboBoxItem Content="Fade" Tag="{x:Static services:TransitionType.Fade}"/>
|
||||
<ComboBoxItem Content="Move" Tag="{x:Static services:TransitionType.Move}"/>
|
||||
<ComboBoxItem Content="Wipe" Tag="{x:Static services:TransitionType.Wipe}"/>
|
||||
</ComboBox>
|
||||
<DockPanel Margin="10,0,0,10">
|
||||
<TextBlock DockPanel.Dock="Right" Foreground="{StaticResource Brush.OnSurfaceVariant}" FontSize="11" VerticalAlignment="Center" Margin="8,0,0,0" Width="56">
|
||||
<Run Text="{Binding TransitionDurationMs, StringFormat={}{0:0}}"/><Run Text=" ms"/>
|
||||
</TextBlock>
|
||||
<Slider Minimum="100" Maximum="2000" TickFrequency="50" IsSnapToTickEnabled="True"
|
||||
Value="{Binding TransitionDurationMs, Mode=TwoWay}" VerticalAlignment="Center"/>
|
||||
</DockPanel>
|
||||
|
||||
<!-- Development panel host. Empty in a Release build: the panel itself is excluded
|
||||
from the Release compile, and the code-behind only fills this inside #if DEBUG. -->
|
||||
<StackPanel x:Name="DeveloperPanelHost"/>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,58 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace ytLive.Controls;
|
||||
|
||||
/// <summary>
|
||||
/// App Settings as a right-edge pull-out (creator ruling 2026-11-01). Hosted by
|
||||
/// <see cref="PreviewPane"/>'s drawer stack and opened by the gear rail tab, exactly like Stream
|
||||
/// Settings / YPP / Socials / TEST.
|
||||
///
|
||||
/// <para>Born from the modal overlay: that shell was MaxHeight 700, so once the section list grew
|
||||
/// the bottom options were clipped and unreachable — including the zoom shape the creator was
|
||||
/// looking for. A drawer is the height of the preview, so the list can grow without a second
|
||||
/// scroll decision to get wrong.</para>
|
||||
/// </summary>
|
||||
public partial class SettingsDrawer : UserControl
|
||||
{
|
||||
public SettingsDrawer()
|
||||
{
|
||||
InitializeComponent();
|
||||
#if DEBUG
|
||||
// Development affordances are added at runtime, never declared in XAML: the panel is
|
||||
// excluded from the Release compile (ytLive.csproj), so this line is the only reference.
|
||||
DeveloperPanelHost.Children.Add(new DeveloperPanel());
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>Is the enlarged brand-logo preview showing? Test seam — a Popup is a separate
|
||||
/// window, so nothing else can observe it.</summary>
|
||||
internal bool IsBrandLogoPreviewOpen => BrandLogoPreviewPopup.IsOpen;
|
||||
|
||||
/// <summary>The thumbnail is hidden when no image is set (see the XAML), so there is nothing to
|
||||
/// open in that case — no empty frame can appear.</summary>
|
||||
internal void OpenBrandLogoPreview() => BrandLogoPreviewPopup.IsOpen = true;
|
||||
|
||||
internal void CloseBrandLogoPreview() => BrandLogoPreviewPopup.IsOpen = false;
|
||||
|
||||
/// <summary>Hover opens the enlargement. Nothing to show when no image is set, so the
|
||||
/// thumbnail simply does nothing in that case.</summary>
|
||||
private void BrandLogoThumb_MouseEnter(object sender, MouseEventArgs e)
|
||||
=> BrandLogoPreviewPopup.IsOpen = true;
|
||||
|
||||
/// <summary>Leaving the thumbnail closes it — unless the pointer walked onto the popup
|
||||
/// itself, where it stays put so the creator can look closely.</summary>
|
||||
private void BrandLogoThumb_MouseLeave(object sender, MouseEventArgs e)
|
||||
{
|
||||
if (BrandLogoPreviewPopup.Child?.IsMouseOver == true) return;
|
||||
BrandLogoPreviewPopup.IsOpen = false;
|
||||
}
|
||||
|
||||
/// <summary>A click collapses the enlargement — the explicit "enough, thanks" gesture.</summary>
|
||||
private void BrandLogoThumb_Click(object sender, MouseButtonEventArgs e)
|
||||
=> BrandLogoPreviewPopup.IsOpen = false;
|
||||
|
||||
private void BrandLogoPreviewPopup_MouseLeave(object sender, MouseEventArgs e)
|
||||
=> BrandLogoPreviewPopup.IsOpen = false;
|
||||
}
|
||||
+14
-3
@@ -21,11 +21,22 @@
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- LOGO / BRAND — clicking opens the in-app About overlay. -->
|
||||
<!-- LOGO / BRAND — left-click opens the in-app About overlay; right-click opens the app
|
||||
menu (Report Bug / Feature Request / About). App Settings is no longer in this
|
||||
menu: the gear rail tab opens the Settings drawer directly (creator ruling
|
||||
2026-11-01). -->
|
||||
<Button Grid.Column="0" Background="Transparent" BorderThickness="0" Cursor="Hand"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center" ToolTip="About LlamaCasty"
|
||||
Command="{Binding OpenAboutCommand}">
|
||||
VerticalAlignment="Center" ToolTip="About LlamaCasty — right-click for the menu"
|
||||
Command="{Binding OpenAboutCommand}"
|
||||
PreviewMouseRightButtonDown="BrandWordmark_PreviewMouseRightButtonDown">
|
||||
<Button.ContextMenu>
|
||||
<ContextMenu DataContext="{Binding PlacementTarget.DataContext, RelativeSource={RelativeSource Self}}">
|
||||
<MenuItem Header="Report Bug" Command="{Binding OpenBugCommand}"/>
|
||||
<MenuItem Header="Feature Request" Command="{Binding OpenFeatureCommand}"/>
|
||||
<MenuItem Header="About" Command="{Binding OpenAboutCommand}"/>
|
||||
</ContextMenu>
|
||||
</Button.ContextMenu>
|
||||
<Button.Template>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<ContentPresenter/>
|
||||
|
||||
@@ -21,6 +21,20 @@ public partial class TopBar : UserControl
|
||||
AvatarInitial.Visibility = System.Windows.Visibility.Collapsed;
|
||||
}
|
||||
|
||||
/// <summary>Right-click on the LlamaCasty wordmark opens the app menu (Report Bug / Feature
|
||||
/// Request / About) — the menu that used to hang off the gear, which now opens the Settings
|
||||
/// drawer directly (creator ruling 2026-11-01).</summary>
|
||||
private void BrandWordmark_PreviewMouseRightButtonDown(object sender, System.Windows.Input.MouseButtonEventArgs e)
|
||||
{
|
||||
if (sender is Button { ContextMenu: { } menu } button)
|
||||
{
|
||||
menu.PlacementTarget = button;
|
||||
menu.Placement = System.Windows.Controls.Primitives.PlacementMode.Bottom;
|
||||
menu.IsOpen = true;
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
|
||||
public void RefreshAvatar()
|
||||
{
|
||||
_ = LoadAvatarImageAsync();
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
namespace ytLive.ViewModels;
|
||||
|
||||
/// <summary>
|
||||
/// App Settings as a right-edge drawer (creator ruling 2026-11-01): the gear rail tab opens it
|
||||
/// immediately, in the same exclusive-slide pattern as Stream Settings / YPP / Socials / TEST.
|
||||
///
|
||||
/// <para>It is a separate flag from <see cref="IsSettingsOpen"/> because the overlay shell now
|
||||
/// only carries Report Bug / Feature Request / About — settings stopped being a modal.</para>
|
||||
/// </summary>
|
||||
public partial class MainViewModel
|
||||
{
|
||||
private bool _settingsDrawerOpen;
|
||||
|
||||
public bool SettingsDrawerOpen
|
||||
{
|
||||
get => _settingsDrawerOpen;
|
||||
private set => SetProperty(ref _settingsDrawerOpen, value);
|
||||
}
|
||||
|
||||
internal void ToggleSettingsDrawer()
|
||||
{
|
||||
if (_settingsDrawerOpen)
|
||||
{
|
||||
SettingsDrawerOpen = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// One drawer at a time, like the rail's other tabs.
|
||||
BroadcastForm.IsDrawerOpen = false;
|
||||
Ypp.IsDrawerOpen = false;
|
||||
TestSession.IsDrawerOpen = false;
|
||||
if (SocialsDrawerOpen) CloseSocialsDrawer();
|
||||
|
||||
SettingsDrawerOpen = true;
|
||||
}
|
||||
|
||||
/// <summary>Called by the other drawers when they open, so settings yields to them.</summary>
|
||||
private void CloseSettingsDrawerIfOpen()
|
||||
{
|
||||
if (_settingsDrawerOpen) SettingsDrawerOpen = false;
|
||||
}
|
||||
}
|
||||
@@ -185,6 +185,7 @@ public partial class MainViewModel
|
||||
BroadcastForm.IsDrawerOpen = false;
|
||||
Ypp.IsDrawerOpen = false;
|
||||
TestSession.IsDrawerOpen = false;
|
||||
CloseSettingsDrawerIfOpen();
|
||||
SocialsDrawerOpen = true;
|
||||
OnPropertyChanged(nameof(SocialsDrawer));
|
||||
OnPropertyChanged(nameof(SocialsDrawerOpen));
|
||||
|
||||
@@ -39,6 +39,7 @@ public partial class MainViewModel
|
||||
Ypp.IsDrawerOpen = false;
|
||||
TestSession.IsDrawerOpen = false;
|
||||
CloseSocialsDrawer();
|
||||
CloseSettingsDrawerIfOpen();
|
||||
}
|
||||
};
|
||||
Ypp.PropertyChanged += (_, e) =>
|
||||
@@ -48,6 +49,7 @@ public partial class MainViewModel
|
||||
BroadcastForm.IsDrawerOpen = false;
|
||||
TestSession.IsDrawerOpen = false;
|
||||
CloseSocialsDrawer();
|
||||
CloseSettingsDrawerIfOpen();
|
||||
}
|
||||
};
|
||||
TestSession.PropertyChanged += (_, e) =>
|
||||
@@ -57,6 +59,7 @@ public partial class MainViewModel
|
||||
BroadcastForm.IsDrawerOpen = false;
|
||||
Ypp.IsDrawerOpen = false;
|
||||
CloseSocialsDrawer();
|
||||
CloseSettingsDrawerIfOpen();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -232,7 +232,7 @@ public partial class MainViewModel : ViewModelBase
|
||||
ToggleMicMuteCommand = new RelayCommand(_ => ToggleMicMute());
|
||||
ToggleGameMuteCommand = new RelayCommand(_ => ToggleGameMute());
|
||||
OpenMicPickerCommand = new RelayCommand(_ => PickMicrophone());
|
||||
OpenSettingsCommand = new RelayCommand(_ => ShowOverlay(nameof(IsSettingsOpen), "App Settings"));
|
||||
OpenSettingsCommand = new RelayCommand(_ => ToggleSettingsDrawer());
|
||||
InitPreviewHistory();
|
||||
OpenBugCommand = new RelayCommand(_ => ShowOverlay(nameof(IsBugOpen), "Report Bug"));
|
||||
OpenFeatureCommand = new RelayCommand(_ => ShowOverlay(nameof(IsFeatureOpen), "Feature Request"));
|
||||
|
||||
@@ -103,6 +103,7 @@ The app is a **director's control surface**, not an editor. This replaces the OB
|
||||
**Current layout (2026-10-26 redesign commits):** `MainWindow` has a top bar and health banner, then a two-column work area with a fixed 360px left column and preview column. Its top row holds stream readouts/resolution on the left and the five-scene strip above the preview; the lower row holds the left panel and central preview. The bottom bar is now only a height-preserving spacer.
|
||||
|
||||
- **Left panel:** `EditingActive` is an explicit `EDIT` toggle, not a mode inferred from `StreamStatus`. On = scene layers + properties; off = YouTube chat. Go Live turns editing off and stopping restores it; recording does not automatically switch it off. The staged scene is the preview/edit target; `LiveScene` is the program output.
|
||||
- **Settings is a right-edge drawer, not a modal (2026-11-01):** the gear rail tab opens `Controls/SettingsDrawer` in the same animated-width stack as Stream Settings / YPP / Socials / TEST (`SettingsDrawerOpen`, exclusive with the other four). Categories are section headings with their controls indented under them — Zoom holds *Zoom view shape* and *Zoom box follows*. The app menu (Report Bug / Feature Request / About) hangs off a **right-click on the LlamaCasty wordmark**; left-click still opens About. The modal shell keeps only Bug / Feature / About, which is why it no longer needs a scroll viewer.
|
||||
- **Top bar:** the `EDIT` toggle sits beside the REC/ON-AIR session controls; the brand and account surfaces remain at the sides. Its **right edge aligns with the layers panel below** (`TopBar` column 0 is a fixed **352** = work-area margin 8 + 360px column − the bar's own 16px padding), so the button and the panel share an edge. The **settings gear moved to the preview rail** (2026-11-01): first tab, orange (`Brush.Attention`), gear icon only — left-click opens the app menu (Settings / Bug / Feature / About), right-click goes straight to App Settings.
|
||||
- **Preview gestures (2026-11-01):** *edit mode* — left selects/drags/resizes, right opens the context menu, **Ctrl+right toggles the zoom box**. *Preview mode* — **left toggles the zoom box**, right opens the context menu; **Ctrl+right does nothing at all** (no chord while live or recording — one hand, always). ⚠️ **Both the Down and the Up event must be handled for a Ctrl+right-click**: WPF's `PopupControlService` opens a context menu on right-button-**up**, so suppressing only the down event leaves the menu appearing under the gesture (found 2026-11-01 in edit mode). Menus exist on the webcam layer and the backdrop; every other layer right-clicks through to the backdrop menu. Left-dragging with the zoom box open moves it, in both modes. **Ctrl-down closes it**: pressing Ctrl while the box is open dismisses it, so the same hand that opened it can put it away (`MainWindow_PreviewKeyDown`; the box cannot cancel itself on creation because Ctrl is already down before it exists). **Follow mode** is a Settings pill, default **off = Anchor**: with Follow on, dragging re-anchors the capture to the box centre (`ZoomFollowsBox`, persisted as the `Zoom.FollowBox` setting). **The gesture activates the box at the cursor**, not merely dismisses it: `ActivateZoom` no longer refuses when idle (it used to return unless `IsLive || IsRecording`, and `ToggleZoom` only ever dismissed — so the gesture did nothing at all outside a session until 2026-11-01). The social bar consumes its own click (toggle top ⇄ bottom), and the preview's tunnelling handler steps aside for it — otherwise the bar is unclickable in preview mode and drags the layer beneath it in edit mode.
|
||||
- **Preview undo/redo (2026-11-01):** `Ctrl+Z` / `Ctrl+Shift+Z`, window-level bindings, live only while `EditingActive` — undo with the preview showing program output would rewrite a scene nobody is looking at. `Models/PreviewHistory` snapshots element **references** plus geometry, visibility, order and membership, so move / resize / reorder / add / remove / hide all undo in one step per gesture (`PushPreviewUndo` is called once at drag or resize start, never inside the move loop). Property edits (colour, opacity, border, font) are **not** captured yet — stated in the type's doc comment so nobody assumes otherwise. The stack clears on layout load. Guard: `ytLive.Tests/PreviewHistoryTests.cs`.
|
||||
|
||||
@@ -21,7 +21,7 @@ public class BrandLogoPreviewTests
|
||||
{
|
||||
// 1. Wiring: the thumbnail is image-bound and hidden when no image is set, the enlarged
|
||||
// popup shows the same source at a readable size, and every gesture reaches a handler.
|
||||
var xaml = SourceAssert.Text("Controls", "OverlayHost.xaml");
|
||||
var xaml = SourceAssert.Text("Controls", "SettingsDrawer.xaml");
|
||||
Assert.Contains("x:Name=\"BrandLogoThumbImage\"", xaml);
|
||||
Assert.Contains("{Binding BrandLogoPreviewImage}", xaml);
|
||||
Assert.Contains("NotNullToVis", xaml);
|
||||
@@ -41,7 +41,7 @@ public class BrandLogoPreviewTests
|
||||
{
|
||||
// A Popup refuses to open until its placement target is in a live visual tree, so the
|
||||
// control is hosted in a real window — the same condition the app provides.
|
||||
var host = new ytLive.Controls.OverlayHost();
|
||||
var host = new ytLive.Controls.SettingsDrawer();
|
||||
var window = new System.Windows.Window { Content = host, Width = 420, Height = 420 };
|
||||
window.Show();
|
||||
try
|
||||
|
||||
Reference in New Issue
Block a user