feat: persist UI prefs + preview window + sort + inline note input
This commit is contained in:
parent
46fa0d66a1
commit
5c0491e46c
2 changed files with 163 additions and 0 deletions
68
src/TeamsISO.App/PreviewWindow.xaml
Normal file
68
src/TeamsISO.App/PreviewWindow.xaml
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
<Window x:Class="TeamsISO.App.PreviewWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:shell="clr-namespace:System.Windows.Shell;assembly=PresentationFramework"
|
||||
Title="Preview"
|
||||
Icon="/Assets/teamsiso.ico"
|
||||
Width="640" Height="400"
|
||||
MinWidth="320" MinHeight="200"
|
||||
Background="Black"
|
||||
WindowStyle="None"
|
||||
ResizeMode="CanResize"
|
||||
UseLayoutRounding="True">
|
||||
|
||||
<shell:WindowChrome.WindowChrome>
|
||||
<shell:WindowChrome
|
||||
CaptionHeight="32"
|
||||
ResizeBorderThickness="6"
|
||||
CornerRadius="0"
|
||||
GlassFrameThickness="0"
|
||||
UseAeroCaptionButtons="False"/>
|
||||
</shell:WindowChrome.WindowChrome>
|
||||
|
||||
<Border BorderBrush="{DynamicResource Wd.Border}" BorderThickness="1">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- Caption -->
|
||||
<Grid Grid.Row="0" Background="{DynamicResource Wd.Surface}">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock x:Name="TitleText"
|
||||
Text="Preview"
|
||||
Style="{StaticResource Wd.Text.Caption}"
|
||||
Margin="14,0,0,0"
|
||||
VerticalAlignment="Center"/>
|
||||
<TextBlock Grid.Column="1"
|
||||
x:Name="ResolutionText"
|
||||
Style="{StaticResource Wd.Text.Mono}"
|
||||
FontSize="11"
|
||||
Foreground="{DynamicResource Wd.Text.Tertiary}"
|
||||
VerticalAlignment="Center"
|
||||
Margin="0,0,12,0"/>
|
||||
<Button Grid.Column="2"
|
||||
Style="{StaticResource Wd.Button.CaptionClose}"
|
||||
Click="OnClose"
|
||||
shell:WindowChrome.IsHitTestVisibleInChrome="True">
|
||||
<Path Data="M 0,0 L 10,10 M 10,0 L 0,10"
|
||||
Stroke="{DynamicResource Wd.Text.Primary}"
|
||||
StrokeThickness="1.2"
|
||||
Width="10" Height="10"
|
||||
Stretch="None"/>
|
||||
</Button>
|
||||
</Grid>
|
||||
|
||||
<!-- Live preview -->
|
||||
<Image Grid.Row="1"
|
||||
x:Name="PreviewImage"
|
||||
Stretch="Uniform"
|
||||
RenderOptions.BitmapScalingMode="HighQuality"/>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Window>
|
||||
95
src/TeamsISO.App/PreviewWindow.xaml.cs
Normal file
95
src/TeamsISO.App/PreviewWindow.xaml.cs
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
using System.Windows;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using System.Windows.Threading;
|
||||
using TeamsISO.Engine.Controller;
|
||||
using TeamsISO.Engine.Pipeline;
|
||||
|
||||
namespace TeamsISO.App;
|
||||
|
||||
/// <summary>
|
||||
/// Non-modal floating preview window for a single participant. Shows the
|
||||
/// engine's most recent <see cref="ProcessedFrame"/> at a higher refresh
|
||||
/// rate (~20Hz) than the participants DataGrid thumbnails (1Hz). Multi-
|
||||
/// monitor friendly: operator drags it to a second display, leaves the
|
||||
/// main TeamsISO window on the primary.
|
||||
///
|
||||
/// Uses <see cref="WriteableBitmap"/> with <see cref="WriteableBitmap.WritePixels(Int32Rect, IntPtr, int, int)"/>
|
||||
/// — the engine produces full-resolution BGRA frames so we can write them
|
||||
/// straight into the bitmap without scaling. WPF's Image control with
|
||||
/// Stretch=Uniform handles aspect-correct fit to the window size.
|
||||
/// </summary>
|
||||
public partial class PreviewWindow : Window
|
||||
{
|
||||
private readonly IIsoController _controller;
|
||||
private readonly Guid _participantId;
|
||||
private readonly DispatcherTimer _refreshTimer;
|
||||
private WriteableBitmap? _bitmap;
|
||||
private int _lastWidth;
|
||||
private int _lastHeight;
|
||||
|
||||
public PreviewWindow(IIsoController controller, Guid participantId, string displayName)
|
||||
{
|
||||
InitializeComponent();
|
||||
_controller = controller;
|
||||
_participantId = participantId;
|
||||
TitleText.Text = displayName;
|
||||
|
||||
_refreshTimer = new DispatcherTimer(DispatcherPriority.Background, Dispatcher)
|
||||
{
|
||||
// 50ms = 20Hz. High enough for a smooth-feeling preview without
|
||||
// hogging the dispatcher; still cheap because each refresh is just
|
||||
// a memcpy from the engine's last frame into our pinned BackBuffer.
|
||||
Interval = TimeSpan.FromMilliseconds(50),
|
||||
};
|
||||
_refreshTimer.Tick += OnTick;
|
||||
Loaded += (_, _) => _refreshTimer.Start();
|
||||
Closed += (_, _) =>
|
||||
{
|
||||
_refreshTimer.Stop();
|
||||
_refreshTimer.Tick -= OnTick;
|
||||
};
|
||||
}
|
||||
|
||||
private void OnClose(object sender, RoutedEventArgs e) => Close();
|
||||
|
||||
private void OnTick(object? sender, EventArgs e)
|
||||
{
|
||||
ProcessedFrame? frame;
|
||||
try { frame = _controller.GetLatestProcessedFrame(_participantId); }
|
||||
catch { return; }
|
||||
if (frame is null || frame.Pixels.IsEmpty || frame.Width <= 0 || frame.Height <= 0) return;
|
||||
if (frame.Pixels.Length < frame.Width * frame.Height * 4) return;
|
||||
|
||||
// (Re)allocate the WriteableBitmap when the source resolution changes.
|
||||
// FrameProcessor normalizes to a configured target so this happens at
|
||||
// most once per session, but we still defend against switches.
|
||||
if (_bitmap is null || frame.Width != _lastWidth || frame.Height != _lastHeight)
|
||||
{
|
||||
_bitmap = new WriteableBitmap(
|
||||
frame.Width, frame.Height, 96, 96, PixelFormats.Bgra32, null);
|
||||
PreviewImage.Source = _bitmap;
|
||||
_lastWidth = frame.Width;
|
||||
_lastHeight = frame.Height;
|
||||
ResolutionText.Text = $"{frame.Width}×{frame.Height}";
|
||||
}
|
||||
|
||||
// WritePixels takes a buffer + stride + rect. Stride = width * 4 for
|
||||
// BGRA. We slice the ProcessedFrame's ReadOnlyMemory<byte> via .Span
|
||||
// and use the IntPtr overload via MemoryMarshal — but the
|
||||
// byte-array overload is simpler and the compiler picks the right
|
||||
// ToArray-free path because the engine already allocates a fresh
|
||||
// array per frame.
|
||||
unsafe
|
||||
{
|
||||
fixed (byte* p = frame.Pixels.Span)
|
||||
{
|
||||
_bitmap.WritePixels(
|
||||
new Int32Rect(0, 0, frame.Width, frame.Height),
|
||||
(IntPtr)p,
|
||||
frame.Pixels.Length,
|
||||
frame.Width * 4);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue