Highlight active speaker row with cyan left border
Some checks failed
CI / build-and-test (push) Has been cancelled
Some checks failed
CI / build-and-test (push) Has been cancelled
Visual cue for who's currently speaking — operators don't need to watch every VU bar. MainViewModel.OnStatsTick scans enabled participants once per tick, picks the loudest above a 0.05 floor (anti-flicker threshold), sets IsActiveSpeaker on the winner and clears on everyone else. DataGridRow DataTrigger swaps in a 3px cyan-accent left border + CyanMuted background tint when IsActiveSpeaker is true. Plays well with sort modes: LoudestFirst makes the highlighted row always the topmost; other sort modes leave the row position alone, just paints the indicator.
This commit is contained in:
parent
9db0875f9e
commit
b0029a51bf
3 changed files with 45 additions and 0 deletions
|
|
@ -740,6 +740,15 @@
|
||||||
<Trigger Property="IsSelected" Value="True">
|
<Trigger Property="IsSelected" Value="True">
|
||||||
<Setter Property="Background" Value="{StaticResource Wd.SurfaceElevated}"/>
|
<Setter Property="Background" Value="{StaticResource Wd.SurfaceElevated}"/>
|
||||||
</Trigger>
|
</Trigger>
|
||||||
|
<!-- Active speaker highlight: set by MainViewModel at the 1Hz
|
||||||
|
stats tick based on DisplayedAudioLevel. Cyan left border
|
||||||
|
(3px) makes whoever's talking pop without changing the
|
||||||
|
background color (which IsMouseOver / IsSelected own). -->
|
||||||
|
<DataTrigger Binding="{Binding IsActiveSpeaker}" Value="True">
|
||||||
|
<Setter Property="BorderBrush" Value="{StaticResource Wd.Accent.Cyan}"/>
|
||||||
|
<Setter Property="BorderThickness" Value="3,0,0,1"/>
|
||||||
|
<Setter Property="Background" Value="{StaticResource Wd.Accent.CyanMuted}"/>
|
||||||
|
</DataTrigger>
|
||||||
</Style.Triggers>
|
</Style.Triggers>
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -752,6 +752,29 @@ public sealed class MainViewModel : ObservableObject, IDisposable
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Active-speaker highlight: find the loudest enabled participant
|
||||||
|
// and mark their IsActiveSpeaker flag. Only one row at a time;
|
||||||
|
// ties broken by enumeration order (first one wins). Threshold of
|
||||||
|
// 0.05 prevents constant flicker between near-silent participants
|
||||||
|
// when nobody's really speaking.
|
||||||
|
ParticipantViewModel? loudest = null;
|
||||||
|
double loudestLevel = 0.05;
|
||||||
|
foreach (var p in Participants)
|
||||||
|
{
|
||||||
|
if (!p.IsEnabled) continue;
|
||||||
|
if (p.DisplayedAudioLevel > loudestLevel)
|
||||||
|
{
|
||||||
|
loudest = p;
|
||||||
|
loudestLevel = p.DisplayedAudioLevel;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
foreach (var p in Participants)
|
||||||
|
{
|
||||||
|
var shouldHighlight = ReferenceEquals(p, loudest);
|
||||||
|
if (p.IsActiveSpeaker != shouldHighlight)
|
||||||
|
p.IsActiveSpeaker = shouldHighlight;
|
||||||
|
}
|
||||||
|
|
||||||
// If sort mode is LoudestFirst, refresh the view so the new audio
|
// If sort mode is LoudestFirst, refresh the view so the new audio
|
||||||
// peaks re-evaluate the sort. Skipped for the other sort modes
|
// peaks re-evaluate the sort. Skipped for the other sort modes
|
||||||
// since their keys (name, online state) don't change every tick —
|
// since their keys (name, online state) don't change every tick —
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,19 @@ public sealed class ParticipantViewModel : ObservableObject
|
||||||
/// <summary>True when <see cref="Thumbnail"/> is non-null. Bound to Visibility in XAML.</summary>
|
/// <summary>True when <see cref="Thumbnail"/> is non-null. Bound to Visibility in XAML.</summary>
|
||||||
public bool HasThumbnail => _thumbnail is not null;
|
public bool HasThumbnail => _thumbnail is not null;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// True when this participant is currently the loudest among the live
|
||||||
|
/// set — set by MainViewModel at the 1Hz stats tick. Bound to a cyan
|
||||||
|
/// border accent on the DataGrid row so operators can spot who's
|
||||||
|
/// speaking without watching every VU bar individually.
|
||||||
|
/// </summary>
|
||||||
|
public bool IsActiveSpeaker
|
||||||
|
{
|
||||||
|
get => _isActiveSpeaker;
|
||||||
|
internal set => SetField(ref _isActiveSpeaker, value);
|
||||||
|
}
|
||||||
|
private bool _isActiveSpeaker;
|
||||||
|
|
||||||
public ParticipantViewModel(IIsoController controller, Participant participant, ToastViewModel? toast = null)
|
public ParticipantViewModel(IIsoController controller, Participant participant, ToastViewModel? toast = null)
|
||||||
{
|
{
|
||||||
_controller = controller;
|
_controller = controller;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue