diff --git a/.vsts-ci.yml b/.vsts-ci.yml index 63db333051fd..06d37df6a01c 100644 --- a/.vsts-ci.yml +++ b/.vsts-ci.yml @@ -56,10 +56,10 @@ variables: windowsScaledPool: 'Windows2022-20240421' linuxVMImage: 'ubuntu-latest' linuxScaledPool: 'Ubuntu2204-20230918' - macOSVMImage: 'macOS-14' + macOSVMImage: 'macOS-15' macOSVMImage_UITests: 'macOS-14' xCodeRoot: '/Applications/Xcode_16.app' - xCodeRoot_iOS_UITests: '/Applications/Xcode_16.app' + xCodeRoot_iOS_UITests: '/Applications/Xcode_15.3.app' # Offline validation to improve build performance NUGET_CERT_REVOCATION_MODE: offline 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/build/ci/.azure-devops-android-tests.yml b/build/ci/.azure-devops-android-tests.yml index bd53d98f5391..df4ed81e043b 100644 --- a/build/ci/.azure-devops-android-tests.yml +++ b/build/ci/.azure-devops-android-tests.yml @@ -36,6 +36,11 @@ jobs: - checkout: self clean: true + # Install android 34 as we're running on macos-15 + - bash: | + echo "y" | $ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager --sdk_root=$ANDROID_HOME --install 'platforms;android-34' | tr '\r' '\n' | uniq + displayName: Install Android 34 + - template: templates/dotnet-mobile-install-mac.yml - template: templates/nuget-cache.yml diff --git a/build/ci/.azure-devops-project-template-tests.yml b/build/ci/.azure-devops-project-template-tests.yml index 6ea10c23a45d..d44825c2442c 100644 --- a/build/ci/.azure-devops-project-template-tests.yml +++ b/build/ci/.azure-devops-project-template-tests.yml @@ -31,8 +31,8 @@ jobs: artifactName: 'Nuget_Packages' - template: templates/gitversion.yml - - template: templates/dotnet-mobile-install-windows.yml + - template: templates/uno-dev-feed.yml - script: copy $(System.ArtifactsDirectory)\Nuget_Packages\vslatest\*.nupkg $(Build.SourcesDirectory)\src\PackageCache displayName: Copy Artifacts to PackageCache @@ -81,6 +81,11 @@ jobs: inputs: artifactName: 'Nuget_Packages' + # Install android 34 as we're running on macos-15 + - bash: | + echo "y" | $ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager --sdk_root=$ANDROID_HOME --install 'platforms;android-34' | tr '\r' '\n' | uniq + displayName: Install Android 34 + - template: templates/gitversion.yml - template: templates/ios-build-select-version.yml @@ -88,6 +93,7 @@ jobs: xCodeRoot: ${{ parameters.xCodeRoot }} - template: templates/dotnet-mobile-install-mac.yml + - template: templates/uno-dev-feed.yml - powershell: cp $(System.ArtifactsDirectory)/Nuget_Packages/vslatest/*.nupkg $(Build.SourcesDirectory)/src/PackageCache displayName: Copy Artifacts to PackageCache @@ -119,6 +125,8 @@ jobs: TestGroup: '1' group_2: TestGroup: '2' + group_3: + TestGroup: '3' steps: - task: DownloadBuildArtifacts@0 @@ -126,11 +134,10 @@ jobs: artifactName: 'Nuget_Packages' - template: templates/gitversion.yml - - template: templates/dotnet-mobile-install-linux.yml - - template: templates/gitversion.yml - + - template: templates/uno-dev-feed.yml + - script: cp $(System.ArtifactsDirectory)/Nuget_Packages/vslatest/*.nupkg $(Build.SourcesDirectory)/src/PackageCache displayName: Copy Artifacts to PackageCache diff --git a/build/ci/templates/uno-dev-feed.yml b/build/ci/templates/uno-dev-feed.yml new file mode 100644 index 000000000000..bc6a002029cc --- /dev/null +++ b/build/ci/templates/uno-dev-feed.yml @@ -0,0 +1,7 @@ +parameters: + nugetPackages: '$(Pipeline.Workspace)/.nuget/packages' + +steps: + + - pwsh: dotnet nuget add source https://pkgs.dev.azure.com/uno-platform/1dd81cbd-cb35-41de-a570-b0df3571a196/_packaging/unoplatformdev/nuget/v3/index.json -n "uno-dev" + displayName: Add dev feed source diff --git a/build/test-scripts/run-net7-template-linux.ps1 b/build/test-scripts/run-net7-template-linux.ps1 index 4d92516b0629..7054ce2dc355 100644 --- a/build/test-scripts/run-net7-template-linux.ps1 +++ b/build/test-scripts/run-net7-template-linux.ps1 @@ -48,13 +48,13 @@ $projects = @(0, "5.1/uno51blank/uno51blank.Wasm/uno51blank.Wasm.csproj", @(), @()), # 5.1 Recommended - @(0, "5.1/uno51recommended/uno51recommended.Skia.Gtk/uno51recommended.Skia.Gtk.csproj", @(), @()), - @(0, "5.1/uno51recommended/uno51recommended.Skia.Linux.FrameBuffer/uno51recommended.Skia.Linux.FrameBuffer.csproj", @(), @()), - @(0, "5.1/uno51recommended/uno51recommended.Skia.WPF/uno51recommended.Skia.WPF.csproj", @(), @()), - @(0, "5.1/uno51recommended/uno51recommended.Wasm/uno51recommended.Wasm.csproj", @(), @()), - @(0, "5.1/uno51recommended/uno51recommended.Server/uno51recommended.Server.csproj", @(), @()), - @(0, "5.1/uno51recommended/uno51recommended.Tests/uno51recommended.Tests.csproj", @(), @()), - @(0, "5.1/uno51recommended/uno51recommended.UITests/uno51recommended.UITests.csproj", @(), @()), + @(1, "5.1/uno51recommended/uno51recommended.Skia.Gtk/uno51recommended.Skia.Gtk.csproj", @(), @()), + @(1, "5.1/uno51recommended/uno51recommended.Skia.Linux.FrameBuffer/uno51recommended.Skia.Linux.FrameBuffer.csproj", @(), @()), + @(1, "5.1/uno51recommended/uno51recommended.Skia.WPF/uno51recommended.Skia.WPF.csproj", @(), @()), + @(1, "5.1/uno51recommended/uno51recommended.Wasm/uno51recommended.Wasm.csproj", @(), @()), + @(1, "5.1/uno51recommended/uno51recommended.Server/uno51recommended.Server.csproj", @(), @()), + @(1, "5.1/uno51recommended/uno51recommended.Tests/uno51recommended.Tests.csproj", @(), @()), + @(1, "5.1/uno51recommended/uno51recommended.UITests/uno51recommended.UITests.csproj", @(), @()), # 5.2 Blank @(1, "5.2/uno52blank/uno52blank/uno52blank.csproj", @(), @()), @@ -69,16 +69,16 @@ $projects = @(1, "5.2/uno52Lib/uno52Lib.csproj", @(), @()), # 5.2 Uno NuGet Lib - @(1, "5.2/uno52NuGetLib/uno52NuGetLib.csproj", @(), @()), + @(2, "5.2/uno52NuGetLib/uno52NuGetLib.csproj", @(), @()), # 5.2 Uno SingleProject Lib - @(1, "5.2/uno52SingleProjectLib/uno52SingleProjectLib.csproj", @(), @()), + @(2, "5.2/uno52SingleProjectLib/uno52SingleProjectLib.csproj", @(), @()), # 5.2 Uno App with Library reference - @(1, "5.2/uno52AppWithLib/uno52AppWithLib/uno52AppWithLib.csproj", @(), @()), + @(2, "5.2/uno52AppWithLib/uno52AppWithLib/uno52AppWithLib.csproj", @(), @()), # 5.3 Blank with net9 - @(2, "5.3/uno53net9blank/uno53net9blank/uno53net9blank.csproj", @(), @()) + @(3, "5.3/uno53net9blank/uno53net9blank/uno53net9blank.csproj", @(), @()) # 5.3 blank publish testing # Disabled for LXD setup issues diff --git a/build/test-scripts/run-netcore-mobile-template-tests.ps1 b/build/test-scripts/run-netcore-mobile-template-tests.ps1 index f5321ab109e4..376a9cb1da84 100644 --- a/build/test-scripts/run-netcore-mobile-template-tests.ps1 +++ b/build/test-scripts/run-netcore-mobile-template-tests.ps1 @@ -271,16 +271,16 @@ $projects = @(1, "5.2/uno52blank/uno52blank/uno52blank.csproj", @("-f", "net8.0-desktop", $sdkFeatures), @("macOS", "NetCore")), # Default mode for the template is WindowsAppSDKSelfContained=true, which requires specifying a target platform. - @(2, "5.2/uno52blank/uno52blank/uno52blank.csproj", @("-p:Platform=x86" , "-p:TargetFramework=net8.0-windows10.0.19041"), @()), - @(2, "5.2/uno52blank/uno52blank/uno52blank.csproj", @("-p:Platform=arm64" , "-p:TargetFramework=net8.0-windows10.0.19041"), @()), + @(2, "5.2/uno52blank/uno52blank/uno52blank.csproj", @("-p:Platform=x86" , "-p:TargetFramework=net8.0-windows10.0.26100"), @()), + @(2, "5.2/uno52blank/uno52blank/uno52blank.csproj", @("-p:Platform=arm64" , "-p:TargetFramework=net8.0-windows10.0.26100"), @()), # Ensure that default without platform builds properly - @(2, "5.2/uno52blank/uno52blank/uno52blank.csproj", @("-p:TargetFramework=net8.0-windows10.0.19041"), @()), + @(2, "5.2/uno52blank/uno52blank/uno52blank.csproj", @("-p:TargetFramework=net8.0-windows10.0.26100"), @()), # Validate building inside VS @(2, "5.2/uno52blank/uno52blank/uno52blank.csproj", @("-p:BuildingInsideVisualStudio=true"), @("NetCore")), @(2, "5.2/uno52blank/uno52blank/uno52blank.csproj", @("-p:BuildingInsideVisualStudio=true", "-p:_UnoSelectedTargetFramework=net8.0-desktop"), @("NetCore")), - @(2, "5.2/uno52blank/uno52blank/uno52blank.csproj", @("-p:BuildingInsideVisualStudio=true", "-p:_UnoSelectedTargetFramework=net8.0-windows10.0.19041"), @()), + @(2, "5.2/uno52blank/uno52blank/uno52blank.csproj", @("-p:BuildingInsideVisualStudio=true", "-p:_UnoSelectedTargetFramework=net8.0-windows10.0.26100"), @()), # # 5.2 Uno Lib @@ -301,7 +301,7 @@ $projects = @(2, "5.2/uno52NuGetLib/uno52NuGetLib.csproj", @(), @("macOS", "NetCore")), # Default mode for the template is WindowsAppSDKSelfContained=true, which requires specifying a target platform. - @(2, "5.2/uno52Lib/uno52Lib.csproj", @("-p:Platform=x86" , "-p:TargetFramework=net8.0-windows10.0.19041"), @("macOS")), + @(2, "5.2/uno52Lib/uno52Lib.csproj", @("-p:Platform=x86" , "-p:TargetFramework=net8.0-windows10.0.26100"), @("macOS")), # # 5.2 Uno SingleProject Lib @@ -314,7 +314,7 @@ $projects = @(2, "5.2/uno52SingleProjectLib/uno52SingleProjectLib.csproj", @("-f", "net8.0-desktop"), @("macOS", "NetCore")), # Default mode for the template is WindowsAppSDKSelfContained=true, which requires specifying a target platform. - @(2, "5.2/uno52SingleProjectLib/uno52SingleProjectLib.csproj", @("-p:Platform=x86" , "-p:TargetFramework=net8.0-windows10.0.19041"), @()), + @(2, "5.2/uno52SingleProjectLib/uno52SingleProjectLib.csproj", @("-p:Platform=x86" , "-p:TargetFramework=net8.0-windows10.0.26100"), @()), # 5.2 Uno App with Library reference @(2, "5.2/uno52AppWithLib/uno52AppWithLib/uno52AppWithLib.csproj", @("-f", "net8.0"), @("macOS", "NetCore")), @@ -339,7 +339,7 @@ $projects = @(3, "5.3/uno53net9blank/uno53net9blank/uno53net9blank.csproj", @("-f", "net9.0-desktop", $sdkFeatures), @("macOS", "NetCore")), # Default mode for the template is WindowsAppSDKSelfContained=true, which requires specifying a target platform. - @(4, "5.2/uno52AppWithLib/uno52AppWithLib/uno52AppWithLib.csproj", @("-p:Platform=x86" , "-p:TargetFramework=net8.0-windows10.0.19041"), @()), + @(4, "5.2/uno52AppWithLib/uno52AppWithLib/uno52AppWithLib.csproj", @("-p:Platform=x86" , "-p:TargetFramework=net8.0-windows10.0.26100"), @()), # Publishing validation @(4, "5.3/uno53net9blank/uno53net9blank/uno53net9blank.csproj", @("-f", "net9.0-desktop", "-p:PackageFormat=app"), @("OnlyMacOS", "NetCore", "Publish")) diff --git a/doc/articles/features/working-with-xaml-hot-reload.md b/doc/articles/features/working-with-xaml-hot-reload.md index da8a2d3bcf96..c32f95322b95 100644 --- a/doc/articles/features/working-with-xaml-hot-reload.md +++ b/doc/articles/features/working-with-xaml-hot-reload.md @@ -293,7 +293,7 @@ Mobile targets are currently using a limited version of XAML Hot Reload and do n ## Hot Reload Indicator -Hot Reload displays a visual indicator to help you further monitor changes while developing. It displays new information every time Hot Reload is triggered. The indicator is enabled by default within the `EnableHotReload()` method which is located in the root `App.xaml.cs` file. This displays an overlay which hosts the visual indicator. If you wish to disable it, you simply have to provide the following boolean: `EnableHotReload(disableIndicator: true)`, removing the overlay from the view. +Hot Reload displays a visual indicator to help you further monitor changes while developing. It displays new information every time Hot Reload is triggered. The indicator is enabled by default within the `UseStudio()` method which is located in the root `App.xaml.cs` file. This displays an overlay which hosts the visual indicator. If you wish to disable it, you simply have to provide the following boolean: `EnableHotReload(disableIndicator: true)`, removing the overlay from the view.

