polish(mainwindow): empty state, table widths, strings, theme tooltip
Walks the v2 polish punch list against MainWindow. - Theme button tooltip is now "Theme (System / Dark / Light)" per the v2 shape brief, replacing the previous "Toggle theme (Ctrl+T)". - Participants table column widths match spec: Output 130px (was 150), ISO pill 100px (was 110). The 24px state LED, 110px audio meter, and 52px row height already matched. The 106px Preview thumbnail column and 32px gear-button column are intentional deviations (live thumbs were restored at 4944de5; per-ISO override gear added at the same time) and are now called out in the column-spec comment so a future reader doesn't try to "fix" them. - Empty-state placeholder finally renders when ParticipantCount == 0: mono sentence "no ndi sources yet — open teams and start a meeting" + a tertiary Refresh discovery button — exactly the copy specified by the shape brief's empty-states section. CountToVisibilityConverter is now declared in MainWindow.Resources (it shipped as a class but was never registered). - OnClosing wraps WindowStateStore.Save in a try/catch so a serialization or filesystem fault on shutdown can never block the window from closing. Save itself already swallows its own IO errors; this is defense-in-depth for anything that escapes. - MessageBox copy in MainWindow.xaml.cs (Hide/show Teams, Launch Teams, Stop Teams) moves to Properties/Strings.resx + a hand-written Properties/Strings.Designer.cs accessor. ResourceManager reads it by basename "TeamsISO.App.Properties.Strings"; LogicalName is set on the EmbeddedResource so the manifest name is predictable regardless of how MSBuild would otherwise compute it. Future-localization seam. OnLaunchTeamsRightClick's confirmation dialog is intentional — it guards a destructive mid-show action — and the code-behind comment now says so; the palette also offers Stop Teams as the keyboard surface, so the right-click affordance isn't the only one. Build clean (0 warnings, 0 errors); 160 tests still pass (56 App + 104 Engine, Category!=ndi&requires!=ndi filter). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
37390026b3
commit
33fca8e955
5 changed files with 186 additions and 18 deletions
|
|
@ -39,6 +39,7 @@
|
|||
FalseValue="Visible"/>
|
||||
<conv:EnumDescriptionConverter x:Key="EnumDesc"/>
|
||||
<conv:LevelThresholdConverter x:Key="LevelGate"/>
|
||||
<conv:CountToVisibilityConverter x:Key="CountToVis"/>
|
||||
</Window.Resources>
|
||||
|
||||
<Window.InputBindings>
|
||||
|
|
@ -138,7 +139,7 @@
|
|||
Command="{Binding ToggleThemeCommand}"
|
||||
Padding="6,4"
|
||||
Margin="0,0,2,0"
|
||||
ToolTip="Toggle theme (Ctrl+T)">
|
||||
ToolTip="Theme (System / Dark / Light)">
|
||||
<Path Data="M 8,2 C 8,2 4,4 4,8 C 4,12 8,14 8,14 C 5,14 2,11 2,8 C 2,5 5,2 8,2 Z"
|
||||
Stroke="{DynamicResource Wd.Text.Secondary}"
|
||||
StrokeThickness="1.4"
|
||||
|
|
@ -410,7 +411,7 @@
|
|||
<!--
|
||||
Participants table — v2 "Studio Terminal" layout.
|
||||
|
||||
Five columns:
|
||||
Spec columns (from docs/shapes/2026-05-13-…studio-terminal.md):
|
||||
1. State LED 24px — 8×8 hard-edged square. Filled cyan
|
||||
when LIVE; filled coral on ERROR;
|
||||
filled amber on NO SIGNAL / STARTING;
|
||||
|
|
@ -422,12 +423,22 @@
|
|||
each lit when DisplayedAudioLevel
|
||||
crosses its threshold (0.2, 0.4,
|
||||
0.6, 0.8, 1.0). No averaging.
|
||||
4. Output name 150px — JetBrains Mono 12 — the NDI source
|
||||
4. Output name 130px — JetBrains Mono 12 — the NDI source
|
||||
name TeamsISO broadcasts as.
|
||||
5. ISO toggle pill 110px — LIVE = cyan-muted fill with cyan
|
||||
5. ISO toggle pill 100px — LIVE = cyan-muted fill with cyan
|
||||
text; OFF = hollow neutral; ERROR
|
||||
gets the existing trigger swap.
|
||||
|
||||
Deliberate deviations from the spec (operator preference, see
|
||||
4944de5 — "restore live thumbnail preview column"):
|
||||
• A 106px live thumbnail column sits between State LED and
|
||||
Name. Replaces the table's previous role as the only place
|
||||
to see what the operator is broadcasting; the pop-out
|
||||
preview window is the secondary view.
|
||||
• A 32px ghost-button cell on the right edge of Name opens
|
||||
the per-ISO override dialog (framerate / resolution /
|
||||
aspect / audio). Hidden on hover-out.
|
||||
|
||||
Row height 52 (down from 56). Active speaker = full-row
|
||||
bg.active-speaker tint set by the global DataGridRow style
|
||||
(avoids the impeccable side-stripe-border ban).
|
||||
|
|
@ -438,6 +449,7 @@
|
|||
BorderThickness="1"
|
||||
CornerRadius="{StaticResource Radius.M}"
|
||||
Background="{DynamicResource Wd.Surface}">
|
||||
<Grid>
|
||||
<DataGrid x:Name="ParticipantsGrid"
|
||||
ItemsSource="{Binding ParticipantsView}"
|
||||
AutoGenerateColumns="False"
|
||||
|
|
@ -451,7 +463,8 @@
|
|||
CanUserResizeRows="False"
|
||||
SelectionMode="Single"
|
||||
SelectionUnit="FullRow"
|
||||
RowHeight="52">
|
||||
RowHeight="52"
|
||||
Visibility="{Binding ParticipantCount, Converter={StaticResource CountToVis}}">
|
||||
<DataGrid.Columns>
|
||||
<!-- Col 1 — State LED. 8×8 hard-edged Rectangle.
|
||||
Default fill is hollow (transparent with stroke). DataTriggers
|
||||
|
|
@ -601,7 +614,7 @@
|
|||
|
||||
<!-- Col 4 — Output name (mono). The NDI source name TeamsISO
|
||||
will broadcast this participant as. -->
|
||||
<DataGridTemplateColumn Header="Output" Width="150" IsReadOnly="True">
|
||||
<DataGridTemplateColumn Header="Output" Width="130" IsReadOnly="True">
|
||||
<DataGridTemplateColumn.CellTemplate>
|
||||
<DataTemplate>
|
||||
<TextBlock Text="{Binding OutputName}"
|
||||
|
|
@ -639,7 +652,7 @@
|
|||
|
||||
<!-- Col 5 — ISO toggle pill. LIVE = cyan-muted fill + cyan border + cyan text.
|
||||
OFF = hollow neutral. Error states use the existing IsoToggle style. -->
|
||||
<DataGridTemplateColumn Header="ISO" Width="110">
|
||||
<DataGridTemplateColumn Header="ISO" Width="100">
|
||||
<DataGridTemplateColumn.CellTemplate>
|
||||
<DataTemplate>
|
||||
<Button Command="{Binding ToggleIsoCommand}"
|
||||
|
|
@ -665,6 +678,30 @@
|
|||
</DataGridTemplateColumn>
|
||||
</DataGrid.Columns>
|
||||
</DataGrid>
|
||||
|
||||
<!-- Empty-state placeholder. Renders when no NDI participants
|
||||
have been discovered yet. Mono sentence + one tertiary
|
||||
Refresh button — no illustration, no mascot, per the v2
|
||||
shape brief's empty-states section. -->
|
||||
<StackPanel HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Visibility="{Binding ParticipantCount, Converter={StaticResource CountToVis}, ConverterParameter=empty}">
|
||||
<TextBlock Text="no ndi sources yet — open teams and start a meeting"
|
||||
FontFamily="{StaticResource Wd.Font.Mono}"
|
||||
FontSize="12"
|
||||
Foreground="{DynamicResource Wd.Text.Tertiary}"
|
||||
HorizontalAlignment="Center"/>
|
||||
<Button Style="{StaticResource Wd.Button.Ghost}"
|
||||
Command="{Binding RefreshDiscoveryCommand}"
|
||||
Content="Refresh discovery (Ctrl+R)"
|
||||
Padding="14,7"
|
||||
Margin="0,14,0,0"
|
||||
HorizontalAlignment="Center"
|
||||
FontFamily="{StaticResource Wd.Font.Mono}"
|
||||
FontSize="11"
|
||||
ToolTip="Rebuild the NDI finder"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Grid>
|
||||
|
||||
|
|
|
|||
|
|
@ -42,7 +42,12 @@ public partial class MainWindow : Window
|
|||
/// <summary>Persist the placement on close so next launch lands in the same spot.</summary>
|
||||
private void OnClosing(object? sender, System.ComponentModel.CancelEventArgs e)
|
||||
{
|
||||
WindowStateStore.Save(this);
|
||||
// A failure persisting window state must NEVER block the window from
|
||||
// closing — operator's shutdown comes first. WindowStateStore.Save
|
||||
// already swallows its own IO errors; this is defense-in-depth for
|
||||
// anything that escapes (NRE, future regression, etc.).
|
||||
try { WindowStateStore.Save(this); }
|
||||
catch { /* best-effort: forgo placement memory for one launch */ }
|
||||
}
|
||||
|
||||
/// <summary>Opens the About dialog — version, NDI runtime, build SHA.</summary>
|
||||
|
|
@ -87,8 +92,8 @@ public partial class MainWindow : Window
|
|||
if (!TeamsLauncher.IsRunning())
|
||||
{
|
||||
MessageBox.Show(
|
||||
"Microsoft Teams isn't running. Click the camera icon above to launch it first.",
|
||||
"TeamsISO — Hide / show Teams",
|
||||
Properties.Strings.HideShowTeams_NotRunning_Message,
|
||||
Properties.Strings.HideShowTeams_Title,
|
||||
MessageBoxButton.OK,
|
||||
MessageBoxImage.Information);
|
||||
return;
|
||||
|
|
@ -125,8 +130,8 @@ public partial class MainWindow : Window
|
|||
if (!TeamsLauncher.TryLaunch(out var error))
|
||||
{
|
||||
MessageBox.Show(
|
||||
$"Could not launch Microsoft Teams.\n\n{error}",
|
||||
"TeamsISO — Launch Teams",
|
||||
string.Format(Properties.Strings.LaunchTeams_Failed_MessageFormat, error),
|
||||
Properties.Strings.LaunchTeams_Title,
|
||||
MessageBoxButton.OK,
|
||||
MessageBoxImage.Warning);
|
||||
}
|
||||
|
|
@ -159,15 +164,19 @@ public partial class MainWindow : Window
|
|||
/// <summary>
|
||||
/// Right-click on the Launch button asks to stop Teams. Split out from the
|
||||
/// left-click so a normal click is "open / surface" rather than the previous
|
||||
/// "open OR ambush you with a stop dialog".
|
||||
/// "open OR ambush you with a stop dialog". The confirmation dialog here is
|
||||
/// intentional — Stop Teams is a destructive mid-show action; explicit
|
||||
/// confirmation is the right pattern, not the "ambush" anti-pattern that
|
||||
/// was fixed for left-click. The palette also offers Stop Teams for
|
||||
/// keyboard-first operators.
|
||||
/// </summary>
|
||||
private void OnLaunchTeamsRightClick(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (!TeamsLauncher.IsRunning()) return;
|
||||
|
||||
var confirm = MessageBox.Show(
|
||||
"Microsoft Teams is currently running.\n\nClose all Teams windows now?",
|
||||
"TeamsISO — Stop Teams",
|
||||
Properties.Strings.StopTeams_Confirm_Message,
|
||||
Properties.Strings.StopTeams_Title,
|
||||
MessageBoxButton.YesNo,
|
||||
MessageBoxImage.Question);
|
||||
if (confirm != MessageBoxResult.Yes) return;
|
||||
|
|
@ -177,9 +186,9 @@ public partial class MainWindow : Window
|
|||
{
|
||||
MessageBox.Show(
|
||||
asked == 0
|
||||
? "No Teams windows responded to close."
|
||||
: $"Sent close to {asked} Teams window(s); some may still be exiting.",
|
||||
"TeamsISO — Stop Teams",
|
||||
? Properties.Strings.StopTeams_NoneResponded
|
||||
: string.Format(Properties.Strings.StopTeams_AskedFormat, asked),
|
||||
Properties.Strings.StopTeams_Title,
|
||||
MessageBoxButton.OK,
|
||||
MessageBoxImage.Information);
|
||||
}
|
||||
|
|
|
|||
36
src/TeamsISO.App/Properties/Strings.Designer.cs
generated
Normal file
36
src/TeamsISO.App/Properties/Strings.Designer.cs
generated
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
// Hand-written strongly-typed accessor for Properties/Strings.resx. Kept
|
||||
// out of Visual Studio's "ResXFileCodeGenerator" auto-regeneration loop
|
||||
// so the .csproj stays simple and the file doesn't churn on every save.
|
||||
// If you add a key in Strings.resx, add a matching property here.
|
||||
|
||||
// The compiler treats `*.Designer.cs` as auto-generated and refuses
|
||||
// nullable annotations without an explicit directive — opt in.
|
||||
#nullable enable
|
||||
|
||||
using System.Globalization;
|
||||
using System.Resources;
|
||||
|
||||
namespace TeamsISO.App.Properties;
|
||||
|
||||
internal static class Strings
|
||||
{
|
||||
private static readonly ResourceManager ResourceManager = new(
|
||||
baseName: "TeamsISO.App.Properties.Strings",
|
||||
assembly: typeof(Strings).Assembly);
|
||||
|
||||
public static CultureInfo? Culture { get; set; }
|
||||
|
||||
private static string Get(string key) =>
|
||||
ResourceManager.GetString(key, Culture) ?? string.Empty;
|
||||
|
||||
public static string HideShowTeams_Title => Get(nameof(HideShowTeams_Title));
|
||||
public static string HideShowTeams_NotRunning_Message => Get(nameof(HideShowTeams_NotRunning_Message));
|
||||
|
||||
public static string LaunchTeams_Title => Get(nameof(LaunchTeams_Title));
|
||||
public static string LaunchTeams_Failed_MessageFormat => Get(nameof(LaunchTeams_Failed_MessageFormat));
|
||||
|
||||
public static string StopTeams_Title => Get(nameof(StopTeams_Title));
|
||||
public static string StopTeams_Confirm_Message => Get(nameof(StopTeams_Confirm_Message));
|
||||
public static string StopTeams_NoneResponded => Get(nameof(StopTeams_NoneResponded));
|
||||
public static string StopTeams_AskedFormat => Get(nameof(StopTeams_AskedFormat));
|
||||
}
|
||||
75
src/TeamsISO.App/Properties/Strings.resx
Normal file
75
src/TeamsISO.App/Properties/Strings.resx
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
User-facing English strings shown by MainWindow's MessageBox prompts.
|
||||
Pulled out of code-behind so a future localizer has a single seam to
|
||||
translate. Strings.Designer.cs is a hand-rolled accessor backed by
|
||||
ResourceManager — no Visual-Studio auto-regeneration needed.
|
||||
-->
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype"><value>text/microsoft-resx</value></resheader>
|
||||
<resheader name="version"><value>2.0</value></resheader>
|
||||
<resheader name="reader"><value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value></resheader>
|
||||
<resheader name="writer"><value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value></resheader>
|
||||
|
||||
<data name="HideShowTeams_Title" xml:space="preserve">
|
||||
<value>TeamsISO — Hide / show Teams</value>
|
||||
</data>
|
||||
<data name="HideShowTeams_NotRunning_Message" xml:space="preserve">
|
||||
<value>Microsoft Teams isn't running. Click the camera icon above to launch it first.</value>
|
||||
</data>
|
||||
|
||||
<data name="LaunchTeams_Title" xml:space="preserve">
|
||||
<value>TeamsISO — Launch Teams</value>
|
||||
</data>
|
||||
<data name="LaunchTeams_Failed_MessageFormat" xml:space="preserve">
|
||||
<value>Could not launch Microsoft Teams.
|
||||
|
||||
{0}</value>
|
||||
<comment>{0} = error string from TeamsLauncher.TryLaunch.</comment>
|
||||
</data>
|
||||
|
||||
<data name="StopTeams_Title" xml:space="preserve">
|
||||
<value>TeamsISO — Stop Teams</value>
|
||||
</data>
|
||||
<data name="StopTeams_Confirm_Message" xml:space="preserve">
|
||||
<value>Microsoft Teams is currently running.
|
||||
|
||||
Close all Teams windows now?</value>
|
||||
</data>
|
||||
<data name="StopTeams_NoneResponded" xml:space="preserve">
|
||||
<value>No Teams windows responded to close.</value>
|
||||
</data>
|
||||
<data name="StopTeams_AskedFormat" xml:space="preserve">
|
||||
<value>Sent close to {0} Teams window(s); some may still be exiting.</value>
|
||||
<comment>{0} = number of windows the launcher asked to close.</comment>
|
||||
</data>
|
||||
</root>
|
||||
|
|
@ -39,6 +39,17 @@
|
|||
</AssemblyAttribute>
|
||||
</ItemGroup>
|
||||
|
||||
<!--
|
||||
Strings.resx — user-facing English MessageBox copy. Embedded as a
|
||||
.NET resource so ResourceManager can resolve TeamsISO.App.Properties.Strings
|
||||
by basename. Strings.Designer.cs is hand-written (see file comment).
|
||||
-->
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Update="Properties\Strings.resx">
|
||||
<LogicalName>TeamsISO.App.Properties.Strings.resources</LogicalName>
|
||||
</EmbeddedResource>
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Wild Dragon brand assets — embedded as resources so the published binary is self-contained. -->
|
||||
<ItemGroup>
|
||||
<Resource Include="Assets\dragon-mark.png" />
|
||||
|
|
|
|||
Loading…
Reference in a new issue