Fix sidebar text cutoff + Teams launch ambush dialog
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.
This commit is contained in:
parent
e020d1c2ac
commit
598938ede5
4 changed files with 143 additions and 41 deletions
|
|
@ -123,11 +123,18 @@
|
|||
</Grid>
|
||||
</Button>
|
||||
|
||||
<!-- Nav: Launch Teams. Subprocess-launches the MS Teams desktop client. -->
|
||||
<!-- Nav: Launch Teams. Click = launch / surface; right-click = stop.
|
||||
The previous "left-click toggles" behavior ambushed operators
|
||||
who'd hidden Teams' windows via the eye toggle next door —
|
||||
they'd hit Launch expecting Teams to come back and instead
|
||||
get a "Close all Teams windows now?" dialog. Now click is
|
||||
always idempotent-progressive (running but hidden → show;
|
||||
not running → launch). Right-click for stop. -->
|
||||
<Button DockPanel.Dock="Top"
|
||||
Style="{StaticResource Wd.Button.RailIcon}"
|
||||
Click="OnLaunchTeamsClick"
|
||||
ToolTip="Launch Microsoft Teams">
|
||||
MouseRightButtonUp="OnLaunchTeamsRightClick"
|
||||
ToolTip="Launch Microsoft Teams (or surface its window). Right-click to stop Teams.">
|
||||
<!-- Stylized 'video meeting' camera icon -->
|
||||
<Path Data="M 4,8 L 16,8 L 16,16 L 4,16 Z M 16,11 L 22,8 L 22,16 L 16,13 Z"
|
||||
Stroke="{DynamicResource Wd.Text.Secondary}"
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
using System.Linq;
|
||||
using System.Windows;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Shapes;
|
||||
using TeamsISO.App.Services;
|
||||
using TeamsISO.App.ViewModels;
|
||||
|
|
@ -113,43 +114,78 @@ public partial class MainWindow : Window
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Toggle behavior: if Teams is already running, ask to stop it; otherwise
|
||||
/// launch via TeamsLauncher's fallback chain. First step toward the
|
||||
/// Embedded-Teams roadmap (Phase E.1).
|
||||
/// 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)
|
||||
{
|
||||
if (TeamsLauncher.IsRunning())
|
||||
{
|
||||
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 toast = (DataContext as MainViewModel)?.Toast;
|
||||
|
||||
var asked = TeamsLauncher.StopAll();
|
||||
if (TeamsLauncher.IsRunning())
|
||||
if (!TeamsLauncher.IsRunning())
|
||||
{
|
||||
if (!TeamsLauncher.TryLaunch(out var error))
|
||||
{
|
||||
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",
|
||||
$"Could not launch Microsoft Teams.\n\n{error}",
|
||||
"TeamsISO — Launch Teams",
|
||||
MessageBoxButton.OK,
|
||||
MessageBoxImage.Information);
|
||||
MessageBoxImage.Warning);
|
||||
}
|
||||
else
|
||||
{
|
||||
toast?.Show("Launching Microsoft Teams…");
|
||||
_teamsWindowsHidden = false;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!TeamsLauncher.TryLaunch(out var error))
|
||||
// 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(
|
||||
$"Could not launch Microsoft Teams.\n\n{error}",
|
||||
"TeamsISO — Launch Teams",
|
||||
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.Warning);
|
||||
MessageBoxImage.Information);
|
||||
}
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -44,21 +44,39 @@ public static class TeamsLauncher
|
|||
/// <summary>
|
||||
/// Launches Teams. Returns true if a launch was started successfully (the
|
||||
/// process may take a few seconds to actually appear). False if every
|
||||
/// fallback path failed.
|
||||
/// fallback path failed; <paramref name="errorMessage"/> includes the
|
||||
/// reasons each attempt was rejected so the operator can see why.
|
||||
///
|
||||
/// Path order matters:
|
||||
/// 1. <c>ms-teams:</c> URI — new Teams (MSTeams AppX) registers this
|
||||
/// handler at install. Activates through the AppX shell so the
|
||||
/// stub <c>ms-teams.exe</c> in WindowsApps gets the right context.
|
||||
/// 2. AppsFolder shell verb — direct AppX activation. Belt-and-braces
|
||||
/// fallback if a misconfigured registry breaks the URI handler.
|
||||
/// 3. Classic Teams Update.exe — pre-2024 Teams installations.
|
||||
/// We deliberately DON'T try the bare <c>ms-teams.exe</c> WindowsApps
|
||||
/// path: it's a 0-byte AppX placeholder that fails silently when invoked
|
||||
/// without AppX activation context. Looked plausible, never worked.
|
||||
/// </summary>
|
||||
public static bool TryLaunch(out string? errorMessage)
|
||||
{
|
||||
errorMessage = null;
|
||||
var attempts = new List<string>();
|
||||
|
||||
// Path 1: URI scheme. The shell handler picks whichever Teams client
|
||||
// is registered (new MSTeams.exe takes priority on modern Windows).
|
||||
if (TryStart("ms-teams:", useShell: true)) return true;
|
||||
// Path 1: URI scheme. The shell handler picks the registered Teams
|
||||
// (new MSTeams takes priority on modern Windows). UseShellExecute=true
|
||||
// is required — Win32 Process creation can't open URIs directly.
|
||||
if (TryStart("ms-teams:", useShell: true, out var err1)) return true;
|
||||
attempts.Add($"ms-teams: URI → {err1}");
|
||||
|
||||
// Path 2: new Teams' WindowsApps shim.
|
||||
var newTeams = Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
||||
"Microsoft", "WindowsApps", "ms-teams.exe");
|
||||
if (File.Exists(newTeams) && TryStart(newTeams, useShell: false)) return true;
|
||||
// Path 2: AppX activation via the explorer.exe shell. Modern Teams
|
||||
// ships as MSTeams_8wekyb3d8bbwe; if other code on the box has
|
||||
// clobbered the URI registration, this still works because it goes
|
||||
// through the AppsFolder verb the OS itself uses for Start menu launches.
|
||||
if (TryStart("explorer.exe", false, out var err2,
|
||||
arguments: "shell:appsFolder\\MSTeams_8wekyb3d8bbwe!MSTeams"))
|
||||
return true;
|
||||
attempts.Add($"AppsFolder shell → {err2}");
|
||||
|
||||
// Path 3: classic Teams Update.exe with --processStart hands off to
|
||||
// the actual Teams.exe via Squirrel.
|
||||
|
|
@ -80,11 +98,17 @@ public static class TeamsLauncher
|
|||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
errorMessage = ex.Message;
|
||||
attempts.Add($"classic Update.exe → {ex.Message}");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
attempts.Add($"classic Update.exe → not found at {classicUpdater}");
|
||||
}
|
||||
|
||||
errorMessage ??= "No Microsoft Teams installation was found. Install Teams from https://www.microsoft.com/microsoft-teams and try again.";
|
||||
errorMessage = "No Microsoft Teams installation could be launched. " +
|
||||
"Install Teams from https://www.microsoft.com/microsoft-teams and try again.\n\n" +
|
||||
"Attempts:\n • " + string.Join("\n • ", attempts);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -123,8 +147,9 @@ public static class TeamsLauncher
|
|||
return asked;
|
||||
}
|
||||
|
||||
private static bool TryStart(string target, bool useShell)
|
||||
private static bool TryStart(string target, bool useShell, out string error, string? arguments = null)
|
||||
{
|
||||
error = string.Empty;
|
||||
try
|
||||
{
|
||||
var info = new ProcessStartInfo
|
||||
|
|
@ -133,11 +158,13 @@ public static class TeamsLauncher
|
|||
UseShellExecute = useShell,
|
||||
CreateNoWindow = true,
|
||||
};
|
||||
if (arguments is not null) info.Arguments = arguments;
|
||||
Process.Start(info);
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
catch (Exception ex)
|
||||
{
|
||||
error = ex.Message;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -538,11 +538,25 @@
|
|||
<Setter Property="Foreground" Value="{StaticResource Wd.Text.Primary}"/>
|
||||
<Setter Property="Cursor" Value="Hand"/>
|
||||
<Setter Property="Padding" Value="10,0,0,0"/>
|
||||
<!--
|
||||
Stretch + a Grid template lets long Content strings wrap rather
|
||||
than get clipped by the 380px settings panel. Without this, the
|
||||
previous StackPanel template let the ContentPresenter grow
|
||||
unbounded horizontally and the parent's clip-to-bounds chopped
|
||||
off the right side.
|
||||
-->
|
||||
<Setter Property="HorizontalAlignment" Value="Stretch"/>
|
||||
<Setter Property="HorizontalContentAlignment" Value="Left"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="CheckBox">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<Grid HorizontalAlignment="Stretch">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Border x:Name="Box"
|
||||
Grid.Column="0"
|
||||
Width="18" Height="18"
|
||||
BorderBrush="{StaticResource Wd.BorderStrong}"
|
||||
BorderThickness="1"
|
||||
|
|
@ -556,9 +570,27 @@
|
|||
StrokeEndLineCap="Round"
|
||||
Visibility="Collapsed"/>
|
||||
</Border>
|
||||
<ContentPresenter Margin="{TemplateBinding Padding}"
|
||||
VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
<!--
|
||||
ContentPresenter resources inject a TextBlock
|
||||
style with TextWrapping=Wrap. When CheckBox's
|
||||
Content is a plain string (the common case here),
|
||||
WPF wraps it in an auto-generated TextBlock; the
|
||||
resource lookup applies our wrapping default so
|
||||
long labels flow onto multiple lines instead of
|
||||
being clipped at the column edge.
|
||||
-->
|
||||
<ContentPresenter Grid.Column="1"
|
||||
Margin="{TemplateBinding Padding}"
|
||||
VerticalAlignment="Center"
|
||||
HorizontalAlignment="Stretch"
|
||||
RecognizesAccessKey="True">
|
||||
<ContentPresenter.Resources>
|
||||
<Style TargetType="TextBlock">
|
||||
<Setter Property="TextWrapping" Value="Wrap"/>
|
||||
</Style>
|
||||
</ContentPresenter.Resources>
|
||||
</ContentPresenter>
|
||||
</Grid>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsChecked" Value="True">
|
||||
<Setter TargetName="Box" Property="Background" Value="{StaticResource Wd.Accent.Cyan}"/>
|
||||
|
|
|
|||
Loading…
Reference in a new issue