A hot reload visual indicator @@ -370,7 +370,7 @@ Here's a summary of what icons and statuses you can expect: //... in the OnLaunched method #if DEBUG - MainWindow.EnableHotReload(); + MainWindow.UseStudio(); #endif ``` diff --git a/doc/articles/migrating-from-previous-releases.md b/doc/articles/migrating-from-previous-releases.md index 6a4c5ff874f3..83be4df90077 100644 --- a/doc/articles/migrating-from-previous-releases.md +++ b/doc/articles/migrating-from-previous-releases.md @@ -17,6 +17,10 @@ A few considerations to take into account: - Moving to .NET 9 or upgrading .NET 9 projects now require the use of .NET 9 RC2 and Visual Studio 17.12 Preview 3. - To migrate a project to .NET 9, [read the directions](xref:Uno.Development.MigratingFromNet8ToNet9) from our documentation. +### The EnableHotReload method is deprecated + +When upgrading to Uno 5.5, in the `App.xaml.cs` file, the `EnableHotReload()` method is deprecated and must be replaced with `UseStudio()` instead. + ## Uno Platform 5.4 Uno Platform 5.4 contains breaking changes for Uno.Extensions. diff --git a/doc/articles/migrating-to-uno-5.md b/doc/articles/migrating-to-uno-5.md index 3c8b93add58b..d5466ef9f1e5 100644 --- a/doc/articles/migrating-to-uno-5.md +++ b/doc/articles/migrating-to-uno-5.md @@ -44,7 +44,7 @@ Hot Reload support has changed in Uno Platform 5.0 and a new API invocation is n //... in the OnLaunched method #if DEBUG - MainWindow.EnableHotReload(); + MainWindow.UseStudio(); #endif ``` diff --git a/doc/articles/uno-build-error-codes.md b/doc/articles/uno-build-error-codes.md index 938187ee28bd..4aa58ec23bb7 100644 --- a/doc/articles/uno-build-error-codes.md +++ b/doc/articles/uno-build-error-codes.md @@ -178,6 +178,18 @@ Some components like `ProgressRing` and `MediaPlayerElement` requires you to ref - For `ProgressRing`, it requires Lottie dependency. For more information about adding Lottie to your project, see [Lottie for Uno](xref:Uno.Features.Lottie). - For `MediaPlayerElement` on WebAssembly or Gtk, it requires `Uno.WinUI.MediaPlayer.WebAssembly` or `Uno.WinUI.MediaPlayer.Skia.Gtk` NuGet package. For more information, see [MediaPlayerElement](xref:Uno.Controls.MediaPlayerElement). +### UNO0008 + +In Uno Platform 5.5, the `EnableHotReload` method has been deprecated and replaced by `UseStudio()`. + +Note that this change only applies to projects using the Uno.Sdk. If you're not using the Uno.Sdk, you can disable this warning using the following code: + +```xml +#pragma warning disable UNO0008 // Replace with UseStudio() when migrating to the Uno.Sdk. +window.EnableHotReload(); +#pragma warning restore UNO0008 +``` + ## XAML Errors ### UNOX0001 diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 43c8b2315996..de865103a91d 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -223,6 +223,9 @@ $(NoWarn);CS0436 + + + $(NoWarn);UNO0008 diff --git a/src/SolutionTemplate/5.1/uno51blank/Directory.Packages.props b/src/SolutionTemplate/5.1/uno51blank/Directory.Packages.props index 296df9d6fe5a..ef290f97a282 100644 --- a/src/SolutionTemplate/5.1/uno51blank/Directory.Packages.props +++ b/src/SolutionTemplate/5.1/uno51blank/Directory.Packages.props @@ -10,17 +10,17 @@ - - - - - - - - - - - + + + + + + + + + + + diff --git a/src/SolutionTemplate/5.1/uno51blank/uno51blank/App.cs b/src/SolutionTemplate/5.1/uno51blank/uno51blank/App.cs index 62e506b2f697..a1f60a65781f 100644 --- a/src/SolutionTemplate/5.1/uno51blank/uno51blank/App.cs +++ b/src/SolutionTemplate/5.1/uno51blank/uno51blank/App.cs @@ -9,7 +9,9 @@ protected override void OnLaunched(LaunchActivatedEventArgs args) MainWindow = new Window(); #if DEBUG +#pragma warning disable UNO0008 MainWindow.EnableHotReload(); +#pragma warning restore UNO0008 #endif diff --git a/src/SolutionTemplate/5.1/uno51recommended/Directory.Packages.props b/src/SolutionTemplate/5.1/uno51recommended/Directory.Packages.props index 1cb2d22cd667..bf4abe062445 100644 --- a/src/SolutionTemplate/5.1/uno51recommended/Directory.Packages.props +++ b/src/SolutionTemplate/5.1/uno51recommended/Directory.Packages.props @@ -17,17 +17,17 @@ - - - - - - - - - - - + + + + + + + + + + + @@ -52,10 +52,10 @@ - + - - + + diff --git a/src/SolutionTemplate/5.1/uno51recommended/uno51recommended/App.cs b/src/SolutionTemplate/5.1/uno51recommended/uno51recommended/App.cs index 19b13588d50f..7f9bc9aacef4 100644 --- a/src/SolutionTemplate/5.1/uno51recommended/uno51recommended/App.cs +++ b/src/SolutionTemplate/5.1/uno51recommended/uno51recommended/App.cs @@ -75,7 +75,9 @@ protected async override void OnLaunched(LaunchActivatedEventArgs args) MainWindow = builder.Window; #if DEBUG +#pragma warning disable UNO0008 MainWindow.EnableHotReload(); +#pragma warning restore UNO0008 #endif Host = await builder.NavigateAsync(); diff --git a/src/SolutionTemplate/5.2/uno52AppWithLib/uno52AppWithLib/App.xaml.cs b/src/SolutionTemplate/5.2/uno52AppWithLib/uno52AppWithLib/App.xaml.cs index fc6929ae914b..0f72526ed656 100644 --- a/src/SolutionTemplate/5.2/uno52AppWithLib/uno52AppWithLib/App.xaml.cs +++ b/src/SolutionTemplate/5.2/uno52AppWithLib/uno52AppWithLib/App.xaml.cs @@ -21,7 +21,9 @@ protected override void OnLaunched(LaunchActivatedEventArgs args) { MainWindow = new Window(); #if DEBUG +#pragma warning disable UNO0008 MainWindow.EnableHotReload(); +#pragma warning restore UNO0008 #endif diff --git a/src/SolutionTemplate/5.2/uno52AppWithLib/uno52AppWithLib/uno52AppWithLib.csproj b/src/SolutionTemplate/5.2/uno52AppWithLib/uno52AppWithLib/uno52AppWithLib.csproj index e7d30b60c49b..66c4752e55b7 100644 --- a/src/SolutionTemplate/5.2/uno52AppWithLib/uno52AppWithLib/uno52AppWithLib.csproj +++ b/src/SolutionTemplate/5.2/uno52AppWithLib/uno52AppWithLib/uno52AppWithLib.csproj @@ -2,7 +2,7 @@ net8.0-browserwasm;net8.0-desktop;net8.0 $(TargetFrameworks);net8.0-android;net8.0-ios;net8.0-maccatalyst;net8.0-desktop - $(TargetFrameworks);net8.0-windows10.0.19041 + $(TargetFrameworks);net8.0-windows10.0.26100 $(TargetFrameworks.Replace('net8.0-android','')) @@ -52,7 +52,7 @@ BeforeTargets="AfterBuild"> + Condition=" '$(TargetFramework)' == 'net8.0-windows10.0.26100' OR '$(TargetFramework)' == 'net8.0-desktop' "> <_AssetsToValidate Include="$(OutputPath)Assets\SharedAssets.md" /> <_AssetsToValidate Include="$(OutputPath)Assets\Icons\icon_foreground.png" /> diff --git a/src/SolutionTemplate/5.2/uno52AppWithLib/uno52emptylib/uno52emptylib.csproj b/src/SolutionTemplate/5.2/uno52AppWithLib/uno52emptylib/uno52emptylib.csproj index 4c1a25d4673f..194ad396fa14 100644 --- a/src/SolutionTemplate/5.2/uno52AppWithLib/uno52emptylib/uno52emptylib.csproj +++ b/src/SolutionTemplate/5.2/uno52AppWithLib/uno52emptylib/uno52emptylib.csproj @@ -2,7 +2,7 @@ net8.0-browserwasm;net8.0-desktop;net8.0 $(TargetFrameworks);net8.0-android;net8.0-ios;net8.0-maccatalyst;net8.0-desktop - $(TargetFrameworks);net8.0-windows10.0.19041 + $(TargetFrameworks);net8.0-windows10.0.26100 $(TargetFrameworks.Replace('net8.0-android','')) @@ -39,4 +39,4 @@ - \ No newline at end of file + diff --git a/src/SolutionTemplate/5.2/uno52AppWithLib/uno52lib/uno52lib.csproj b/src/SolutionTemplate/5.2/uno52AppWithLib/uno52lib/uno52lib.csproj index 3b536a6df335..45540750ad7a 100644 --- a/src/SolutionTemplate/5.2/uno52AppWithLib/uno52lib/uno52lib.csproj +++ b/src/SolutionTemplate/5.2/uno52AppWithLib/uno52lib/uno52lib.csproj @@ -2,7 +2,7 @@ net8.0-browserwasm;net8.0-desktop;net8.0 $(TargetFrameworks);net8.0-android;net8.0-ios;net8.0-maccatalyst;net8.0-desktop - $(TargetFrameworks);net8.0-windows10.0.19041 + $(TargetFrameworks);net8.0-windows10.0.26100 $(TargetFrameworks.Replace('net8.0-android','')) @@ -39,4 +39,4 @@ - \ No newline at end of file + diff --git a/src/SolutionTemplate/5.2/uno52Lib/uno52Lib.csproj b/src/SolutionTemplate/5.2/uno52Lib/uno52Lib.csproj index ef105687423f..fedfadc3f2d5 100644 --- a/src/SolutionTemplate/5.2/uno52Lib/uno52Lib.csproj +++ b/src/SolutionTemplate/5.2/uno52Lib/uno52Lib.csproj @@ -1,6 +1,6 @@ - net8.0;net8.0-ios;net8.0-maccatalyst;net8.0-windows10.0.19041;net8.0-browserwasm;net8.0-desktop + net8.0;net8.0-ios;net8.0-maccatalyst;net8.0-windows10.0.26100;net8.0-browserwasm;net8.0-desktop $(TargetFrameworks);net8.0-android diff --git a/src/SolutionTemplate/5.2/uno52NuGetLib/uno52NuGetLib.csproj b/src/SolutionTemplate/5.2/uno52NuGetLib/uno52NuGetLib.csproj index ceb179b8eb27..3f8e15879332 100644 --- a/src/SolutionTemplate/5.2/uno52NuGetLib/uno52NuGetLib.csproj +++ b/src/SolutionTemplate/5.2/uno52NuGetLib/uno52NuGetLib.csproj @@ -1,6 +1,6 @@ - net8.0;net8.0-ios;net8.0-maccatalyst;net8.0-windows10.0.19041;net8.0-browserwasm;net8.0-desktop + net8.0;net8.0-ios;net8.0-maccatalyst;net8.0-windows10.0.26100;net8.0-browserwasm;net8.0-desktop $(TargetFrameworks);net8.0-android diff --git a/src/SolutionTemplate/5.2/uno52SingleProjectLib/uno52SingleProjectLib.csproj b/src/SolutionTemplate/5.2/uno52SingleProjectLib/uno52SingleProjectLib.csproj index c2bd98afb43f..88cdd66cf72f 100644 --- a/src/SolutionTemplate/5.2/uno52SingleProjectLib/uno52SingleProjectLib.csproj +++ b/src/SolutionTemplate/5.2/uno52SingleProjectLib/uno52SingleProjectLib.csproj @@ -1,6 +1,6 @@ - net8.0;net8.0-ios;net8.0-maccatalyst;net8.0-windows10.0.19041;net8.0-browserwasm;net8.0-desktop + net8.0;net8.0-ios;net8.0-maccatalyst;net8.0-windows10.0.26100;net8.0-browserwasm;net8.0-desktop $(TargetFrameworks);net8.0-android diff --git a/src/SolutionTemplate/5.2/uno52blank/uno52blank/App.xaml.cs b/src/SolutionTemplate/5.2/uno52blank/uno52blank/App.xaml.cs index 3d3bb856f7be..0afcd05f0c42 100644 --- a/src/SolutionTemplate/5.2/uno52blank/uno52blank/App.xaml.cs +++ b/src/SolutionTemplate/5.2/uno52blank/uno52blank/App.xaml.cs @@ -21,7 +21,9 @@ protected override void OnLaunched(LaunchActivatedEventArgs args) { MainWindow = new Window(); #if DEBUG +#pragma warning disable UNO0008 MainWindow.EnableHotReload(); +#pragma warning restore UNO0008 #endif diff --git a/src/SolutionTemplate/5.2/uno52blank/uno52blank/uno52blank.csproj b/src/SolutionTemplate/5.2/uno52blank/uno52blank/uno52blank.csproj index 20a7d5370285..b2359027a9da 100644 --- a/src/SolutionTemplate/5.2/uno52blank/uno52blank/uno52blank.csproj +++ b/src/SolutionTemplate/5.2/uno52blank/uno52blank/uno52blank.csproj @@ -2,7 +2,7 @@ net8.0-browserwasm;net8.0-desktop;net8.0 $(TargetFrameworks);net8.0-android;net8.0-ios;net8.0-maccatalyst;net8.0-desktop - $(TargetFrameworks);net8.0-windows10.0.19041 + $(TargetFrameworks);net8.0-windows10.0.26100 $(TargetFrameworks.Replace('net8.0-android','')) @@ -46,7 +46,7 @@ + Condition="'$(TargetFramework)'=='net8.0-windows10.0.26100'"> net9.0-browserwasm;net9.0-desktop;net9.0 $(TargetFrameworks);net9.0-android;net9.0-ios;net9.0-maccatalyst;net9.0-desktop - $(TargetFrameworks);net9.0-windows10.0.19041 + $(TargetFrameworks);net9.0-windows10.0.26100 $(TargetFrameworks.Replace('net9.0-android','')) @@ -65,7 +65,7 @@ BeforeTargets="AfterBuild"> + Condition=" '$(TargetFramework)' == 'net9.0-windows10.0.26100' OR '$(TargetFramework)' == 'net9.0-desktop' "> <_AssetsToValidate Include="$(OutputPath)Assets\SharedAssets.md" /> <_AssetsToValidate Include="$(OutputPath)Assets\Icons\icon_foreground.png" /> diff --git a/src/Uno.Foundation/Diagnostics/DiagnosticViewRegistry.cs b/src/Uno.Foundation/Diagnostics/DiagnosticViewRegistry.cs index ed14cec7ab3d..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,15 +35,17 @@ public static void Register(IDiagnosticView view, DiagnosticViewRegistrationMode } } -internal record DiagnosticViewRegistration(DiagnosticViewRegistrationMode Mode, IDiagnosticView View); +internal sealed record DiagnosticViewRegistration( + DiagnosticViewRegistrationMode Mode, + IDiagnosticView View); -internal enum DiagnosticViewRegistrationMode +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 @@ internal 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.Sdk/Sdk/Sdk.props.buildschema.json b/src/Uno.Sdk/Sdk/Sdk.props.buildschema.json index bf5cf925b110..5e5918a36b3a 100644 --- a/src/Uno.Sdk/Sdk/Sdk.props.buildschema.json +++ b/src/Uno.Sdk/Sdk/Sdk.props.buildschema.json @@ -234,6 +234,10 @@ "description": "Provides an explicit override for the version of Uno.Settings to use.", "type": "nuget-version" }, + "UnoHotDesignVersion": { + "description": "Provides an explicit override for the version of Uno.HotDesign to use.", + "type": "nuget-version" + }, "MicrosoftLoggingVersion": { "description": "Provides an explicit override for the version of Microsoft.Extensions.Logging to use.", "type": "nuget-version" diff --git a/src/Uno.Sdk/Services/PackageManifest.cs b/src/Uno.Sdk/Services/PackageManifest.cs index a212120e9308..208ec470be13 100644 --- a/src/Uno.Sdk/Services/PackageManifest.cs +++ b/src/Uno.Sdk/Services/PackageManifest.cs @@ -138,6 +138,7 @@ public class Group public const string Resizetizer = nameof(Resizetizer); public const string SdkExtras = nameof(SdkExtras); public const string Settings = nameof(Settings); + public const string HotDesign = nameof(HotDesign); public const string SkiaSharp = nameof(SkiaSharp); public const string SvgSkia = nameof(SvgSkia); public const string WinAppSdk = nameof(WinAppSdk); diff --git a/src/Uno.Sdk/Tasks/ImplicitPackagesResolver.cs b/src/Uno.Sdk/Tasks/ImplicitPackagesResolver.cs index ba60a5f67d5d..5a6e6a621e51 100644 --- a/src/Uno.Sdk/Tasks/ImplicitPackagesResolver.cs +++ b/src/Uno.Sdk/Tasks/ImplicitPackagesResolver.cs @@ -85,6 +85,8 @@ public sealed class ImplicitPackagesResolver_v0 : Task public string? UnoSettingsVersion { get; set; } + public string? UnoHotDesignVersion { get; set; } + public string? MicrosoftLoggingVersion { get; set; } public string? WinAppSdkVersion { get; set; } @@ -246,6 +248,7 @@ private void SetupRuntimePackageManifestUpdates(PackageManifest manifest) .UpdateManifest(PackageManifest.Group.Resizetizer, UnoResizetizerVersion) .UpdateManifest(PackageManifest.Group.SdkExtras, UnoSdkExtrasVersion) .UpdateManifest(PackageManifest.Group.Settings, UnoSettingsVersion) + .UpdateManifest(PackageManifest.Group.HotDesign, UnoHotDesignVersion) .UpdateManifest(PackageManifest.Group.SkiaSharp, SkiaSharpVersion) .UpdateManifest(PackageManifest.Group.SvgSkia, SvgSkiaVersion) .UpdateManifest(PackageManifest.Group.WinAppSdk, WinAppSdkVersion) diff --git a/src/Uno.Sdk/packages.json b/src/Uno.Sdk/packages.json index ce74950b283e..bcfc9ee81c8e 100644 --- a/src/Uno.Sdk/packages.json +++ b/src/Uno.Sdk/packages.json @@ -85,9 +85,16 @@ "Uno.Settings.DevServer" ] }, + { + "group": "hotdesign", + "version": "1.0.0-dev.53", + "packages": [ + "Uno.UI.HotDesign" + ] + }, { "group": "SkiaSharp", - "version": "2.88.8", + "version": "2.88.9-preview.2.2", "packages": [ "SkiaSharp.Skottie", "SkiaSharp.Views.Uno.WinUI", diff --git a/src/Uno.Sdk/targets/Uno.Implicit.Packages.ProjectSystem.targets b/src/Uno.Sdk/targets/Uno.Implicit.Packages.ProjectSystem.targets index e0880a8dce18..7b02dded7571 100644 --- a/src/Uno.Sdk/targets/Uno.Implicit.Packages.ProjectSystem.targets +++ b/src/Uno.Sdk/targets/Uno.Implicit.Packages.ProjectSystem.targets @@ -11,6 +11,10 @@ <_UnoProjectSystemPackageReference Include="Uno.Settings.DevServer" ProjectSystem="true" PrivateAssets="all" /> + + <_UnoProjectSystemPackageReference Include="Uno.UI.HotDesign" ProjectSystem="true" PrivateAssets="all" Condition=" '$(Optimize)' != 'true' " /> + + <_UnoProjectSystemPackageReference Include="Uno.WinUI.DevServer" ProjectSystem="true" Condition="$(Optimize) != 'true'" /> <_UnoProjectSystemPackageReference Include="Uno.WinUI.DevServer" ProjectSystem="true" Exclude="all" Condition="$(Optimize) == 'true'" /> diff --git a/src/Uno.Sdk/targets/Uno.Implicit.Packages.targets b/src/Uno.Sdk/targets/Uno.Implicit.Packages.targets index ec578d78d73e..53ff56988041 100644 --- a/src/Uno.Sdk/targets/Uno.Implicit.Packages.targets +++ b/src/Uno.Sdk/targets/Uno.Implicit.Packages.targets @@ -63,6 +63,7 @@ UnoResizetizerVersion="$(UnoResizetizerVersion)" UnoSdkExtrasVersion="$(UnoSdkExtrasVersion)" UnoSettingsVersion="$(UnoSettingsVersion)" + UnoHotDesignVersion="$(UnoHotDesignVersion)" MicrosoftLoggingVersion="$(MicrosoftLoggingVersion)" WinAppSdkVersion="$(WinAppSdkVersion)" WinAppSdkBuildToolsVersion="$(WinAppSdkBuildToolsVersion)" diff --git a/src/Uno.UI.RemoteControl.Host/Extensibility/AddInsExtensions.cs b/src/Uno.UI.RemoteControl.Host/Extensibility/AddInsExtensions.cs index 80491025e1b3..a9b7fec6dc9a 100644 --- a/src/Uno.UI.RemoteControl.Host/Extensibility/AddInsExtensions.cs +++ b/src/Uno.UI.RemoteControl.Host/Extensibility/AddInsExtensions.cs @@ -10,8 +10,8 @@ public static class AddInsExtensions { public static IWebHostBuilder ConfigureAddIns(this IWebHostBuilder builder, string solutionFile) { - AssemblyHelper.Load(AddIns.Discover(solutionFile), throwIfLoadFailed: true); + AssemblyHelper.Load(AddIns.Discover(solutionFile), throwIfLoadFailed: false); - return builder.ConfigureServices(svc => svc.AddFromAttribute()); + return builder.ConfigureServices(svc => svc.AddFromAttributes()); } } diff --git a/src/Uno.UI.RemoteControl.Host/Extensibility/Uno.Utils.DependencyInjection/ServiceAttribute.cs b/src/Uno.UI.RemoteControl.Host/Extensibility/Uno.Utils.DependencyInjection/ServiceAttribute.cs index 136798d820de..ce2b81f08f01 100644 --- a/src/Uno.UI.RemoteControl.Host/Extensibility/Uno.Utils.DependencyInjection/ServiceAttribute.cs +++ b/src/Uno.UI.RemoteControl.Host/Extensibility/Uno.Utils.DependencyInjection/ServiceAttribute.cs @@ -10,7 +10,7 @@ namespace Uno.Utils.DependencyInjection; /// Type of the contract (i.e. interface) implemented by the concrete type. /// Concrete type to register in the service collection. [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] -public class ServiceAttribute(Type contract, Type implementation) : Attribute +public sealed class ServiceAttribute(Type contract, Type implementation) : Attribute { /// /// Creates a new instance of the class with only a concrete type (used as contract and implementation). diff --git a/src/Uno.UI.RemoteControl.Host/Extensibility/Uno.Utils.DependencyInjection/ServiceCollectionExtensionAttribute.cs b/src/Uno.UI.RemoteControl.Host/Extensibility/Uno.Utils.DependencyInjection/ServiceCollectionExtensionAttribute.cs new file mode 100644 index 000000000000..27bb145cd539 --- /dev/null +++ b/src/Uno.UI.RemoteControl.Host/Extensibility/Uno.Utils.DependencyInjection/ServiceCollectionExtensionAttribute.cs @@ -0,0 +1,23 @@ +using System; +using System.Linq; +using Microsoft.Extensions.DependencyInjection; + +namespace Uno.Utils.DependencyInjection; + +/// +/// Attribute to define a type able to registers some services in a service collection. +/// +/// Type of the extension that should be instantiated with a as parameter. +/// +/// The given type is expected to have a single constructor which takes a single parameter of type . +/// An instance of the given type will be created during the service collection registration process (with the service collection as parameter), +/// so the implementation will be able to add some custom services on it. +/// +[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] +public sealed class ServiceCollectionExtensionAttribute(Type type) : Attribute +{ + /// + /// Type of the extension that should be instantiated with a as parameter. + /// + public Type Type { get; } = type; +} diff --git a/src/Uno.UI.RemoteControl.Host/Extensibility/Uno.Utils.DependencyInjection/ServiceCollectionServiceExtensions.cs b/src/Uno.UI.RemoteControl.Host/Extensibility/Uno.Utils.DependencyInjection/ServiceCollectionServiceExtensions.cs index 27cd270e5aa7..52d206340797 100644 --- a/src/Uno.UI.RemoteControl.Host/Extensibility/Uno.Utils.DependencyInjection/ServiceCollectionServiceExtensions.cs +++ b/src/Uno.UI.RemoteControl.Host/Extensibility/Uno.Utils.DependencyInjection/ServiceCollectionServiceExtensions.cs @@ -12,12 +12,22 @@ namespace Uno.Utils.DependencyInjection; public static class ServiceCollectionServiceExtensions { + /// + /// Register services configured with the and attributes from all loaded assemblies. + /// + /// The service collection on which services should be registered. + /// The service collection for fluent usage. + public static IServiceCollection AddFromAttributes(this IServiceCollection svc) + => svc + .AddFromServiceAttributes() + .AddFromServiceExtensionAttributes(); + /// /// Register services configured with the attribute from all loaded assemblies. /// /// The service collection on which services should be registered. /// The service collection for fluent usage. - public static IServiceCollection AddFromAttribute(this IServiceCollection svc) + public static IServiceCollection AddFromServiceAttributes(this IServiceCollection svc) { var attribute = typeof(ServiceAttribute); var services = AppDomain @@ -37,6 +47,40 @@ public static IServiceCollection AddFromAttribute(this IServiceCollection svc) return svc; } + /// + /// Register services configured with the attribute from all loaded assemblies. + /// + /// The service collection on which services should be registered. + /// The service collection for fluent usage. + public static IServiceCollection AddFromServiceExtensionAttributes(this IServiceCollection svc) + { + var attribute = typeof(ServiceCollectionExtensionAttribute); + var extensions = AppDomain + .CurrentDomain + .GetAssemblies() + .SelectMany(assembly => assembly.GetCustomAttributesData()) + .Select(attrData => attrData.TryCreate(attribute) as ServiceCollectionExtensionAttribute) + .Where(attr => attr is not null) + .ToImmutableList(); + + foreach (var extension in extensions) + { + try + { + Activator.CreateInstance(extension!.Type, args: [svc]); + } + catch (Exception error) + { + if (svc.Log().IsEnabled(LogLevel.Error)) + { + svc.Log().Log(LogLevel.Error, error, $"Failed to create an instance of extensions {extension?.Type} for dynamic service discovery.."); + } + } + } + + return svc; + } + private class AutoInitService(IServiceProvider services, IImmutableList types) : BackgroundService, IHostedService { /// diff --git a/src/Uno.UI.RemoteControl.Host/Helpers/AssemblyHelper.cs b/src/Uno.UI.RemoteControl.Host/Helpers/AssemblyHelper.cs index be3d40bf1acd..d614fc56da3c 100644 --- a/src/Uno.UI.RemoteControl.Host/Helpers/AssemblyHelper.cs +++ b/src/Uno.UI.RemoteControl.Host/Helpers/AssemblyHelper.cs @@ -18,6 +18,8 @@ public static IImmutableList Load(IImmutableList dllFiles, boo { try { + _log.Log(LogLevel.Debug, $"Loading add-in assembly '{dll}'."); + assemblies.Add(Assembly.LoadFrom(dll)); } catch (Exception err) diff --git a/src/Uno.UI.RemoteControl/HotReload/ClientHotReloadProcessor.ClientApi.cs b/src/Uno.UI.RemoteControl/HotReload/ClientHotReloadProcessor.ClientApi.cs index fe911e17b0e5..df45c03eaea0 100644 --- a/src/Uno.UI.RemoteControl/HotReload/ClientHotReloadProcessor.ClientApi.cs +++ b/src/Uno.UI.RemoteControl/HotReload/ClientHotReloadProcessor.ClientApi.cs @@ -21,7 +21,7 @@ public partial class ClientHotReloadProcessor /// /// Result details of a file update /// - /// Indicates if is known to have been updated on server-side. + /// Indicates if file is known to have been updated on server-side. /// Indicates if the change had an impact on the compilation of the application (might be a success-full build or an error). /// Gets the error if any happened during the update. public record struct UpdateResult( diff --git a/src/Uno.UI.RemoteControl/HotReload/ClientHotReloadProcessor.Common.Status.cs b/src/Uno.UI.RemoteControl/HotReload/ClientHotReloadProcessor.Common.Status.cs index 8fad9fe3bb56..16791f6d6d1b 100644 --- a/src/Uno.UI.RemoteControl/HotReload/ClientHotReloadProcessor.Common.Status.cs +++ b/src/Uno.UI.RemoteControl/HotReload/ClientHotReloadProcessor.Common.Status.cs @@ -23,23 +23,23 @@ public partial class ClientHotReloadProcessor /// /// Raised when the status of the hot-reload engine changes. /// - internal EventHandler? StatusChanged; + public EventHandler? StatusChanged; /// /// The current status of the hot-reload engine. /// - internal Status CurrentStatus => _status.Current; + public Status CurrentStatus => _status.Current; private readonly StatusSink _status; - internal enum HotReloadSource + public enum HotReloadSource { Runtime, DevServer, Manual } - internal enum HotReloadClientResult + public enum HotReloadClientResult { /// /// Successful hot-reload. @@ -63,17 +63,13 @@ internal enum HotReloadClientResult /// The global state of the hot-reload engine (combining server and client state). /// State and history of all hot-reload operations detected on the server. /// State and history of all hot-reload operation received by this client. - internal record Status( + public record Status( HotReloadState State, (HotReloadState State, IImmutableList Operations) Server, (HotReloadState State, IImmutableList Operations) Local); private class StatusSink(ClientHotReloadProcessor owner) { -#if HAS_UNO_WINUI - private readonly DiagnosticView _view = DiagnosticView.Register("Hot reload", ctx => new HotReloadStatusView(ctx), static (view, status) => view.OnHotReloadStatusChanged(status)); -#endif - private HotReloadState? _serverState; private bool _isFinalServerState; private ImmutableDictionary _serverOperations = ImmutableDictionary.Empty; @@ -142,9 +138,6 @@ private void NotifyStatusChanged() var status = BuildStatus(); Current = status; -#if HAS_UNO_WINUI - _view.Update(status); -#endif owner.StatusChanged?.Invoke(this, status); } @@ -163,22 +156,22 @@ private Status BuildStatus() } } - internal class HotReloadClientOperation + public class HotReloadClientOperation { #region Current [ThreadStatic] private static HotReloadClientOperation? _opForCurrentUiThread; - public static HotReloadClientOperation? GetForCurrentThread() + internal static HotReloadClientOperation? GetForCurrentThread() => _opForCurrentUiThread; - public void SetCurrent() + internal void SetCurrent() { Debug.Assert(_opForCurrentUiThread == null, "Only one operation should be active at once for a given UI thread."); _opForCurrentUiThread = this; } - public void ResignCurrent() + internal void ResignCurrent() { Debug.Assert(_opForCurrentUiThread == this, "Another operation has been started for teh current UI thread."); _opForCurrentUiThread = null; @@ -208,7 +201,7 @@ internal HotReloadClientOperation(HotReloadSource source, Type[] types, Action o public Type[] Types { get; private set; } - internal string[] CuratedTypes => _curatedTypes ??= GetCuratedTypes(); + public string[] CuratedTypes => _curatedTypes ??= GetCuratedTypes(); private string[] GetCuratedTypes() { diff --git a/src/Uno.UI.RemoteControl/HotReload/ClientHotReloadProcessor.MetadataUpdate.cs b/src/Uno.UI.RemoteControl/HotReload/ClientHotReloadProcessor.MetadataUpdate.cs index 5069bd7935a2..0a38206683d8 100644 --- a/src/Uno.UI.RemoteControl/HotReload/ClientHotReloadProcessor.MetadataUpdate.cs +++ b/src/Uno.UI.RemoteControl/HotReload/ClientHotReloadProcessor.MetadataUpdate.cs @@ -499,7 +499,7 @@ private static void UpdateApplicationCore(Type[] types) #endif else { - var errorMsg = $"Unable to access Dispatcher/DispatcherQueue in order to invoke {nameof(ReloadWithUpdatedTypes)}. Make sure you have enabled hot-reload (Window.EnableHotReload()) in app startup. See https://aka.platform.uno/hot-reload"; + var errorMsg = $"Unable to access Dispatcher/DispatcherQueue in order to invoke {nameof(ReloadWithUpdatedTypes)}. Make sure you have enabled hot-reload (Window.UseStudio()) in app startup. See https://aka.platform.uno/hot-reload"; hr?.ReportError(new InvalidOperationException(errorMsg)); if (_log.IsEnabled(LogLevel.Warning)) { diff --git a/src/Uno.UI.RemoteControl/HotReload/HotReloadStatusView.Entries.cs b/src/Uno.UI.RemoteControl/HotReload/HotReloadStatusView.Entries.cs deleted file mode 100644 index 70ede1d818c5..000000000000 --- a/src/Uno.UI.RemoteControl/HotReload/HotReloadStatusView.Entries.cs +++ /dev/null @@ -1,215 +0,0 @@ -#nullable enable - -using System; -using System.ComponentModel; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Text; -using Uno.UI.RemoteControl.HotReload.Messages; -using static Uno.UI.RemoteControl.HotReload.ClientHotReloadProcessor; -using static Uno.UI.RemoteControl.RemoteControlStatus; - -namespace Uno.UI.RemoteControl.HotReload; - -internal record DevServerEntry() : HotReloadLogEntry(EntrySource.DevServer, -1, DateTimeOffset.Now) -{ - public static DevServerEntry? TryCreateNew(RemoteControlStatus? oldStatus, RemoteControlStatus newStatus) - { - if (oldStatus is not null && oldStatus.State == newStatus.State) - { - return null; - } - - var (iconState, desc) = (oldStatus, newStatus) switch - { - (_, { State: ConnectionState.NoServer }) => (EntryIcon.Error, "No endpoint found"), - (not null, { State: ConnectionState.Connecting }) => (EntryIcon.Loading, "Connecting..."), - (null or { State: not ConnectionState.ConnectionTimeout }, { State: ConnectionState.ConnectionTimeout }) => (EntryIcon.Error, "Timeout"), - (null or { State: not ConnectionState.ConnectionFailed }, { State: ConnectionState.ConnectionFailed }) => (EntryIcon.Error, "Connection error"), - - (null or { IsVersionValid: not false }, { IsVersionValid: false }) => (EntryIcon.Warning, "Version mismatch"), - (null or { InvalidFrames.Count: 0 }, { InvalidFrames.Count: > 0 }) => (EntryIcon.Warning, "Unknown messages"), - (null or { MissingRequiredProcessors.IsEmpty: true }, { MissingRequiredProcessors.IsEmpty: false }) => (EntryIcon.Warning, "Processors missing"), - - ({ KeepAlive.State: KeepAliveState.Idle or KeepAliveState.Ok }, { KeepAlive.State: KeepAliveState.Late }) => (EntryIcon.Error, "Connection lost (>1000ms)"), - ({ KeepAlive.State: KeepAliveState.Idle or KeepAliveState.Ok }, { KeepAlive.State: KeepAliveState.Lost }) => (EntryIcon.Error, "Connection lost (>1s)"), - ({ KeepAlive.State: KeepAliveState.Idle or KeepAliveState.Ok }, { KeepAlive.State: KeepAliveState.Aborted }) => (EntryIcon.Error, "Connection lost (keep-alive)"), - ({ State: ConnectionState.Connected }, { State: ConnectionState.Disconnected }) => (EntryIcon.Error, "Connection lost"), - - ({ State: ConnectionState.Connected }, { State: ConnectionState.Reconnecting }) => (EntryIcon.Error, "Connection lost (reconnecting)"), - - _ => (default, default) - }; - - return desc is null - ? null - : new DevServerEntry { Title = desc, Icon = iconState | EntryIcon.Connection }; - } -} - -internal record EngineEntry() : HotReloadLogEntry(EntrySource.Engine, -1, DateTimeOffset.Now) -{ - public static EngineEntry? TryCreateNew(Status? oldStatus, Status status) - => (oldStatus?.State ?? HotReloadState.Initializing, status.State) switch - { - ( < HotReloadState.Ready, HotReloadState.Ready) => new EngineEntry { Title = "Connected", Icon = EntryIcon.Connection | EntryIcon.Success }, - (not HotReloadState.Disabled, HotReloadState.Disabled) => new EngineEntry { Title = "Cannot initialize with debugger attached", Icon = EntryIcon.Connection | EntryIcon.Error }, - _ => null - }; -} - -internal record ServerEntry : HotReloadLogEntry -{ - public ServerEntry(HotReloadServerOperationData srvOp) - : base(EntrySource.Server, srvOp.Id, srvOp.StartTime) - { - Update(srvOp); - } - - /// - /// Indicates if this notification is the final one for the operation, INCLUDING application wide. - /// - public bool IsFinal { get; private set; } - - public void Update(HotReloadServerOperationData srvOp) - { - IsFinal = srvOp.Result is not HotReloadServerResult.Success; - (IsSuccess, Icon) = srvOp.Result switch - { - null => (default, EntryIcon.HotReload | EntryIcon.Loading), - HotReloadServerResult.Success or HotReloadServerResult.NoChanges => (true, EntryIcon.HotReload | EntryIcon.Success), - _ => (false, EntryIcon.HotReload | EntryIcon.Error) - }; - Title = srvOp.Result switch - { - HotReloadServerResult.NoChanges => "No changes detected", - HotReloadServerResult.RudeEdit => "Rude edit detected, restart required", - HotReloadServerResult.Failed => "Compilation errors", - HotReloadServerResult.Aborted => "Operation cancelled", - HotReloadServerResult.InternalError => "An error occured", - _ => null - }; - Description = Join("file", srvOp.FilePaths.Select(Path.GetFileName).ToArray()!); - Duration = srvOp.EndTime is not null ? srvOp.EndTime - srvOp.StartTime : null; - - RaiseChanged(); - } -} - -internal record ApplicationEntry : HotReloadLogEntry -{ - public ApplicationEntry(HotReloadClientOperation localOp) - : base(EntrySource.Application, localOp.Id, localOp.StartTime) - { - Update(localOp); - } - - internal void Update(HotReloadClientOperation localOp) - { - (IsSuccess, Icon) = localOp.Result switch - { - null => (default(bool?), EntryIcon.HotReload | EntryIcon.Loading), - HotReloadClientResult.Success => (true, EntryIcon.HotReload | EntryIcon.Success), - _ => (false, EntryIcon.HotReload | EntryIcon.Error) - }; - Title = localOp.Result switch - { - null => "Processing...", - HotReloadClientResult.Success => "Update successful", - HotReloadClientResult.Failed => "An error occured", - _ => null - }; - Description = Join("type", localOp.CuratedTypes); - Duration = localOp.EndTime is not null ? localOp.EndTime - localOp.StartTime : null; - - RaiseChanged(); - } -} - -public enum EntrySource -{ - DevServer, - Engine, - Server, - Application -} - -[Flags] -public enum EntryIcon -{ - // Kind - Loading = 1 << 0, - Success = 1 << 1, - Warning = 1 << 2, - Error = 1 << 3, - - // Source - Connection = 1 << 8, - HotReload = 2 << 8, -} - - -[Microsoft.UI.Xaml.Data.Bindable] -public record HotReloadLogEntry(EntrySource Source, long Id, DateTimeOffset Timestamp) : INotifyPropertyChanged -{ - /// - public event PropertyChangedEventHandler? PropertyChanged; - - public bool? IsSuccess { get; set; } - public TimeSpan? Duration { get; set; } - public EntryIcon Icon { get; set; } - - public string? Title { get; set; } - public string? Description { get; set; } - public string? ToastDescription => Description ?? Title; - - public string TimeInfo => Duration switch - { - null => $"{Timestamp:T}", - { TotalMilliseconds: < 1000 } ms => $"{ms.TotalMilliseconds:F0} ms - {Timestamp:T}", - { } s => $"{s.TotalSeconds:N0} s - {Timestamp:T}", - }; - - protected void RaiseChanged() - => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("")); - - protected static string? Join(string kind, string[] items, int? total = null, int max = 5) - { - const int maxLength = 70; - - if (items is { Length: 0 } && total is null) - { - return null; - } - - var sb = new StringBuilder(maxLength + 12 /* and xx more*/); - int count; - for (count = 0; count < Math.Min(items.Length, max); count++) - { - var item = items[count]; - if (sb.Length + 2 /*, */ + item.Length < maxLength) - { - if (count is not 0) sb.Append(", "); - sb.Append(item); - } - else - { - break; - } - } - - var remaining = total - count; - if (remaining > 0) - { - sb.Append((count, remaining) switch - { - (0, 1) => $"1 {kind}", - (0, _) => $"{remaining} {kind}s", - _ => $" and {remaining} more" - }); - } - - return sb.ToString(); - } -} diff --git a/src/Uno.UI.RemoteControl/HotReload/HotReloadStatusView.Resources.cs b/src/Uno.UI.RemoteControl/HotReload/HotReloadStatusView.Resources.cs deleted file mode 100644 index 9e43ebd2302a..000000000000 --- a/src/Uno.UI.RemoteControl/HotReload/HotReloadStatusView.Resources.cs +++ /dev/null @@ -1,59 +0,0 @@ -#nullable enable - -using System; -using System.Linq; -using Microsoft.UI.Xaml; -using Microsoft.UI.Xaml.Data; -using Uno.Extensions; - -namespace Uno.UI.RemoteControl.HotReload; - -internal sealed class EntryIconToObjectConverter : IValueConverter -{ - public object? SuccessValue { get; set; } - public object? FailedValue { get; set; } - public object? ConnectionSuccessValue { get; set; } - public object? ConnectionFailedValue { get; set; } - public object? ConnectionWarningValue { get; set; } - - public object? Convert(object? value, Type targetType, object parameter, string language) - { - if (value is not null) - { - var ei = (EntryIcon)value; - - if (ei.HasFlag(EntryIcon.Connection)) - { - if (ei.HasFlag(EntryIcon.Success)) return ConnectionSuccessValue; - if (ei.HasFlag(EntryIcon.Error)) return ConnectionFailedValue; - if (ei.HasFlag(EntryIcon.Warning)) return ConnectionWarningValue; - } - else if (ei.HasFlag(EntryIcon.HotReload)) - { - if (ei.HasFlag(EntryIcon.Success)) return SuccessValue; - if (ei.HasFlag(EntryIcon.Error)) return FailedValue; - } - } - - return ConnectionWarningValue; - } - - public object ConvertBack(object value, Type targetType, object parameter, string language) - => throw new NotSupportedException("Only one-way conversion is supported."); -} - -internal sealed class NullStringToCollapsedConverter : IValueConverter -{ - public object? Convert(object? value, Type targetType, object parameter, string language) - { - if (value is string s && !string.IsNullOrEmpty(s)) - { - return Visibility.Visible; - } - - return Visibility.Collapsed; - } - - public object ConvertBack(object value, Type targetType, object parameter, string language) - => throw new NotSupportedException("Only one-way conversion is supported."); -} diff --git a/src/Uno.UI.RemoteControl/HotReload/HotReloadStatusView.cs b/src/Uno.UI.RemoteControl/HotReload/HotReloadStatusView.cs deleted file mode 100644 index 92bfb5f3fc84..000000000000 --- a/src/Uno.UI.RemoteControl/HotReload/HotReloadStatusView.cs +++ /dev/null @@ -1,350 +0,0 @@ -#nullable enable - -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Linq; -using System.Runtime.InteropServices; -using Microsoft.UI.Xaml; -using Microsoft.UI.Xaml.Controls; -using Uno.Diagnostics.UI; -using static Uno.UI.RemoteControl.HotReload.ClientHotReloadProcessor; - -namespace Uno.UI.RemoteControl.HotReload; - -[TemplateVisualState(GroupName = "Status", Name = StatusUnknownVisualStateName)] -[TemplateVisualState(GroupName = "Status", Name = StatusInitializingVisualStateName)] -[TemplateVisualState(GroupName = "Status", Name = StatusReadyVisualStateName)] -[TemplateVisualState(GroupName = "Status", Name = StatusWarningVisualStateName)] -[TemplateVisualState(GroupName = "Status", Name = StatusErrorVisualStateName)] -[TemplateVisualState(GroupName = "Result", Name = ResultNoneVisualStateName)] -[TemplateVisualState(GroupName = "Result", Name = ResultSuccessVisualStateName)] -[TemplateVisualState(GroupName = "Result", Name = ResultFailedVisualStateName)] -public sealed partial class HotReloadStatusView : Control -{ - private const string StatusUnknownVisualStateName = "Unknown"; - private const string StatusInitializingVisualStateName = "Initializing"; - private const string StatusReadyVisualStateName = "Ready"; - private const string StatusErrorVisualStateName = "Error"; - private const string StatusWarningVisualStateName = "Warning"; - - private const string ResultNoneVisualStateName = "None"; - private const string ResultSuccessVisualStateName = "Success"; - private const string ResultFailedVisualStateName = "Failed"; - - #region HeadLine (DP) - public static DependencyProperty HeadLineProperty { get; } = DependencyProperty.Register( - nameof(HeadLine), - typeof(string), - typeof(HotReloadStatusView), - new PropertyMetadata(default(string), (snd, args) => ToolTipService.SetToolTip(snd, args.NewValue?.ToString()))); - - public string? HeadLine - { - get => (string?)GetValue(HeadLineProperty); - private set => SetValue(HeadLineProperty, value); - } - #endregion - - #region History (DP) - public static DependencyProperty HistoryProperty { get; } = DependencyProperty.Register( - nameof(History), - typeof(ObservableCollection), - typeof(HotReloadStatusView), - new PropertyMetadata(default(ObservableCollection))); - - public ObservableCollection History - { - get => (ObservableCollection)GetValue(HistoryProperty); - private init => SetValue(HistoryProperty, value); - } - #endregion - - #region ProcessingNotification (DP) - public static readonly DependencyProperty ProcessingNotificationProperty = DependencyProperty.Register( - nameof(ProcessingNotification), - typeof(DiagnosticViewNotification), - typeof(HotReloadStatusView), - new PropertyMetadata(default(DiagnosticViewNotification?))); - - public DiagnosticViewNotification? ProcessingNotification - { - get => (DiagnosticViewNotification?)GetValue(ProcessingNotificationProperty); - set => SetValue(ProcessingNotificationProperty, value); - } - #endregion - - #region SuccessNotification (DP) - public static readonly DependencyProperty SuccessNotificationProperty = DependencyProperty.Register( - nameof(SuccessNotification), - typeof(DiagnosticViewNotification), - typeof(HotReloadStatusView), - new PropertyMetadata(default(DiagnosticViewNotification?))); - - public DiagnosticViewNotification? SuccessNotification - { - get => (DiagnosticViewNotification?)GetValue(SuccessNotificationProperty); - set => SetValue(SuccessNotificationProperty, value); - } - #endregion - - #region FailureNotification (DP) - public static readonly DependencyProperty FailureNotificationProperty = DependencyProperty.Register( - nameof(FailureNotification), - typeof(DiagnosticViewNotification), - typeof(HotReloadStatusView), - new PropertyMetadata(default(DiagnosticViewNotification?))); - - public DiagnosticViewNotification? FailureNotification - { - get => (DiagnosticViewNotification?)GetValue(FailureNotificationProperty); - set => SetValue(FailureNotificationProperty, value); - } - #endregion - - private readonly IDiagnosticViewContext _ctx; - private (string state, HotReloadLogEntry? entry) _result = (ResultNoneVisualStateName, null); - - private Status? _hotReloadStatus; - private RemoteControlStatus? _devServerStatus; - - private readonly Dictionary _serverHrEntries = new(); - private readonly Dictionary _appHrEntries = new(); - private readonly ClientHotReloadProcessor? _processor; // Only when used by external tool like HD. - - public static HotReloadStatusView Create(IDiagnosticViewContext ctx) - { - var processor = RemoteControlClient.Instance?.Processors?.OfType().FirstOrDefault(); - if (processor is null) - { - throw new InvalidOperationException("Cannot resolve the hot-reload client."); - } - - return new HotReloadStatusView(ctx, processor); - } - - internal HotReloadStatusView(IDiagnosticViewContext ctx, ClientHotReloadProcessor? processor = null) - { - _ctx = ctx; - _processor = processor; - - DefaultStyleKey = typeof(HotReloadStatusView); - History = []; - - UpdateVisualStates(false); - - Loaded += static (snd, _) => - { - // Make sure to hide the diagnostics overlay when the view is loaded (in case the template was applied while out of the visual tree). - if (snd is HotReloadStatusView { XamlRoot: { } root } that) - { - DiagnosticsOverlay.Get(root).Hide(RemoteControlStatusView.Id); - if (RemoteControlClient.Instance is { } devServer) - { - devServer.StatusChanged += that.OnDevServerStatusChanged; - that.OnDevServerStatusChanged(null, devServer.Status); - } - - if (that._processor is not null) - { - that._processor.StatusChanged += that.OnHotReloadStatusChanged; - that.OnHotReloadStatusChanged(that._processor.CurrentStatus); - } - } - }; - Unloaded += static (snd, _) => - { - if (snd is HotReloadStatusView that) - { - if (RemoteControlClient.Instance is { } devServer) - { - devServer.StatusChanged -= that.OnDevServerStatusChanged; - } - - if (that._processor is not null) - { - that._processor.StatusChanged -= that.OnHotReloadStatusChanged; - } - } - }; - } - - private void OnDevServerStatusChanged(object? sender, RemoteControlStatus devServerStatus) - { - var oldStatus = _devServerStatus; - _devServerStatus = devServerStatus; - - DispatcherQueue.TryEnqueue(() => - { - UpdateLog(oldStatus, devServerStatus); - - UpdateVisualStates(); - }); - } - - private void OnHotReloadStatusChanged(object? sender, Status status) - => OnHotReloadStatusChanged(status); - - internal void OnHotReloadStatusChanged(Status status) - { - var oldStatus = _hotReloadStatus; - _hotReloadStatus = status; - - UpdateLog(oldStatus, status); - - UpdateVisualStates(); - } - - private void UpdateLog(RemoteControlStatus? oldStatus, RemoteControlStatus newStatus) - { - if (DevServerEntry.TryCreateNew(oldStatus, newStatus) is { } entry) - { - Insert(History, entry); - } - } - - private void UpdateLog(Status? oldStatus, Status status) - { - // Add or update the entries for the **operations** (server and the application). - if (status.Server.Operations is { }) // can be null during loading, creating a NRE - { - foreach (var srvOp in status.Server.Operations) - { - ref var entry = ref CollectionsMarshal.GetValueRefOrAddDefault(_serverHrEntries, srvOp.Id, out var exists); - if (exists) - { - entry!.Update(srvOp); - } - else - { - entry = new ServerEntry(srvOp); - } - } - } - - if (status.Local.Operations is { }) // can be null during loading, creating a NRE - { - foreach (var localOp in status.Local.Operations) - { - ref var entry = ref CollectionsMarshal.GetValueRefOrAddDefault(_appHrEntries, localOp.Id, out var exists); - if (exists) - { - entry!.Update(localOp); - } - else - { - entry = new ApplicationEntry(localOp); - } - } - } - - var log = History; - SyncLog(log, _serverHrEntries.Values); - SyncLog(log, _appHrEntries.Values); - - // Add a log entry for the **status** change. - if (EngineEntry.TryCreateNew(oldStatus, status) is { } engineEntry) - { - Insert(log, engineEntry); - } - } - - public void UpdateVisualStates(bool useTransitions = true) - { - var log = History; - - var connectionEntry = log.FirstOrDefault(e => e.Source is EntrySource.Engine or EntrySource.DevServer); - var operationEntries = log.Where(entry => entry.Source is EntrySource.Server or EntrySource.Application).ToList(); - - // Update the "status"(a.k.a. "connection state") visual state. - if (connectionEntry is null) - { - HeadLine = null; - VisualStateManager.GoToState(this, StatusUnknownVisualStateName, useTransitions); - } - else - { - HeadLine = connectionEntry.Description; - var state = (connectionEntry.Icon & ~(EntryIcon.HotReload | EntryIcon.Connection)) switch - { - EntryIcon.Loading => StatusInitializingVisualStateName, - EntryIcon.Success => StatusReadyVisualStateName, - EntryIcon.Warning when operationEntries.Any(op => op.IsSuccess ?? false) => StatusReadyVisualStateName, - EntryIcon.Warning => StatusWarningVisualStateName, - EntryIcon.Error => StatusErrorVisualStateName, - _ => StatusUnknownVisualStateName - }; - VisualStateManager.GoToState(this, state, useTransitions); - } - - // Then the "result" visual state (en send notifications). - var result = operationEntries switch - { - { Count: 0 } => (ResultNoneVisualStateName, default), - _ when operationEntries.Any(op => op.IsSuccess is null) => (ResultNoneVisualStateName, default), - [ServerEntry { IsFinal: true, IsSuccess: true } e, ..] => (ResultSuccessVisualStateName, e), - [ServerEntry { IsFinal: true, IsSuccess: false } e, ..] => (ResultFailedVisualStateName, e), - [ApplicationEntry { IsSuccess: true } e, ..] => (ResultSuccessVisualStateName, e), - [ApplicationEntry { IsSuccess: false } e, ..] => (ResultFailedVisualStateName, e), - _ => (ResultNoneVisualStateName, default(HotReloadLogEntry)) - }; - if (result != _result) - { - _result = result; - VisualStateManager.GoToState(this, _result.state, useTransitions); - - var notif = _result.state switch - { - ResultNoneVisualStateName when operationEntries is { Count: > 0 } => ProcessingNotification, - ResultSuccessVisualStateName => SuccessNotification, - ResultFailedVisualStateName => FailureNotification, - _ => default - }; - if (notif is not null) - { - if (notif.Content is null or HotReloadLogEntry) - { - notif.Content = operationEntries[0]; - } - - _ctx.Notify(notif); - } - } - } - - #region Misc helpers - private static void SyncLog(ObservableCollection history, ICollection entries) - where TEntry : HotReloadLogEntry - { - foreach (var entry in entries) - { - if (entry.Title is null) - { - history.Remove(entry); - } - else if (!history.Contains(entry)) - { - Insert(history, entry); - } - } - } - - private static void Insert(ObservableCollection history, HotReloadLogEntry entry) - { - history.Insert(FindIndex(entry.Timestamp), entry); - - int FindIndex(DateTimeOffset date) - { - for (var i = 0; i < history.Count; i++) - { - if (history[i].Timestamp > date) - { - return i; - } - } - - return 0; - } - } - #endregion -} diff --git a/src/Uno.UI.RemoteControl/HotReload/HotReloadStatusView.xaml b/src/Uno.UI.RemoteControl/HotReload/HotReloadStatusView.xaml deleted file mode 100644 index 5b8e1e6af2a3..000000000000 --- a/src/Uno.UI.RemoteControl/HotReload/HotReloadStatusView.xaml +++ /dev/null @@ -1,346 +0,0 @@ - - - - - #000000 - #F9F9F9 - #EBEBEB - - - #FFFFFF - #282828 - #1C1C1C - - - - #C42B1C - #09B509 - #FD9E0F - #8A8A8A - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/Uno.UI.RemoteControl/HotReload/Messages/HotReloadServerResult.cs b/src/Uno.UI.RemoteControl/HotReload/Messages/HotReloadServerResult.cs index f7e0ad2e5f95..8a5f19d1905b 100644 --- a/src/Uno.UI.RemoteControl/HotReload/Messages/HotReloadServerResult.cs +++ b/src/Uno.UI.RemoteControl/HotReload/Messages/HotReloadServerResult.cs @@ -6,7 +6,7 @@ namespace Uno.UI.RemoteControl.HotReload.Messages; /// /// The result of a hot-reload operation on server. /// -internal enum HotReloadServerResult +public enum HotReloadServerResult { /// /// Hot-reload completed with no changes. diff --git a/src/Uno.UI.RemoteControl/HotReload/Messages/HotReloadState.cs b/src/Uno.UI.RemoteControl/HotReload/Messages/HotReloadState.cs index f814f093fff8..e0f28e817910 100644 --- a/src/Uno.UI.RemoteControl/HotReload/Messages/HotReloadState.cs +++ b/src/Uno.UI.RemoteControl/HotReload/Messages/HotReloadState.cs @@ -3,7 +3,7 @@ namespace Uno.UI.RemoteControl.HotReload; -internal enum HotReloadState +public enum HotReloadState { /// /// Hot reload is disabled. diff --git a/src/Uno.UI.RemoteControl/HotReload/Messages/HotReloadStatusMessage.cs b/src/Uno.UI.RemoteControl/HotReload/Messages/HotReloadStatusMessage.cs index 59af405fd427..3aaabd1454b4 100644 --- a/src/Uno.UI.RemoteControl/HotReload/Messages/HotReloadStatusMessage.cs +++ b/src/Uno.UI.RemoteControl/HotReload/Messages/HotReloadStatusMessage.cs @@ -30,7 +30,7 @@ internal record HotReloadStatusMessage( string IMessage.Name => Name; } - internal record HotReloadServerOperationData( + public record HotReloadServerOperationData( long Id, DateTimeOffset StartTime, ImmutableHashSet FilePaths, diff --git a/src/Uno.UI.RemoteControl/HotReload/WindowExtensions.cs b/src/Uno.UI.RemoteControl/HotReload/WindowExtensions.cs index 7f268ae0b9ce..45defc62fbb2 100644 --- a/src/Uno.UI.RemoteControl/HotReload/WindowExtensions.cs +++ b/src/Uno.UI.RemoteControl/HotReload/WindowExtensions.cs @@ -2,6 +2,7 @@ using Uno.UI.RemoteControl.HotReload; using Microsoft.UI.Xaml; using Uno.Diagnostics.UI; +using System.ComponentModel; namespace Uno.UI; @@ -14,6 +15,9 @@ public static class WindowExtensions /// Enables the UI Update cycle of HotReload to be handled by Uno /// /// The window of the application where UI updates will be applied + [Obsolete("Use the UseStudio() method instead if using the Uno.SDK, otherwise see https://aka.platform.uno/UNO0008 for more details.", DiagnosticId = "UNO0008", UrlFormat = "https://aka.platform.uno/UNO0008")] + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] public static void EnableHotReload(this Window window) => ClientHotReloadProcessor.SetWindow(window, false); @@ -22,6 +26,9 @@ public static void EnableHotReload(this Window window) /// /// The window of the application where UI updates will be applied /// Request to not show the on-canvas indicator by default. + [Obsolete("Use the UseStudio() method instead if using the Uno.SDK, otherwise see https://aka.platform.uno/UNO0008 for more details.", DiagnosticId = "UNO0008", UrlFormat = "https://aka.platform.uno/UNO0008")] + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] public static void EnableHotReload(this Window window, bool disableIndicator) => ClientHotReloadProcessor.SetWindow(window, disableIndicator); diff --git a/src/Uno.UI.RemoteControl/RemoteControlClient.Status.cs b/src/Uno.UI.RemoteControl/RemoteControlClient.Status.cs index 33ffe9cb172c..f7a9dd9543bd 100644 --- a/src/Uno.UI.RemoteControl/RemoteControlClient.Status.cs +++ b/src/Uno.UI.RemoteControl/RemoteControlClient.Status.cs @@ -13,9 +13,9 @@ namespace Uno.UI.RemoteControl; public partial class RemoteControlClient { - internal event EventHandler? StatusChanged; + public event EventHandler? StatusChanged; - internal RemoteControlStatus Status => _status.BuildStatus(); + public RemoteControlStatus Status => _status.BuildStatus(); private class StatusSink : DevServerDiagnostics.ISink { diff --git a/src/Uno.UI.RemoteControl/RemoteControlClient.cs b/src/Uno.UI.RemoteControl/RemoteControlClient.cs index c05abcabf61a..5df9baac1e12 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); @@ -599,7 +656,7 @@ private void StartKeepAliveTimer() if (Interlocked.CompareExchange(ref _keepAliveTimer, timer, null) is null) { - timer.Change(_keepAliveInterval, _keepAliveInterval); + timer.Change(TimeSpan.Zero, _keepAliveInterval); } } diff --git a/src/Uno.UI.RemoteControl/RemoteControlStatus.cs b/src/Uno.UI.RemoteControl/RemoteControlStatus.cs index 1aa2846f45ba..0d1c3e1ac1e5 100644 --- a/src/Uno.UI.RemoteControl/RemoteControlStatus.cs +++ b/src/Uno.UI.RemoteControl/RemoteControlStatus.cs @@ -5,15 +5,28 @@ namespace Uno.UI.RemoteControl; -internal record RemoteControlStatus( +public record RemoteControlStatus( RemoteControlStatus.ConnectionState State, bool? IsVersionValid, (RemoteControlStatus.KeepAliveState State, long RoundTrip) KeepAlive, ImmutableHashSet MissingRequiredProcessors, (long Count, ImmutableHashSet Types) InvalidFrames) { - public bool IsAllGood => State == ConnectionState.Connected && IsVersionValid == true && MissingRequiredProcessors.IsEmpty && KeepAlive.State == KeepAliveState.Ok && InvalidFrames.Count == 0; + /// + /// An ***aggregated*** state of the connection to determine if everything is fine. + /// This is for visual representation only, the actual state of the connection is in . + /// + public bool IsAllGood => + State == ConnectionState.Connected + && IsVersionValid == true + && MissingRequiredProcessors.IsEmpty + && KeepAlive.State == KeepAliveState.Ok + && InvalidFrames.Count == 0; + + /// + /// Not (for binding purposes). + /// public bool IsProblematic => !IsAllGood; public (Classification kind, string message) GetSummary() @@ -82,9 +95,9 @@ internal string GetDescription() return details.ToString(); } - internal record struct MissingProcessor(string TypeFullName, string Version, string Details, string? Error = null); + public readonly record struct MissingProcessor(string TypeFullName, string Version, string Details, string? Error = null); - internal enum KeepAliveState + public enum KeepAliveState { Idle, Ok, // Got ping/pong in expected delays @@ -93,7 +106,7 @@ internal enum KeepAliveState Aborted // KeepAlive was aborted } - internal enum ConnectionState + public enum ConnectionState { /// /// Client as not been started yet @@ -140,7 +153,7 @@ internal enum ConnectionState Disconnected } - internal enum Classification + public enum Classification { Ok, Info, diff --git a/src/Uno.UI.RemoteControl/RemoteControlStatusView.cs b/src/Uno.UI.RemoteControl/RemoteControlStatusView.cs index d8b79d89a947..35e46939435f 100644 --- a/src/Uno.UI.RemoteControl/RemoteControlStatusView.cs +++ b/src/Uno.UI.RemoteControl/RemoteControlStatusView.cs @@ -11,7 +11,7 @@ namespace Uno.UI.RemoteControl; -internal sealed partial class RemoteControlStatusView : Ellipse +public sealed partial class RemoteControlStatusView : Ellipse { #if __ANDROID__ public new const string Id = nameof(RemoteControlStatusView); diff --git a/src/Uno.UI.RemoteControl/Themes/Generic.xaml b/src/Uno.UI.RemoteControl/Themes/Generic.xaml index f56651e9b43a..585844b453c4 100644 --- a/src/Uno.UI.RemoteControl/Themes/Generic.xaml +++ b/src/Uno.UI.RemoteControl/Themes/Generic.xaml @@ -1,10 +1,7 @@ - + - - - - + + + diff --git a/src/Uno.UI.RemoteControl/Uno.UI.RemoteControl.Skia.csproj b/src/Uno.UI.RemoteControl/Uno.UI.RemoteControl.Skia.csproj index 700f5c7d46bb..f2b9a0b0c4d0 100644 --- a/src/Uno.UI.RemoteControl/Uno.UI.RemoteControl.Skia.csproj +++ b/src/Uno.UI.RemoteControl/Uno.UI.RemoteControl.Skia.csproj @@ -68,10 +68,6 @@ - - - - $(MSBuildThisFileDirectory)**\*.xaml diff --git a/src/Uno.UI.RuntimeTests/Tests/HotReload/Frame/HRApp/Tests/Given_TextBlock.cs b/src/Uno.UI.RuntimeTests/Tests/HotReload/Frame/HRApp/Tests/Given_TextBlock.cs index 41784dacaac8..c87b9915f418 100644 --- a/src/Uno.UI.RuntimeTests/Tests/HotReload/Frame/HRApp/Tests/Given_TextBlock.cs +++ b/src/Uno.UI.RuntimeTests/Tests/HotReload/Frame/HRApp/Tests/Given_TextBlock.cs @@ -70,13 +70,15 @@ public async Task When_Changing_TextBlock_UsingHRClient() { var ct = new CancellationTokenSource(TimeSpan.FromSeconds(60)).Token; + var content = new HR_Frame_Pages_Page2(); + UnitTestsUIContentHelper.Content = new ContentControl { - Content = new HR_Frame_Pages_Page2() + Content = content }; var hr = Uno.UI.RemoteControl.RemoteControlClient.Instance?.Processors.OfType().Single(); - var ctx = Uno.UI.RuntimeTests.Tests.HotReload.FrameworkElementExtensions.GetDebugParseContext(new HR_Frame_Pages_Page2()); + var ctx = Uno.UI.RuntimeTests.Tests.HotReload.FrameworkElementExtensions.GetDebugParseContext(content); var req = new Uno.UI.RemoteControl.HotReload.ClientHotReloadProcessor.UpdateRequest( ctx.FileName, SecondPageTextBlockOriginalText, @@ -94,20 +96,49 @@ public async Task When_Changing_TextBlock_UsingHRClient() await hr.UpdateFileAsync(req.Undo(waitForHotReload: false), CancellationToken.None); } } - + + /// + /// Ensure that UpdateFileAsync() completes when no changes are made to the file. + /// + [TestMethod] + public async Task When_Changing_TextBlock_UsingHRClient_NoChanges() + { + var ct = new CancellationTokenSource(TimeSpan.FromSeconds(60)).Token; + + var content = new HR_Frame_Pages_Page2(); + + UnitTestsUIContentHelper.Content = new ContentControl + { + Content = content + }; + + var hr = Uno.UI.RemoteControl.RemoteControlClient.Instance?.Processors.OfType().Single(); + var ctx = Uno.UI.RuntimeTests.Tests.HotReload.FrameworkElementExtensions.GetDebugParseContext(content); + var req = new Uno.UI.RemoteControl.HotReload.ClientHotReloadProcessor.UpdateRequest( + ctx.FileName, + SecondPageTextBlockOriginalText, + SecondPageTextBlockOriginalText + Environment.NewLine, + true) + .WithExtendedTimeouts(); // Required for CI + + await hr.UpdateFileAsync(req, ct); + } + // Another version of the test above, but pausing the TypeMapping before calling the file update [TestMethod] public async Task When_Changing_TextBlock_UsingHRClient_PausingTypeMapping() { var ct = new CancellationTokenSource(TimeSpan.FromSeconds(25)).Token; + var content = new HR_Frame_Pages_Page1(); + UnitTestsUIContentHelper.Content = new ContentControl { - Content = new HR_Frame_Pages_Page1() + Content = content }; var hr = Uno.UI.RemoteControl.RemoteControlClient.Instance?.Processors.OfType().Single(); - var ctx = Uno.UI.RuntimeTests.Tests.HotReload.FrameworkElementExtensions.GetDebugParseContext(new HR_Frame_Pages_Page1()); + var ctx = Uno.UI.RuntimeTests.Tests.HotReload.FrameworkElementExtensions.GetDebugParseContext(content); var req = new Uno.UI.RemoteControl.HotReload.ClientHotReloadProcessor.UpdateRequest( ctx.FileName, FirstPageTextBlockOriginalText, 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 7e14c271fb0e..393379f0fcc6 100644 --- a/src/Uno.UI/Diagnostics/DiagnosticView.Factories.cs +++ b/src/Uno.UI/Diagnostics/DiagnosticView.Factories.cs @@ -7,7 +7,7 @@ namespace Uno.Diagnostics.UI; -internal partial class DiagnosticView +partial class DiagnosticView { /// /// Registers a dedicated diagnostic view to be displayed by the diagnostic overlay. @@ -21,11 +21,16 @@ internal 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 3a87abcd3071..2c8fbbabcf79 100644 --- a/src/Uno.UI/Diagnostics/DiagnosticView.TView.TState.cs +++ b/src/Uno.UI/Diagnostics/DiagnosticView.TView.TState.cs @@ -10,12 +10,13 @@ namespace Uno.Diagnostics.UI; /// /// A generic diagnostic view that can be updated with a state. /// -internal class DiagnosticView( +public class DiagnosticView( string id, 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 5f833c7b6a4c..619d748bea12 100644 --- a/src/Uno.UI/Diagnostics/DiagnosticView.TView.cs +++ b/src/Uno.UI/Diagnostics/DiagnosticView.TView.cs @@ -10,11 +10,12 @@ namespace Uno.Diagnostics.UI; /// /// A generic diagnostic view. /// -internal class DiagnosticView( +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 fd1711cb6234..4a7eebf30157 100644 --- a/src/Uno.UI/Diagnostics/DiagnosticView.cs +++ b/src/Uno.UI/Diagnostics/DiagnosticView.cs @@ -10,19 +10,21 @@ namespace Uno.Diagnostics.UI; /// /// A generic diagnostic view. /// -internal partial class DiagnosticView( +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;