diff --git a/build/PackageDiffIgnore.xml b/build/PackageDiffIgnore.xml index 875317d81ea1..6f0c700a3375 100644 --- a/build/PackageDiffIgnore.xml +++ b/build/PackageDiffIgnore.xml @@ -1982,6 +1982,10 @@ + + + + diff --git a/src/Uno.Foundation/Diagnostics/DiagnosticViewRegistry.cs b/src/Uno.Foundation/Diagnostics/DiagnosticViewRegistry.cs index 41a3a91a7b64..d153d4bf4347 100644 --- a/src/Uno.Foundation/Diagnostics/DiagnosticViewRegistry.cs +++ b/src/Uno.Foundation/Diagnostics/DiagnosticViewRegistry.cs @@ -12,7 +12,7 @@ internal static class DiagnosticViewRegistry { internal static EventHandler>? Added; - private static ImmutableArray _registrations = ImmutableArray.Empty; + private static ImmutableArray _registrations = []; /// /// Gets the list of registered diagnostic providers. @@ -35,7 +35,9 @@ public static void Register(IDiagnosticView view, DiagnosticViewRegistrationMode } } -internal record DiagnosticViewRegistration(DiagnosticViewRegistrationMode Mode, IDiagnosticView View); +internal sealed record DiagnosticViewRegistration( + DiagnosticViewRegistrationMode Mode, + IDiagnosticView View); public enum DiagnosticViewRegistrationMode { @@ -43,7 +45,7 @@ public enum DiagnosticViewRegistrationMode /// Diagnostic is being display on at least one window. /// I.e. only the main/first opened but move to the next one if the current window is closed. /// - One, + One, // Default /// /// Diagnostic is being rendered as overlay on each window. @@ -55,3 +57,18 @@ public enum DiagnosticViewRegistrationMode /// OnDemand } + +public enum DiagnosticViewRegistrationPosition +{ + Normal = 0, // Default + + /// + /// Register as the first diagnostic view, ensuring it is displayed first. + /// + First = -1, + + /// + /// Register as the last diagnostic view, ensuring it is displayed last. + /// + Last = 1, +} diff --git a/src/Uno.Foundation/Diagnostics/IDiagnosticView.cs b/src/Uno.Foundation/Diagnostics/IDiagnosticView.cs index 34847ce89737..2e35bbd3021b 100644 --- a/src/Uno.Foundation/Diagnostics/IDiagnosticView.cs +++ b/src/Uno.Foundation/Diagnostics/IDiagnosticView.cs @@ -21,6 +21,8 @@ public interface IDiagnosticView /// string Name { get; } + DiagnosticViewRegistrationPosition Position => DiagnosticViewRegistrationPosition.Normal; + /// /// Gets a visual element of the diagnostic, usually a value or an icon. /// diff --git a/src/Uno.UI.RemoteControl/RemoteControlClient.cs b/src/Uno.UI.RemoteControl/RemoteControlClient.cs index c05abcabf61a..12f6223f5874 100644 --- a/src/Uno.UI.RemoteControl/RemoteControlClient.cs +++ b/src/Uno.UI.RemoteControl/RemoteControlClient.cs @@ -33,7 +33,63 @@ public partial class RemoteControlClient : IRemoteControlClient public delegate void RemoteControlClientEventEventHandler(object sender, ClientEventEventArgs args); public delegate void SendMessageFailedEventHandler(object sender, SendMessageFailedEventArgs args); - public static RemoteControlClient? Instance { get; private set; } + public static RemoteControlClient? Instance + { + get => _instance; + private set + { + _instance = value; + + if (value is { }) + { + while (Interlocked.Exchange(ref _waitingList, null) is { } waitingList) + { + foreach (var action in waitingList) + { + action(value); + } + } + } + } + } + + private static IReadOnlyCollection>? _waitingList; + + /// + /// Add a callback to be called when the Instance is available. + /// + /// + /// Will be called synchronously if the instance is already available, no need to check for it before. + /// + public static void OnRemoteControlClientAvailable(Action action) + { + if (Instance is { }) + { + action(Instance); + } + else + { + // Thread-safe way to add the action to a waiting list for the client to be available + while (true) + { + var waitingList = _waitingList; + IReadOnlyCollection> newList = waitingList is null + ? [action] + : [.. waitingList, action]; + + if (Instance is { } i) // Last chance to avoid the waiting list + { + action(i); + break; + } + + if (ReferenceEquals(Interlocked.CompareExchange(ref _waitingList, newList, waitingList), waitingList)) + { + break; + } + } + } + } public static RemoteControlClient Initialize(Type appType) => Instance = new RemoteControlClient(appType); @@ -59,6 +115,7 @@ internal static RemoteControlClient Initialize(Type appType, ServerEndpointAttri private readonly StatusSink _status; private static readonly TimeSpan _keepAliveInterval = TimeSpan.FromSeconds(30); + private static RemoteControlClient? _instance; private readonly (string endpoint, int port)[]? _serverAddresses; private readonly Dictionary _processors = new(); private readonly List _preprocessors = new(); @@ -223,7 +280,6 @@ public void RegisterPreProcessor(IRemoteControlPreProcessor preprocessor) _status.Report(ConnectionState.Connecting); - const string lastEndpointKey = "__UNO__" + nameof(RemoteControlClient) + "__last_endpoint"; var preferred = ApplicationData.Current.LocalSettings.Values.TryGetValue(lastEndpointKey, out var lastValue) && lastValue is string lastEp ? _serverAddresses.FirstOrDefault(srv => srv.endpoint.Equals(lastEp, StringComparison.OrdinalIgnoreCase)).endpoint @@ -429,7 +485,8 @@ private async Task Connect(Uri serverUri, int delay, CancellationTok { if (this.Log().IsEnabled(LogLevel.Trace)) { - this.Log().Trace($"Connecting to [{serverUri}] failed: {e.Message}"); + var innerMessage = e.InnerException is { } ie ? $" ({ie.Message})" : ""; + this.Log().Trace($"Connecting to [{serverUri}] failed: {e.Message}{innerMessage}"); } return new(this, serverUri, watch, null); diff --git a/src/Uno.UI.RemoteControl/RemoteControlStatus.cs b/src/Uno.UI.RemoteControl/RemoteControlStatus.cs index 87de6eb8d095..f1915e455f34 100644 --- a/src/Uno.UI.RemoteControl/RemoteControlStatus.cs +++ b/src/Uno.UI.RemoteControl/RemoteControlStatus.cs @@ -12,8 +12,27 @@ public record RemoteControlStatus( ImmutableHashSet MissingRequiredProcessors, (long Count, ImmutableHashSet Types) InvalidFrames) { - public bool IsAllGood => State == ConnectionState.Connected && IsVersionValid == true && MissingRequiredProcessors.IsEmpty && KeepAlive.State == KeepAliveState.Ok && InvalidFrames.Count == 0; + /// + /// A boolean indicating if everything is fine with the connection and the handshaking succeeded. + /// + public bool IsAllGood => + State == ConnectionState.Connected +#if !DEBUG + // For debug builds, it's annoying to have the version mismatch preventing the connection + // Only Uno devs should get this issue, let's not block them. + && IsVersionValid == true +#endif + && MissingRequiredProcessors.IsEmpty + && KeepAlive.State == KeepAliveState.Ok + && InvalidFrames.Count == 0; + + /// + /// If the connection is problematic, meaning that the connection is not in a good state. + /// + /// + /// It's just a negation of . + /// public bool IsProblematic => !IsAllGood; public (Classification kind, string message) GetSummary() diff --git a/src/Uno.UI.Toolkit/Diagnostics/DiagnosticsOverlay.cs b/src/Uno.UI.Toolkit/Diagnostics/DiagnosticsOverlay.cs index 20ad0d418211..b26d78949ad9 100644 --- a/src/Uno.UI.Toolkit/Diagnostics/DiagnosticsOverlay.cs +++ b/src/Uno.UI.Toolkit/Diagnostics/DiagnosticsOverlay.cs @@ -2,6 +2,7 @@ #if WINUI || HAS_UNO_WINUI using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Diagnostics; using System.Linq; using System.Runtime.CompilerServices; @@ -316,8 +317,8 @@ public void Show(string viewId) /// Add a UI diagnostic element to this overlay. /// /// This will also make this overlay visible (cf. ). - public void Add(string id, string name, UIElement preview, Func? details = null) - => Add(new DiagnosticView(id, name, _ => preview, (_, ct) => new(details?.Invoke()))); + public void Add(string id, string name, UIElement preview, Func? details = null, DiagnosticViewRegistrationPosition position = default) + => Add(new DiagnosticView(id, name, _ => preview, (_, ct) => new(details?.Invoke()), position)); /// /// Add a UI diagnostic element to this overlay. @@ -464,8 +465,8 @@ private void EnqueueUpdate(bool forceUpdate = false) .Where(ShouldMaterialize) .Select(reg => reg.View) .Concat(_localRegistrations) - .Distinct() - .ToList(); + .OrderBy(r => (int)r.Position) + .Distinct(); foreach (var view in viewsThatShouldBeMaterialized) { diff --git a/src/Uno.UI/Diagnostics/DiagnosticView.Factories.cs b/src/Uno.UI/Diagnostics/DiagnosticView.Factories.cs index d67177d8ce0f..393379f0fcc6 100644 --- a/src/Uno.UI/Diagnostics/DiagnosticView.Factories.cs +++ b/src/Uno.UI/Diagnostics/DiagnosticView.Factories.cs @@ -21,11 +21,16 @@ partial class DiagnosticView /// /// Type of the control. /// The user-friendly name of the diagnostics view. - public static DiagnosticView Register(string friendlyName) + /// Defines when the registered diagnostic view should be displayed. + /// Defines where the item should be placed in the overlay. + public static DiagnosticView Register( + string friendlyName, + DiagnosticViewRegistrationMode mode = default, + DiagnosticViewRegistrationPosition position = default) where TView : UIElement, new() { - var provider = new DiagnosticView(typeof(TView).Name, friendlyName, () => new TView()); - DiagnosticViewRegistry.Register(provider); + var provider = new DiagnosticView(typeof(TView).Name, friendlyName, () => new TView(), position: position); + DiagnosticViewRegistry.Register(provider, mode); return provider; } @@ -43,10 +48,15 @@ public static DiagnosticView Register(string friendlyName) /// The user-friendly name of the diagnostics view. /// Factory to create an instance of the control. /// Defines when the registered diagnostic view should be displayed. - public static DiagnosticView Register(string friendlyName, Func factory, DiagnosticViewRegistrationMode mode = default) + /// Defines where the item should be placed in the overlay. + public static DiagnosticView Register( + string friendlyName, + Func factory, + DiagnosticViewRegistrationMode mode = default, + DiagnosticViewRegistrationPosition position = default) where TView : UIElement { - var provider = new DiagnosticView(typeof(TView).Name, friendlyName, factory); + var provider = new DiagnosticView(typeof(TView).Name, friendlyName, factory, position: position); DiagnosticViewRegistry.Register(provider, mode); return provider; } @@ -62,17 +72,21 @@ public static DiagnosticView Register(string friendlyName, FuncThe user-friendly name of the diagnostics view. /// Delegate to use to update the when the is being updated. /// Optional delegate used to show more details about the diagnostic info when user taps on the view. + /// Defines when the registered diagnostic view should be displayed. + /// Defines where the item should be placed in the overlay. /// A diagnostic view helper class which can be used to push updates of the state (cf. ). public static DiagnosticView Register( string friendlyName, Action update, - Func? details = null) + Func? details = null, + DiagnosticViewRegistrationMode mode = default, + DiagnosticViewRegistrationPosition position = default) where TView : FrameworkElement, new() { var provider = details is null - ? new DiagnosticView(typeof(TView).Name, friendlyName, _ => new TView(), update) - : new DiagnosticView(typeof(TView).Name, friendlyName, _ => new TView(), update, (ctx, state, ct) => new(details(state))); - DiagnosticViewRegistry.Register(provider); + ? new DiagnosticView(typeof(TView).Name, friendlyName, _ => new TView(), update, position: position) + : new DiagnosticView(typeof(TView).Name, friendlyName, _ => new TView(), update, (ctx, state, ct) => new(details(state)), position: position); + DiagnosticViewRegistry.Register(provider, mode); return provider; } @@ -88,18 +102,22 @@ public static DiagnosticView Register( /// Factory to create an instance of the generic element. /// Delegate to use to update the when the is being updated. /// Optional delegate used to show more details about the diagnostic info when user taps on the view. + /// Defines when the registered diagnostic view should be displayed. + /// Defines where the item should be placed in the overlay. /// A diagnostic view helper class which can be used to push updates of the state (cf. ). public static DiagnosticView Register( string friendlyName, Func factory, Action update, - Func? details = null) + Func? details = null, + DiagnosticViewRegistrationMode mode = default, + DiagnosticViewRegistrationPosition position = default) where TView : FrameworkElement { var provider = details is null - ? new DiagnosticView(typeof(TView).Name, friendlyName, factory, update) - : new DiagnosticView(typeof(TView).Name, friendlyName, factory, update, (ctx, state, ct) => new(details(state))); - DiagnosticViewRegistry.Register(provider); + ? new DiagnosticView(typeof(TView).Name, friendlyName, factory, update, position: position) + : new DiagnosticView(typeof(TView).Name, friendlyName, factory, update, (ctx, state, ct) => new(details(state)), position: position); + DiagnosticViewRegistry.Register(provider, mode); return provider; } } diff --git a/src/Uno.UI/Diagnostics/DiagnosticView.TView.TState.cs b/src/Uno.UI/Diagnostics/DiagnosticView.TView.TState.cs index a2bcbf6b0851..2c8fbbabcf79 100644 --- a/src/Uno.UI/Diagnostics/DiagnosticView.TView.TState.cs +++ b/src/Uno.UI/Diagnostics/DiagnosticView.TView.TState.cs @@ -15,7 +15,8 @@ public class DiagnosticView( string name, Func factory, Action update, - Func>? details = null) + Func>? details = null, + DiagnosticViewRegistrationPosition position = default) : IDiagnosticView where TView : FrameworkElement { @@ -37,6 +38,8 @@ public void Update(TState status) /// string IDiagnosticView.Name => name; + DiagnosticViewRegistrationPosition IDiagnosticView.Position => position; + /// object IDiagnosticView.GetElement(IDiagnosticViewContext context) => _elementsManager.GetView(context); diff --git a/src/Uno.UI/Diagnostics/DiagnosticView.TView.cs b/src/Uno.UI/Diagnostics/DiagnosticView.TView.cs index 3f0cf8997cb1..619d748bea12 100644 --- a/src/Uno.UI/Diagnostics/DiagnosticView.TView.cs +++ b/src/Uno.UI/Diagnostics/DiagnosticView.TView.cs @@ -14,7 +14,8 @@ public class DiagnosticView( string id, string name, Func factory, - Func>? details = null) + Func>? details = null, + DiagnosticViewRegistrationPosition position = default) : IDiagnosticView where TView : UIElement { @@ -22,8 +23,9 @@ public DiagnosticView( string id, string name, Func preview, - Func>? details = null) - : this(id, name, _ => preview(), async (_, ct) => details is null ? null : await details(ct)) + Func>? details = null, + DiagnosticViewRegistrationPosition position = default) + : this(id, name, _ => preview(), async (_, ct) => details is null ? null : await details(ct), position) { } @@ -33,6 +35,8 @@ public DiagnosticView( /// string IDiagnosticView.Name => name; + DiagnosticViewRegistrationPosition IDiagnosticView.Position => position; + /// object IDiagnosticView.GetElement(IDiagnosticViewContext context) => factory(context); diff --git a/src/Uno.UI/Diagnostics/DiagnosticView.cs b/src/Uno.UI/Diagnostics/DiagnosticView.cs index 9849ba1e2294..4a7eebf30157 100644 --- a/src/Uno.UI/Diagnostics/DiagnosticView.cs +++ b/src/Uno.UI/Diagnostics/DiagnosticView.cs @@ -14,15 +14,17 @@ public partial class DiagnosticView( string id, string name, Func factory, - Func>? details = null) - : DiagnosticView(id, name, factory, details) + Func>? details = null, + DiagnosticViewRegistrationPosition position = default) + : DiagnosticView(id, name, factory, details, position) { public DiagnosticView( string id, string name, Func preview, - Func>? details = null) - : this(id, name, _ => preview(), async (_, ct) => details is null ? null : await details(ct)) + Func>? details = null, + DiagnosticViewRegistrationPosition position = default) + : this(id, name, _ => preview(), async (_, ct) => details is null ? null : await details(ct), position) { } } diff --git a/src/Uno.UI/Diagnostics/DiagnosticViewManager.TView.TState.cs b/src/Uno.UI/Diagnostics/DiagnosticViewManager.TView.TState.cs index 9598b2262813..ece3e7235dc3 100644 --- a/src/Uno.UI/Diagnostics/DiagnosticViewManager.TView.TState.cs +++ b/src/Uno.UI/Diagnostics/DiagnosticViewManager.TView.TState.cs @@ -12,7 +12,9 @@ namespace Uno.Diagnostics.UI; /// Type of the state used to update the . /// Factory to create an instance of the . /// Delegate to use to update the on . -internal class DiagnosticViewManager(Func factory, Action update) +internal class DiagnosticViewManager( + Func factory, + Action update) where TView : FrameworkElement { private event EventHandler? _changed;