feat(text): add static text editor and preview
TextOverlay layers now have a multiline editor, Windows font and size controls, color/emphasis/alignment settings, and a visible draggable preview. Text sources follow OBS's static text-source conventions: https://obsproject.com/kb/text-sources
This commit is contained in:
+63
-1
@@ -2,7 +2,8 @@
|
||||
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:models="clr-namespace:ytLive.Models">
|
||||
xmlns:models="clr-namespace:ytLive.Models"
|
||||
xmlns:media="clr-namespace:System.Windows.Media;assembly=PresentationCore">
|
||||
<UserControl.Resources>
|
||||
<!-- TASK 63: the border-colour drop-down is the app's own YtComboBox, so the
|
||||
colour entries are drawn by TEMPLATES, in the theme's colours and with the
|
||||
@@ -317,6 +318,67 @@
|
||||
<ScrollViewer x:Name="ElementsScrollViewer" Grid.Row="6" VerticalScrollBarVisibility="Auto" Background="Transparent"
|
||||
Visibility="{Binding SelectedElement, Converter={StaticResource NotNullToVis}}">
|
||||
<StackPanel x:Name="ElementsPanel" Margin="8,4,0,0">
|
||||
<StackPanel Visibility="{Binding SelectedElement.IsTextOverlay, Converter={StaticResource BoolToVis}}" Margin="0,0,0,8">
|
||||
<TextBlock Text="Text" Style="{StaticResource PropertyHeader}"/>
|
||||
<TextBlock Text="Static text — it will not animate automatically. Move and resize it in the preview."
|
||||
Style="{StaticResource PropertyHint}" Margin="0,0,0,6" TextWrapping="Wrap"/>
|
||||
<TextBox x:Name="TextLayerEditor" Text="{Binding SelectedElement.TextContent, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
|
||||
Style="{StaticResource YtTextBoxCompact}" AcceptsReturn="True" TextWrapping="Wrap"
|
||||
VerticalScrollBarVisibility="Auto" MinHeight="72" MaxHeight="120"
|
||||
AutomationProperties.Name="Text layer content"/>
|
||||
<TextBlock Text="Font" Style="{StaticResource PropertyHeader}"/>
|
||||
<ComboBox IsEditable="True" ItemsSource="{x:Static media:Fonts.SystemFontFamilies}"
|
||||
DisplayMemberPath="Source"
|
||||
Text="{Binding SelectedElement.TextFontFamily, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
|
||||
Style="{StaticResource YtComboBox}" Height="26" Margin="0,0,0,6"
|
||||
AutomationProperties.Name="Text layer font"/>
|
||||
<DockPanel Margin="0,0,0,6" LastChildFill="False">
|
||||
<TextBlock DockPanel.Dock="Left" Text="Size" Style="{StaticResource PropertyFieldLabel}"
|
||||
Width="42" VerticalAlignment="Center"/>
|
||||
<ComboBox IsEditable="True"
|
||||
Text="{Binding SelectedElement.TextFontSize, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
|
||||
Style="{StaticResource YtComboBox}" Height="26" Width="76"
|
||||
AutomationProperties.Name="Text layer size">
|
||||
<ComboBoxItem Content="12"/><ComboBoxItem Content="16"/>
|
||||
<ComboBoxItem Content="20"/><ComboBoxItem Content="24"/>
|
||||
<ComboBoxItem Content="32"/><ComboBoxItem Content="36"/>
|
||||
<ComboBoxItem Content="48"/><ComboBoxItem Content="60"/>
|
||||
<ComboBoxItem Content="72"/><ComboBoxItem Content="96"/>
|
||||
<ComboBoxItem Content="120"/><ComboBoxItem Content="144"/>
|
||||
<ComboBoxItem Content="180"/><ComboBoxItem Content="240"/>
|
||||
<ComboBoxItem Content="300"/>
|
||||
</ComboBox>
|
||||
</DockPanel>
|
||||
<DockPanel Margin="0,0,0,6" LastChildFill="False">
|
||||
<TextBlock DockPanel.Dock="Left" Text="Color" Style="{StaticResource PropertyFieldLabel}"
|
||||
Width="42" VerticalAlignment="Center"/>
|
||||
<TextBox x:Name="TextLayerColorEditor"
|
||||
Text="{Binding SelectedElement.TextColor, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
|
||||
Style="{StaticResource YtTextBoxCompact}" MaxLength="9" Width="82"
|
||||
ToolTip="#RRGGBB or #AARRGGBB" AutomationProperties.Name="Text layer color"/>
|
||||
<Border Width="18" Height="18" Margin="8,0,0,0" CornerRadius="2"
|
||||
Background="{Binding SelectedElement.TextColor, Converter={StaticResource HexToBrush}}"
|
||||
BorderBrush="{StaticResource Brush.Outline}" BorderThickness="1"
|
||||
VerticalAlignment="Center"/>
|
||||
</DockPanel>
|
||||
<WrapPanel Margin="0,0,0,4">
|
||||
<CheckBox Style="{StaticResource PropertyPillCheck}" Content="Bold" Margin="0,0,8,4"
|
||||
IsChecked="{Binding SelectedElement.TextBold, Mode=TwoWay}"/>
|
||||
<CheckBox Style="{StaticResource PropertyPillCheck}" Content="Italic" Margin="0,0,8,4"
|
||||
IsChecked="{Binding SelectedElement.TextItalic, Mode=TwoWay}"/>
|
||||
<CheckBox Style="{StaticResource PropertyPillCheck}" Content="Underline" Margin="0,0,8,4"
|
||||
IsChecked="{Binding SelectedElement.TextUnderline, Mode=TwoWay}"/>
|
||||
</WrapPanel>
|
||||
<TextBlock Text="Alignment" Style="{StaticResource PropertyHeader}"/>
|
||||
<ComboBox SelectedValuePath="Tag"
|
||||
SelectedValue="{Binding SelectedElement.TextAlignment, Mode=TwoWay}"
|
||||
Style="{StaticResource YtComboBox}" Height="26">
|
||||
<ComboBoxItem Content="Left" Tag="{x:Static models:TextLayerAlignment.Left}"/>
|
||||
<ComboBoxItem Content="Center" Tag="{x:Static models:TextLayerAlignment.Center}"/>
|
||||
<ComboBoxItem Content="Right" Tag="{x:Static models:TextLayerAlignment.Right}"/>
|
||||
</ComboBox>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Opacity + mirror/shape (draggable elements: image, webcam) -->
|
||||
<StackPanel Visibility="{Binding SelectedElement.IsDraggable, Converter={StaticResource BoolToVis}}" Margin="0,0,0,8">
|
||||
<StackPanel Visibility="{Binding SelectedElement.IsChatBox, Converter={StaticResource InverseBoolToVis}}">
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
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:controls="clr-namespace:ytLive.Controls">
|
||||
xmlns:local="clr-namespace:ytLive" xmlns:controls="clr-namespace:ytLive.Controls"
|
||||
xmlns:models="clr-namespace:ytLive.Models">
|
||||
<Border Background="{StaticResource Brush.SurfaceContainerLowest}" CornerRadius="6" Margin="8,0"
|
||||
BorderBrush="{Binding PreviewGlowBrush}" BorderThickness="{Binding PreviewGlowThickness}">
|
||||
<Grid>
|
||||
@@ -175,6 +176,41 @@
|
||||
</Style>
|
||||
</Image.Style>
|
||||
</Image>
|
||||
<TextBlock Tag="TextLayerPreview" Text="{Binding TextContent}"
|
||||
FontFamily="{Binding TextFontFamily}"
|
||||
FontSize="{Binding TextFontSize}"
|
||||
Foreground="{Binding TextColor, Converter={StaticResource HexToBrush}}"
|
||||
TextWrapping="Wrap" TextTrimming="CharacterEllipsis"
|
||||
IsHitTestVisible="False">
|
||||
<TextBlock.Style>
|
||||
<Style TargetType="TextBlock">
|
||||
<Setter Property="Visibility" Value="Collapsed"/>
|
||||
<Setter Property="TextAlignment" Value="Left"/>
|
||||
<Setter Property="FontWeight" Value="Normal"/>
|
||||
<Setter Property="FontStyle" Value="Normal"/>
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding IsTextOverlay}" Value="True">
|
||||
<Setter Property="Visibility" Value="Visible"/>
|
||||
</DataTrigger>
|
||||
<DataTrigger Binding="{Binding TextBold}" Value="True">
|
||||
<Setter Property="FontWeight" Value="Bold"/>
|
||||
</DataTrigger>
|
||||
<DataTrigger Binding="{Binding TextItalic}" Value="True">
|
||||
<Setter Property="FontStyle" Value="Italic"/>
|
||||
</DataTrigger>
|
||||
<DataTrigger Binding="{Binding TextUnderline}" Value="True">
|
||||
<Setter Property="TextDecorations" Value="{x:Static TextDecorations.Underline}"/>
|
||||
</DataTrigger>
|
||||
<DataTrigger Binding="{Binding TextAlignment}" Value="{x:Static models:TextLayerAlignment.Center}">
|
||||
<Setter Property="TextAlignment" Value="Center"/>
|
||||
</DataTrigger>
|
||||
<DataTrigger Binding="{Binding TextAlignment}" Value="{x:Static models:TextLayerAlignment.Right}">
|
||||
<Setter Property="TextAlignment" Value="Right"/>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</TextBlock.Style>
|
||||
</TextBlock>
|
||||
<Viewbox Stretch="Uniform">
|
||||
<Grid Width="1" Height="1">
|
||||
<!-- Round webcam: a plain Image (the same efficient WriteableBitmap
|
||||
|
||||
+2
-2
@@ -10,9 +10,9 @@ See [`schema.md`](../schema.md) for the memory-map conventions.
|
||||
| `BottomBar.xaml(.cs)` | Footer (Row 3): intentionally empty, height-preserving spacer; stream readouts and resolution picker moved to the top-left cell in the 2026-10-26 surface redesign |
|
||||
| `HealthBanner.xaml(.cs)` | Row 1 status banner + the 7 value→visibility converters live in the app theme, not here |
|
||||
| `TopBar.xaml(.cs)` | Row 0 session surface: brand + gear menu (left), explicit `EDIT` toggle beside the REC|ON-AIR mode switch and world actions (Record: Start Recording · Stream: Go Live + Test), running-state reality line + End (center), account zone (right). Left-click gear opens the menu; right-click opens App Settings directly. `RefreshAvatar()`, `GearButton_Click` |
|
||||
| `PreviewPane.xaml(.cs)` | Row 2 Col 1 center: 1920×1080 `OverlayCanvas`/`SelectionOverlay`/`BrandFlashLayer`/`SocialBarElement`, text drawer, outgoing-picture transition overlay, right-edge Stream Info/YPP/Socials/TEST tabs and drawers, and live controls (TRAX / game / mic meters + sliders + speakers). The list-selected draggable layer is temporarily raised in preview z-order and custom hit-testing; deselection restores the scene's unchanged order (`SelectedLayerPreviewHitTestingTests`). **TASK 57:** dragging/resizing a webcam in the vertical tier routes through `MainViewModel.MoveWebcamInVerticalTier` / `ResizeWebcamInVerticalTier` for the six-spot snap or Chat camera's two sizes. |
|
||||
| `PreviewPane.xaml(.cs)` | Row 2 Col 1 center: 1920×1080 `OverlayCanvas`/`SelectionOverlay`/`BrandFlashLayer`/`SocialBarElement`, static TextOverlay `TextBlock` preview, text drawer, outgoing-picture transition overlay, right-edge Stream Info/YPP/Socials/TEST tabs and drawers, and live controls (TRAX / game / mic meters + sliders + speakers). The list-selected draggable layer is temporarily raised in preview z-order and custom hit-testing; deselection restores the scene's unchanged order (`SelectedLayerPreviewHitTestingTests`). **TASK 57:** dragging/resizing a webcam in the vertical tier routes through `MainViewModel.MoveWebcamInVerticalTier` / `ResizeWebcamInVerticalTier` for the six-spot snap or Chat camera's two sizes. |
|
||||
| `SceneThumbnailStrip.xaml(.cs)` | Top row over the preview column: five fixed-size scene cards with scene name, hotkey, staged `PROGRAM` badge, orange staged/live border |
|
||||
| `LeftPanel.xaml(.cs)` | Row 2 Col 0 left panel: explicit EditingActive toggle on → Scene Layers+properties; off → YouTube chat, independent of stream status. Source add/edit/remove, list drag-to-reorder, snapshot revert-accept. **TASK 51:** the webcam properties carry the Background section — `RemoveBackgroundPill` / `BlurBackgroundPill` (`PillToggle`, radio-exclusive via `WebcamSceneConfig.RemoveBackground`/`BlurBackground`) + `BackgroundStrengthSlider` (0..1, 5% snaps, shown only while an effect is on). **TASK 57:** the webcam properties carry a Geometry section (X/Y/W/H) — manual placement of the camera, bound to the **live** placement (the one the current tier shows), gated on `IsWebcamSelected` (not `SelectedElement.IsWebcam` — the TASK 51 test finds that panel by binding text and would match twice); the Shape button's `Visibility` is bound to `CanToggleClipShape`, so the circle option is NOT OFFERED in the vertical Chat layout. **TASK 58:** every drop-down, check box and field in the properties panel carries the theme's style (`YtComboBox`/`YtCheckBox`/`YtTextBoxCompact`) and paints no colour of its own — the guard is `ytLive.Tests/PropertiesStyleTests.cs`. **TASK 59:** the webcam's properties are a HIERARCHY — a `Shape` title owns the mirror switch and the shape choice (`Rect` | `Circle` as two `SegmentToggle` options, Rect selected by default, the circle option hidden where `CanToggleClipShape` is false), and every titled block (Geometry, Border Color, Border Thickness, Background) owns its content one indent deeper (Strength sits under Blur). **TASK 60:** the border-colour drop-down builds its menu from `Models/BorderColorPalette.cs` — each row is the hex plus the colour's NAME (`#e94560 Crimson`, tooltip "Crimson (#e94560)"); the swatch icon is unchanged. **TASK 61:** the four geometry fields (X/Y/W/H) share one fixed width (44px — the largest value, 1920, plus a sign) and their rows set `LastChildFill="False"`, so Y/H no longer stretch to fill the row. **TASK 62:** the border-colour field shows the palette's name beside the hex (`SelectedElement.BorderColorName`, empty for a custom hex) and keeps a fixed width so the name stays next to the hex. **TASK 63:** the border colour is picked with the **app's own drop-down** (`YtComboBox`) — the `...` button and its code-built `ContextMenu` are gone; the closed box shows the swatch + the colour's NAME, the rows show swatch + hex + name (the panel's own `DataTemplate`s + `Helpers/ClosedBoxTemplateSelector.cs`), and the hex field stays for a custom colour; `IsDescendantOf` falls back to `LogicalTreeHelper.GetParent` / `TemplatedParent` when `VisualTreeHelper.GetParent` returns `null` (at `PopupRoot`) so clicking a popup row does not trigger `MainWindow`'s deselection guard |
|
||||
| `LeftPanel.xaml(.cs)` | Row 2 Col 0 left panel: explicit EditingActive toggle on → Scene Layers+properties; off → YouTube chat, independent of stream status. Source add/edit/remove, list drag-to-reorder, snapshot revert-accept; TextOverlay properties edit multiline content, font, size, color, emphasis, and alignment with a static-text notice. **TASK 51:** the webcam properties carry the Background section — `RemoveBackgroundPill` / `BlurBackgroundPill` (`PillToggle`, radio-exclusive via `WebcamSceneConfig.RemoveBackground`/`BlurBackground`) + `BackgroundStrengthSlider` (0..1, 5% snaps, shown only while an effect is on). **TASK 57:** the webcam properties carry a Geometry section (X/Y/W/H) — manual placement of the camera, bound to the **live** placement (the one the current tier shows), gated on `IsWebcamSelected` (not `SelectedElement.IsWebcam` — the TASK 51 test finds that panel by binding text and would match twice); the Shape button's `Visibility` is bound to `CanToggleClipShape`, so the circle option is NOT OFFERED in the vertical Chat layout. **TASK 58:** every drop-down, check box and field in the properties panel carries the theme's style (`YtComboBox`/`YtCheckBox`/`YtTextBoxCompact`) and paints no colour of its own — the guard is `ytLive.Tests/PropertiesStyleTests.cs`. **TASK 59:** the webcam's properties are a HIERARCHY — a `Shape` title owns the mirror switch and the shape choice (`Rect` | `Circle` as two `SegmentToggle` options, Rect selected by default, the circle option hidden where `CanToggleClipShape` is false), and every titled block (Geometry, Border Color, Border Thickness, Background) owns its content one indent deeper (Strength sits under Blur). **TASK 60:** the border-colour drop-down builds its menu from `Models/BorderColorPalette.cs` — each row is the hex plus the colour's NAME (`#e94560 Crimson`, tooltip "Crimson (#e94560)"); the swatch icon is unchanged. **TASK 61:** the four geometry fields (X/Y/W/H) share one fixed width (44px — the largest value, 1920, plus a sign) and their rows set `LastChildFill="False"`, so Y/H no longer stretch to fill the row. **TASK 62:** the border-colour field shows the palette's name beside the hex (`SelectedElement.BorderColorName`, empty for a custom hex) and keeps a fixed width so the name stays next to the hex. **TASK 63:** the border colour is picked with the **app's own drop-down** (`YtComboBox`) — the `...` button and its code-built `ContextMenu` are gone; the closed box shows the swatch + the colour's NAME, the rows show swatch + hex + name (the panel's own `DataTemplate`s + `Helpers/ClosedBoxTemplateSelector.cs`), and the hex field stays for a custom colour; `IsDescendantOf` falls back to `LogicalTreeHelper.GetParent` / `TemplatedParent` when `VisualTreeHelper.GetParent` returns `null` (at `PopupRoot`) so clicking a popup row does not trigger `MainWindow`'s deselection guard |
|
||||
| `OverlayHost.xaml(.cs)` | Root-grid overlays: App Settings / Report Bug / Feature Request / About; About includes the output-quality conversion copy and an explicit llamacasty.com link, while the licensing/legal panel remains in-app. App Settings groups recording, brand-logo, preview, and transition controls. `BackToAbout_Click` lives here |
|
||||
| `SceneThumbnailStrip.xaml.cs` | Code-behind for the strip above |
|
||||
| `LeftPanel.xaml.cs` | Code-behind for the panel above |
|
||||
|
||||
+1
-1
@@ -111,7 +111,7 @@ public class Source : SceneElement
|
||||
/// and sizes it in the preview, so it needs the same selection box, grab and resize handle
|
||||
/// (creator ruling 2026-11-01).</summary>
|
||||
public override bool IsDraggable => Type is SourceType.Image or SourceType.WebSource or SourceType.ChatBox
|
||||
or SourceType.AlertBox or SourceType.Blur or SourceType.Ticker;
|
||||
or SourceType.TextOverlay or SourceType.AlertBox or SourceType.Blur or SourceType.Ticker;
|
||||
|
||||
/// <summary>Live captures, chat, web sources and media are dynamic; static art is fixed.</summary>
|
||||
public override ElementKind Kind => Type is SourceType.DisplayCapture or SourceType.WindowCapture
|
||||
|
||||
@@ -62,22 +62,26 @@ public partial class MainViewModel
|
||||
? $"{baseName}-{webCount}"
|
||||
: NextSourceName(scene, baseName),
|
||||
Type = sourceType,
|
||||
X = sourceType == SourceType.ChatBox ? 1400
|
||||
X = sourceType == SourceType.TextOverlay ? 160
|
||||
: sourceType == SourceType.ChatBox ? 1400
|
||||
: sourceType == SourceType.AlertBox ? 620
|
||||
: sourceType == SourceType.Blur ? 700 : 0,
|
||||
// The scoreboard is a full-width bar: its Y comes from the edge it owns (resolved
|
||||
// against the social bar at render time), never from a drag.
|
||||
Y = sourceType == SourceType.Scoreboard ? 1024
|
||||
Y = sourceType == SourceType.TextOverlay ? 120
|
||||
: sourceType == SourceType.Scoreboard ? 1024
|
||||
: sourceType == SourceType.ChatBox ? 20
|
||||
: sourceType == SourceType.AlertBox ? 430
|
||||
: sourceType == SourceType.Blur ? 600 : 0,
|
||||
Width = sourceType == SourceType.WebSource ? 640
|
||||
Width = sourceType == SourceType.TextOverlay ? 900
|
||||
: sourceType == SourceType.WebSource ? 640
|
||||
: sourceType == SourceType.ChatBox ? 500
|
||||
: sourceType == SourceType.AlertBox ? 680
|
||||
: sourceType == SourceType.Scoreboard ? 1920
|
||||
: sourceType == SourceType.Ticker ? 1920
|
||||
: sourceType == SourceType.Blur ? 240 : 0,
|
||||
Height = sourceType == SourceType.WebSource ? 480
|
||||
Height = sourceType == SourceType.TextOverlay ? 140
|
||||
: sourceType == SourceType.WebSource ? 480
|
||||
: sourceType == SourceType.ChatBox ? 648
|
||||
: sourceType == SourceType.AlertBox ? 200
|
||||
: sourceType == SourceType.Scoreboard ? 56
|
||||
|
||||
+1
-1
@@ -29,7 +29,7 @@ areas' docs in their own rows keeps this index small to read.
|
||||
| `MicPickerViewModel.cs` | Select Microphone dialog (voice source): async mic list (loading / has / none states), `UseRequested(MicrophoneDeviceInfo)`/`CancelRequested` |
|
||||
| `SocialsDialogViewModel.cs` | Social Media Site Promotion dialog (six fixed slots, `SocialSlotViewModel`): row 0 = YouTube sign-in gate (sign-in provider fills the channel, delete = sign-out); rows 1-5 validated entries / empty / freemium locks 2-5 (`isPremium` seam); `DetectService` → `ISocialValidator` on confirm (input snaps to text + real logo on success, red do-not + stays editable on failure); `CanSave` blocked while any slot edits/validates/errors; `BarEnabled` switch; commit via `CommittedEntries` + `BuildSocialsConfig()` (YouTube first) + `SaveRequested`/`CancelRequested`. WPF-free — the window owns the dialogs (delete confirm, sign-in gate) and calls `StartEdit`/`ConfirmEdit`/`DeleteSlotAsync`. |
|
||||
|
||||
| `MainViewModel.Sources.cs` | Source creation/removal; `AddSource` creates the standalone ticker layer with one-per-scene gating and its 1920×48 default geometry. |
|
||||
| `MainViewModel.Sources.cs` | Source creation/removal; `AddSource` creates the standalone ticker layer with one-per-scene gating and its 1920×48 default geometry; TextOverlay starts as a visible 900×140 draggable text layer. |
|
||||
| `MainViewModel.AlertTicker.cs` | AlertBox ticker preview routing: selects the preview scene's AlertBox settings and publishes the positioned box-sized frame to PreviewPane. |
|
||||
|
||||
Related: [`Models/index.md`](../Models/index.md) for the data; [`Services/index.md`](../Services/index.md)
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Media;
|
||||
using Microsoft.Data.Sqlite;
|
||||
using Xunit;
|
||||
using ytLive.Controls;
|
||||
using ytLive.Models;
|
||||
using ytLive.ViewModels;
|
||||
|
||||
namespace ytLive.Tests;
|
||||
|
||||
[Collection("RealApp")]
|
||||
public sealed class TextLayerEditorPreviewTests
|
||||
{
|
||||
private readonly RealAppHost _app;
|
||||
|
||||
public TextLayerEditorPreviewTests(RealAppHost app) => _app = app;
|
||||
|
||||
[Fact]
|
||||
public void Editing_One_Text_Layer_Updates_Only_That_Preview()
|
||||
{
|
||||
_app.Run(Run);
|
||||
}
|
||||
|
||||
private static void Run()
|
||||
{
|
||||
var dbPath = Path.Combine(Path.GetTempPath(), $"ytLlive-text-editor-{Guid.NewGuid():N}.db");
|
||||
MainViewModel.LayoutPathOverride = dbPath;
|
||||
MainWindow? window = null;
|
||||
try
|
||||
{
|
||||
window = new MainWindow();
|
||||
var vm = (MainViewModel)window.DataContext;
|
||||
vm.EditingActive = true;
|
||||
vm.StagedScene = vm.Scenes.Single(scene => SceneCatalog.Is(scene.Name, SceneCatalog.Live));
|
||||
vm.AddSourceCommand.Execute(SourceType.TextOverlay);
|
||||
vm.AddSourceCommand.Execute(SourceType.TextOverlay);
|
||||
|
||||
var textLayers = vm.StagedScene!.Elements.OfType<Source>()
|
||||
.Where(source => source.Type == SourceType.TextOverlay).ToArray();
|
||||
Assert.Equal(2, textLayers.Length);
|
||||
var first = textLayers[0];
|
||||
var second = textLayers[1];
|
||||
second.TextContent = "Leave this caption alone";
|
||||
vm.SelectedElement = first;
|
||||
|
||||
window.Show();
|
||||
window.UpdateLayout();
|
||||
|
||||
var leftPanel = (LeftPanel)window.FindName("LeftPanel")!;
|
||||
var editor = (TextBox)leftPanel.FindName("TextLayerEditor")!;
|
||||
Assert.Equal("Text", editor.Text);
|
||||
Assert.True(first.IsDraggable);
|
||||
Assert.Equal(900, first.Width);
|
||||
Assert.Equal(140, first.Height);
|
||||
|
||||
editor.Text = "Updated headline\nSecond line";
|
||||
editor.GetBindingExpression(TextBox.TextProperty)?.UpdateSource();
|
||||
window.UpdateLayout();
|
||||
|
||||
Assert.Equal("Updated headline\nSecond line", first.TextContent);
|
||||
Assert.Equal("Leave this caption alone", second.TextContent);
|
||||
|
||||
var preview = (PreviewPane)window.FindName("PreviewPane")!;
|
||||
var firstPreview = FindTextPreview(preview, first);
|
||||
var secondPreview = FindTextPreview(preview, second);
|
||||
Assert.NotNull(firstPreview);
|
||||
Assert.NotNull(secondPreview);
|
||||
Assert.Equal("Updated headline\nSecond line", firstPreview!.Text);
|
||||
Assert.Equal("Leave this caption alone", secondPreview!.Text);
|
||||
Assert.True(firstPreview.IsVisible);
|
||||
Assert.True(secondPreview.IsVisible);
|
||||
}
|
||||
finally
|
||||
{
|
||||
window?.Close();
|
||||
MainViewModel.LayoutPathOverride = null;
|
||||
SqliteConnection.ClearAllPools();
|
||||
try { File.Delete(dbPath); } catch { /* best-effort cleanup */ }
|
||||
}
|
||||
}
|
||||
|
||||
private static TextBlock? FindTextPreview(DependencyObject parent, Source source)
|
||||
{
|
||||
if (parent is TextBlock text && Equals(text.Tag, "TextLayerPreview")
|
||||
&& ReferenceEquals(text.DataContext, source))
|
||||
return text;
|
||||
|
||||
for (var i = 0; i < VisualTreeHelper.GetChildrenCount(parent); i++)
|
||||
{
|
||||
var result = FindTextPreview(VisualTreeHelper.GetChild(parent, i), source);
|
||||
if (result != null) return result;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user