Two user-reported bugs:
1) CheckBox content was clipping in the 380px settings panel ('Control surface (Stream Deck / Companion / w...' / 'LAN-reachable (allow other machines on yo...'). The Wd.CheckBox template used a horizontal StackPanel which doesn't bound child width, so long Content strings ran off the column without wrapping. Replaced StackPanel with a Grid (Auto + *) and injected a TextBlock style with TextWrapping=Wrap into the ContentPresenter resources — when WPF auto-wraps a string Content in a TextBlock, the resource lookup gives it Wrap.
2) The rail Launch Teams button ambushed operators: clicking with Teams already running (which is common when the eye-toggle has hidden Teams' windows) opened a 'Close all Teams windows now?' dialog. Operators expect Launch to mean 'show me Teams', not 'stop Teams'. Split the actions:
- Left-click: Teams not running → launch; Teams hidden → restore + foreground; Teams visible → bring to front. Always idempotent-progressive.
- Right-click: ask to stop Teams (preserves the kill path for those who want it).
TeamsLauncher.TryLaunch now collects per-attempt errors instead of swallowing them — a real failure surfaces 'ms-teams: URI → <reason>' / 'AppsFolder shell → <reason>' / 'classic Update.exe → not found at <path>' so 'No Teams found' isn't a black box.
Also added a 2nd path: explorer.exe shell:appsFolder\\\\MSTeams_8wekyb3d8bbwe!MSTeams (AppX activation via the OS's own Start-menu verb) as a fallback if the URI handler is misconfigured. Removed the broken bare-stub call to %LOCALAPPDATA%\\\\Microsoft\\\\WindowsApps\\\\ms-teams.exe — that's a 0-byte AppX placeholder that never worked outside an AppX context.
205 lines
8.1 KiB
C#
205 lines
8.1 KiB
C#
using System.Linq;
|
|
using System.Windows;
|
|
using System.Windows.Input;
|
|
using System.Windows.Shapes;
|
|
using TeamsISO.App.Services;
|
|
using TeamsISO.App.ViewModels;
|
|
|
|
namespace TeamsISO.App;
|
|
|
|
public partial class MainWindow : Window
|
|
{
|
|
public MainWindow()
|
|
{
|
|
InitializeComponent();
|
|
StateChanged += OnWindowStateChanged;
|
|
SourceInitialized += OnSourceInitialized;
|
|
Closing += OnClosing;
|
|
}
|
|
|
|
public MainWindow(MainViewModel viewModel) : this()
|
|
{
|
|
DataContext = viewModel;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Restore the window's previous placement after the HWND is created (so
|
|
/// SetWindowPos / WindowState transitions actually take effect). Falls
|
|
/// silently back to the XAML-default startup location if no snapshot exists.
|
|
/// </summary>
|
|
private void OnSourceInitialized(object? sender, EventArgs e)
|
|
{
|
|
WindowStateStore.TryApply(this);
|
|
}
|
|
|
|
/// <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);
|
|
}
|
|
|
|
/// <summary>Custom min button — chrome'd window has no system caption buttons.</summary>
|
|
private void OnMinimize(object sender, RoutedEventArgs e) =>
|
|
WindowState = WindowState.Minimized;
|
|
|
|
/// <summary>Toggles maximize/restore. Bound to the maximize button + double-click on the drag region.</summary>
|
|
private void OnMaximizeRestore(object sender, RoutedEventArgs e) =>
|
|
WindowState = WindowState == WindowState.Maximized ? WindowState.Normal : WindowState.Maximized;
|
|
|
|
/// <summary>Custom close button.</summary>
|
|
private void OnClose(object sender, RoutedEventArgs e) => Close();
|
|
|
|
/// <summary>Opens the About dialog — version, NDI runtime, build SHA.</summary>
|
|
private void OnAboutClick(object sender, RoutedEventArgs e)
|
|
{
|
|
var about = new AboutWindow { Owner = this };
|
|
about.ShowDialog();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Opens the operator-presets dialog. Hands it the current participants
|
|
/// snapshot (so Save captures live state) and the engine controller (so
|
|
/// Apply can reconcile enable/disable). Owner is set so the chromeless
|
|
/// dialog centers over the main window and inherits z-order.
|
|
/// </summary>
|
|
private void OnPresetsClick(object sender, RoutedEventArgs e)
|
|
{
|
|
if (DataContext is not MainViewModel vm) return;
|
|
var dialog = new PresetsDialog(vm.Controller, vm.Participants.ToList(), vm.Toast)
|
|
{
|
|
Owner = this,
|
|
};
|
|
dialog.ShowDialog();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tracks whether we have hidden Teams' windows so the next click reverses
|
|
/// the action. We treat this as "intent" rather than a query of OS state
|
|
/// because hidden windows still report as hidden if the operator manually
|
|
/// re-opens them and we only care about TeamsISO's own toggle history.
|
|
/// </summary>
|
|
private bool _teamsWindowsHidden;
|
|
|
|
/// <summary>
|
|
/// Phase E.2 toggle. Hides every visible top-level Teams window on first
|
|
/// click; shows them again on the next. Surfaces the result via the toast
|
|
/// so the operator gets feedback even though the affected windows aren't
|
|
/// visible anymore.
|
|
/// </summary>
|
|
private void OnToggleTeamsWindowClick(object sender, RoutedEventArgs e)
|
|
{
|
|
if (!TeamsLauncher.IsRunning())
|
|
{
|
|
MessageBox.Show(
|
|
"Microsoft Teams isn't running. Click the camera icon above to launch it first.",
|
|
"TeamsISO — Hide / show Teams",
|
|
MessageBoxButton.OK,
|
|
MessageBoxImage.Information);
|
|
return;
|
|
}
|
|
|
|
var toast = (DataContext as MainViewModel)?.Toast;
|
|
if (_teamsWindowsHidden)
|
|
{
|
|
var shown = TeamsLauncher.ShowWindows();
|
|
_teamsWindowsHidden = false;
|
|
toast?.Show(shown > 0 ? $"Restored {shown} Teams window(s)" : "No Teams windows to restore");
|
|
}
|
|
else
|
|
{
|
|
var hidden = TeamsLauncher.HideWindows();
|
|
_teamsWindowsHidden = hidden > 0;
|
|
toast?.Show(hidden > 0 ? $"Hid {hidden} Teams window(s)" : "Teams has no visible windows yet");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Three-state click behavior matching operator intuition:
|
|
/// 1. Teams not running → launch it via TeamsLauncher's fallback chain.
|
|
/// 2. Teams running but its windows are hidden (we toggled them off, OR
|
|
/// it launched into the tray) → restore the windows + foreground them.
|
|
/// This is the case the previous "ask to stop" dialog was ambushing —
|
|
/// operators don't think of a hidden Teams as "running and ready to
|
|
/// stop", they think of it as "I clicked Launch and nothing happened".
|
|
/// 3. Teams running with visible windows → bring the most recent one to
|
|
/// the foreground. (Stopping Teams is now a right-click action;
|
|
/// see OnLaunchTeamsRightClick.)
|
|
/// </summary>
|
|
private void OnLaunchTeamsClick(object sender, RoutedEventArgs e)
|
|
{
|
|
var toast = (DataContext as MainViewModel)?.Toast;
|
|
|
|
if (!TeamsLauncher.IsRunning())
|
|
{
|
|
if (!TeamsLauncher.TryLaunch(out var error))
|
|
{
|
|
MessageBox.Show(
|
|
$"Could not launch Microsoft Teams.\n\n{error}",
|
|
"TeamsISO — Launch Teams",
|
|
MessageBoxButton.OK,
|
|
MessageBoxImage.Warning);
|
|
}
|
|
else
|
|
{
|
|
toast?.Show("Launching Microsoft Teams…");
|
|
_teamsWindowsHidden = false;
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Teams is running. Always try to restore + foreground its window —
|
|
// if windows are already visible, ShowWindows is a SetForegroundWindow
|
|
// no-op besides bringing them forward; if they were hidden by our
|
|
// own toggle, this is the operator's intuitive "show me Teams" path.
|
|
var shown = TeamsLauncher.ShowWindows();
|
|
_teamsWindowsHidden = false;
|
|
toast?.Show(shown > 0
|
|
? $"Teams is already running — surfaced {shown} window(s)"
|
|
: "Teams is running but has no visible windows yet");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Right-click on the rail 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".
|
|
/// </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",
|
|
MessageBoxButton.YesNo,
|
|
MessageBoxImage.Question);
|
|
if (confirm != MessageBoxResult.Yes) return;
|
|
|
|
var asked = TeamsLauncher.StopAll();
|
|
if (TeamsLauncher.IsRunning())
|
|
{
|
|
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",
|
|
MessageBoxButton.OK,
|
|
MessageBoxImage.Information);
|
|
}
|
|
e.Handled = true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Swap the maximize-button glyph between the "single rectangle" (when normal) and the
|
|
/// "two-overlapping-rectangles" (when maximized) variants, matching the Windows 11
|
|
/// caption-button conventions.
|
|
/// </summary>
|
|
private void OnWindowStateChanged(object? sender, EventArgs e)
|
|
{
|
|
if (FindName("MaximizeIcon") is not Path icon) return;
|
|
icon.Data = WindowState == WindowState.Maximized
|
|
// Two-rectangle "restore" glyph
|
|
? System.Windows.Media.Geometry.Parse("M 2,0 L 10,0 L 10,8 M 0,2 L 8,2 L 8,10 L 0,10 Z")
|
|
// Single-rectangle "maximize" glyph
|
|
: System.Windows.Media.Geometry.Parse("M 0,0 L 10,0 L 10,10 L 0,10 Z");
|
|
}
|
|
}
|