Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(vs): Ensure that infobars get closed properly #18606

Merged
merged 2 commits into from
Oct 29, 2024
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
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 @@ -475,17 +490,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 @@ -497,13 +512,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 @@ -525,12 +539,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 @@ -647,7 +663,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
Loading