Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions TrayApp.Test/TrayApplicationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,32 @@ public void TrayIconClick_WithTheToggleDisabled_DoesNotRunTheToolsSetter()
Assert.AreEqual(0, setterCalls);
}

[TestMethod]
public void TrayIconClick_WithTheFirstToggleDisabled_LeavesTheSecondToggleAlone()
{
int firstSetterCalls = 0;
int secondSetterCalls = 0;
int remembered = 0;

TrayAppDefinition app = TrayAppBuilder.Create("demo")
.DisplayName("Demo")
.Toggle("Keep awake", () => false, _ => firstSetterCalls++, isEnabled: () => false)
.Toggle("Start at login", () => false, _ => secondSetterCalls++)
.Build();

TrayMenu menu = new(app.DisplayName, app.Status, app.Items);
using TrayApplication host = new(app, menu, duration: null, onToggled: _ => remembered++);
menu.Refresh();

host.OnTrayIconClicked(this, EventArgs.Empty);

// The icon follows the first toggle, so a click on it that flipped the second would change - and
// remember - a setting the icon gives no sign of. A disabled first toggle makes the click do nothing.
Assert.AreEqual(0, firstSetterCalls);
Assert.AreEqual(0, secondSetterCalls);
Assert.AreEqual(0, remembered);
}

[TestMethod]
public void TrayIconClick_WithTheToggleEnabled_RunsTheToolsSetter()
{
Expand Down
18 changes: 14 additions & 4 deletions TrayApp/Tray/TrayApplication.cs
Original file line number Diff line number Diff line change
Expand Up @@ -214,14 +214,20 @@ private void BuildNativeMenu()
}

/// <summary>
/// Flips the first toggle the user could have clicked in the menu.
/// Flips the first toggle in the menu, the one the icon shows by default, and does nothing when that
/// toggle is disabled.
/// </summary>
/// <remarks>
/// A disabled toggle is skipped, because this is the only activation path with nothing between the
/// A disabled first toggle is not passed over for the next one. The icon follows the first toggle unless
/// the tool says otherwise, so flipping a later one would change - and, with a <c>persistAs</c> key,
/// remember - a setting the icon gives no sign of.
/// <para>
/// A disabled toggle is left alone because this is the only activation path with nothing between the
/// click and the tool's setter. The menu path is projected onto <see cref="NativeMenuItem"/>s that
/// the toolkit greys out and refuses to raise <c>Click</c> for, so the tray icon is where a tool's
/// <c>isEnabled</c> predicate would otherwise be silently ignored - the menu would show the action
/// as unavailable while a click on the icon ran it anyway.
/// </para>
/// <para>
/// Internal so that it can be driven without a platform: everything above this reads menu state
/// that <see cref="TrayMenu.Refresh"/> populates with no display attached.
Expand All @@ -231,9 +237,13 @@ internal void OnTrayIconClicked(object? sender, EventArgs e)
{
foreach ((TrayMenuItem model, _) in entries)
{
if (model is TrayToggleItem { IsEnabled: true })
if (model is TrayToggleItem toggle)
{
Activate(model);
if (toggle.IsEnabled)
{
Activate(model);
}

return;
}
}
Expand Down
Loading