diff --git a/src/Android/Avalonia.Android/AvaloniaActivity.cs b/src/Android/Avalonia.Android/AvaloniaActivity.cs new file mode 100644 index 0000000000..187349fbb6 --- /dev/null +++ b/src/Android/Avalonia.Android/AvaloniaActivity.cs @@ -0,0 +1,170 @@ +#nullable enable +using System; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.Versioning; +using Android.App; +using Android.Content; +using Android.Content.PM; +using Android.OS; +using Android.Runtime; +using Android.Views; +using AndroidX.AppCompat.App; +using Avalonia.Controls.ApplicationLifetimes; + +namespace Avalonia.Android; + +/// +/// Common implementation of android activity that is integrated with Avalonia views. +/// If you need a base class for main activity of Avalonia app, see or . +/// +public class AvaloniaActivity : AppCompatActivity, IAvaloniaActivity +{ + private EventHandler? _onActivated, _onDeactivated; + private GlobalLayoutListener? _listener; + private object? _content; + internal AvaloniaView? _view; + + public Action? ActivityResult { get; set; } + public Action? RequestPermissionsResult { get; set; } + + public event EventHandler? BackRequested; + + public object? Content + { + get => _content; + set + { + if (_content != value) + { + _content = value; + if (_view is not null) + { + _view.Content = _content; + } + } + } + } + + event EventHandler? IAvaloniaActivity.Activated + { + add { _onActivated += value; } + remove { _onActivated -= value; } + } + + event EventHandler? IAvaloniaActivity.Deactivated + { + add { _onDeactivated += value; } + remove { _onDeactivated -= value; } + } + + [ObsoletedOSPlatform("android33.0")] + public override void OnBackPressed() + { + var eventArgs = new AndroidBackRequestedEventArgs(); + + BackRequested?.Invoke(this, eventArgs); + + if (!eventArgs.Handled) + { + base.OnBackPressed(); + } + } + + protected override void OnCreate(Bundle? savedInstanceState) + { + InitializeAvaloniaView(_content); + + base.OnCreate(savedInstanceState); + + SetContentView(_view); + + _listener = new GlobalLayoutListener(_view); + + _view.ViewTreeObserver?.AddOnGlobalLayoutListener(_listener); + + if (Intent?.Data is {} androidUri + && androidUri.IsAbsolute + && Uri.TryCreate(androidUri.ToString(), UriKind.Absolute, out var protocolUri)) + { + _onActivated?.Invoke(this, new ProtocolActivatedEventArgs(ActivationKind.OpenUri, protocolUri)); + } + } + + protected override void OnStop() + { + _onDeactivated?.Invoke(this, new ActivatedEventArgs(ActivationKind.Background)); + base.OnStop(); + } + + protected override void OnStart() + { + _onActivated?.Invoke(this, new ActivatedEventArgs(ActivationKind.Background)); + base.OnStart(); + } + + protected override void OnResume() + { + base.OnResume(); + + // Android only respects LayoutInDisplayCutoutMode value if it has been set once before window becomes visible. + if (OperatingSystem.IsAndroidVersionAtLeast(28) && Window is { Attributes: { } attributes }) + { + attributes.LayoutInDisplayCutoutMode = LayoutInDisplayCutoutMode.ShortEdges; + } + } + + protected override void OnDestroy() + { + if (_view is not null) + { + _view.Content = null; + _view.ViewTreeObserver?.RemoveOnGlobalLayoutListener(_listener); + _view.Dispose(); + _view = null; + } + + base.OnDestroy(); + } + + protected override void OnActivityResult(int requestCode, [GeneratedEnum] Result resultCode, Intent? data) + { + base.OnActivityResult(requestCode, resultCode, data); + + ActivityResult?.Invoke(requestCode, resultCode, data); + } + + [SupportedOSPlatform("android23.0")] + public override void OnRequestPermissionsResult(int requestCode, string[] permissions, Permission[] grantResults) + { + base.OnRequestPermissionsResult(requestCode, permissions, grantResults); + + RequestPermissionsResult?.Invoke(requestCode, permissions, grantResults); + } + + [MemberNotNull(nameof(_view))] + private protected virtual void InitializeAvaloniaView(object? initialContent) + { + if (Avalonia.Application.Current is null) + { + throw new InvalidOperationException( + "Avalonia Application was not initialized. Make sure you have created AvaloniaMainActivity."); + } + + _view = new AvaloniaView(this) { Content = initialContent }; + } + + private class GlobalLayoutListener : Java.Lang.Object, ViewTreeObserver.IOnGlobalLayoutListener + { + private readonly AvaloniaView _view; + + public GlobalLayoutListener(AvaloniaView view) + { + _view = view; + } + + public void OnGlobalLayout() + { + _view.TopLevelImpl?.Resize(_view.TopLevelImpl.ClientSize); + } + } +} diff --git a/src/Android/Avalonia.Android/AvaloniaMainActivity.App.cs b/src/Android/Avalonia.Android/AvaloniaMainActivity.App.cs index f203b6c544..baed45ab8e 100644 --- a/src/Android/Avalonia.Android/AvaloniaMainActivity.App.cs +++ b/src/Android/Avalonia.Android/AvaloniaMainActivity.App.cs @@ -1,66 +1,15 @@ #nullable enable +using Android.OS; +using Android.Views; using Avalonia.Android.Platform; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Platform; -namespace Avalonia.Android -{ - partial class AvaloniaMainActivity where TApp : Application, new() - { - protected virtual AppBuilder CustomizeAppBuilder(AppBuilder builder) => builder.UseAndroid(); - - private static AppBuilder? s_appBuilder; - internal static object? ViewContent; - - public object? Content - { - get - { - return ViewContent; - } - set - { - ViewContent = value; - if (View != null) - View.Content = value; - } - } - - protected AppBuilder CreateAppBuilder() - { - var builder = AppBuilder.Configure(); - - return CustomizeAppBuilder(builder); - } - - private void InitializeApp() - { - if (s_appBuilder == null) - { - var builder = CreateAppBuilder(); +namespace Avalonia.Android; - builder.SetupWithLifetime(new SingleViewLifetime()); - - s_appBuilder = builder; - } - - if (Avalonia.Application.Current?.TryGetFeature() - is AndroidActivatableLifetime activatableLifetime) - { - activatableLifetime.Activity = this; - } - - View = new AvaloniaView(this); - if (ViewContent != null) - { - View.Content = ViewContent; - } - - if (Avalonia.Application.Current?.ApplicationLifetime is SingleViewLifetime lifetime) - { - lifetime.View = View; - } - } - } +public class AvaloniaMainActivity : AvaloniaMainActivity + where TApp : Application, new() +{ + protected override AppBuilder CreateAppBuilder() => AppBuilder.Configure(); } diff --git a/src/Android/Avalonia.Android/AvaloniaMainActivity.cs b/src/Android/Avalonia.Android/AvaloniaMainActivity.cs index eed6a68eb5..48411d792f 100644 --- a/src/Android/Avalonia.Android/AvaloniaMainActivity.cs +++ b/src/Android/Avalonia.Android/AvaloniaMainActivity.cs @@ -1,141 +1,63 @@ +#nullable enable + using System; -using System.Runtime.Versioning; -using Android.App; -using Android.Content; -using Android.Content.PM; +using System.Diagnostics.CodeAnalysis; using Android.OS; -using Android.Runtime; -using Android.Views; -using AndroidX.AppCompat.App; +using Avalonia.Android.Platform; using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Platform; -namespace Avalonia.Android -{ - public class AvaloniaMainActivity : AppCompatActivity, IAvaloniaActivity - { - private EventHandler _onActivated, _onDeactivated; - - public Action ActivityResult { get; set; } - public Action RequestPermissionsResult { get; set; } +namespace Avalonia.Android; - public event EventHandler BackRequested; - event EventHandler IAvaloniaActivity.Activated - { - add { _onActivated += value; } - remove { _onActivated -= value; } - } - - event EventHandler IAvaloniaActivity.Deactivated - { - add { _onDeactivated += value; } - remove { _onDeactivated -= value; } - } - - [ObsoletedOSPlatform("android33.0")] - public override void OnBackPressed() - { - var eventArgs = new AndroidBackRequestedEventArgs(); - - BackRequested?.Invoke(this, eventArgs); - - if (!eventArgs.Handled) - { - base.OnBackPressed(); - } - } - - protected override void OnCreate(Bundle savedInstanceState) - { - base.OnCreate(savedInstanceState); - - if (Intent?.Data is {} androidUri - && androidUri.IsAbsolute - && Uri.TryCreate(androidUri.ToString(), UriKind.Absolute, out var protocolUri)) - { - _onActivated?.Invoke(this, new ProtocolActivatedEventArgs(ActivationKind.OpenUri, protocolUri)); - } - } - - protected override void OnStop() - { - _onDeactivated?.Invoke(this, new ActivatedEventArgs(ActivationKind.Background)); - base.OnStop(); - } - - protected override void OnStart() - { - _onActivated?.Invoke(this, new ActivatedEventArgs(ActivationKind.Background)); - base.OnStart(); - } +public class AvaloniaMainActivity : AvaloniaActivity +{ + private protected static SingleViewLifetime? Lifetime; - protected override void OnActivityResult(int requestCode, [GeneratedEnum] Result resultCode, Intent data) + public override void OnCreate(Bundle? savedInstanceState, PersistableBundle? persistentState) + { + // Global IActivatableLifetime expects a main activity, so we need to replace it on each OnCreate. + if (Avalonia.Application.Current?.TryGetFeature() + is AndroidActivatableLifetime activatableLifetime) { - base.OnActivityResult(requestCode, resultCode, data); - - ActivityResult?.Invoke(requestCode, resultCode, data); + activatableLifetime.Activity = this; } - [SupportedOSPlatform("android23.0")] - public override void OnRequestPermissionsResult(int requestCode, string[] permissions, Permission[] grantResults) - { - base.OnRequestPermissionsResult(requestCode, permissions, grantResults); - - RequestPermissionsResult?.Invoke(requestCode, permissions, grantResults); - } + base.OnCreate(savedInstanceState, persistentState); } - public abstract partial class AvaloniaMainActivity : AvaloniaMainActivity where TApp : Application, new() + private protected override void InitializeAvaloniaView(object? initialContent) { - internal AvaloniaView View { get; set; } - - private GlobalLayoutListener _listener; - - protected override void OnCreate(Bundle savedInstanceState) - { - InitializeApp(); - - base.OnCreate(savedInstanceState); - - SetContentView(View); - - _listener = new GlobalLayoutListener(View); - - View.ViewTreeObserver?.AddOnGlobalLayoutListener(_listener); - } - - protected override void OnResume() - { - base.OnResume(); - - // Android only respects LayoutInDisplayCutoutMode value if it has been set once before window becomes visible. - if (OperatingSystem.IsAndroidVersionAtLeast(28) && Window is { Attributes: { } attributes }) - { - attributes.LayoutInDisplayCutoutMode = LayoutInDisplayCutoutMode.ShortEdges; - } - } - - protected override void OnDestroy() - { - View.Content = null; - - View.ViewTreeObserver?.RemoveOnGlobalLayoutListener(_listener); - - base.OnDestroy(); - } - - private class GlobalLayoutListener : Java.Lang.Object, ViewTreeObserver.IOnGlobalLayoutListener - { - private readonly AvaloniaView _view; - - public GlobalLayoutListener(AvaloniaView view) - { - _view = view; - } - - public void OnGlobalLayout() - { - _view.TopLevelImpl?.Resize(_view.TopLevelImpl.ClientSize); - } + // Android can run OnCreate + InitializeAvaloniaView multiple times per process lifetime. + // On each call we need to create new AvaloniaView, but we can't recreate Avalonia nor Avalonia controls. + // So, if lifetime was already created previously - recreate AvaloniaView. + // If not, initialize Avalonia, and create AvaloniaView inside of AfterSetup callback. + // We need this AfterSetup callback to match iOS/Browser behavior and ensure that view/toplevel is available in custom AfterSetup calls. + if (Lifetime is not null) + { + Lifetime.Activity = this; + _view = new AvaloniaView(this) { Content = initialContent }; + } + else + { + var builder = CreateAppBuilder(); + builder = CustomizeAppBuilder(builder); + + Lifetime = new SingleViewLifetime(); + Lifetime.Activity = this; + + builder + .AfterApplicationSetup(_ => + { + _view = new AvaloniaView(this) { Content = initialContent }; + }) + .SetupWithLifetime(Lifetime); + + // AfterPlatformServicesSetup should always be called. If it wasn't, we have an unusual problem. + if (_view is null) + throw new InvalidOperationException("Unknown error: AvaloniaView initialization has failed."); } } + + protected virtual AppBuilder CreateAppBuilder() => AppBuilder.Configure().UseAndroid(); + protected virtual AppBuilder CustomizeAppBuilder(AppBuilder builder) => builder; } diff --git a/src/Android/Avalonia.Android/AvaloniaView.cs b/src/Android/Avalonia.Android/AvaloniaView.cs index c120f9ee77..e9ea41ab61 100644 --- a/src/Android/Avalonia.Android/AvaloniaView.cs +++ b/src/Android/Avalonia.Android/AvaloniaView.cs @@ -35,6 +35,7 @@ namespace Avalonia.Android } internal TopLevelImpl TopLevelImpl => _view; + internal TopLevel TopLevel => _root; public object Content { @@ -121,9 +122,6 @@ namespace Avalonia.Android MaxClientSize = size; base.OnResized(size); } - - public WindowState WindowState { get; set; } - public IDisposable ShowDialog() => null; } } } diff --git a/src/Android/Avalonia.Android/IAvaloniaActivity.cs b/src/Android/Avalonia.Android/IAvaloniaActivity.cs index 005096a0bd..d6239400e8 100644 --- a/src/Android/Avalonia.Android/IAvaloniaActivity.cs +++ b/src/Android/Avalonia.Android/IAvaloniaActivity.cs @@ -1,10 +1,12 @@ -using System; +#nullable enable +using System; using Avalonia.Controls.ApplicationLifetimes; namespace Avalonia.Android; public interface IAvaloniaActivity : IActivityResultHandler, IActivityNavigationService { - event EventHandler Activated; - event EventHandler Deactivated; + object? Content { get; set; } + event EventHandler? Activated; + event EventHandler? Deactivated; } diff --git a/src/Android/Avalonia.Android/SingleViewLifetime.cs b/src/Android/Avalonia.Android/SingleViewLifetime.cs index c912d2f890..f525a2773a 100644 --- a/src/Android/Avalonia.Android/SingleViewLifetime.cs +++ b/src/Android/Avalonia.Android/SingleViewLifetime.cs @@ -1,26 +1,47 @@ -using Avalonia.Controls; +#nullable enable +using System.Diagnostics.CodeAnalysis; +using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; -namespace Avalonia.Android +namespace Avalonia.Android; + +internal class SingleViewLifetime : ISingleViewApplicationLifetime, ISingleTopLevelApplicationLifetime { - internal class SingleViewLifetime : ISingleViewApplicationLifetime - { - private AvaloniaView _view; + private Control? _mainView; + private AvaloniaMainActivity? _activity; - public AvaloniaView View + /// + /// Since Main Activity can be swapped, we should adjust litetime as well. + /// + public AvaloniaMainActivity Activity + { + [return: MaybeNull] get => _activity!; + internal set + { + if (_activity != null) + { + _activity.Content = null; + } + _activity = value; + _activity.Content = _mainView; + } + } + + public Control? MainView + { + get => _mainView; + set { - get => _view; internal set + if (_mainView != value) { - if (_view != null) + _mainView = value; + if (_activity != null) { - _view.Content = null; - _view.Dispose(); + _activity.Content = _mainView; } - _view = value; - _view.Content = MainView; } } - - public Control MainView { get; set; } } + + public TopLevel? TopLevel => _activity?._view?.TopLevel; } diff --git a/src/Avalonia.Controls/AppBuilder.cs b/src/Avalonia.Controls/AppBuilder.cs index b77ce7ebb2..6ee987a0c8 100644 --- a/src/Avalonia.Controls/AppBuilder.cs +++ b/src/Avalonia.Controls/AppBuilder.cs @@ -69,10 +69,14 @@ namespace Avalonia public string? RenderingSubsystemName { get; private set; } /// - /// Gets or sets a method to call after the is setup. + /// Gets a method to call after the is setup. /// public Action AfterSetupCallback { get; private set; } = builder => { }; + /// + /// Callbacks that are commonly used by backends to initialize avalonia views. + /// + private Action AfterApplicationSetupCallback { get; set; } = builder => { }; public Action AfterPlatformServicesSetupCallback { get; private set; } = builder => { }; @@ -159,8 +163,14 @@ namespace Avalonia AfterSetupCallback = (Action)Delegate.Combine(AfterSetupCallback, callback); return Self; } - - + + [PrivateApi] + public AppBuilder AfterApplicationSetup(Action callback) + { + AfterApplicationSetupCallback = (Action)Delegate.Combine(AfterPlatformServicesSetupCallback, callback); + return Self; + } + public AppBuilder AfterPlatformServicesSetup(Action callback) { AfterPlatformServicesSetupCallback = (Action)Delegate.Combine(AfterPlatformServicesSetupCallback, callback); @@ -328,6 +338,7 @@ namespace Avalonia AvaloniaLocator.CurrentMutable.BindToSelf(Instance); Instance.RegisterServices(); Instance.Initialize(); + AfterApplicationSetupCallback?.Invoke(Self); AfterSetupCallback?.Invoke(Self); Instance.OnFrameworkInitializationCompleted(); } diff --git a/src/Avalonia.Controls/ApplicationLifetimes/ISingleTopLevelApplicationLifetime.cs b/src/Avalonia.Controls/ApplicationLifetimes/ISingleTopLevelApplicationLifetime.cs new file mode 100644 index 0000000000..fe9078a133 --- /dev/null +++ b/src/Avalonia.Controls/ApplicationLifetimes/ISingleTopLevelApplicationLifetime.cs @@ -0,0 +1,15 @@ +using System.ComponentModel; +using Avalonia.Metadata; + +namespace Avalonia.Controls.ApplicationLifetimes; + +/// +/// Used in our internal projects. Until we figure out way to add this information to the public API. +/// +[NotClientImplementable] +[PrivateApi] +[EditorBrowsable(EditorBrowsableState.Never)] +public interface ISingleTopLevelApplicationLifetime : IApplicationLifetime +{ + TopLevel? TopLevel { get; } +} diff --git a/src/Browser/Avalonia.Browser/AvaloniaView.cs b/src/Browser/Avalonia.Browser/AvaloniaView.cs index 5129f3095c..e2a6212e13 100644 --- a/src/Browser/Avalonia.Browser/AvaloniaView.cs +++ b/src/Browser/Avalonia.Browser/AvaloniaView.cs @@ -140,6 +140,16 @@ namespace Avalonia.Browser InputHelper.FocusElement(_containerElement); } + public Control? Content + { + get => (Control)_topLevel.Content!; + set => _topLevel.Content = value; + } + + public bool IsComposing { get; private set; } + + internal TopLevel TopLevel => _topLevel; + private static RawPointerPoint ExtractRawPointerFromJSArgs(JSObject args) { var point = new RawPointerPoint @@ -429,15 +439,7 @@ namespace Avalonia.Browser Dispatcher.UIThread.RunJobs(DispatcherPriority.UiThreadRender); ManualTriggerRenderTimer.Instance.RaiseTick(); } - - public Control? Content - { - get => (Control)_topLevel.Content!; - set => _topLevel.Content = value; - } - - public bool IsComposing { get; private set; } - + internal INativeControlHostImpl GetNativeControlHostImpl() { return new BrowserNativeControlHost(_nativeControlsContainer); diff --git a/src/Browser/Avalonia.Browser/BrowserAppBuilder.cs b/src/Browser/Avalonia.Browser/BrowserAppBuilder.cs index eaa72c1931..9c41fff43f 100644 --- a/src/Browser/Avalonia.Browser/BrowserAppBuilder.cs +++ b/src/Browser/Avalonia.Browser/BrowserAppBuilder.cs @@ -55,7 +55,7 @@ public static class BrowserAppBuilder var lifetime = new BrowserSingleViewLifetime(); builder - .AfterSetup(_ => + .AfterApplicationSetup(_ => { lifetime.View = new AvaloniaView(mainDivId); }) diff --git a/src/Browser/Avalonia.Browser/BrowserSingleViewLifetime.cs b/src/Browser/Avalonia.Browser/BrowserSingleViewLifetime.cs index ba404c2600..a28bb469f3 100644 --- a/src/Browser/Avalonia.Browser/BrowserSingleViewLifetime.cs +++ b/src/Browser/Avalonia.Browser/BrowserSingleViewLifetime.cs @@ -6,7 +6,7 @@ using Avalonia.Browser; namespace Avalonia; -internal class BrowserSingleViewLifetime : ISingleViewApplicationLifetime +internal class BrowserSingleViewLifetime : ISingleViewApplicationLifetime, ISingleTopLevelApplicationLifetime { public AvaloniaView? View; @@ -32,4 +32,6 @@ internal class BrowserSingleViewLifetime : ISingleViewApplicationLifetime throw new InvalidOperationException("Browser lifetime was not initialized. Make sure AppBuilder.StartBrowserApp was called."); } } + + public TopLevel? TopLevel => View?.TopLevel; } diff --git a/src/Headless/Avalonia.Headless.Vnc/HeadlessVncPlatformExtensions.cs b/src/Headless/Avalonia.Headless.Vnc/HeadlessVncPlatformExtensions.cs index 29ca911a82..9c01d356dd 100644 --- a/src/Headless/Avalonia.Headless.Vnc/HeadlessVncPlatformExtensions.cs +++ b/src/Headless/Avalonia.Headless.Vnc/HeadlessVncPlatformExtensions.cs @@ -26,7 +26,7 @@ namespace Avalonia UseHeadlessDrawing = false, FrameBufferFormat = PixelFormat.Bgra8888 }) - .AfterSetup(_ => + .AfterApplicationSetup(_ => { var lt = ((IClassicDesktopStyleApplicationLifetime)builder.Instance!.ApplicationLifetime!); lt.Startup += async delegate diff --git a/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs b/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs index 7eb3088cf7..d226a89e7d 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs @@ -1,5 +1,6 @@ using System; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Threading; using Avalonia; @@ -88,7 +89,7 @@ namespace Avalonia.LinuxFramebuffer } } - class LinuxFramebufferLifetime : IControlledApplicationLifetime, ISingleViewApplicationLifetime + class LinuxFramebufferLifetime : IControlledApplicationLifetime, ISingleViewApplicationLifetime, ISingleTopLevelApplicationLifetime { private readonly IOutputBackend _fb; private readonly IInputBackend? _inputBackend; @@ -114,25 +115,7 @@ namespace Avalonia.LinuxFramebuffer { if (_topLevel == null) { - var inputBackend = _inputBackend; - if (inputBackend == null) - { - if (Environment.GetEnvironmentVariable("AVALONIA_USE_EVDEV") == "1") - inputBackend = EvDevBackend.CreateFromEnvironment(); - else - inputBackend = new LibInputBackend(); - } - - var tl = new EmbeddableControlRoot(new FramebufferToplevelImpl(_fb, inputBackend)); - tl.Prepare(); - tl.StartRendering(); - _topLevel = tl; - - - if (_topLevel is IFocusScope scope && _topLevel.FocusManager is FocusManager focusManager) - { - focusManager.SetFocusScope(scope); - } + EnsureTopLevel(); } _topLevel.Content = value; @@ -156,6 +139,38 @@ namespace Avalonia.LinuxFramebuffer ExitCode = e.ApplicationExitCode; _cts.Cancel(); } + + public TopLevel? TopLevel + { + get + { + EnsureTopLevel(); + return _topLevel; + } + } + + [MemberNotNull(nameof(_topLevel))] + private void EnsureTopLevel() + { + var inputBackend = _inputBackend; + if (inputBackend == null) + { + if (Environment.GetEnvironmentVariable("AVALONIA_USE_EVDEV") == "1") + inputBackend = EvDevBackend.CreateFromEnvironment(); + else + inputBackend = new LibInputBackend(); + } + + var tl = new EmbeddableControlRoot(new FramebufferToplevelImpl(_fb, inputBackend)); + tl.Prepare(); + tl.StartRendering(); + _topLevel = tl; + + if (_topLevel is IFocusScope scope && _topLevel.FocusManager is FocusManager focusManager) + { + focusManager.SetFocusScope(scope); + } + } } } diff --git a/src/Tizen/Avalonia.Tizen/NuiAvaloniaView.cs b/src/Tizen/Avalonia.Tizen/NuiAvaloniaView.cs index 3e8abf2e54..8118ae2707 100644 --- a/src/Tizen/Avalonia.Tizen/NuiAvaloniaView.cs +++ b/src/Tizen/Avalonia.Tizen/NuiAvaloniaView.cs @@ -36,7 +36,7 @@ public class NuiAvaloniaView : GLView, ITizenView, ITextInputMethodImpl set => _inputRoot = value; } - private TopLevel TopLevel + internal TopLevel TopLevel => _topLevel ?? throw new InvalidOperationException($"{nameof(NuiAvaloniaView)} hasn't been initialized"); internal TopLevelImpl TopLevelImpl diff --git a/src/Tizen/Avalonia.Tizen/NuiTizenApplication.cs b/src/Tizen/Avalonia.Tizen/NuiTizenApplication.cs index 105bce26bd..d0745ab038 100644 --- a/src/Tizen/Avalonia.Tizen/NuiTizenApplication.cs +++ b/src/Tizen/Avalonia.Tizen/NuiTizenApplication.cs @@ -13,7 +13,7 @@ public class NuiTizenApplication : NUIApplication private SingleViewLifetime? _lifetime; - private class SingleViewLifetime : ISingleViewApplicationLifetime + private class SingleViewLifetime : ISingleViewApplicationLifetime, ISingleTopLevelApplicationLifetime { public NuiAvaloniaView View { get; } @@ -27,8 +27,11 @@ public class NuiTizenApplication : NUIApplication get => View.Content; set => View.Content = value; } + + public TopLevel? TopLevel => View.TopLevel; } + protected virtual AppBuilder CreateAppBuilder() => AppBuilder.Configure().UseTizen(); protected virtual AppBuilder CustomizeAppBuilder(AppBuilder builder) => builder; protected override void OnCreate() @@ -55,13 +58,12 @@ public class NuiTizenApplication : NUIApplication SynchronizationContext.SetSynchronizationContext(new SynchronizationContext()); Logger.TryGet(LogEventLevel.Debug, LogKey)?.Log(null, "App builder"); - var builder = AppBuilder.Configure().UseTizen(); + var builder = CreateAppBuilder(); TizenThreadingInterface.MainloopContext.Post(_ => { - CustomizeAppBuilder(builder); - - builder.AfterSetup(_ => _lifetime!.View.Initialise()); + builder = CustomizeAppBuilder(builder); + builder.AfterApplicationSetup(_ => _lifetime!.View.Initialise()); Logger.TryGet(LogEventLevel.Debug, LogKey)?.Log(null, "Setup lifetime"); builder.SetupWithLifetime(_lifetime!); diff --git a/src/iOS/Avalonia.iOS/AvaloniaAppDelegate.cs b/src/iOS/Avalonia.iOS/AvaloniaAppDelegate.cs index 7a07fab2fe..4aeb9fa8f5 100644 --- a/src/iOS/Avalonia.iOS/AvaloniaAppDelegate.cs +++ b/src/iOS/Avalonia.iOS/AvaloniaAppDelegate.cs @@ -33,20 +33,22 @@ namespace Avalonia.iOS add { _onDeactivated += value; } remove { _onDeactivated -= value; } } - + + protected virtual AppBuilder CreateAppBuilder() => AppBuilder.Configure().UseiOS(); protected virtual AppBuilder CustomizeAppBuilder(AppBuilder builder) => builder; - + [Export("window")] public UIWindow? Window { get; set; } [Export("application:didFinishLaunchingWithOptions:")] public bool FinishedLaunching(UIApplication application, NSDictionary launchOptions) { - var builder = AppBuilder.Configure().UseiOS(this); + var builder = CreateAppBuilder(); + builder = CustomizeAppBuilder(builder); var lifetime = new SingleViewLifetime(); - builder.AfterSetup(_ => + builder.AfterApplicationSetup(_ => { Window = new UIWindow(); @@ -60,8 +62,6 @@ namespace Avalonia.iOS view.InitWithController(controller); }); - CustomizeAppBuilder(builder); - builder.SetupWithLifetime(lifetime); Window!.MakeKeyAndVisible(); diff --git a/src/iOS/Avalonia.iOS/AvaloniaView.cs b/src/iOS/Avalonia.iOS/AvaloniaView.cs index ed04a989d3..43317286c9 100644 --- a/src/iOS/Avalonia.iOS/AvaloniaView.cs +++ b/src/iOS/Avalonia.iOS/AvaloniaView.cs @@ -29,6 +29,7 @@ namespace Avalonia.iOS { internal IInputRoot InputRoot => _inputRoot ?? throw new InvalidOperationException($"{nameof(IWindowImpl.SetInputRoot)} must have been called"); + internal TopLevel TopLevel => _topLevel; private readonly TopLevelImpl _topLevelImpl; private readonly EmbeddableControlRoot _topLevel; diff --git a/src/iOS/Avalonia.iOS/SingleViewLifetime.cs b/src/iOS/Avalonia.iOS/SingleViewLifetime.cs index 49b8e97769..7c56904fdc 100644 --- a/src/iOS/Avalonia.iOS/SingleViewLifetime.cs +++ b/src/iOS/Avalonia.iOS/SingleViewLifetime.cs @@ -1,16 +1,45 @@ using System; +using System.Diagnostics.CodeAnalysis; using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; namespace Avalonia.iOS; -internal class SingleViewLifetime : ISingleViewApplicationLifetime -{ - public AvaloniaView? View; +internal class SingleViewLifetime : ISingleViewApplicationLifetime, ISingleTopLevelApplicationLifetime +{ + private Control? _mainView; + private AvaloniaView? _view; + + public AvaloniaView View + { + [return: MaybeNull] get => _view!; + internal set + { + if (_view != null) + { + _view.Content = null; + _view.Dispose(); + } + _view = value; + _view.Content = _mainView; + } + } public Control? MainView { - get => View!.Content; - set => View!.Content = value; + get => _mainView; + set + { + if (_mainView != value) + { + _mainView = value; + if (_view != null) + { + _view.Content = _mainView; + } + } + } } + + public TopLevel? TopLevel => View?.TopLevel; }