Skip to content

Commit

Permalink
Merge pull request #18635 from unoplatform/mergify/bp/release/stable/…
Browse files Browse the repository at this point in the history
…5.5/pr-18606

fix(vs): Ensure that infobars get closed properly (backport #18606)
  • Loading branch information
jeromelaban authored Oct 31, 2024
2 parents 7048f4d + f6c1d3e commit f878f66
Show file tree
Hide file tree
Showing 5 changed files with 135 additions and 53 deletions.
82 changes: 54 additions & 28 deletions src/Uno.UI.RemoteControl.VS/Commands/UnoMenuCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
using System.ComponentModel.Design;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Management.Instrumentation;
using System.Threading.Tasks;
using Microsoft.VisualStudio.Shell;
using Uno.UI.RemoteControl.Messaging.IdeChannel;
using Uno.UI.RemoteControl.VS.Commands;
Expand All @@ -11,62 +13,70 @@

namespace Uno.UI.RemoteControl.VS;

internal sealed class UnoMenuCommand
internal sealed class UnoMenuCommand : IDisposable
{
private readonly AsyncPackage _package;
private OleMenuCommandService CommandService { get; set; }
private IdeChannelClient IdeChannelClient;

private DynamicItemMenuCommand? _dynamicMenuCommand;
private OleMenuCommand? _unoMainMenuItem;
private static readonly Guid UnoStudioPackageCmdSet = new Guid("6c532d75-ee35-4726-a1cd-338c5243e38f");
private static readonly int UnoMainMenu = 0x4100;
private static readonly int DynamicMenuCommandId = 0x4103;

public List<AddMenuItemRequestIdeMessage> CommandList { get; set; } = [];
public static UnoMenuCommand? Instance { get; private set; }

private UnoMenuCommand(AsyncPackage package, IdeChannelClient ideChannelClient, OleMenuCommandService commandService, AddMenuItemRequestIdeMessage cr)
private UnoMenuCommand(
AsyncPackage package
, IdeChannelClient ideChannelClient
, OleMenuCommandService commandService
, AddMenuItemRequestIdeMessage cr)
{
_package = package ?? throw new ArgumentNullException(nameof(_package));
CommandService = commandService = commandService ?? throw new ArgumentNullException(nameof(commandService));
IdeChannelClient = ideChannelClient ?? throw new ArgumentNullException(nameof(ideChannelClient));
CommandList.Add(cr);

CommandID dynamicItemRootId = new CommandID(UnoStudioPackageCmdSet, DynamicMenuCommandId);
var dynamicItemRootId = new CommandID(UnoStudioPackageCmdSet, DynamicMenuCommandId);
if (commandService.FindCommand(dynamicItemRootId) is not DynamicItemMenuCommand)
{
DynamicItemMenuCommand dynamicMenuCommand = new DynamicItemMenuCommand(
_dynamicMenuCommand = new DynamicItemMenuCommand(
dynamicItemRootId,
IsValidDynamicItem,
OnInvokedDynamicItem,
OnBeforeQueryStatusDynamicItem);
commandService.AddCommand(dynamicMenuCommand);
commandService.AddCommand(_dynamicMenuCommand);
}

var unoMainMenuId = new CommandID(UnoStudioPackageCmdSet, UnoMainMenu);
if (commandService.FindCommand(unoMainMenuId) is not OleMenuCommand)
{
_unoMainMenuItem = new OleMenuCommand(null, unoMainMenuId);
_unoMainMenuItem.BeforeQueryStatus += OnBeforeQueryStatus;
commandService.AddCommand(_unoMainMenuItem);
}

var dynamicMenuCommandIdId = new CommandID(UnoStudioPackageCmdSet, DynamicMenuCommandId);
if (commandService.FindCommand(dynamicMenuCommandIdId) is DynamicItemMenuCommand dynamicMenuItem)
{
dynamicMenuItem.BeforeQueryStatus += OnBeforeQueryStatus;
}
}

public static async Task InitializeAsync(AsyncPackage package, IdeChannelClient ideChannelClient, AddMenuItemRequestIdeMessage cr)
public static async Task<UnoMenuCommand> InitializeAsync(
AsyncPackage package
, IdeChannelClient ideChannelClient
, AddMenuItemRequestIdeMessage cr)
{
// Switch to the main thread - the call to AddCommand in DynamicMenu's constructor requires the UI thread.
await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(package.DisposalToken);

if (Instance is null
&& await package.GetServiceAsync(typeof(IMenuCommandService)) is OleMenuCommandService commandService)
if (await package.GetServiceAsync(typeof(IMenuCommandService)) is OleMenuCommandService commandService)
{
Instance = new UnoMenuCommand(package, ideChannelClient, commandService, cr);

CommandID unoMainMenuId = new CommandID(UnoStudioPackageCmdSet, UnoMainMenu);
if (Instance.CommandService.FindCommand(unoMainMenuId) is not OleMenuCommand)
{
var unoMenuItem = new OleMenuCommand(null, unoMainMenuId);
unoMenuItem.BeforeQueryStatus += Instance.OnBeforeQueryStatus;
commandService.AddCommand(unoMenuItem);
}

CommandID dynamicMenuCommandIdId = new CommandID(UnoStudioPackageCmdSet, DynamicMenuCommandId);
if (Instance.CommandService.FindCommand(dynamicMenuCommandIdId) is DynamicItemMenuCommand dynamicMenuItem)
{
dynamicMenuItem.BeforeQueryStatus += Instance.OnBeforeQueryStatus;
}
return new UnoMenuCommand(package, ideChannelClient, commandService, cr);
}

throw new InvalidOperationException("IMenuCommandService is not availabe");
}

private void OnBeforeQueryStatus(object sender, EventArgs e)
Expand Down Expand Up @@ -108,10 +118,12 @@ sender is DynamicItemMenuCommand matchedCommand &&
private void OnBeforeQueryStatusDynamicItem(object sender, EventArgs args)
{
ThreadHelper.ThrowIfNotOnUIThread();

if (!CommandList.Any())
{
return;
}

DynamicItemMenuCommand matchedCommand = (DynamicItemMenuCommand)sender;
matchedCommand.Enabled = true;
matchedCommand.Visible = true;
Expand All @@ -124,10 +136,24 @@ private void OnBeforeQueryStatusDynamicItem(object sender, EventArgs args)
matchedCommand.MatchedCommandId = 0;
}

private static int GetCurrentPosition(DynamicItemMenuCommand matchedCommand) =>
// The position of the command is the command ID minus the ID of the root dynamic start item.
matchedCommand.MatchedCommandId == 0 ? 0 : matchedCommand.MatchedCommandId - (int)DynamicMenuCommandId;
private static int GetCurrentPosition(DynamicItemMenuCommand matchedCommand)
// The position of the command is the command ID minus the ID of the root dynamic start item.
=> matchedCommand.MatchedCommandId == 0 ? 0 : matchedCommand.MatchedCommandId - (int)DynamicMenuCommandId;

private bool TryGetCommandRequestIdeMessage(DynamicItemMenuCommand matchedCommand, [NotNullWhen(true)] out AddMenuItemRequestIdeMessage result)
=> (result = CommandList.Skip(GetCurrentPosition(matchedCommand)).FirstOrDefault()) != null;

public void Dispose()
{
if (_dynamicMenuCommand is not null)
{
CommandService.RemoveCommand(_dynamicMenuCommand);
}

if (_unoMainMenuItem is not null)
{
_unoMainMenuItem.Enabled = false;
_unoMainMenuItem.Visible = false;
}
}
}
12 changes: 9 additions & 3 deletions src/Uno.UI.RemoteControl.VS/EntryPoint.ActiveProfileSync.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ private async Task OnDebugFrameworkChangedAsync(string? previousFramework, strin
await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();

// In this case, a new TargetFramework was selected. We need to file a matching launch profile, if any.
if (GetTargetFrameworkIdentifier(newFramework) is { } targetFrameworkIdentifier)
if (GetTargetFrameworkIdentifier(newFramework) is { } targetFrameworkIdentifier && _debuggerObserver is not null)
{
_debugAction?.Invoke($"OnDebugFrameworkChangedAsync({previousFramework}, {newFramework}, {targetFrameworkIdentifier}, forceReload: {forceReload})");

Expand Down Expand Up @@ -134,6 +134,11 @@ private async Task OnDebugProfileChangedAsync(string? previousProfile, string ne
return;
}

if (_debuggerObserver is null)
{
return;
}

var targetFrameworks = await _debuggerObserver.GetActiveTargetFrameworksAsync();
var profiles = await _debuggerObserver.GetLaunchProfilesAsync();

Expand Down Expand Up @@ -305,7 +310,7 @@ previousTargetFrameworkIdentifier is WasmTargetFrameworkIdentifier

private async Task OnStartupProjectChangedAsync()
{
if (!await EnsureProjectUserSettingsAsync())
if (!await EnsureProjectUserSettingsAsync() && _debuggerObserver is not null)
{
_debugAction?.Invoke($"The user setting is not yet initialized, aligning framework and profile");

Expand All @@ -332,7 +337,8 @@ private async Task OnStartupProjectChangedAsync()
private async Task<bool> EnsureProjectUserSettingsAsync()
{
if (await _asyncPackage.GetServiceAsync(typeof(SVsSolution)) is IVsSolution solution
&& await _dte.GetStartupProjectsAsync() is { Length: > 0 } startupProjects)
&& await _dte.GetStartupProjectsAsync() is { Length: > 0 } startupProjects
&& _debuggerObserver is not null)
{
// Convert DTE project to IVsHierarchy
solution.GetProjectOfUniqueName(startupProjects[0].UniqueName, out var hierarchy);
Expand Down
58 changes: 38 additions & 20 deletions src/Uno.UI.RemoteControl.VS/EntryPoint.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,13 +58,15 @@ public partial class EntryPoint : IDisposable
private bool _closing;
private bool _isDisposed;
private IdeChannelClient? _ideChannelClient;
private ProfilesObserver _debuggerObserver;
private GlobalJsonObserver _globalJsonObserver;
private ProfilesObserver? _debuggerObserver;
private InfoBarFactory? _infoBarFactory;
private GlobalJsonObserver? _globalJsonObserver;
private readonly Func<Task> _globalPropertiesChanged;
private readonly _dispSolutionEvents_BeforeClosingEventHandler _closeHandler;
private readonly _dispBuildEvents_OnBuildBeginEventHandler _onBuildBeginHandler;
private readonly _dispBuildEvents_OnBuildDoneEventHandler _onBuildDoneHandler;
private readonly _dispBuildEvents_OnBuildProjConfigBeginEventHandler _onBuildProjConfigBeginHandler;
private _dispSolutionEvents_BeforeClosingEventHandler? _closeHandler;
private _dispBuildEvents_OnBuildBeginEventHandler? _onBuildBeginHandler;
private _dispBuildEvents_OnBuildDoneEventHandler? _onBuildDoneHandler;
private _dispBuildEvents_OnBuildProjConfigBeginEventHandler? _onBuildProjConfigBeginHandler;
private UnoMenuCommand? _unoMenuCommand;

public EntryPoint(
DTE2 dte2
Expand All @@ -80,6 +82,13 @@ DTE2 dte2
globalPropertiesProvider(OnProvideGlobalPropertiesAsync);
_globalPropertiesChanged = globalPropertiesChanged;

_ = ThreadHelper.JoinableTaskFactory.RunAsync(() => InitializeAsync(asyncPackage));
}

private async Task InitializeAsync(AsyncPackage asyncPackage)
{
await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();

SetupOutputWindow();

_closeHandler = () => SolutionEvents_BeforeClosing();
Expand Down Expand Up @@ -108,7 +117,13 @@ DTE2 dte2
, OnStartupProjectChangedAsync
, _debugAction);

_globalJsonObserver = new GlobalJsonObserver(asyncPackage, _dte, _debugAction, _infoAction, _warningAction, _errorAction);
if (await _asyncPackage.GetServiceAsync(typeof(SVsShell)) is IVsShell shell
&& await _asyncPackage.GetServiceAsync(typeof(SVsInfoBarUIFactory)) is IVsInfoBarUIFactory infoBarFactory)
{
_infoBarFactory = new InfoBarFactory(infoBarFactory, shell);

_globalJsonObserver = new GlobalJsonObserver(asyncPackage, _dte, _infoBarFactory, _debugAction, _infoAction, _warningAction, _errorAction);
}

_ = _debuggerObserver.ObserveProfilesAsync();

Expand Down Expand Up @@ -450,17 +465,17 @@ private async Task OnAddMenuItemRequestIdeMessageAsync(object? sender, AddMenuIt
return;
}

if (UnoMenuCommand.Instance is { } instance)
if (_unoMenuCommand is not null)
{
//ignore when duplicated
if (!instance.CommandList.Contains(cr))
if (!_unoMenuCommand.CommandList.Contains(cr))
{
instance.CommandList.Add(cr);
_unoMenuCommand.CommandList.Add(cr);
}
}
else
{
await UnoMenuCommand.InitializeAsync(_asyncPackage, _ideChannelClient, cr);
_unoMenuCommand = await UnoMenuCommand.InitializeAsync(_asyncPackage, _ideChannelClient, cr);
}
}
catch (Exception e)
Expand All @@ -472,13 +487,12 @@ private async Task OnAddMenuItemRequestIdeMessageAsync(object? sender, AddMenuIt

private async Task CreateInfoBarAsync(NotificationRequestIdeMessage e, IVsShell shell, IVsInfoBarUIFactory infoBarFactory)
{
if (_ideChannelClient is null)
if (_ideChannelClient is null || _infoBarFactory is null)
{
return;
}
var factory = new InfoBarFactory(infoBarFactory, shell);

var infoBar = await factory.CreateAsync(
var infoBar = await _infoBarFactory.CreateAsync(
new InfoBarModel(
e.Message,
e.Commands.Select(Commands => new ActionBarItem
Expand All @@ -500,12 +514,14 @@ private async Task CreateInfoBarAsync(NotificationRequestIdeMessage e, IVsShell
if (e.ActionItem is ActionBarItem action &&
action.Name is { } command)
{
var cmd =
new CommandRequestIdeMessage(
System.Diagnostics.Process.GetCurrentProcess().Id,
command,
action.ActionContext?.ToString());
var cmd = new CommandRequestIdeMessage(
System.Diagnostics.Process.GetCurrentProcess().Id,
command,
action.ActionContext?.ToString());

await _ideChannelClient.SendToDevServerAsync(cmd, _ct.Token);

infoBar.Close();
}
});
};
Expand Down Expand Up @@ -622,7 +638,9 @@ public void Dispose()
_dte.Events.BuildEvents.OnBuildBegin -= _onBuildBeginHandler;
_dte.Events.BuildEvents.OnBuildDone -= _onBuildDoneHandler;
_dte.Events.BuildEvents.OnBuildProjConfigBegin -= _onBuildProjConfigBeginHandler;
_globalJsonObserver.Dispose();
_globalJsonObserver?.Dispose();
_infoBarFactory?.Dispose();
_unoMenuCommand?.Dispose();
}
catch (Exception e)
{
Expand Down
3 changes: 3 additions & 0 deletions src/Uno.UI.RemoteControl.VS/GlobalJsonObserver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,14 @@ internal class GlobalJsonObserver
private readonly Action<string> _infoAction;
private readonly Action<string> _warningAction;
private readonly Action<string> _errorAction;
private readonly InfoBarFactory _infoBarFactory;
private FileSystemWatcher? _fileWatcher;
private readonly JsonSerializerOptions _readerOptions = new() { ReadCommentHandling = JsonCommentHandling.Skip };

public GlobalJsonObserver(
AsyncPackage asyncPackage
, DTE dte
, InfoBarFactory infoBarFactory
, Action<string> debugAction
, Action<string> infoAction
, Action<string> warningAction
Expand All @@ -40,6 +42,7 @@ AsyncPackage asyncPackage
_infoAction = infoAction;
_warningAction = warningAction;
_errorAction = errorAction;
_infoBarFactory = infoBarFactory;

_debugAction("GlobalJsonObserver: Starting");

Expand Down
Loading

0 comments on commit f878f66

Please sign in to comment.