From a05c0a75d2e022732e4e5ffe7811a051ff9dc5a1 Mon Sep 17 00:00:00 2001 From: Zac Gaetano Date: Wed, 13 May 2026 00:50:54 -0400 Subject: [PATCH] =?UTF-8?q?feat(winui3):=20SettingsDrawer=20hosts=20succes?= =?UTF-8?q?sfully=20=E2=80=94=20NavigationView=20swap?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces the NavigationView in SettingsDrawer with a simpler StackPanel of tab buttons built imperatively at runtime. The NavigationView's resource-dictionary expansion (or its default template loading) was crashing the XAML parser at SettingsDrawer's InitializeComponent on WinUI 3 1.8. New shape: - `TabStrip` StackPanel populated in BuildTabStrip() with five Tertiary-styled Button instances. Selection updates the foreground to AccentCyanText for the active tab and FgSecondary for the rest. - `TabContent` ScrollViewer remains; RebuildTabContent(key) clears and rebuilds via the same helpers as before (SettingHeader, SettingRow, SettingNote, AccentSwatch). - Each tab's content moved into its own helper method (BuildAppearanceTab / Routing / Display / Control / Advanced) so the switch in the old OnTabSelectionChanged disappears. MainWindow re-hosts the drawer at Grid.Row=0, RowSpan=4, right- aligned, 400px wide, Visibility=Collapsed. OnSettingsClick toggles visibility. Verified: dotnet build + run launches cleanly, and the window stays alive (PID confirmed via Get-Process). This closes Phase 6 (secondary windows) for the drawer specifically. The Help, About, and Onboarding dialogs are ContentDialogs that don't host inline so they should be straightforward to wire to their respective triggers (F1 / About button / first launch) in Phase 7. --- src/TeamsISO.App.WinUI/Views/MainWindow.xaml | 15 +- .../Views/MainWindow.xaml.cs | 23 ++- .../Views/SettingsDrawer.xaml | 37 ++--- .../Views/SettingsDrawer.xaml.cs | 152 ++++++++++++------ 4 files changed, 144 insertions(+), 83 deletions(-) diff --git a/src/TeamsISO.App.WinUI/Views/MainWindow.xaml b/src/TeamsISO.App.WinUI/Views/MainWindow.xaml index d41407f..0dfcb1a 100644 --- a/src/TeamsISO.App.WinUI/Views/MainWindow.xaml +++ b/src/TeamsISO.App.WinUI/Views/MainWindow.xaml @@ -377,14 +377,13 @@ - + + - /// Settings drawer toggle. Currently a no-op because the drawer host - /// can't be inlined in MainWindow.xaml without crashing the XAML parser; - /// see the comment in MainWindow.xaml at the drawer placeholder. + /// Toggle the settings drawer. Visibility-only for now; composition- + /// layer Translation animation lands as part of the polish pass. /// private void OnSettingsClick(object sender, RoutedEventArgs e) { - // No-op until SettingsDrawer.xaml is simplified for WinUI 3 1.8. + _drawerOpen = !_drawerOpen; + SettingsDrawerHost.Visibility = _drawerOpen + ? Visibility.Visible + : Visibility.Collapsed; + if (_drawerOpen) + { + SettingsDrawerHost.CloseRequested -= OnDrawerCloseRequested; + SettingsDrawerHost.CloseRequested += OnDrawerCloseRequested; + } + } + + private void OnDrawerCloseRequested(object? sender, System.EventArgs e) + { + _drawerOpen = false; + SettingsDrawerHost.Visibility = Visibility.Collapsed; } /// diff --git a/src/TeamsISO.App.WinUI/Views/SettingsDrawer.xaml b/src/TeamsISO.App.WinUI/Views/SettingsDrawer.xaml index 6b41439..058e780 100644 --- a/src/TeamsISO.App.WinUI/Views/SettingsDrawer.xaml +++ b/src/TeamsISO.App.WinUI/Views/SettingsDrawer.xaml @@ -48,27 +48,28 @@ - - - - - - - - - - - + + + + + + + - + Build the tab buttons imperatively so the parse doesn't + /// trigger a SelectionChanged before the code-behind is ready. + private void BuildTabStrip() + { + foreach (var (label, key) in new[] + { + ("Appearance", "Appearance"), + ("Routing", "Routing"), + ("Display", "Display"), + ("Control", "Control"), + ("Advanced", "Advanced"), + }) + { + var btn = new Button + { + Content = label, + Tag = key, + Style = (Style)Application.Current.Resources["ButtonTertiary"], + }; + btn.Click += (s, _) => + { + if (s is Button b && b.Tag is string k) SelectTab(k); + }; + TabStrip.Children.Add(btn); + } + } + + private void SelectTab(string key) + { + _currentTab = key; + foreach (var child in TabStrip.Children) + { + if (child is Button b) + { + var isActive = (b.Tag as string) == key; + b.Foreground = (Microsoft.UI.Xaml.Media.SolidColorBrush)Application.Current.Resources[ + isActive ? "AccentCyanText" : "FgSecondary"]; + } + } + RebuildTabContent(key); + } + + private void RebuildTabContent(string key) + { + TabContent.Children.Clear(); + switch (key) + { + case "Appearance": BuildAppearanceTab(); break; + case "Routing": BuildRoutingTab(); break; + case "Display": BuildDisplayTab(); break; + case "Control": BuildControlTab(); break; + case "Advanced": BuildAdvancedTab(); break; + } + } + + private void BuildRoutingTab() + { + TabContent.Children.Add(SettingHeader("Routing")); + TabContent.Children.Add(SettingRow("Framerate", "30 fps", "Target framerate the engine normalizes incoming NDI feeds to.")); + TabContent.Children.Add(SettingRow("Resolution", "1920 x 1080", "Output resolution applied to every routed participant.")); + TabContent.Children.Add(SettingRow("Aspect mode", "Letterbox", "How sources whose aspect ratio doesn't match the target are framed.")); + TabContent.Children.Add(SettingRow("Audio routing", "Per-participant", "Embed each participant's audio in their own NDI source.")); + TabContent.Children.Add(SettingNote("Settings wired to the engine in a follow-up commit.")); + } + + private void BuildDisplayTab() + { + TabContent.Children.Add(SettingHeader("Display & participants")); + TabContent.Children.Add(SettingRow("Hide local self", "Yes", "Filter the operator's own preview from the participants table.")); + TabContent.Children.Add(SettingRow("Auto-disable on departure", "No", "Keep routing alive when a participant goes offline.")); + TabContent.Children.Add(SettingRow("Participant sort", "Join order", "Default sort for the participants table.")); + TabContent.Children.Add(SettingRow("Minimize to tray", "No", "Keep TeamsISO running in the system tray when the window is minimized.")); + TabContent.Children.Add(SettingRow("Launch Teams on startup", "No", "Start Teams in the background when TeamsISO opens.")); + TabContent.Children.Add(SettingRow("Auto-hide Teams windows", "No", "Hide every visible Teams window after launch.")); + TabContent.Children.Add(SettingRow("Auto-record on call", "No", "Start recording every active ISO when Teams enters a call.")); + } + + private void BuildControlTab() + { + TabContent.Children.Add(SettingHeader("External control surface")); + TabContent.Children.Add(SettingRow("REST + WebSocket", "127.0.0.1:9755", "HTTP control surface for Companion / Stream Deck.")); + TabContent.Children.Add(SettingRow("OSC bridge", "127.0.0.1:9000", "UDP OSC for TouchOSC / hardware surfaces.")); + TabContent.Children.Add(SettingRow("LAN reachable", "No", "Bind the REST surface to all interfaces (warning: no auth).")); + TabContent.Children.Add(SettingNote("Control surface protocol unchanged from the WPF host. See docs/CONTROL-SURFACE.md.")); + } + + private void BuildAdvancedTab() + { + TabContent.Children.Add(SettingHeader("Advanced")); + TabContent.Children.Add(SettingRow("Embed Teams window", "No", "Experimental SetParent reparent of Teams' main window.")); + TabContent.Children.Add(SettingRow("Logs", "%LOCALAPPDATA%\\TeamsISO\\Logs", "Where rolling daily Serilog files write.")); + TabContent.Children.Add(SettingRow("Recordings", "%USERPROFILE%\\Videos\\TeamsISO", "Default per-show recording directory.")); + TabContent.Children.Add(SettingRow("Diagnostic bundle", "Export", "Zip logs + config + presets for a bug report.")); } private void OnCloseClick(object sender, RoutedEventArgs e) => CloseRequested?.Invoke(this, EventArgs.Empty); @@ -37,58 +135,6 @@ public sealed partial class SettingsDrawer : UserControl DirtyHint.Text = "Reset queued — apply or close to commit."; } - /// - /// Tab-switch handler builds the body for the selected tab. We assemble - /// content imperatively rather than via separate XAML pages because the - /// drawer's content is data-driven (most rows are simple label + control - /// + tooltip triples) and pulling that into five XAML files would - /// triplicate the row layout. The body recomposes on every selection - /// change, which is cheap given the small surface. - /// - private void OnTabSelectionChanged(NavigationView sender, NavigationViewSelectionChangedEventArgs args) - { - TabContent.Children.Clear(); - if (args.SelectedItemContainer is not NavigationViewItem item) return; - - switch (item.Tag as string) - { - case "Appearance": - BuildAppearanceTab(); - break; - case "Routing": - TabContent.Children.Add(SettingHeader("Routing")); - TabContent.Children.Add(SettingRow("Framerate", "30 fps", "Target framerate the engine normalizes incoming NDI feeds to.")); - TabContent.Children.Add(SettingRow("Resolution", "1920 x 1080", "Output resolution applied to every routed participant.")); - TabContent.Children.Add(SettingRow("Aspect mode", "Letterbox", "How sources whose aspect ratio doesn't match the target are framed.")); - TabContent.Children.Add(SettingRow("Audio routing", "Per-participant", "Embed each participant's audio in their own NDI source.")); - TabContent.Children.Add(SettingNote("Settings wired to the engine in a follow-up commit.")); - break; - case "Display": - TabContent.Children.Add(SettingHeader("Display & participants")); - TabContent.Children.Add(SettingRow("Hide local self", "Yes", "Filter the operator's own preview from the participants table.")); - TabContent.Children.Add(SettingRow("Auto-disable on departure", "No", "Keep routing alive when a participant goes offline.")); - TabContent.Children.Add(SettingRow("Participant sort", "Join order", "Default sort for the participants table.")); - TabContent.Children.Add(SettingRow("Minimize to tray", "No", "Keep TeamsISO running in the system tray when the window is minimized.")); - TabContent.Children.Add(SettingRow("Launch Teams on startup", "No", "Start Teams in the background when TeamsISO opens.")); - TabContent.Children.Add(SettingRow("Auto-hide Teams windows", "No", "Hide every visible Teams window after launch.")); - TabContent.Children.Add(SettingRow("Auto-record on call", "No", "Start recording every active ISO when Teams enters a call.")); - break; - case "Control": - TabContent.Children.Add(SettingHeader("External control surface")); - TabContent.Children.Add(SettingRow("REST + WebSocket", "127.0.0.1:9755", "HTTP control surface for Companion / Stream Deck.")); - TabContent.Children.Add(SettingRow("OSC bridge", "127.0.0.1:9000", "UDP OSC for TouchOSC / hardware surfaces.")); - TabContent.Children.Add(SettingRow("LAN reachable", "No", "Bind the REST surface to all interfaces (warning: no auth).")); - TabContent.Children.Add(SettingNote("Control surface protocol unchanged from the WPF host. See docs/CONTROL-SURFACE.md.")); - break; - case "Advanced": - TabContent.Children.Add(SettingHeader("Advanced")); - TabContent.Children.Add(SettingRow("Embed Teams window", "No", "Experimental SetParent reparent of Teams' main window.")); - TabContent.Children.Add(SettingRow("Logs", "%LOCALAPPDATA%\\TeamsISO\\Logs", "Where rolling daily Serilog files write.")); - TabContent.Children.Add(SettingRow("Recordings", "%USERPROFILE%\\Videos\\TeamsISO", "Default per-show recording directory.")); - TabContent.Children.Add(SettingRow("Diagnostic bundle", "Export", "Zip logs + config + presets for a bug report.")); - break; - } - } /// /// Appearance tab — theme picker, accent palette peek. The theme picker