diff --git a/.github/workflows/nuget_windows.yml b/.github/workflows/nuget_windows.yml new file mode 100644 index 0000000..84ef3ff --- /dev/null +++ b/.github/workflows/nuget_windows.yml @@ -0,0 +1,73 @@ +name: Deploy Nuget + +on: + push: + tags: + - 'v[0-9]+.[0-9]+.[0-9]+' + +env: + PATH_TO_PROJECTS: ${{ github.workspace }}\src + PACKAGE_OUTPUT_DIRECTORY: ${{ github.workspace }}\output\ + NUGET_SOURCE_URL: 'https://api.nuget.org/v3/index.json' + GITHUB_PACKAGES_URL: 'https://nuget.pkg.github.com/asv-soft/index.json' + +jobs: + deploy: + name: 'Deploy' + runs-on: windows-2019 + if: startsWith(github.ref, 'refs/tags/v') + steps: + - name: 'Checkout' + uses: actions/checkout@v3 + + - name: Setup .Net + uses: actions/setup-dotnet@v3 + with: + dotnet-version: | + 8.x.x + + - name: Get version + id: version + uses: battila7/get-version-action@v2 + + - name: Check version + run: echo ${{ steps.version.outputs.version-without-v }} + + - name: Set project version + run: | + dotnet tool install -g dotnet-setversion + setversion ${{ steps.version.outputs.version-without-v }} ${{ env.PATH_TO_PROJECTS }}\Asv.Drones.Gui.Api\Asv.Drones.Gui.Api.csproj + + - name: Add NuGet source + run: | + dotnet nuget add source ${{ env.GITHUB_PACKAGES_URL }} --username '${{ secrets.USER_NAME }}' --password '${{ secrets.GIHUB_NUGET_AUTH_TOKEN }}' --store-password-in-clear-text + + - name: Restore dependencies + run: | + cd src + dotnet restore + + - name: Build projects + run: | + cd src + dotnet build -c Release --no-restore + + - name: Running all tests + run: | + cd src + dotnet test --no-restore --verbosity normal + + - name: Pack projects to Nuget + run: | + cd src + dotnet pack -c Release --no-build --no-restore -p:PackageVersion=${{ steps.version.outputs.version-without-v }} --output ${{ env.PACKAGE_OUTPUT_DIRECTORY }} + + - name: Push packages to Nuget + run: | + cd src + dotnet nuget push ${{ env.PACKAGE_OUTPUT_DIRECTORY }}Asv.Drones.Gui.Api.${{ steps.version.outputs.version-without-v }}.nupkg -k ${{ secrets.NUGET_AUTH_TOKEN }} -s ${{ env.NUGET_SOURCE_URL }} + + - name: Push packages to Github + run: | + cd src + dotnet nuget push ${{ env.PACKAGE_OUTPUT_DIRECTORY }}Asv.Drones.Gui.Api.${{ steps.version.outputs.version-without-v }}.nupkg -k ${{ secrets.GIHUB_NUGET_AUTH_TOKEN }} -s ${{ env.GITHUB_PACKAGES_URL }} diff --git a/.github/workflows/release-debug-version.yml b/.github/workflows/release-debug-version.yml new file mode 100644 index 0000000..8a42f1a --- /dev/null +++ b/.github/workflows/release-debug-version.yml @@ -0,0 +1,69 @@ +name: Build and Publish Debug Version + +on: + push: + tags: + - "v[0-9]+.[0-9]+.[0-9]+-dev.[0-9]+" + - "v[0-9]+.[0-9]+.[0-9]+-dev" + +env: + PATH_TO_PROJECTS: ${{ github.workspace }}\src + PACKAGE_OUTPUT_DIRECTORY: ${{ github.workspace }}\output\ + NUGET_SOURCE_URL: 'https://api.nuget.org/v3/index.json' + GITHUB_PACKAGES_URL: 'https://nuget.pkg.github.com/asv-soft/index.json' + +jobs: + deploy: + name: 'Deploy' + runs-on: windows-2019 + if: startsWith(github.ref, 'refs/tags/v') + steps: + - name: 'Checkout' + uses: actions/checkout@v3 + + - name: Setup .Net + uses: actions/setup-dotnet@v3 + with: + dotnet-version: | + 8.x.x + + - name: Get version + id: version + uses: battila7/get-version-action@v2 + + - name: Check version + run: echo ${{ steps.version.outputs.version-without-v }} + + - name: Set project version + run: | + dotnet tool install -g dotnet-setversion + setversion ${{ steps.version.outputs.version-without-v }} ${{ env.PATH_TO_PROJECTS }}\Asv.Drones.Gui.Api\Asv.Drones.Gui.Api.csproj + + - name: Add NuGet source + run: | + dotnet nuget add source ${{ env.GITHUB_PACKAGES_URL }} --username '${{ secrets.USER_NAME }}' --password '${{ secrets.GIHUB_NUGET_AUTH_TOKEN }}' --store-password-in-clear-text + + - name: Restore dependencies + run: | + cd src + dotnet restore + + - name: Build projects + run: | + cd src + dotnet build -c Release --no-restore + + - name: Running all tests + run: | + cd src + dotnet test --no-restore --verbosity normal + + - name: Pack projects to Nuget + run: | + cd src + dotnet pack -c Release --no-build --no-restore -p:PackageVersion=${{ steps.version.outputs.version-without-v }} --output ${{ env.PACKAGE_OUTPUT_DIRECTORY }} + + - name: Push packages to Github + run: | + cd src + dotnet nuget push ${{ env.PACKAGE_OUTPUT_DIRECTORY }}Asv.Drones.Gui.Api.${{ steps.version.outputs.version-without-v }}.nupkg -k ${{ secrets.GIHUB_NUGET_AUTH_TOKEN }} -s ${{ env.GITHUB_PACKAGES_URL }} diff --git a/.github/workflows/release-pre-version.yml b/.github/workflows/release-pre-version.yml new file mode 100644 index 0000000..32c01d5 --- /dev/null +++ b/.github/workflows/release-pre-version.yml @@ -0,0 +1,74 @@ +name: Build and Publish Pre-Release Version + +on: + push: + tags: + - "v[0-9]+.[0-9]+.[0-9]+-rc.[0-9]+" + - "v[0-9]+.[0-9]+.[0-9]+-rc" + +env: + PATH_TO_PROJECTS: ${{ github.workspace }}\src + PACKAGE_OUTPUT_DIRECTORY: ${{ github.workspace }}\output\ + NUGET_SOURCE_URL: 'https://api.nuget.org/v3/index.json' + GITHUB_PACKAGES_URL: 'https://nuget.pkg.github.com/asv-soft/index.json' + +jobs: + deploy: + name: 'Deploy' + runs-on: windows-2019 + if: startsWith(github.ref, 'refs/tags/v') + steps: + - name: 'Checkout' + uses: actions/checkout@v3 + + - name: Setup .Net + uses: actions/setup-dotnet@v3 + with: + dotnet-version: | + 8.x.x + + - name: Get version + id: version + uses: battila7/get-version-action@v2 + + - name: Check version + run: echo ${{ steps.version.outputs.version-without-v }} + + - name: Set project version + run: | + dotnet tool install -g dotnet-setversion + setversion ${{ steps.version.outputs.version-without-v }} ${{ env.PATH_TO_PROJECTS }}\Asv.Drones.Gui.Api\Asv.Drones.Gui.Api.csproj + + - name: Add NuGet source + run: | + dotnet nuget add source ${{ env.GITHUB_PACKAGES_URL }} --username '${{ secrets.USER_NAME }}' --password '${{ secrets.GIHUB_NUGET_AUTH_TOKEN }}' --store-password-in-clear-text + + - name: Restore dependencies + run: | + cd src + dotnet restore + + - name: Build projects + run: | + cd src + dotnet build -c Release --no-restore + + - name: Running all tests + run: | + cd src + dotnet test --no-restore --verbosity normal + + - name: Pack projects to Nuget + run: | + cd src + dotnet pack -c Release --no-build --no-restore -p:PackageVersion=${{ steps.version.outputs.version-without-v }} --output ${{ env.PACKAGE_OUTPUT_DIRECTORY }} + + - name: Push packages to Nuget + run: | + cd src + dotnet nuget push ${{ env.PACKAGE_OUTPUT_DIRECTORY }}Asv.Drones.Gui.Api.${{ steps.version.outputs.version-without-v }}.nupkg -k ${{ secrets.NUGET_AUTH_TOKEN }} -s ${{ env.NUGET_SOURCE_URL }} --prerelease + + - name: Push packages to Github + run: | + cd src + dotnet nuget push ${{ env.PACKAGE_OUTPUT_DIRECTORY }}Asv.Drones.Gui.Api.${{ steps.version.outputs.version-without-v }}.nupkg -k ${{ secrets.GIHUB_NUGET_AUTH_TOKEN }} -s ${{ env.GITHUB_PACKAGES_URL }} --prerelease diff --git a/.idea/.idea.Asv.Drones.Gui.Api/.idea/workspace.xml b/.idea/.idea.Asv.Drones.Gui.Api/.idea/workspace.xml new file mode 100644 index 0000000..dc0cb84 --- /dev/null +++ b/.idea/.idea.Asv.Drones.Gui.Api/.idea/workspace.xml @@ -0,0 +1,216 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1726462703553 + + + + + + + + + + + + \ No newline at end of file diff --git a/src/.idea/.idea.Asv.Drones.Gui.Api/.idea/.gitignore b/src/.idea/.idea.Asv.Drones.Gui.Api/.idea/.gitignore new file mode 100644 index 0000000..79b7c64 --- /dev/null +++ b/src/.idea/.idea.Asv.Drones.Gui.Api/.idea/.gitignore @@ -0,0 +1,10 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Rider ignored files +/projectSettingsUpdater.xml +/contentModel.xml +/.idea.Asv.Drones.Gui.Api.iml +/modules.xml +# Editor-based HTTP Client requests +/httpRequests/ diff --git a/src/Asv.Drones.Gui.Api.Design/App.axaml b/src/Asv.Drones.Gui.Api.Design/App.axaml new file mode 100644 index 0000000..f2789d8 --- /dev/null +++ b/src/Asv.Drones.Gui.Api.Design/App.axaml @@ -0,0 +1,13 @@ + + + + + + + + \ No newline at end of file diff --git a/src/Asv.Drones.Gui.Api.Design/App.axaml.cs b/src/Asv.Drones.Gui.Api.Design/App.axaml.cs new file mode 100644 index 0000000..1c222a6 --- /dev/null +++ b/src/Asv.Drones.Gui.Api.Design/App.axaml.cs @@ -0,0 +1,26 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Markup.Xaml; +using Avalonia.Styling; + +namespace Asv.Drones.Gui.Api.Design; + +public partial class App : Application +{ + public override void Initialize() + { + AvaloniaXamlLoader.Load(this); + RequestedThemeVariant = ThemeVariant.Dark; + } + + public override void OnFrameworkInitializationCompleted() + { + if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + { + desktop.MainWindow = new Window() { }; + } + + base.OnFrameworkInitializationCompleted(); + } +} \ No newline at end of file diff --git a/src/Asv.Drones.Gui.Api.Design/Asv.Drones.Gui.Api.Design.csproj b/src/Asv.Drones.Gui.Api.Design/Asv.Drones.Gui.Api.Design.csproj new file mode 100644 index 0000000..676ffe4 --- /dev/null +++ b/src/Asv.Drones.Gui.Api.Design/Asv.Drones.Gui.Api.Design.csproj @@ -0,0 +1,21 @@ + + + WinExe + net8.0 + enable + true + app.manifest + true + + + + + + + + + + + + + diff --git a/src/Asv.Drones.Gui.Api.Design/Program.cs b/src/Asv.Drones.Gui.Api.Design/Program.cs new file mode 100644 index 0000000..03836aa --- /dev/null +++ b/src/Asv.Drones.Gui.Api.Design/Program.cs @@ -0,0 +1,23 @@ +using Avalonia; +using System; +using Avalonia.ReactiveUI; + +namespace Asv.Drones.Gui.Api.Design; + +class Program +{ + // Initialization code. Don't use any Avalonia, third-party APIs or any + // SynchronizationContext-reliant code before AppMain is called: things aren't initialized + // yet and stuff might break. + [STAThread] + public static void Main(string[] args) => BuildAvaloniaApp() + .StartWithClassicDesktopLifetime(args); + + // Avalonia configuration, don't remove; also used by visual designer. + public static AppBuilder BuildAvaloniaApp() + => AppBuilder.Configure() + .UsePlatformDetect() + .WithInterFont() + .LogToTrace() + .UseReactiveUI(); +} \ No newline at end of file diff --git a/src/Asv.Drones.Gui.Api.Design/app.manifest b/src/Asv.Drones.Gui.Api.Design/app.manifest new file mode 100644 index 0000000..94d82de --- /dev/null +++ b/src/Asv.Drones.Gui.Api.Design/app.manifest @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + diff --git a/src/Asv.Drones.Gui.Api.sln b/src/Asv.Drones.Gui.Api.sln new file mode 100644 index 0000000..c79aeb3 --- /dev/null +++ b/src/Asv.Drones.Gui.Api.sln @@ -0,0 +1,30 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Asv.Drones.Gui.Api", "Asv.Drones.Gui.Api\Asv.Drones.Gui.Api.csproj", "{232EF70A-616D-47F2-9CA5-04A3D18F2C95}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solutions items", "Solutions items", "{A8CD2F07-B6AA-44CC-8548-CDC3AC6F35B3}" + ProjectSection(SolutionItems) = preProject + Directory.Build.props = Directory.Build.props + ..\.github\workflows\nuget_windows.yml = ..\.github\workflows\nuget_windows.yml + ..\.github\workflows\release-debug-version.yml = ..\.github\workflows\release-debug-version.yml + ..\.github\workflows\release-pre-version.yml = ..\.github\workflows\release-pre-version.yml + EndProjectSection +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Asv.Drones.Gui.Api.Design", "Asv.Drones.Gui.Api.Design\Asv.Drones.Gui.Api.Design.csproj", "{8019E3DE-D23C-4CB9-AC23-74B84D436A30}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {232EF70A-616D-47F2-9CA5-04A3D18F2C95}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {232EF70A-616D-47F2-9CA5-04A3D18F2C95}.Debug|Any CPU.Build.0 = Debug|Any CPU + {232EF70A-616D-47F2-9CA5-04A3D18F2C95}.Release|Any CPU.ActiveCfg = Release|Any CPU + {232EF70A-616D-47F2-9CA5-04A3D18F2C95}.Release|Any CPU.Build.0 = Release|Any CPU + {8019E3DE-D23C-4CB9-AC23-74B84D436A30}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8019E3DE-D23C-4CB9-AC23-74B84D436A30}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8019E3DE-D23C-4CB9-AC23-74B84D436A30}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8019E3DE-D23C-4CB9-AC23-74B84D436A30}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/src/Asv.Drones.Gui.Api/App.axaml b/src/Asv.Drones.Gui.Api/App.axaml new file mode 100644 index 0000000..efc3b28 --- /dev/null +++ b/src/Asv.Drones.Gui.Api/App.axaml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Asv.Drones.Gui.Api/Asv.Drones.Gui.Api.csproj b/src/Asv.Drones.Gui.Api/Asv.Drones.Gui.Api.csproj new file mode 100644 index 0000000..e807925 --- /dev/null +++ b/src/Asv.Drones.Gui.Api/Asv.Drones.Gui.Api.csproj @@ -0,0 +1,92 @@ + + + + net8.0 + enable + enable + $(ProductVersion) + $(ProductVersion) + $(ProductVersion) + true + https://github.com/asv-soft/asv-drones-gui-api + git + asv-drones;api;mavlink;drone;PX4;Ardupilot;.net + https://github.com/asv-soft/asv-drones-gui-api + + https://github.com/asv-soft + API reference for Asv.Drones GUI application + https://github.com/asv-soft + true + true + $(ProductPrevVersion) + + + + + + + + + + + + + + + + + + + + + + + all + + + + + PublicResXFileCodeGenerator + RS.Designer.cs + + + + + True + True + RS.resx + + + Code + AttitudeIndicator.axaml + + + TreePageExampleView.axaml + Code + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Asv.Drones.Gui.Api/Asv.Drones.Gui.Api.csproj.DotSettings b/src/Asv.Drones.Gui.Api/Asv.Drones.Gui.Api.csproj.DotSettings new file mode 100644 index 0000000..6df4da7 --- /dev/null +++ b/src/Asv.Drones.Gui.Api/Asv.Drones.Gui.Api.csproj.DotSettings @@ -0,0 +1,68 @@ + + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True \ No newline at end of file diff --git a/src/Asv.Drones.Gui.Api/CompatibilitySuppressions.xml b/src/Asv.Drones.Gui.Api/CompatibilitySuppressions.xml new file mode 100644 index 0000000..0e4b9f2 --- /dev/null +++ b/src/Asv.Drones.Gui.Api/CompatibilitySuppressions.xml @@ -0,0 +1,109 @@ + + + + + CP0001 + T:Asv.Drones.Gui.Api.LogMessageType + lib/net8.0/Asv.Drones.Gui.Api.dll + lib/net8.0/Asv.Drones.Gui.Api.dll + true + + + CP0002 + M:Asv.Drones.Gui.Api.LogItemViewModel.#ctor(System.Int32,System.String,Asv.Drones.Gui.Api.LogMessageType,System.DateTime,System.String,System.String) + lib/net8.0/Asv.Drones.Gui.Api.dll + lib/net8.0/Asv.Drones.Gui.Api.dll + true + + + CP0002 + M:Asv.Drones.Gui.Api.LogItemViewModel.get_Level + lib/net8.0/Asv.Drones.Gui.Api.dll + lib/net8.0/Asv.Drones.Gui.Api.dll + true + + + CP0002 + M:Asv.Drones.Gui.Api.LogItemViewModel.get_ThreadId + lib/net8.0/Asv.Drones.Gui.Api.dll + lib/net8.0/Asv.Drones.Gui.Api.dll + true + + + CP0002 + M:Asv.Drones.Gui.Api.LogItemViewModel.set_Message(System.String) + lib/net8.0/Asv.Drones.Gui.Api.dll + lib/net8.0/Asv.Drones.Gui.Api.dll + true + + + CP0002 + M:Asv.Drones.Gui.Api.LogMessage.#ctor(System.DateTime,Asv.Drones.Gui.Api.LogMessageType,System.String,System.String,System.String) + lib/net8.0/Asv.Drones.Gui.Api.dll + lib/net8.0/Asv.Drones.Gui.Api.dll + true + + + CP0002 + M:Asv.Drones.Gui.Api.LogMessage.get_DateTime + lib/net8.0/Asv.Drones.Gui.Api.dll + lib/net8.0/Asv.Drones.Gui.Api.dll + true + + + CP0002 + M:Asv.Drones.Gui.Api.LogMessage.get_Source + lib/net8.0/Asv.Drones.Gui.Api.dll + lib/net8.0/Asv.Drones.Gui.Api.dll + true + + + CP0002 + M:Asv.Drones.Gui.Api.LogMessage.get_Type + lib/net8.0/Asv.Drones.Gui.Api.dll + lib/net8.0/Asv.Drones.Gui.Api.dll + true + + + CP0006 + M:Asv.Drones.Gui.Api.IPluginManager.InstallManually(System.String,System.IProgress{Asv.Drones.Gui.Api.ProgressMessage},System.Threading.CancellationToken) + lib/net8.0/Asv.Drones.Gui.Api.dll + lib/net8.0/Asv.Drones.Gui.Api.dll + true + + + CP0006 + M:Asv.Drones.Gui.Api.IPluginManager.ListPluginVersions(Asv.Drones.Gui.Api.SearchQuery,System.String,System.Threading.CancellationToken) + lib/net8.0/Asv.Drones.Gui.Api.dll + lib/net8.0/Asv.Drones.Gui.Api.dll + true + + + CP0006 + P:Asv.Drones.Gui.Api.IApplicationHost.Dialogs + lib/net8.0/Asv.Drones.Gui.Api.dll + lib/net8.0/Asv.Drones.Gui.Api.dll + true + + + CP0006 + P:Asv.Drones.Gui.Api.ILocalPluginInfo.Icon + lib/net8.0/Asv.Drones.Gui.Api.dll + lib/net8.0/Asv.Drones.Gui.Api.dll + true + + + CP0006 + P:Asv.Drones.Gui.Api.IPluginSearchInfo.Dependencies + lib/net8.0/Asv.Drones.Gui.Api.dll + lib/net8.0/Asv.Drones.Gui.Api.dll + true + + + CP0006 + P:Asv.Drones.Gui.Api.IPluginSpecification.IsVerified + lib/net8.0/Asv.Drones.Gui.Api.dll + lib/net8.0/Asv.Drones.Gui.Api.dll + true + + \ No newline at end of file diff --git a/src/Asv.Drones.Gui.Api/FodyWeavers.xml b/src/Asv.Drones.Gui.Api/FodyWeavers.xml new file mode 100644 index 0000000..63fc148 --- /dev/null +++ b/src/Asv.Drones.Gui.Api/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/Asv.Drones.Gui.Api/RS.Designer.cs b/src/Asv.Drones.Gui.Api/RS.Designer.cs new file mode 100644 index 0000000..cf9ec6b --- /dev/null +++ b/src/Asv.Drones.Gui.Api/RS.Designer.cs @@ -0,0 +1,548 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Asv.Drones.Gui.Api { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + public class RS { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal RS() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + public static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Asv.Drones.Gui.Api.RS", typeof(RS).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + public static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to Vibration and clipping of UAV. + /// + public static string AltitudeIndicator_Vibration_ToolTip { + get { + return ResourceManager.GetString("AltitudeIndicator_Vibration_ToolTip", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Actions. + /// + public static string AnchorsEditorView_TextBlock_Actions { + get { + return ResourceManager.GetString("AnchorsEditorView_TextBlock_Actions", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Altitude. + /// + public static string AnchorsEditorView_TextBlock_Altitude_Text { + get { + return ResourceManager.GetString("AnchorsEditorView_TextBlock_Altitude_Text", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Latitude. + /// + public static string AnchorsEditorView_TextBlock_Latitude_Text { + get { + return ResourceManager.GetString("AnchorsEditorView_TextBlock_Latitude_Text", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Longitude. + /// + public static string AnchorsEditorView_TextBlock_Longitude_Text { + get { + return ResourceManager.GetString("AnchorsEditorView_TextBlock_Longitude_Text", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Anchor Editor. + /// + public static string AnchorsEditorViewModel_Title { + get { + return ResourceManager.GetString("AnchorsEditorViewModel_Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Altitude. + /// + public static string AttitudeIndicator_Altitude_ToolTip { + get { + return ResourceManager.GetString("AttitudeIndicator_Altitude_ToolTip", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Compass. + /// + public static string AttitudeIndicator_Compass_ToolTip { + get { + return ResourceManager.GetString("AttitudeIndicator_Compass_ToolTip", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Velocity. + /// + public static string AttitudeIndicator_Velocity_ToolTip { + get { + return ResourceManager.GetString("AttitudeIndicator_Velocity_ToolTip", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Value must be a number. + /// + public static string DistanceMeasureUnit_ErrorMessage_NotANumber { + get { + return ResourceManager.GetString("DistanceMeasureUnit_ErrorMessage_NotANumber", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Invalid value. + /// + public static string DistanceMeasureUnit_ErrorMessage_NotValid { + get { + return ResourceManager.GetString("DistanceMeasureUnit_ErrorMessage_NotValid", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Value can't be null or white space. + /// + public static string DistanceMeasureUnit_ErrorMessage_NullOrWhiteSpace { + get { + return ResourceManager.GetString("DistanceMeasureUnit_ErrorMessage_NullOrWhiteSpace", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Rename. + /// + public static string HierarchicalStoreView_Button_EditFileName { + get { + return ResourceManager.GetString("HierarchicalStoreView_Button_EditFileName", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Create New File. + /// + public static string HierarchicalStoreView_Button_ToolTip_CreateNewFile { + get { + return ResourceManager.GetString("HierarchicalStoreView_Button_ToolTip_CreateNewFile", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Create New Folder. + /// + public static string HierarchicalStoreView_Button_ToolTip_CreateNewFolder { + get { + return ResourceManager.GetString("HierarchicalStoreView_Button_ToolTip_CreateNewFolder", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Refresh . + /// + public static string HierarchicalStoreView_Button_ToolTip_Refresh { + get { + return ResourceManager.GetString("HierarchicalStoreView_Button_ToolTip_Refresh", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Delete File. + /// + public static string HierarchicalStoreView_File_Button_DeleteFile { + get { + return ResourceManager.GetString("HierarchicalStoreView_File_Button_DeleteFile", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Move File. + /// + public static string HierarchicalStoreView_File_Button_MoveFile { + get { + return ResourceManager.GetString("HierarchicalStoreView_File_Button_MoveFile", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Delete Folder. + /// + public static string HierarchicalStoreView_Folder_Button_DeleteFolder { + get { + return ResourceManager.GetString("HierarchicalStoreView_Folder_Button_DeleteFolder", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Rename Folder. + /// + public static string HierarchicalStoreView_Folder_Button_EditFolderName { + get { + return ResourceManager.GetString("HierarchicalStoreView_Folder_Button_EditFolderName", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Move Folder. + /// + public static string HierarchicalStoreView_Folder_Button_MoveFolderToFolder { + get { + return ResourceManager.GetString("HierarchicalStoreView_Folder_Button_MoveFolderToFolder", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Cancel. + /// + public static string HierarchicalStoreView_TextBlock_Text_Cancel { + get { + return ResourceManager.GetString("HierarchicalStoreView_TextBlock_Text_Cancel", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Move Folder. + /// + public static string HierarchicalStoreView_TextBlock_Text_MoveFolder { + get { + return ResourceManager.GetString("HierarchicalStoreView_TextBlock_Text_MoveFolder", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Move Here. + /// + public static string HierarchicalStoreView_TextBlock_Text_MoveHere { + get { + return ResourceManager.GetString("HierarchicalStoreView_TextBlock_Text_MoveHere", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Search. + /// + public static string HierarhicalStoreView_Search_Textbox_Watermark { + get { + return ResourceManager.GetString("HierarhicalStoreView_Search_Textbox_Watermark", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Move anchors. + /// + public static string MapMoverActionView_Title { + get { + return ResourceManager.GetString("MapMoverActionView_Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Fixed wing. + /// + public static string MavlinkHelper_GetTypeName_FixedWing { + get { + return ResourceManager.GetString("MavlinkHelper_GetTypeName_FixedWing", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Helicopter. + /// + public static string MavlinkHelper_GetTypeName_Helicopter { + get { + return ResourceManager.GetString("MavlinkHelper_GetTypeName_Helicopter", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Hexarotor. + /// + public static string MavlinkHelper_GetTypeName_HexaRotor { + get { + return ResourceManager.GetString("MavlinkHelper_GetTypeName_HexaRotor", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Octorotor. + /// + public static string MavlinkHelper_GetTypeName_OctoRotor { + get { + return ResourceManager.GetString("MavlinkHelper_GetTypeName_OctoRotor", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Quadrotor. + /// + public static string MavlinkHelper_GetTypeName_QuadRotor { + get { + return ResourceManager.GetString("MavlinkHelper_GetTypeName_QuadRotor", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Tricopter. + /// + public static string MavlinkHelper_GetTypeName_TriCopter { + get { + return ResourceManager.GetString("MavlinkHelper_GetTypeName_TriCopter", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Unknown type. + /// + public static string MavlinkHelper_GetTypeName_UnknownType { + get { + return ResourceManager.GetString("MavlinkHelper_GetTypeName_UnknownType", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Value must be a number. + /// + public static string MeasureUnitBase_ErrorMessage_NotANumber { + get { + return ResourceManager.GetString("MeasureUnitBase_ErrorMessage_NotANumber", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Value can't be null or white space. + /// + public static string MeasureUnitBase_ErrorMessage_NullOrWhiteSpace { + get { + return ResourceManager.GetString("MeasureUnitBase_ErrorMessage_NullOrWhiteSpace", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Value must be greater than {0} ({1}). + /// + public static string MeasureUnitExtensions_ErrorMessage_GreaterValue { + get { + return ResourceManager.GetString("MeasureUnitExtensions_ErrorMessage_GreaterValue", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Value must be less than {0} ({1}). + /// + public static string MeasureUnitExtensions_ErrorMessage_LesserValue { + get { + return ResourceManager.GetString("MeasureUnitExtensions_ErrorMessage_LesserValue", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Remove all pinned parameters. + /// + public static string ParametersEditorPageView_PinsOffButton_ToolTip { + get { + return ResourceManager.GetString("ParametersEditorPageView_PinsOffButton_ToolTip", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Star this parameter. + /// + public static string ParametersEditorPageView_StarButton_ToolTip { + get { + return ResourceManager.GetString("ParametersEditorPageView_StarButton_ToolTip", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Show only starred parameters. + /// + public static string ParametersEditorPageView_StarsToggleButton_ToolTip { + get { + return ResourceManager.GetString("ParametersEditorPageView_StarsToggleButton_ToolTip", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Update all parameters. + /// + public static string ParametersEditorPageView_UpdateButton_ToolTip { + get { + return ResourceManager.GetString("ParametersEditorPageView_UpdateButton_ToolTip", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Search. + /// + public static string ParametersEditorPageViewModel_Search { + get { + return ResourceManager.GetString("ParametersEditorPageViewModel_Search", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Total: {0}. + /// + public static string ParametersEditorPageViewModel_Total { + get { + return ResourceManager.GetString("ParametersEditorPageViewModel_Total", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to On/off pin for this parameter. + /// + public static string ParametersEditorParameterView_PinToggleButton_ToolTip { + get { + return ResourceManager.GetString("ParametersEditorParameterView_PinToggleButton_ToolTip", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Reboot required. + /// + public static string ParametersEditorParameterView_RebootRequired { + get { + return ResourceManager.GetString("ParametersEditorParameterView_RebootRequired", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Update. + /// + public static string ParametersEditorParameterView_UpdateButton { + get { + return ResourceManager.GetString("ParametersEditorParameterView_UpdateButton", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Update this parameter from UAV. + /// + public static string ParametersEditorParameterView_UpdateButton_ToolTip { + get { + return ResourceManager.GetString("ParametersEditorParameterView_UpdateButton_ToolTip", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Write. + /// + public static string ParametersEditorParameterView_WriteButton { + get { + return ResourceManager.GetString("ParametersEditorParameterView_WriteButton", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Write this parameter to UAV. + /// + public static string ParametersEditorParameterView_WriteButton_ToolTip { + get { + return ResourceManager.GetString("ParametersEditorParameterView_WriteButton_ToolTip", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Cancel. + /// + public static string ParamPageViewModel_DataLossDialog_CloseButtonText { + get { + return ResourceManager.GetString("ParamPageViewModel_DataLossDialog_CloseButtonText", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You're trying to open another menu folder, but you have unsaved changes in the current one. Do you want to save them?. + /// + public static string ParamPageViewModel_DataLossDialog_Content { + get { + return ResourceManager.GetString("ParamPageViewModel_DataLossDialog_Content", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Save. + /// + public static string ParamPageViewModel_DataLossDialog_PrimaryButtonText { + get { + return ResourceManager.GetString("ParamPageViewModel_DataLossDialog_PrimaryButtonText", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Don't save. + /// + public static string ParamPageViewModel_DataLossDialog_SecondaryButtonText { + get { + return ResourceManager.GetString("ParamPageViewModel_DataLossDialog_SecondaryButtonText", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Potential data loss warning. + /// + public static string ParamPageViewModel_DataLossDialog_Title { + get { + return ResourceManager.GetString("ParamPageViewModel_DataLossDialog_Title", resourceCulture); + } + } + } +} diff --git a/src/Asv.Drones.Gui.Api/RS.resx b/src/Asv.Drones.Gui.Api/RS.resx new file mode 100644 index 0000000..78b6fec --- /dev/null +++ b/src/Asv.Drones.Gui.Api/RS.resx @@ -0,0 +1,190 @@ + + + + + + + + + + text/microsoft-resx + + + 1.3 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, + PublicKeyToken=b77a5c561934e089 + + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, + PublicKeyToken=b77a5c561934e089 + + + + Value can't be null or white space + + + Value must be a number + + + Value must be greater than {0} ({1}) + + + Value must be less than {0} ({1}) + + + + + Move anchors + + + Latitude + + + Altitude + + + Longitude + + + Vibration and clipping of UAV + + + Velocity + + + Altitude + + + Compass + + + Refresh + + + Create New Folder + + + Create New File + + + Rename Folder + + + Move Folder + + + Delete Folder + + + Delete File + + + Rename + + + Move File + + + Search + + + Move Folder + + + Move Here + + + Cancel + + + On/off pin for this parameter + + + Update this parameter from UAV + + + Write this parameter to UAV + + + Reboot required + + + Update + + + Write + + + Potential data loss warning + + + You're trying to open another menu folder, but you have unsaved changes in the current one. Do you want to save them? + + + Save + + + Don't save + + + Cancel + + + Show only starred parameters + + + Update all parameters + + + Remove all pinned parameters + + + Search + + + Total: {0} + + + Star this parameter + + + Fixed wing + + + Quadrotor + + + Hexarotor + + + Octorotor + + + Helicopter + + + Tricopter + + + Unknown type + + + Actions + + + Anchor Editor + + + Value can't be null or white space + + + Value must be a number + + + Invalid value + + \ No newline at end of file diff --git a/src/Asv.Drones.Gui.Api/RS.ru.resx b/src/Asv.Drones.Gui.Api/RS.ru.resx new file mode 100644 index 0000000..4f26940 --- /dev/null +++ b/src/Asv.Drones.Gui.Api/RS.ru.resx @@ -0,0 +1,186 @@ + + + text/microsoft-resx + + + 1.3 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, + PublicKeyToken=b77a5c561934e089 + + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, + PublicKeyToken=b77a5c561934e089 + + + + sdsd + + + Значение не должно быть пустым или пробелом + + + Значение должно быть числом + + + Значение должно быть больше чем {0} ({1}) + + + Значение должно быть меньше чем {0} ({1}) + + + + + Переместить якоря + + + Широта + + + Высота + + + Долгота + + + Вибрация и клиппинг БПЛА + + + Скорость + + + Высота + + + Компас + + + Обновить + + + Переименовать + + + Переместить папку + + + Переименовать папку + + + Удалить файл + + + Переместить файл + + + Удалить папку + + + Поиск + + + Переместить папку + + + Переместить сюда + + + Отмена + + + Включить/выключить закрепление этого параметра + + + Обновить этот парметр из БПЛА + + + Записать этот парметр в БПЛА + + + Требуется перезагрузка + + + Обновить + + + Записать + + + Отмена + + + Вы пытаетесь открыть другой пункт меню, но в текущем у вас есть несохраненные изменения. Хотите сохранить их? + + + Сохранить + + + Не сохранять + + + Предупреждение о возможной потере данных + + + Показать только избранные параметры + + + Обновить все параметры + + + Убрать все прикреплённые параметры + + + + Добавить параметр в избранные + + + Найти + + + Всего: {0} + + + Создать файл + + + Создать папку + + + Неподвижное крыло + + + Вертолёт + + + Гексакоптер + + + Октокоптер + + + Квадрокоптер + + + Трикоптер + + + Неизвестный тип + + + Действия + + + Редактор якорей + + + Значение не должно быть пустым или пробелом + + + Значение должно быть числом + + + Недопустимое значение + + \ No newline at end of file diff --git a/src/Asv.Drones.Gui.Api/Services/AppHost/IAppArgs.cs b/src/Asv.Drones.Gui.Api/Services/AppHost/IAppArgs.cs new file mode 100644 index 0000000..8ab4986 --- /dev/null +++ b/src/Asv.Drones.Gui.Api/Services/AppHost/IAppArgs.cs @@ -0,0 +1,12 @@ +namespace Asv.Drones.Gui.Api; + +public interface IAppArgs +{ + IReadOnlyDictionary Args { get; } + IReadOnlySet Tags { get; } + + bool TryParse(IEnumerable args); + bool TryParseFile(string argsFile = "app.args"); + + string this[string key, string defaultValue] { get; } +} \ No newline at end of file diff --git a/src/Asv.Drones.Gui.Api/Services/AppHost/IAppInfo.cs b/src/Asv.Drones.Gui.Api/Services/AppHost/IAppInfo.cs new file mode 100644 index 0000000..b170420 --- /dev/null +++ b/src/Asv.Drones.Gui.Api/Services/AppHost/IAppInfo.cs @@ -0,0 +1,34 @@ +namespace Asv.Drones.Gui.Api; + +public interface IAppInfo +{ + /// + /// Application title + /// + string Name { get; } + + /// + /// Application version + /// + string Version { get; } + + /// + /// Authors + /// + string Author { get; } + + /// + /// Application home page URL + /// + string AppUrl { get; } + + /// + /// Licence name + /// + string AppLicense { get; } + + /// + /// Avalonia UI package version + /// + string AvaloniaVersion { get; } +} \ No newline at end of file diff --git a/src/Asv.Drones.Gui.Api/Services/AppHost/IAppPathInfo.cs b/src/Asv.Drones.Gui.Api/Services/AppHost/IAppPathInfo.cs new file mode 100644 index 0000000..a775b62 --- /dev/null +++ b/src/Asv.Drones.Gui.Api/Services/AppHost/IAppPathInfo.cs @@ -0,0 +1,15 @@ +namespace Asv.Drones.Gui.Api; + +/// +/// Information about the application's path +/// +public interface IAppPathInfo +{ + /// + /// The folder where the application stores its data. + /// This is the folder where the application stores its data, such as configuration files, logs, and plugins. + /// Folder is created by the application if it does not exist. + /// The folder is created in the user's home directory and will not be deleted when the application is uninstalled or updated. + /// + string AppDataFolder { get; } +} \ No newline at end of file diff --git a/src/Asv.Drones.Gui.Api/Services/AppHost/IApplicationHost.cs b/src/Asv.Drones.Gui.Api/Services/AppHost/IApplicationHost.cs new file mode 100644 index 0000000..53933fc --- /dev/null +++ b/src/Asv.Drones.Gui.Api/Services/AppHost/IApplicationHost.cs @@ -0,0 +1,78 @@ +using System.Composition.Hosting; +using Asv.Cfg; +using Asv.Common; +using Avalonia.Controls.Templates; + +namespace Asv.Drones.Gui.Api; + +/// +/// Application host +/// +public interface IApplicationHost +{ + /// + /// Application args + /// + IAppArgs Args { get; } + /// + /// Base information about current application + /// + IAppInfo Info { get; } + + /// + /// Path helper + /// + IAppPathInfo Paths { get; } + + /// + /// Host for add data templates + /// + IDataTemplateHost DataTemplateHost { get; } + + /// + /// Configuration of the application + /// + IConfiguration Configuration { get; } + + ILocalizationService Localization { get; } + ILogService Logs { get; } + IPluginManager PluginManager { get; } + + /// + /// IoC container + /// + CompositionHost Container { get; } + + /// + /// Gets an enumerable collection of theme items. + /// + /// + /// An enumerable collection of theme items. + /// + IEnumerable Themes { get; } + + /// + /// Gets the current theme of the application. + /// + /// + /// This property returns an instance of which represents the current theme. + /// + /// + /// An instance representing the current theme of the application. + /// + IRxEditableValue CurrentTheme { get; } + /// + /// Main application view. Can be NULL! before main activity is loading + /// + IShell? Shell { get; } + /// + /// Base class for all user dialogs + /// Can be NULL! before main activity is loading + /// + IDialogService? Dialogs { get; } + + /// + /// Try to restart application + /// + void RestartApplication(); +} \ No newline at end of file diff --git a/src/Asv.Drones.Gui.Api/Services/AppHost/IThemeInfo.cs b/src/Asv.Drones.Gui.Api/Services/AppHost/IThemeInfo.cs new file mode 100644 index 0000000..4039c3f --- /dev/null +++ b/src/Asv.Drones.Gui.Api/Services/AppHost/IThemeInfo.cs @@ -0,0 +1,10 @@ +namespace Asv.Drones.Gui.Api; + +/// +/// Represents a theme item. +/// +public interface IThemeInfo +{ + string Id { get; } + string Name { get; } +} \ No newline at end of file diff --git a/src/Asv.Drones.Gui.Api/Services/DialogService/IDialogService.cs b/src/Asv.Drones.Gui.Api/Services/DialogService/IDialogService.cs new file mode 100644 index 0000000..2a033bb --- /dev/null +++ b/src/Asv.Drones.Gui.Api/Services/DialogService/IDialogService.cs @@ -0,0 +1,45 @@ +namespace Asv.Drones.Gui.Api; + +public interface IDialogService +{ + public bool IsImplementedShowOpenFileDialog { get; } + public bool IsImplementedShowSaveFileDialog { get; } + public bool IsImplementedShowSelectFolderDialog { get; } + public bool IsImplementedShowObserveFolderDialog { get; } + + /// + /// Opens dialog to choose a file + /// + /// caption of the dialog + /// extension filter, example: "txt, *, nupkg" + /// directory where to start search + /// + public Task ShowOpenFileDialog(string title, string? typeFilter = null, string? initialDirectory = null); + + /// + /// Opens dialog to save a file + /// + /// caption of the dialog + /// default extension of the file + /// extension filter, example: "txt, *, nupkg" + /// directory where to start search + /// + public Task ShowSaveFileDialog(string title, string? defaultExt = null, string? typeFilter = null, string? initialDirectory = null); + + /// + /// Opens dialog to select a folder + /// + /// caption of the dialog + /// default path + /// + public Task ShowSelectFolderDialog(string title, string? oldPath = null); + + /// + /// Opens dialog to observe a folder + /// + /// caption of the dialog + /// default path + /// + public Task ShowObserveFolderDialog(string title, string? defaultPath); + +} \ No newline at end of file diff --git a/src/Asv.Drones.Gui.Api/Services/Localization/ILocalizationService.cs b/src/Asv.Drones.Gui.Api/Services/Localization/ILocalizationService.cs new file mode 100644 index 0000000..d9ac76c --- /dev/null +++ b/src/Asv.Drones.Gui.Api/Services/Localization/ILocalizationService.cs @@ -0,0 +1,182 @@ +using Asv.Common; + +namespace Asv.Drones.Gui.Api +{ + public interface ILocalizationService + { + /// + /// Allows you to select or get the current application language + /// + IRxEditableValue CurrentLanguage { get; } + + /// + /// Returns the list of available languages + /// + IEnumerable AvailableLanguages { get; } + + #region Units + + /// + /// Convert bytes rate to short localized string + /// For example: 1024 => 1 KB/s + /// + /// + IReadOnlyMeasureUnit ByteRate { get; } + + /// + /// Convert items rate to short localized string + /// For example: 1000 => 1 KHz + /// + /// + IReadOnlyMeasureUnit ItemsRate { get; } + + /// + /// Convert bytes count to short localized string + /// For example: 1024 => 1 KB + /// + /// + IReadOnlyMeasureUnit ByteSize { get; } + + IReadOnlyMeasureUnit RelativeTime { get; } + + IReadOnlyMeasureUnit Voltage { get; } + + IReadOnlyMeasureUnit Current { get; } + IReadOnlyMeasureUnit MAh { get; } + + IMeasureUnit Altitude { get; } + + IMeasureUnit Distance { get; } + + IMeasureUnit Accuracy { get; } // field for gbs plugin only + + IMeasureUnit Latitude { get; } + IMeasureUnit Longitude { get; } + + IMeasureUnit Velocity { get; } + + IMeasureUnit DdmLlz { get; } + IMeasureUnit DdmGp { get; } + + IMeasureUnit Sdm { get; } + + IMeasureUnit Power { get; } + IMeasureUnit AmplitudeModulation { get; } + IMeasureUnit Frequency { get; } + IMeasureUnit Phase { get; } + IMeasureUnit Bearing { get; } + IMeasureUnit Temperature { get; } + IMeasureUnit Degree { get; } + IMeasureUnit FieldStrength { get; } + + #endregion + + public GeoPoint ToSiGeoPoint(string? latitude, string? longitude, string? altitude) + { + var lat = Latitude.IsValid(latitude) ? Latitude.ConvertToSi(latitude) : double.NaN; + var lon = Longitude.IsValid(longitude) ? Longitude.ConvertToSi(longitude) : double.NaN; + var alt = Altitude.IsValid(altitude) ? Altitude.ConvertToSi(altitude) : double.NaN; + return new GeoPoint(lat, lon, alt); + } + } + + public interface ILanguageInfo + { + string Id { get; } + string DisplayName { get; } + } + + public enum AltitudeUnits + { + Meters, + Feets + } + + public enum AmplitudeModulationUnits + { + Percent, + InParts + } + + public enum BearingUnits + { + Degree, + DegreesMinutes + } + + public enum DdmUnits + { + InParts, + Percent, + MicroAmp, + MicroAmpRu + } + + public enum DegreeUnits + { + Degrees, + MinutesSeconds, + DegreesMinutesSeconds + } + + public enum DistanceUnits + { + Meters, + NauticalMiles + } + + public enum FieldStrengthUnits + { + MicroVoltsPerMeter + } + + public enum FrequencyUnits + { + Hz, + KHz, + MHz, + GHz + } + + public enum LatitudeUnits + { + Deg, + Dms + } + + public enum LongitudeUnits + { + Deg, + Dms + } + + public enum PhaseUnits + { + Degree, + Radian + } + + public enum PowerUnits + { + Dbm + } + + public enum SdmUnits + { + Percent + } + + public enum TemperatureUnits + { + Celsius, + Farenheit, + Kelvin + } + + public enum VelocityUnits + { + MetersPerSecond, + KilometersPerHour, + MilesPerHour + } +} \ No newline at end of file diff --git a/src/Asv.Drones.Gui.Api/Services/Localization/IMeasureUnit.cs b/src/Asv.Drones.Gui.Api/Services/Localization/IMeasureUnit.cs new file mode 100644 index 0000000..bb75e72 --- /dev/null +++ b/src/Asv.Drones.Gui.Api/Services/Localization/IMeasureUnit.cs @@ -0,0 +1,147 @@ +#nullable enable +using System.Globalization; +using Asv.Common; +using Avalonia; +using Avalonia.Data.Converters; + +namespace Asv.Drones.Gui.Api +{ + public interface IMeasureUnitItem + { + public string Title { get; } + public string Unit { get; } + public bool IsSiUnit { get; } + public TValue ConvertFromSi(TValue siValue); + public TValue ConvertToSi(TValue value); + public TValue Parse(string? value); + bool IsValid(string? value); + string GetErrorMessage(string? value); + string Print(TValue value); + string PrintWithUnits(TValue value); + + public TValue ConvertToSi(string? value) + { + return ConvertToSi(Parse(value)); + } + + public string FromSiToString(TValue value) + { + return Print(ConvertFromSi(value)); + } + + public string FromSiToStringWithUnits(TValue value) + { + return PrintWithUnits(ConvertFromSi(value)); + } + } + + public interface IMeasureUnitItem : IMeasureUnitItem + { + public TEnum Id { get; } + } + + public interface IMeasureUnit + { + string Title { get; } + string Description { get; } + IEnumerable> AvailableUnits { get; } + IRxEditableValue> CurrentUnit { get; } + IMeasureUnitItem SiUnit { get; } + + public string FromSiToStringWithUnits(TValue value) + { + return CurrentUnit.Value.FromSiToStringWithUnits(value); + } + + public string FromSiToString(TValue value) + { + return CurrentUnit.Value.FromSiToString(value); + } + + public TValue ConvertFromSi(TValue value) + { + return CurrentUnit.Value.ConvertFromSi(value); + } + + public TValue ConvertToSi(TValue value) + { + return CurrentUnit.Value.ConvertToSi(value); + } + + public TValue ConvertToSi(string? value) + { + return CurrentUnit.Value.ConvertToSi(value); + } + + public bool IsValid(string? value) + { + return CurrentUnit.Value.IsValid(value); + } + } + + public static class MeasureUnitExtensions + { + private const string DefaultErrorMessage = "Something went wrong"; + + public static bool IsValid(this IMeasureUnit src, double minSiValue, double maxSiValue, + string value) + { + if (src.CurrentUnit.Value.IsValid(value) == false) return false; + if (src.CurrentUnit.Value.ConvertToSi(value) < minSiValue) return false; + if (src.CurrentUnit.Value.ConvertToSi(value) > maxSiValue) return false; + return true; + } + + public static string GetErrorMessage(this IMeasureUnit src, string? value) + { + return src.CurrentUnit.Value.GetErrorMessage(value); + } + + public static string GetErrorMessage(this IMeasureUnit src, double minSiValue, + double maxSiValue, string? value) + { + var msg = src.CurrentUnit.Value.GetErrorMessage(value); + if (string.IsNullOrWhiteSpace(msg) == false) return msg; + var siValue = src.CurrentUnit.Value.ConvertToSi(value); + if (siValue < minSiValue) + return string.Format(RS.MeasureUnitExtensions_ErrorMessage_GreaterValue, + src.CurrentUnit.Value.FromSiToStringWithUnits(minSiValue), + src.SiUnit.FromSiToStringWithUnits(siValue)); + if (siValue > maxSiValue) + return string.Format(RS.MeasureUnitExtensions_ErrorMessage_LesserValue, + src.CurrentUnit.Value.FromSiToStringWithUnits(minSiValue), + src.SiUnit.FromSiToStringWithUnits(siValue)); + return DefaultErrorMessage; + } + } + + + public static class MeasureUnitConverter + { + static MeasureUnitConverter() + { + DoubleInstance = new MeasureUnitConverter(); + UlongInstance = new MeasureUnitConverter(); + } + + public static MeasureUnitConverter UlongInstance { get; set; } + public static MeasureUnitConverter DoubleInstance { get; } + } + + + public class MeasureUnitConverter : IMultiValueConverter + { + public object? Convert(IList values, Type targetType, object? parameter, CultureInfo culture) + { + if (values.Count == 1) + return System.Convert.ChangeType(values[0], targetType, culture); + if (values is [_, IMeasureUnitItem measureUnit, ..]) + { + var value = (TValue)System.Convert.ChangeType(values[0], typeof(TValue), culture)!; + return measureUnit.Print(value); + } + + return AvaloniaProperty.UnsetValue; + } + } +} \ No newline at end of file diff --git a/src/Asv.Drones.Gui.Api/Services/Localization/IReadOnlyMeasureUnit.cs b/src/Asv.Drones.Gui.Api/Services/Localization/IReadOnlyMeasureUnit.cs new file mode 100644 index 0000000..876aa55 --- /dev/null +++ b/src/Asv.Drones.Gui.Api/Services/Localization/IReadOnlyMeasureUnit.cs @@ -0,0 +1,15 @@ +namespace Asv.Drones.Gui.Api; + +public interface IReadOnlyMeasureUnit +{ + string? GetUnit(TValue value); + string ConvertToString(TValue value); +} + +public static class ReadOnlyMeasureUnitExtensions +{ + public static string ConvertToStringWithUnits(this IReadOnlyMeasureUnit src, TValue value) + { + return src.ConvertToString(value) + src.GetUnit(value); + } +} \ No newline at end of file diff --git a/src/Asv.Drones.Gui.Api/Services/Localization/LocalizationHelper.cs b/src/Asv.Drones.Gui.Api/Services/Localization/LocalizationHelper.cs new file mode 100644 index 0000000..c057d9c --- /dev/null +++ b/src/Asv.Drones.Gui.Api/Services/Localization/LocalizationHelper.cs @@ -0,0 +1,48 @@ +using System.Linq.Expressions; +using System.Reactive.Linq; +using DynamicData.Binding; +using ReactiveUI; +using ReactiveUI.Validation.Abstractions; +using ReactiveUI.Validation.Extensions; +using ReactiveUI.Validation.Helpers; + +namespace Asv.Drones.Gui.Api; + +public static class LocalizationHelper +{ + public static IDisposable BindMeasureUnit(this TObject source, + Expression> valuePropertyAccessor, Action setter, + Expression> stringPropertyAccessor, Action stringSetter, + IMeasureUnitItem measureUnit) + where TObject : IReactiveObject, IValidatableViewModel + { + return new MeasureUnitBind(source, valuePropertyAccessor, setter, stringPropertyAccessor, + stringSetter, measureUnit); + } + + class MeasureUnitBind : IDisposable + where TObject : IReactiveObject, IValidatableViewModel + { + private readonly ValidationHelper _sub1; + private readonly IDisposable _sub2; + private readonly IDisposable _sub3; + + public MeasureUnitBind(TObject source, + Expression> valueProperty, Action setter, + Expression> stringProperty, Action stringSetter, + IMeasureUnitItem measureUnit) + { + _sub1 = source.ValidationRule(stringProperty, measureUnit.IsValid, + x => measureUnit.GetErrorMessage(x) ?? string.Empty); + _sub2 = source.WhenValueChanged(valueProperty).Select(measureUnit.FromSiToString!).Subscribe(stringSetter); + _sub3 = source.WhenValueChanged(stringProperty).Select(measureUnit.ConvertToSi).Subscribe(setter); + } + + public void Dispose() + { + _sub1.Dispose(); + _sub2.Dispose(); + _sub3.Dispose(); + } + } +} \ No newline at end of file diff --git a/src/Asv.Drones.Gui.Api/Services/Localization/MeasureUnitBase.cs b/src/Asv.Drones.Gui.Api/Services/Localization/MeasureUnitBase.cs new file mode 100644 index 0000000..64fdbf9 --- /dev/null +++ b/src/Asv.Drones.Gui.Api/Services/Localization/MeasureUnitBase.cs @@ -0,0 +1,99 @@ +using System.Diagnostics; +using System.Globalization; +using System.Reactive.Linq; +using Asv.Cfg; +using Asv.Common; + +namespace Asv.Drones.Gui.Api; + +public abstract class MeasureUnitBase : DisposableOnceWithCancel, IMeasureUnit +{ + protected MeasureUnitBase(IConfiguration cfgSvc, string cfgKey, IMeasureUnitItem[] items) + { + if (cfgSvc == null) throw new ArgumentNullException(nameof(cfgSvc)); + if (cfgKey == null) throw new ArgumentNullException(nameof(cfgKey)); + AvailableUnits = items ?? throw new ArgumentNullException(nameof(items)); + var id = cfgSvc.Get(cfgKey, default(TEnum)); + SiUnit = AvailableUnits.First(_ => _.IsSiUnit); + var item = AvailableUnits.FirstOrDefault(x => + { + Debug.Assert(x.Id != null, "x.Id != null"); + return x.Id.Equals(id); + }) ?? SiUnit; + CurrentUnit = new RxValue>(item).DisposeItWith(Disposable); + CurrentUnit.DistinctUntilChanged(_ => _.Id) + .Subscribe(_ => { cfgSvc.Set(cfgKey, _.Id); }) + .DisposeItWith(Disposable); + } + + public abstract string Title { get; } + public abstract string Description { get; } + public IEnumerable> AvailableUnits { get; } + public IRxEditableValue> CurrentUnit { get; } + public IMeasureUnitItem SiUnit { get; } +} + +public class DoubleMeasureUnitItem : IMeasureUnitItem +{ + private readonly string _formatString; + private readonly double _multiplierCoef; + + public DoubleMeasureUnitItem(TEnum id, string title, string unit, bool isSiUnit, string formatString, + double multiplierCoef) + { + _formatString = formatString; + _multiplierCoef = multiplierCoef; + Id = id; + Title = title; + Unit = unit; + IsSiUnit = isSiUnit; + } + + public TEnum Id { get; } + public string Title { get; } + public string Unit { get; } + public bool IsSiUnit { get; } + + public virtual double ConvertFromSi(double siValue) + { + return siValue / _multiplierCoef; + } + + public virtual double ConvertToSi(double value) + { + return value * _multiplierCoef; + } + + public double Parse(string? value) + { + if (string.IsNullOrWhiteSpace(value)) return double.NaN; + value = value.Replace(',', '.'); + return double.Parse(value, NumberStyles.Any, CultureInfo.InvariantCulture); + } + + public string Print(double value) + { + return value.ToString(_formatString, CultureInfo.InvariantCulture); + } + + public string PrintWithUnits(double value) + { + return $"{value.ToString(_formatString, CultureInfo.InvariantCulture)} {Unit}"; + } + + public virtual bool IsValid(string? value) + { + if (string.IsNullOrWhiteSpace(value)) return false; + value = value.Replace(',', '.'); + return double.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var _); + } + + public virtual string? GetErrorMessage(string? value) + { + if (value.IsNullOrWhiteSpace()) return RS.MeasureUnitBase_ErrorMessage_NullOrWhiteSpace; + value = value.Replace(',', '.'); + return double.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out _) == false + ? RS.MeasureUnitBase_ErrorMessage_NotANumber + : null; + } +} \ No newline at end of file diff --git a/src/Asv.Drones.Gui.Api/Services/LogService/ILogService.cs b/src/Asv.Drones.Gui.Api/Services/LogService/ILogService.cs new file mode 100644 index 0000000..5718fc0 --- /dev/null +++ b/src/Asv.Drones.Gui.Api/Services/LogService/ILogService.cs @@ -0,0 +1,74 @@ +using Microsoft.Extensions.Logging; +using ReactiveUI; + +namespace Asv.Drones.Gui.Api; + +public static class LogHelper +{ + public static IDisposable CatchToLog(this ReactiveCommand cmd, ILogService log, string sender) + { + return cmd.ThrownExceptions.Subscribe(ex=>log.Error(sender, ex.Message,ex)); + } +} + + +public interface ILogService: ILoggerFactory +{ + IObservable OnMessage { get; } + void SaveMessage(LogMessage logMessage); + IEnumerable LoadItemsFromLogFile(); + void DeleteLogFile(); + + public IDisposable CatchToLog( ReactiveCommand cmd, string sender) + { + return cmd.ThrownExceptions.Subscribe(ex=>Error(sender, ex.Message,ex)); + } + + public void Fatal(string sender, string message, + Exception? ex = default) + { + SaveMessage(new LogMessage(DateTime.Now, LogLevel.Critical, sender, message, ex?.Message)); + } + + public void Error(string sender, string message, + Exception? ex = default) + { + SaveMessage(new LogMessage(DateTime.Now, LogLevel.Error, sender, message, ex?.Message)); + } + + public void Info(string sender, string message) + { + SaveMessage(new LogMessage(DateTime.Now, LogLevel.Information, sender, message, default)); + } + + public void Warning(string sender, string message) + { + SaveMessage(new LogMessage(DateTime.Now, LogLevel.Warning, sender, message, default)); + } + + public void Trace(string sender, string message) + { + SaveMessage(new LogMessage(DateTime.Now, LogLevel.Trace, sender, message, default)); + } + + public void Debug(string sender, string message) + { + SaveMessage(new LogMessage(DateTime.Now, LogLevel.Debug, sender, message, default)); + } +} + + + +public class LogMessage(DateTime timestamp, LogLevel logLevel, string category, string message, string? description) +{ + public DateTime Timestamp { get; } = timestamp; + public LogLevel LogLevel { get; } = logLevel; + public string Category { get; internal set; } = category; + public string Message { get; } = message; + public string? Description { get; } = description; + + public override string ToString() + { + return $"{Category} {Message}"; + } +} \ No newline at end of file diff --git a/src/Asv.Drones.Gui.Api/Services/LogService/NullLogService.cs b/src/Asv.Drones.Gui.Api/Services/LogService/NullLogService.cs new file mode 100644 index 0000000..08de0b9 --- /dev/null +++ b/src/Asv.Drones.Gui.Api/Services/LogService/NullLogService.cs @@ -0,0 +1,45 @@ +using System.Reactive.Subjects; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; + +namespace Asv.Drones.Gui.Api; + +public class NullLogService : ILogService +{ + public static NullLogService Instance { get; } = new(); + + public NullLogService() + { + OnMessage = new Subject(); + } + + public IObservable OnMessage { get; } + + public void SaveMessage(LogMessage logMessage) + { + } + + public IEnumerable LoadItemsFromLogFile() + { + return Array.Empty(); + } + + public void DeleteLogFile() + { + } + + public void Dispose() + { + + } + + public ILogger CreateLogger(string categoryName) + { + return NullLoggerFactory.Instance.CreateLogger(categoryName); + } + + public void AddProvider(ILoggerProvider provider) + { + + } +} \ No newline at end of file diff --git a/src/Asv.Drones.Gui.Api/Services/Map/IMapService.cs b/src/Asv.Drones.Gui.Api/Services/Map/IMapService.cs new file mode 100644 index 0000000..c792da9 --- /dev/null +++ b/src/Asv.Drones.Gui.Api/Services/Map/IMapService.cs @@ -0,0 +1,16 @@ +using Asv.Avalonia.Map; +using Asv.Common; + +namespace Asv.Drones.Gui.Api +{ + public interface IMapService + { + long CalculateMapCacheSize(); + void SetMapCacheDirectory(string path); + string MapCacheDirectory { get; } + IRxEditableValue CurrentMapProvider { get; } + IEnumerable AvailableProviders { get; } + IRxEditableValue CurrentMapAccessMode { get; } + IEnumerable AvailableAccessModes { get; } + } +} \ No newline at end of file diff --git a/src/Asv.Drones.Gui.Api/Services/Mavlink/IMavlinkDevicesService.cs b/src/Asv.Drones.Gui.Api/Services/Mavlink/IMavlinkDevicesService.cs new file mode 100644 index 0000000..026f889 --- /dev/null +++ b/src/Asv.Drones.Gui.Api/Services/Mavlink/IMavlinkDevicesService.cs @@ -0,0 +1,68 @@ +using Asv.Common; +using Asv.Mavlink; +using DynamicData; + +namespace Asv.Drones.Gui.Api +{ + public interface IMavlinkDevicesService + { + /// + /// Collection with all devices in network + /// + IObservable> Devices { get; } + + /// + /// Timeout for device connection. If device not response in this time, device will be removed from collection + /// + IRxEditableValue DeviceTimeout { get; } + + /// + /// Mavlink router + /// + IMavlinkRouter Router { get; } + + /// + /// Specify that need reload app to apply new config + /// + IRxValue NeedReloadToApplyConfig { get; } + + /// + /// ComponentId identifier of this app in mavlink network + /// + IRxEditableValue ComponentId { get; } + + /// + /// SystemId identifier of this app in mavlink network + /// + IRxEditableValue SystemId { get; } + + /// + /// Rate of heartbeat packets for sending to network + /// + IRxEditableValue HeartbeatRate { get; } + + IObservable> AllDevices { get; } + + /// + /// List of all founded vehicles in network + /// + IObservable> Vehicles { get; } + + /// + /// Gets vehicle by it's id + /// + /// Id of searched vehicle + /// Vehicle object + IVehicleClient? GetVehicleByFullId(ushort id); + IObservable> BaseStations { get; } + IGbsClientDevice? GetGbsByFullId(ushort id); + IObservable> Payloads { get; } + ISdrClientDevice? GetPayloadsByFullId(ushort id); + IObservable> AdsbDevices { get; } + IAdsbClientDevice? GetAdsbVehicleByFullId(ushort id); + IObservable> RfsaDevices { get; } + IRfsaClientDevice? GetRfsaByFullId(ushort id); + IObservable> RsgaDevices { get; } + IRsgaClientDevice? GetRsgaByFullId(ushort id); + } +} \ No newline at end of file diff --git a/src/Asv.Drones.Gui.Api/Services/Mavlink/MavlinkHelper.cs b/src/Asv.Drones.Gui.Api/Services/Mavlink/MavlinkHelper.cs new file mode 100644 index 0000000..da5eae0 --- /dev/null +++ b/src/Asv.Drones.Gui.Api/Services/Mavlink/MavlinkHelper.cs @@ -0,0 +1,63 @@ +using Asv.Mavlink; +using Asv.Mavlink.V2.Common; +using Asv.Mavlink.V2.Minimal; +using Material.Icons; + +namespace Asv.Drones.Gui.Api +{ + public static class MavlinkHelper + { + public static string GetTitle(this MavCmd cmd) + { + return cmd.ToString("G").Replace("MavCmd", ""); + } + + public static MaterialIconKind GetIcon(MavType type) + { + return type switch + { + MavType.MavTypeFixedWing => MaterialIconKind.Airplane, + MavType.MavTypeGeneric => MaterialIconKind.Quadcopter, + MavType.MavTypeQuadrotor => MaterialIconKind.Quadcopter, + MavType.MavTypeHexarotor => MaterialIconKind.Quadcopter, + MavType.MavTypeOctorotor => MaterialIconKind.Quadcopter, + MavType.MavTypeTricopter => MaterialIconKind.Quadcopter, + MavType.MavTypeHelicopter => MaterialIconKind.Helicopter, + MavType.MavTypeAntennaTracker => MaterialIconKind.Antenna, + MavType.MavTypeGcs => MaterialIconKind.Computer, + _ => MaterialIconKind.HelpNetworkOutline + }; + } + + public static string GetTypeName(MavType type) + { + // DONE: Localize + return type switch + { + MavType.MavTypeFixedWing => RS.MavlinkHelper_GetTypeName_FixedWing, + MavType.MavTypeGeneric => RS.MavlinkHelper_GetTypeName_QuadRotor, + MavType.MavTypeQuadrotor => RS.MavlinkHelper_GetTypeName_QuadRotor, + MavType.MavTypeHexarotor => RS.MavlinkHelper_GetTypeName_HexaRotor, + MavType.MavTypeOctorotor => RS.MavlinkHelper_GetTypeName_OctoRotor, + MavType.MavTypeTricopter => RS.MavlinkHelper_GetTypeName_TriCopter, + MavType.MavTypeHelicopter => RS.MavlinkHelper_GetTypeName_Helicopter, + _ => RS.MavlinkHelper_GetTypeName_UnknownType + }; + } + + public static MaterialIconKind GetIcon(DeviceClass type) + { + return type switch + { + DeviceClass.Plane => MaterialIconKind.Plane, + DeviceClass.Copter => MaterialIconKind.Navigation, + DeviceClass.SdrPayload => MaterialIconKind.Radio, + DeviceClass.GbsRtk => MaterialIconKind.RouterWireless, + DeviceClass.Adsb => MaterialIconKind.Radar, + DeviceClass.Rfsa => MaterialIconKind.Waveform, + DeviceClass.Rsga => MaterialIconKind.CellphoneWireless, + _ => MaterialIconKind.HelpNetworkOutline, + }; + } + } +} \ No newline at end of file diff --git a/src/Asv.Drones.Gui.Api/Services/MissionPlaning/IPlaningMission.cs b/src/Asv.Drones.Gui.Api/Services/MissionPlaning/IPlaningMission.cs new file mode 100644 index 0000000..cb6f94b --- /dev/null +++ b/src/Asv.Drones.Gui.Api/Services/MissionPlaning/IPlaningMission.cs @@ -0,0 +1,22 @@ +using Asv.Mavlink; + +namespace Asv.Drones.Gui.Api; + +/// +/// Represents a planning mission. +/// +public interface IPlaningMission +{ + /// + /// Represents the mission store that stores planning mission files in a hierarchical structure. + /// + /// + /// The MissionStore property provides access to a hierarchical store that is used to store planning mission files. + /// The store is designed to organize the mission files in a hierarchical manner, allowing easy storage and retrieval + /// of mission files based on their unique identifier. + /// + /// + /// An instance of the IHierarchicalStore interface representing the mission store. + /// + IHierarchicalStore MissionStore { get; } +} \ No newline at end of file diff --git a/src/Asv.Drones.Gui.Api/Services/MissionPlaning/PlaningMissionFile.cs b/src/Asv.Drones.Gui.Api/Services/MissionPlaning/PlaningMissionFile.cs new file mode 100644 index 0000000..c1df274 --- /dev/null +++ b/src/Asv.Drones.Gui.Api/Services/MissionPlaning/PlaningMissionFile.cs @@ -0,0 +1,31 @@ +using Asv.Cfg; +using Asv.Cfg.Json; +using Asv.Common; + +namespace Asv.Drones.Gui.Api; + +public class PlaningMissionFile : ZipJsonVersionedFile +{ + public static readonly SemVersion Version1_0_0 = new(1, 0, 0); + public static readonly SemVersion LastVersion = Version1_0_0; + private const string FileType = "AsvDronesMission"; + + + public PlaningMissionFile(Stream stream, Guid id, string name) : base(stream, LastVersion, FileType, true) + { + } + + public PlaningMissionFile(Stream stream) : base(stream, LastVersion, FileType, false) + { + } + + public PlaningMissionModel Load() + { + return this.Get(); + } + + public void Save(PlaningMissionModel model) + { + this.Set(model); + } +} \ No newline at end of file diff --git a/src/Asv.Drones.Gui.Api/Services/MissionPlaning/PlaningMissionModel.cs b/src/Asv.Drones.Gui.Api/Services/MissionPlaning/PlaningMissionModel.cs new file mode 100644 index 0000000..11b4a03 --- /dev/null +++ b/src/Asv.Drones.Gui.Api/Services/MissionPlaning/PlaningMissionModel.cs @@ -0,0 +1,59 @@ +using Asv.Common; +using Asv.Mavlink.V2.Common; +using ReactiveUI.Fody.Helpers; + +namespace Asv.Drones.Gui.Api; + +public record PlaningMissionModel +{ + public List Points { get; set; } = new(); +} + +public class PlaningMissionPointModel +{ + /// + /// Item index + /// + [Reactive] + public int Index { get; set; } + + /// + /// Command type + /// + [Reactive] + public MavCmd Type { get; set; } + + /// + /// Location + /// + [Reactive] + public GeoPoint Location { get; set; } + + /// + /// PARAM1, see MAV_CMD enum + /// OriginName: param1, Units: , IsExtended: false + /// + [Reactive] + public float Param1 { get; set; } + + /// + /// PARAM2, see MAV_CMD enum + /// OriginName: param2, Units: , IsExtended: false + /// + [Reactive] + public float Param2 { get; set; } + + /// + /// PARAM3, see MAV_CMD enum + /// OriginName: param3, Units: , IsExtended: false + /// + [Reactive] + public float Param3 { get; set; } + + /// + /// PARAM4, see MAV_CMD enum + /// OriginName: param4, Units: , IsExtended: false + /// + [Reactive] + public float Param4 { get; set; } +} \ No newline at end of file diff --git a/src/Asv.Drones.Gui.Api/Services/Plugins/ILocalPluginInfo.cs b/src/Asv.Drones.Gui.Api/Services/Plugins/ILocalPluginInfo.cs new file mode 100644 index 0000000..763642d --- /dev/null +++ b/src/Asv.Drones.Gui.Api/Services/Plugins/ILocalPluginInfo.cs @@ -0,0 +1,38 @@ +using Asv.Common; +using Avalonia.Media.Imaging; +using NuGet.Packaging.Core; + +namespace Asv.Drones.Gui.Api; + +public interface ILocalPluginInfo : IPluginSpecification +{ + string Id => $"{SourceUri}|{PackageId}"; + string SourceUri { get; } + string LocalFolder { get; } + string Version { get; } + bool IsUninstalled { get; } + bool IsLoaded { get; } + string LoadingError { get; } + Bitmap? Icon {get;} +} + +public interface IPluginSearchInfo : IPluginSpecification +{ + string Id => $"{Source.SourceUri}|{PackageId}"; + IPluginServerInfo Source { get; } + IEnumerable Dependencies { get; } + string LastVersion { get; } + long? DownloadCount { get; } +} + +public interface IPluginSpecification +{ + SemVersion ApiVersion { get; } + string PackageId { get; } + string? Title { get; } + public string? Description { get; } + string? Authors { get; } + string? Tags { get; } + bool IsVerified { get; } + +} \ No newline at end of file diff --git a/src/Asv.Drones.Gui.Api/Services/Plugins/IPluginEntryPoint.cs b/src/Asv.Drones.Gui.Api/Services/Plugins/IPluginEntryPoint.cs new file mode 100644 index 0000000..ebb912d --- /dev/null +++ b/src/Asv.Drones.Gui.Api/Services/Plugins/IPluginEntryPoint.cs @@ -0,0 +1,32 @@ +namespace Asv.Drones.Gui.Api +{ + /// + /// This interface is used as the entry point when loading plugins + /// + public interface IPluginEntryPoint + { + /// + /// Call when initializes the application Application.Initialize() + /// Will be called before main window\activity is shown + /// + void Initialize(); + + /// + /// Will be called after main window\activity is shown and Application.nFrameworkInitializationCompleted() + /// + void OnFrameworkInitializationCompleted(); + } + + + public interface IPluginMetadata + { + string[] Dependency { get; } + string Name { get; } + } + + public class PluginMetadata : IPluginMetadata + { + public string[] Dependency { get; set; } + public string Name { get; set; } + } +} \ No newline at end of file diff --git a/src/Asv.Drones.Gui.Api/Services/Plugins/IPluginManager.cs b/src/Asv.Drones.Gui.Api/Services/Plugins/IPluginManager.cs new file mode 100644 index 0000000..44ca5d2 --- /dev/null +++ b/src/Asv.Drones.Gui.Api/Services/Plugins/IPluginManager.cs @@ -0,0 +1,56 @@ +using Asv.Common; + +namespace Asv.Drones.Gui.Api; + +public class PluginServer(string name, string sourceUri, string? username = null, string? password = null) +{ + public string Name => name; + public string SourceUri => sourceUri; + public string? Username => username; + public string? Password => password; +} + +public interface IPluginServerInfo +{ + public string Name { get; } + public string SourceUri { get; } + public string? Username { get; } +} + +public interface IPluginManager +{ + IReadOnlyList Servers { get; } + void AddServer(PluginServer server); + void RemoveServer(IPluginServerInfo server); + Task> Search(SearchQuery query, CancellationToken cancel); + Task> ListPluginVersions(SearchQuery query, string pluginId, CancellationToken cancel); + + Task Install(IPluginServerInfo source, string packageId, string version, IProgress? progress, + CancellationToken cancel); + + Task InstallManually(string from, IProgress? progress, + CancellationToken cancel); + + void Uninstall(ILocalPluginInfo plugin); + void CancelUninstall(ILocalPluginInfo pluginInfo); + IEnumerable Installed { get; } + bool IsInstalled(string packageId, out ILocalPluginInfo? info); + SemVersion ApiVersion { get; } +} + +public class SearchQuery +{ + public static readonly SearchQuery Empty = new() + { + Name = null, + IncludePrerelease = false, + Skip = 0, + Take = 20 + }; + + public string? Name { get; set; } + public bool IncludePrerelease { get; set; } + public int Skip { get; set; } = 0; + public int Take { get; set; } = 20; + public HashSet Sources { get; } = new(); +} \ No newline at end of file diff --git a/src/Asv.Drones.Gui.Api/Services/Plugins/IPluginManuallyInstallable.cs b/src/Asv.Drones.Gui.Api/Services/Plugins/IPluginManuallyInstallable.cs new file mode 100644 index 0000000..83bd5ef --- /dev/null +++ b/src/Asv.Drones.Gui.Api/Services/Plugins/IPluginManuallyInstallable.cs @@ -0,0 +1,7 @@ +namespace Asv.Drones.Gui.Api; + +public interface IPluginManuallyInstallable +{ + Task InstallManually(string from, IPluginServerInfo source, string packageId, string version, IProgress? progress, + CancellationToken cancel); +} \ No newline at end of file diff --git a/src/Asv.Drones.Gui.Api/Services/Plugins/NullPluginManager.cs b/src/Asv.Drones.Gui.Api/Services/Plugins/NullPluginManager.cs new file mode 100644 index 0000000..62c20ba --- /dev/null +++ b/src/Asv.Drones.Gui.Api/Services/Plugins/NullPluginManager.cs @@ -0,0 +1,59 @@ +using Asv.Common; + +namespace Asv.Drones.Gui.Api; + +public class NullPluginManager : IPluginManager +{ + public static IPluginManager Instance { get; } = new NullPluginManager(); + + public IReadOnlyList Sources { get; } = new List(); + + + public IReadOnlyList Servers { get; } + + public void AddServer(PluginServer server) + { + } + + public void RemoveServer(IPluginServerInfo server) + { + } + + public Task> Search(SearchQuery query, CancellationToken cancel) + { + return Task.FromResult((IReadOnlyList)new List()); + } + public Task> ListPluginVersions(SearchQuery query, string pluginId, CancellationToken cancel) + { + return Task.FromResult((IReadOnlyList)new List()); + } + + public Task Install(IPluginServerInfo source, string packageId, string version, + IProgress? progress, CancellationToken cancel) + { + return Task.CompletedTask; + } + + public Task InstallManually(string from, IProgress? progress, CancellationToken cancel) + { + return Task.CompletedTask; + } + + public void Uninstall(ILocalPluginInfo plugin) + { + } + + public void CancelUninstall(ILocalPluginInfo pluginInfo) + { + } + + public IEnumerable Installed { get; } = new List(); + + public bool IsInstalled(string packageId, out ILocalPluginInfo? info) + { + info = null; + return false; + } + + public SemVersion ApiVersion { get; } = new(0); +} \ No newline at end of file diff --git a/src/Asv.Drones.Gui.Api/Services/Plugins/PluginEntryPointAttribute.cs b/src/Asv.Drones.Gui.Api/Services/Plugins/PluginEntryPointAttribute.cs new file mode 100644 index 0000000..4da4ec1 --- /dev/null +++ b/src/Asv.Drones.Gui.Api/Services/Plugins/PluginEntryPointAttribute.cs @@ -0,0 +1,23 @@ +using System.Composition; + +namespace Asv.Drones.Gui.Api; + +/// +/// This attribute is used to find a matching plugin entry points +/// +[MetadataAttribute] +[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] +public class PluginEntryPointAttribute : ExportAttribute, IPluginMetadata +{ + public PluginEntryPointAttribute(string name, params string[] dependency) + : base(typeof(IPluginEntryPoint)) + { + if (string.IsNullOrWhiteSpace(name)) + throw new ArgumentException("Value cannot be null or whitespace.", nameof(name)); + Name = name; + Dependency = dependency; + } + + public string[] Dependency { get; } + public string Name { get; } +} \ No newline at end of file diff --git a/src/Asv.Drones.Gui.Api/Services/SdrStore/ISdrStoreService.cs b/src/Asv.Drones.Gui.Api/Services/SdrStore/ISdrStoreService.cs new file mode 100644 index 0000000..7c79373 --- /dev/null +++ b/src/Asv.Drones.Gui.Api/Services/SdrStore/ISdrStoreService.cs @@ -0,0 +1,17 @@ +using Asv.Mavlink; + +namespace Asv.Drones.Gui.Api; + +/// +/// Represents a service for storing and retrieving data files associated with ASV SDR record file metadata. +/// +public interface ISdrStoreService +{ + /// + /// Represents a hierarchical store that stores a collection of IListDataFile with AsvSdrRecordFileMetadata metadata, using Guid as the key. + /// + /// + /// The hierarchical store. + /// + IHierarchicalStore> Store { get; } +} \ No newline at end of file diff --git a/src/Asv.Drones.Gui.Api/Services/ServiceWithConfigBase.cs b/src/Asv.Drones.Gui.Api/Services/ServiceWithConfigBase.cs new file mode 100644 index 0000000..448bb34 --- /dev/null +++ b/src/Asv.Drones.Gui.Api/Services/ServiceWithConfigBase.cs @@ -0,0 +1,35 @@ +using Asv.Cfg; + +namespace Asv.Drones.Gui.Api +{ + public class ServiceWithConfigBase : DisposableReactiveObject + where TConfig : new() + { + private readonly IConfiguration _cfgService; + private readonly object _sync = new(); + private readonly TConfig _config; + + protected ServiceWithConfigBase(IConfiguration cfg) + { + _cfgService = cfg ?? throw new ArgumentNullException(nameof(cfg)); + _config = cfg.Get(); + } + + protected TConfigValue InternalGetConfig(Func getProperty) + { + lock (_sync) + { + return getProperty(_config); + } + } + + protected void InternalSaveConfig(Action changeCallback) + { + lock (_sync) + { + changeCallback(_config); + _cfgService.Set(_config); + } + } + } +} \ No newline at end of file diff --git a/src/Asv.Drones.Gui.Api/Services/SoundNotification/ISoundNotificationService.cs b/src/Asv.Drones.Gui.Api/Services/SoundNotification/ISoundNotificationService.cs new file mode 100644 index 0000000..ed5d177 --- /dev/null +++ b/src/Asv.Drones.Gui.Api/Services/SoundNotification/ISoundNotificationService.cs @@ -0,0 +1,6 @@ +namespace Asv.Drones.Gui.Api; + +public interface ISoundNotificationService +{ + public void Notify(); +} \ No newline at end of file diff --git a/src/Asv.Drones.Gui.Api/Shell/Header/DefaultHeaderMenuProvider.cs b/src/Asv.Drones.Gui.Api/Shell/Header/DefaultHeaderMenuProvider.cs new file mode 100644 index 0000000..1147b94 --- /dev/null +++ b/src/Asv.Drones.Gui.Api/Shell/Header/DefaultHeaderMenuProvider.cs @@ -0,0 +1,15 @@ +using System.Composition; +using DynamicData; + +namespace Asv.Drones.Gui.Api +{ + [Export(WellKnownUri.ShellHeaderMenu, typeof(IViewModelProvider))] + public class DefaultHeaderMenuProvider : ViewModelProviderBase + { + [ImportingConstructor] + public DefaultHeaderMenuProvider([ImportMany(WellKnownUri.ShellHeaderMenu)] IEnumerable menuItems) + { + Source.AddOrUpdate(menuItems); + } + } +} \ No newline at end of file diff --git a/src/Asv.Drones.Gui.Api/Shell/Header/IMenuItem.cs b/src/Asv.Drones.Gui.Api/Shell/Header/IMenuItem.cs new file mode 100644 index 0000000..1da7707 --- /dev/null +++ b/src/Asv.Drones.Gui.Api/Shell/Header/IMenuItem.cs @@ -0,0 +1,21 @@ +using System.Collections.ObjectModel; +using System.Windows.Input; +using Avalonia.Input; +using Material.Icons; + +namespace Asv.Drones.Gui.Api +{ + public interface IMenuItem : IViewModel + { + int Order { get; } + MaterialIconKind Icon { get; } + string Header { get; } + ICommand Command { get; } + object? CommandParameter { get; } + bool IsVisible { get; } + bool StaysOpenOnClick { get; } + ReadOnlyObservableCollection? Items { get; set; } + public bool IsEnabled { get; } + public KeyGesture? HotKey { get; } + } +} \ No newline at end of file diff --git a/src/Asv.Drones.Gui.Api/Shell/Header/MenuItem.cs b/src/Asv.Drones.Gui.Api/Shell/Header/MenuItem.cs new file mode 100644 index 0000000..b82ac92 --- /dev/null +++ b/src/Asv.Drones.Gui.Api/Shell/Header/MenuItem.cs @@ -0,0 +1,30 @@ +using System.Collections.ObjectModel; +using System.Windows.Input; +using Avalonia.Input; +using Material.Icons; +using ReactiveUI.Fody.Helpers; + +namespace Asv.Drones.Gui.Api +{ + public class MenuItem : ViewModelBase, IMenuItem + { + public MenuItem(Uri id) : base(id) + { + } + + public MenuItem(string id) : base(id) + { + } + + [Reactive] public int Order { get; set; } + [Reactive] public MaterialIconKind Icon { get; set; } + [Reactive] public string Header { get; set; } + [Reactive] public ICommand Command { get; set; } + [Reactive] public object? CommandParameter { get; set; } + [Reactive] public bool IsVisible { get; set; } = true; + [Reactive] public bool StaysOpenOnClick { get; set; } + [Reactive] public bool IsEnabled { get; set; } = true; + public virtual ReadOnlyObservableCollection? Items { get; set; } + [Reactive] public KeyGesture? HotKey { get; set; } + } +} \ No newline at end of file diff --git a/src/Asv.Drones.Gui.Api/Shell/IShell.cs b/src/Asv.Drones.Gui.Api/Shell/IShell.cs new file mode 100644 index 0000000..825b113 --- /dev/null +++ b/src/Asv.Drones.Gui.Api/Shell/IShell.cs @@ -0,0 +1,11 @@ +namespace Asv.Drones.Gui.Api +{ + /// + /// Main view interface + /// + public interface IShell + { + Task GoTo(Uri uri); + IShellPage? CurrentPage { get; set; } + } +} \ No newline at end of file diff --git a/src/Asv.Drones.Gui.Api/Shell/Menu/IShellMenuItem.cs b/src/Asv.Drones.Gui.Api/Shell/Menu/IShellMenuItem.cs new file mode 100644 index 0000000..4ef98ff --- /dev/null +++ b/src/Asv.Drones.Gui.Api/Shell/Menu/IShellMenuItem.cs @@ -0,0 +1,39 @@ +using System.Collections.ObjectModel; +using FluentAvalonia.UI.Controls; + +namespace Asv.Drones.Gui.Api +{ + public interface IShellMenuItem : IViewModel + { + InfoBadge InfoBadge { get; set; } + IShellMenuItem? Parent { get; set; } + string Name { get; set; } + Uri NavigateTo { get; set; } + string Icon { get; } + ShellMenuPosition Position { get; } + ShellMenuItemType Type { get; } + int Order { get; } + ReadOnlyObservableCollection? Items { get; } + bool IsSelected { get; set; } + bool IsVisible { get; set; } + } + + public enum ShellMenuPosition + { + Top, + Bottom, + } + + public enum ShellMenuItemType + { + Header, + Group, + PageNavigation + } + + + public interface IShellMenuItem : IShellMenuItem + { + IShellMenuItem Init(TTarget target); + } +} \ No newline at end of file diff --git a/src/Asv.Drones.Gui.Api/Shell/Menu/ShellMenuItem.cs b/src/Asv.Drones.Gui.Api/Shell/Menu/ShellMenuItem.cs new file mode 100644 index 0000000..733081d --- /dev/null +++ b/src/Asv.Drones.Gui.Api/Shell/Menu/ShellMenuItem.cs @@ -0,0 +1,59 @@ +#nullable enable +using System.Collections.ObjectModel; +using Asv.Common; +using DynamicData.Binding; +using FluentAvalonia.UI.Controls; +using ReactiveUI.Fody.Helpers; + +namespace Asv.Drones.Gui.Api +{ + public class ShellMenuItem : ViewModelBase, IShellMenuItem + { + private readonly ReadOnlyObservableCollection? _items; + + public ShellMenuItem(Uri id) : base(id) + { + } + + public ShellMenuItem(string id) : base(id) + { + } + + public InfoBadge InfoBadge { get; set; } + public IShellMenuItem Parent { get; set; } + + [Reactive] public string Name { get; set; } + [Reactive] public Uri NavigateTo { get; set; } + [Reactive] public string Icon { get; init; } + public ShellMenuPosition Position { get; init; } + public ShellMenuItemType Type { get; init; } + public int Order { get; init; } + + public ReadOnlyObservableCollection? Items + { + get => _items; + init + { + _items = value; + if (value == null) return; + foreach (var item in value) + { + item.Parent = this; + } + + value.ObserveCollectionChanges().Subscribe(_ => + { + if (_.EventArgs.NewItems == null) return; + foreach (var newItem in _.EventArgs.NewItems) + { + (newItem as IShellMenuItem)!.Parent = this; + } + }).DisposeItWith(Disposable); + } + } + + [Reactive] public bool IsSelected { get; set; } + + [Reactive] public bool IsVisible { get; set; } = true; + } +} \ No newline at end of file diff --git a/src/Asv.Drones.Gui.Api/Shell/Pages/ExportShellPageAttribute.cs b/src/Asv.Drones.Gui.Api/Shell/Pages/ExportShellPageAttribute.cs new file mode 100644 index 0000000..69f9858 --- /dev/null +++ b/src/Asv.Drones.Gui.Api/Shell/Pages/ExportShellPageAttribute.cs @@ -0,0 +1,17 @@ +using System.Composition; + +namespace Asv.Drones.Gui.Api +{ + /// + /// Define this attribute to export shell page + /// + [MetadataAttribute] + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] + public class ExportShellPageAttribute : ExportAttribute + { + public ExportShellPageAttribute(string baseUri) + : base(new Uri(baseUri).AbsolutePath, typeof(IShellPage)) + { + } + } +} \ No newline at end of file diff --git a/src/Asv.Drones.Gui.Api/Shell/Pages/IShellPage.cs b/src/Asv.Drones.Gui.Api/Shell/Pages/IShellPage.cs new file mode 100644 index 0000000..384901f --- /dev/null +++ b/src/Asv.Drones.Gui.Api/Shell/Pages/IShellPage.cs @@ -0,0 +1,24 @@ +using System.Collections.Specialized; +using DynamicData; +using Material.Icons; + +namespace Asv.Drones.Gui.Api +{ + /// + /// All pages in shell must implement this interface + /// + public interface IShellPage : IViewModel + { + MaterialIconKind Icon { get; } + string Title { get; } + IObservable> HeaderItems { get; } + IObservable> StatusItems { get; } + + /// + /// Addition arguments for page + /// + void SetArgs(NameValueCollection args); + + Task TryClose(); + } +} \ No newline at end of file diff --git a/src/Asv.Drones.Gui.Api/Shell/Pages/LogViewer/LogItemViewModel.cs b/src/Asv.Drones.Gui.Api/Shell/Pages/LogViewer/LogItemViewModel.cs new file mode 100644 index 0000000..2081eb6 --- /dev/null +++ b/src/Asv.Drones.Gui.Api/Shell/Pages/LogViewer/LogItemViewModel.cs @@ -0,0 +1,34 @@ +using Material.Icons; +using Microsoft.Extensions.Logging; + + +namespace Asv.Drones.Gui.Api; + +public class LogItemViewModel( + int itemIndex, LogMessage msg) : DisposableReactiveObject +{ + public int Index { get; } = itemIndex; + + public MaterialIconKind Kind { get; } = msg.LogLevel switch + { + LogLevel.Debug => MaterialIconKind.Bug, + LogLevel.Trace => MaterialIconKind.Tractor, + LogLevel.Information => MaterialIconKind.Info, + LogLevel.Warning => MaterialIconKind.Bullhorn, + LogLevel.Error => MaterialIconKind.Fire, + LogLevel.Critical => MaterialIconKind.FlashAlert, + _ => MaterialIconKind.QuestionMark + }; + + public DateTime Timestamp => msg.Timestamp; + public LogLevel Level => msg.LogLevel; + public string Class => msg.Category; + public string Message => msg.Message; + + public bool IsTrace { get; } = msg.LogLevel == LogLevel.Trace; + public bool IsDebug { get; } = msg.LogLevel == LogLevel.Debug; + public bool IsInfo { get; } = msg.LogLevel == LogLevel.Information; + public bool IsWarning { get; } = msg.LogLevel == LogLevel.Warning; + public bool IsError { get; } = msg.LogLevel == LogLevel.Error; + public bool IsFatal { get; } = msg.LogLevel == LogLevel.Critical; +} \ No newline at end of file diff --git a/src/Asv.Drones.Gui.Api/Shell/Pages/PacketViewer/Converters/DefaultPacketConverter.cs b/src/Asv.Drones.Gui.Api/Shell/Pages/PacketViewer/Converters/DefaultPacketConverter.cs new file mode 100644 index 0000000..d498b85 --- /dev/null +++ b/src/Asv.Drones.Gui.Api/Shell/Pages/PacketViewer/Converters/DefaultPacketConverter.cs @@ -0,0 +1,44 @@ +using System.Composition; +using Asv.Mavlink; +using Newtonsoft.Json; + +namespace Asv.Drones.Gui.Api; + +/// +/// Default packet converter. Used when there is no specialized converter for some packet type. +/// +[Export(typeof(IPacketConverter))] +public class DefaultPacketConverter : IPacketConverter +{ + public int Order => int.MaxValue; + + public bool CanConvert(IPacketV2 packet) + { + if (packet == null) throw new ArgumentException("Incoming packet was not initialized!"); + + return true; + } + + public string Convert(IPacketV2 packet, PacketFormatting formatting = PacketFormatting.None) + { + if (packet == null) throw new ArgumentException("Incoming packet was not initialized!"); + if (!CanConvert(packet)) throw new ArgumentException("Converter can not convert incoming packet!"); + + string result = string.Empty; + + if (formatting == PacketFormatting.None) + { + result = JsonConvert.SerializeObject(packet.Payload, Formatting.None); + } + else if (formatting == PacketFormatting.Indented) + { + result = JsonConvert.SerializeObject(packet.Payload, Formatting.Indented); + } + else + { + throw new ArgumentException("Wrong packet formatting!"); + } + + return result; + } +} \ No newline at end of file diff --git a/src/Asv.Drones.Gui.Api/Shell/Pages/PacketViewer/Converters/IPacketConverter.cs b/src/Asv.Drones.Gui.Api/Shell/Pages/PacketViewer/Converters/IPacketConverter.cs new file mode 100644 index 0000000..eff1092 --- /dev/null +++ b/src/Asv.Drones.Gui.Api/Shell/Pages/PacketViewer/Converters/IPacketConverter.cs @@ -0,0 +1,49 @@ +using Asv.Mavlink; + +namespace Asv.Drones.Gui.Api; + +/// +/// Represents the formatting options for packet data. +/// +public enum PacketFormatting +{ + /// + /// One-line formatting + /// + None, + + /// + /// Represents the formatting options for packet content. + /// + Indented +} + +/// +/// Represents an interface for converting packet payloads to string representation. +/// +public interface IPacketConverter +{ + /// + /// Gets the order of the converter in the list of all converters. + /// + int Order { get; } + + /// + /// Checks whether the converter can convert the payload of a given packet. + /// + /// The packet to convert + /// Returns true if the converter can convert the payload, false otherwise + bool CanConvert(IPacketV2 packet); + + /// + /// Converts packet's payload to string. + /// + /// The packet to convert. + /// + /// The formatting of the result string. This is optional and is used to create packets with special formatting. The default value is 'None'. + /// + /// + /// A string representation of the packet. + /// + string Convert(IPacketV2 packet, PacketFormatting formatting = PacketFormatting.None); +} \ No newline at end of file diff --git a/src/Asv.Drones.Gui.Api/Shell/Pages/PacketViewer/Converters/StatusTextConverter.cs b/src/Asv.Drones.Gui.Api/Shell/Pages/PacketViewer/Converters/StatusTextConverter.cs new file mode 100644 index 0000000..97dccad --- /dev/null +++ b/src/Asv.Drones.Gui.Api/Shell/Pages/PacketViewer/Converters/StatusTextConverter.cs @@ -0,0 +1,57 @@ +using System.Composition; +using System.Text; +using Asv.Mavlink; +using Asv.Mavlink.V2.Common; + +namespace Asv.Drones.Gui.Api; + +/// +/// StatusText packet converter. +/// +[Export(typeof(IPacketConverter))] +public class StatusTextConverter : IPacketConverter +{ + public int Order => 0; + + public bool CanConvert(IPacketV2 packet) + { + if (packet == null) throw new ArgumentException("Incoming packet was not initialized!"); + + return packet.Payload is StatustextPayload; + } + + public string Convert(IPacketV2 packet, PacketFormatting formatting = PacketFormatting.None) + { + if (packet == null) throw new ArgumentException("Incoming packet was not initialized!"); + if (!CanConvert(packet)) throw new ArgumentException("Converter can not convert incoming packet!"); + + StringBuilder sb = new StringBuilder(); + + var payload = packet.Payload as StatustextPayload; + + if (formatting == PacketFormatting.None) + { + sb.Append("{"); + sb.Append("\"Severity\":"); + sb.Append($"{payload.Severity},"); + sb.Append("\"Text\":"); + sb.Append($"\"{MavlinkTypesHelper.GetString(payload.Text)}\""); + sb.Append("}"); + } + else if (formatting == PacketFormatting.Indented) + { + sb.Append("{\n"); + sb.Append("\"Severity\": "); + sb.Append($"{payload.Severity},\n"); + sb.Append("\"Text\": "); + sb.Append($"\"{MavlinkTypesHelper.GetString(payload.Text)}\"\n"); + sb.Append("}"); + } + else + { + throw new ArgumentException("Wrong packet formatting!"); + } + + return sb.ToString(); + } +} \ No newline at end of file diff --git a/src/Asv.Drones.Gui.Api/Shell/Pages/Settings/ISettingsPageContext.cs b/src/Asv.Drones.Gui.Api/Shell/Pages/Settings/ISettingsPageContext.cs new file mode 100644 index 0000000..18a6ace --- /dev/null +++ b/src/Asv.Drones.Gui.Api/Shell/Pages/Settings/ISettingsPageContext.cs @@ -0,0 +1,6 @@ +namespace Asv.Drones.Gui.Api; + +public interface ISettingsPageContext : ITreePageContext +{ + void SetRebootRequired(); +} \ No newline at end of file diff --git a/src/Asv.Drones.Gui.Api/Shell/Pages/ShellPage.cs b/src/Asv.Drones.Gui.Api/Shell/Pages/ShellPage.cs new file mode 100644 index 0000000..9d84b79 --- /dev/null +++ b/src/Asv.Drones.Gui.Api/Shell/Pages/ShellPage.cs @@ -0,0 +1,39 @@ +using System.Collections.Specialized; +using Asv.Common; +using DynamicData; +using Material.Icons; +using ReactiveUI.Fody.Helpers; + +namespace Asv.Drones.Gui.Api; + +public class ShellPage : ViewModelBase, IShellPage +{ + protected ShellPage(Uri uri) : base(uri) + { + HeaderItemsSource = new SourceCache(x => x.Id).DisposeItWith(Disposable); + StatusItemsSource = new SourceCache(x => x.Id).DisposeItWith(Disposable); + } + + protected ShellPage(string uri) : this(new Uri(uri)) + { + } + + [Reactive] public MaterialIconKind Icon { get; set; } + [Reactive] public string Title { get; set; } + + protected ISourceCache HeaderItemsSource { get; } + protected ISourceCache StatusItemsSource { get; } + + public IObservable> HeaderItems => HeaderItemsSource.Connect(); + + public IObservable> StatusItems => StatusItemsSource.Connect(); + + public virtual void SetArgs(NameValueCollection args) + { + } + + public virtual Task TryClose() + { + return Task.FromResult(true); + } +} \ No newline at end of file diff --git a/src/Asv.Drones.Gui.Api/Shell/Status/IShellStatusItem.cs b/src/Asv.Drones.Gui.Api/Shell/Status/IShellStatusItem.cs new file mode 100644 index 0000000..b0a0b85 --- /dev/null +++ b/src/Asv.Drones.Gui.Api/Shell/Status/IShellStatusItem.cs @@ -0,0 +1,13 @@ +namespace Asv.Drones.Gui.Api +{ + /// + /// All status items in shell must implement this interface + /// + public interface IShellStatusItem : IViewModel + { + /// + /// Display order + /// + int Order { get; } + } +} \ No newline at end of file diff --git a/src/Asv.Drones.Gui.Api/Shell/Status/ShellStatusItem.cs b/src/Asv.Drones.Gui.Api/Shell/Status/ShellStatusItem.cs new file mode 100644 index 0000000..bd18bc2 --- /dev/null +++ b/src/Asv.Drones.Gui.Api/Shell/Status/ShellStatusItem.cs @@ -0,0 +1,15 @@ +namespace Asv.Drones.Gui.Api +{ + public abstract class ShellStatusItem : ViewModelBase, IShellStatusItem + { + protected ShellStatusItem(Uri id) : base(id) + { + } + + protected ShellStatusItem(string id) : base(id) + { + } + + public abstract int Order { get; } + } +} \ No newline at end of file diff --git a/src/Asv.Drones.Gui.Api/Tools/Behavior/LostFocusUpdateBindingBehavior.cs b/src/Asv.Drones.Gui.Api/Tools/Behavior/LostFocusUpdateBindingBehavior.cs new file mode 100644 index 0000000..38decfe --- /dev/null +++ b/src/Asv.Drones.Gui.Api/Tools/Behavior/LostFocusUpdateBindingBehavior.cs @@ -0,0 +1,87 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Data; +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.Xaml.Interactivity; + +namespace Asv.Drones.Gui.Api; + +public class LostFocusUpdateBindingBehavior : Behavior +{ + static LostFocusUpdateBindingBehavior() + { + TextProperty.Changed.Subscribe(e => { ((LostFocusUpdateBindingBehavior)e.Sender).OnBindingValueChanged(); }); + } + + protected override void UpdateDataValidation(AvaloniaProperty property, BindingValueType state, Exception? error) + { + base.UpdateDataValidation(property, state, error); + if (property == TextProperty && AssociatedObject != null) + { + if (error != null) + { + DataValidationErrors.SetError(AssociatedObject, error); + } + else + { + DataValidationErrors.ClearErrors(AssociatedObject); + } + } + } + + + protected override void OnAttached() + { + if (AssociatedObject != null) + { + AssociatedObject.LostFocus += OnLostFocus; + AssociatedObject.KeyDown += OnKeyDown; + } + + base.OnAttached(); + } + + protected override void OnDetaching() + { + if (AssociatedObject != null) + { + AssociatedObject.LostFocus -= OnLostFocus; + AssociatedObject.KeyDown -= OnKeyDown; + } + + base.OnDetaching(); + } + + private void OnKeyDown(object sender, KeyEventArgs e) + { + if (AssociatedObject != null && e.Key == Key.Enter) + { + Text = AssociatedObject.Text; + } + } + + private void OnLostFocus(object sender, RoutedEventArgs e) + { + if (AssociatedObject != null) + Text = AssociatedObject.Text; + } + + private void OnBindingValueChanged() + { + if (AssociatedObject != null) + AssociatedObject.Text = Text; + } + + public static readonly DirectProperty TextProperty + = AvaloniaProperty.RegisterDirect(nameof(Text), o => o.Text, + (o, v) => o.Text = v, null, BindingMode.TwoWay, true); + + private string _text; + + public string Text + { + get { return _text; } + set { this.SetAndRaise(TextProperty, ref _text, value); } + } +} \ No newline at end of file diff --git a/src/Asv.Drones.Gui.Api/Tools/Controls/Attitude/AttitudeIndicator.axaml b/src/Asv.Drones.Gui.Api/Tools/Controls/Attitude/AttitudeIndicator.axaml new file mode 100644 index 0000000..71ea892 --- /dev/null +++ b/src/Asv.Drones.Gui.Api/Tools/Controls/Attitude/AttitudeIndicator.axaml @@ -0,0 +1,428 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Asv.Drones.Gui.Api/Tools/Controls/Attitude/AttitudeIndicator.axaml.cs b/src/Asv.Drones.Gui.Api/Tools/Controls/Attitude/AttitudeIndicator.axaml.cs new file mode 100644 index 0000000..2ac0d89 --- /dev/null +++ b/src/Asv.Drones.Gui.Api/Tools/Controls/Attitude/AttitudeIndicator.axaml.cs @@ -0,0 +1,828 @@ +using System.Reactive.Linq; +using Avalonia; +using Avalonia.Collections; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Media; +using ReactiveUI; + +namespace Asv.Drones.Gui.Api +{ + public class AttitudeIndicator : TemplatedControl + { + private const int VelocityItemCount = 6; + private const int VelocityValueRange = 5; + private const double VelocityControlLengthPrc = 0.4; + private const int AltitudeItemCount = 6; + private const int AltitudeValueRange = 5; + private const double AltitudeControlLengthPrc = 0.4; + private const int HeadingItemCount = 10; + private const double HeadingControlLengthPrc = 1.0; + private const int HeadingValueRange = 15; + + private static double _headingPositionStep; + private static double _headingCenterPosition; + + private IEnumerable _pitchItems; + private IEnumerable _rollItems; + private IEnumerable _velocityItems; + private IEnumerable _altitudeItems; + private IEnumerable _headingItems; + private double _internalWidth = 1000; + private double _internalHeight = 1000; + private double _pitchTranslateX; + private double _pitchTranslateY; + private double _homeAzimuthPosition = -100; + private string _statusText; + private string _rightStatusText; + + public double Scale { get; } + + private Color _brushVibrationX; + + public static readonly DirectProperty brushVibrationXProperty = + AvaloniaProperty.RegisterDirect( + nameof(BrushVibrationX), o => o.BrushVibrationX, (o, v) => o.BrushVibrationX = v); + + public Color BrushVibrationX + { + get => _brushVibrationX; + set => SetAndRaise(brushVibrationXProperty, ref _brushVibrationX, value); + } + + private Color _brushVibrationY; + + public static readonly DirectProperty brushVibrationYProperty = + AvaloniaProperty.RegisterDirect( + nameof(BrushVibrationY), o => o.BrushVibrationY, (o, v) => o.BrushVibrationY = v); + + public Color BrushVibrationY + { + get => _brushVibrationY; + set => SetAndRaise(brushVibrationYProperty, ref _brushVibrationY, value); + } + + private Color _brushVibrationZ; + + public static readonly DirectProperty brushVibrationZProperty = + AvaloniaProperty.RegisterDirect( + nameof(BrushVibrationZ), o => o.BrushVibrationZ, (o, v) => o.BrushVibrationZ = v); + + public Color BrushVibrationZ + { + get => _brushVibrationZ; + set => SetAndRaise(brushVibrationZProperty, ref _brushVibrationZ, value); + } + + public static readonly StyledProperty VibrationXProperty = + AvaloniaProperty.Register( + nameof(VibrationX), defaultValue: -1); + + public float VibrationX + { + get => GetValue(VibrationXProperty); + set => SetValue(VibrationXProperty, value); + } + + public static readonly StyledProperty VibrationYProperty = + AvaloniaProperty.Register( + nameof(VibrationY), defaultValue: -1); + + public float VibrationY + { + get => GetValue(VibrationYProperty); + set => SetValue(VibrationYProperty, value); + } + + public static readonly StyledProperty VibrationZProperty = + AvaloniaProperty.Register( + nameof(VibrationZ), defaultValue: -1); + + public float VibrationZ + { + get => GetValue(VibrationZProperty); + set => SetValue(VibrationZProperty, value); + } + + public static readonly StyledProperty Clipping0Property = + AvaloniaProperty.Register( + nameof(Clipping0)); + + public uint Clipping0 + { + get => GetValue(Clipping0Property); + set => SetValue(Clipping0Property, value); + } + + public static readonly StyledProperty Clipping1Property = + AvaloniaProperty.Register( + nameof(Clipping1)); + + public uint Clipping1 + { + get => GetValue(Clipping1Property); + set => SetValue(Clipping1Property, value); + } + + public static readonly StyledProperty Clipping2Property = + AvaloniaProperty.Register( + nameof(Clipping2)); + + public uint Clipping2 + { + get => GetValue(Clipping2Property); + set => SetValue(Clipping2Property, value); + } + + public static readonly StyledProperty RollAngleProperty = + AvaloniaProperty.Register(nameof(RollAngle), default(double)); + + public double RollAngle + { + get => GetValue(RollAngleProperty); + set => SetValue(RollAngleProperty, value); + } + + public static readonly StyledProperty PitchAngleProperty = + AvaloniaProperty.Register(nameof(PitchAngle), default(double)); + + public double PitchAngle + { + get => GetValue(PitchAngleProperty); + set => SetValue(PitchAngleProperty, value); + } + + public static readonly StyledProperty VelocityProperty = + AvaloniaProperty.Register(nameof(Velocity), default(double)); + + public double Velocity + { + get => GetValue(VelocityProperty); + set => SetValue(VelocityProperty, value); + } + + public static readonly StyledProperty AltitudeProperty = + AvaloniaProperty.Register(nameof(Altitude), default(double)); + + public double Altitude + { + get => GetValue(AltitudeProperty); + set => SetValue(AltitudeProperty, value); + } + + public static readonly StyledProperty HeadingProperty = + AvaloniaProperty.Register(nameof(Heading), default(double)); + + public double Heading + { + get => GetValue(HeadingProperty); + set => SetValue(HeadingProperty, value); + } + + public static readonly StyledProperty HomeAzimuthProperty = + AvaloniaProperty.Register(nameof(HomeAzimuth), default(double?)); + + public double? HomeAzimuth + { + get => GetValue(HomeAzimuthProperty); + set => SetValue(HomeAzimuthProperty, value); + } + + public static readonly StyledProperty IsArmedProperty = + AvaloniaProperty.Register(nameof(IsArmed), default(bool)); + + public bool IsArmed + { + get => GetValue(IsArmedProperty); + set => SetValue(IsArmedProperty, value); + } + + public static readonly DirectProperty StatusTextProperty = + AvaloniaProperty.RegisterDirect(nameof(StatusText), _ => _.StatusText, + (_, value) => _.StatusText = value); + + public string StatusText + { + get => _statusText; + set => SetAndRaise(StatusTextProperty, ref _statusText, value); + } + + public static readonly DirectProperty RightStatusTextProperty = + AvaloniaProperty.RegisterDirect(nameof(RightStatusText), _ => _.RightStatusText, + (_, value) => _.RightStatusText = value); + + public string RightStatusText + { + get => _rightStatusText; + set => SetAndRaise(RightStatusTextProperty, ref _rightStatusText, value); + } + + public static readonly StyledProperty ArmedTimeProperty = + AvaloniaProperty.Register(nameof(ArmedTime), default(TimeSpan)); + + public TimeSpan ArmedTime + { + get => GetValue(ArmedTimeProperty); + set => SetValue(ArmedTimeProperty, value); + } + + #region Internal direct property + + private static readonly DirectProperty InternalWidthProperty = + AvaloniaProperty.RegisterDirect(nameof(InternalWidth), _ => _.InternalWidth, + (_, value) => _.InternalWidth = value); + + private double InternalWidth + { + get => _internalWidth; + set => SetAndRaise(InternalWidthProperty, ref _internalWidth, value); + } + + private static readonly DirectProperty InternalHeightProperty = + AvaloniaProperty.RegisterDirect(nameof(InternalHeight), _ => _.InternalHeight, + (_, value) => _.InternalHeight = value); + + private double InternalHeight + { + get => _internalHeight; + set => SetAndRaise(InternalHeightProperty, ref _internalHeight, value); + } + + private static readonly DirectProperty PitchTranslateXProperty = + AvaloniaProperty.RegisterDirect(nameof(PitchTranslateX), _ => _.PitchTranslateX, + (_, value) => _.PitchTranslateX = value); + + private double PitchTranslateX + { + get => _pitchTranslateX; + set => SetAndRaise(PitchTranslateXProperty, ref _pitchTranslateX, value); + } + + private static readonly DirectProperty PitchTranslateYProperty = + AvaloniaProperty.RegisterDirect(nameof(PitchTranslateY), _ => _.PitchTranslateY, + (_, value) => _.PitchTranslateY = value); + + private double PitchTranslateY + { + get => _pitchTranslateY; + set => SetAndRaise(PitchTranslateYProperty, ref _pitchTranslateY, value); + } + + private static readonly DirectProperty> RollItemsProperty = + AvaloniaProperty.RegisterDirect>(nameof(RollItems), + _ => _.RollItems, (_, value) => _.RollItems = value); + + private IEnumerable RollItems + { + get => _rollItems; + set => SetAndRaise(RollItemsProperty, ref _rollItems, value); + } + + private static readonly DirectProperty> PitchItemsProperty = + AvaloniaProperty.RegisterDirect>(nameof(PitchItems), + _ => _.PitchItems, (_, value) => _.PitchItems = value); + + private IEnumerable PitchItems + { + get => _pitchItems; + set => SetAndRaise(PitchItemsProperty, ref _pitchItems, value); + } + + private static readonly DirectProperty> VelocityItemsProperty = + AvaloniaProperty.RegisterDirect>(nameof(VelocityItems), + _ => _.VelocityItems, (_, value) => _.VelocityItems = value); + + private IEnumerable VelocityItems + { + get => _velocityItems; + set => SetAndRaise(VelocityItemsProperty, ref _velocityItems, value); + } + + private static readonly DirectProperty> AltitudeItemsProperty = + AvaloniaProperty.RegisterDirect>(nameof(AltitudeItems), + _ => _.AltitudeItems, (_, value) => _.AltitudeItems = value); + + private IEnumerable AltitudeItems + { + get => _altitudeItems; + set => SetAndRaise(AltitudeItemsProperty, ref _altitudeItems, value); + } + + private static readonly DirectProperty> HeadingItemsProperty = + AvaloniaProperty.RegisterDirect>(nameof(HeadingItems), + _ => _.HeadingItems, (_, value) => _.HeadingItems = value); + + private IEnumerable HeadingItems + { + get => _headingItems; + set => SetAndRaise(HeadingItemsProperty, ref _headingItems, value); + } + + private static readonly DirectProperty HomeAzimuthPositionProperty = + AvaloniaProperty.RegisterDirect(nameof(HomeAzimuthPosition), + _ => _.HomeAzimuthPosition, (_, value) => _.HomeAzimuthPosition = value); + + + private double HomeAzimuthPosition + { + get => _homeAzimuthPosition; + set => SetAndRaise(HomeAzimuthPositionProperty, ref _homeAzimuthPosition, value); + } + + #endregion + + + public AttitudeIndicator() + { + if (Design.IsDesignMode) + { + var status = new[] { "Armed", "Disarmed" }; + Observable.Timer(TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(1), RxApp.MainThreadScheduler) + .Subscribe(_ => { StatusText = status[_ % 2]; }); + StatusText = status[1]; + } + + var smallerSide = Math.Min(InternalWidth, InternalHeight); + Scale = smallerSide / 100; + + RollItems = new AvaloniaList( + new RollItem(0), new RollItem(10), new RollItem(20), new RollItem(30), new RollItem(45), + new RollItem(60), new RollItem(300), new RollItem(315), new RollItem(330), new RollItem(340), + new RollItem(350)); + + PitchItems = new AvaloniaList( + new PitchItem(135, Scale, false), new PitchItem(130, Scale), new PitchItem(125, Scale, false), + new PitchItem(120, Scale), + new PitchItem(115, Scale, false), new PitchItem(110, Scale), new PitchItem(105, Scale, false), + new PitchItem(100, Scale), + new PitchItem(95, Scale, false), new PitchItem(90, Scale), new PitchItem(85, Scale, false), + new PitchItem(80, Scale), + new PitchItem(75, Scale, false), new PitchItem(70, Scale), new PitchItem(65, Scale, false), + new PitchItem(60, Scale), + new PitchItem(55, Scale, false), new PitchItem(50, Scale), new PitchItem(45, Scale, false), + new PitchItem(40, Scale), + new PitchItem(35, Scale, false), new PitchItem(30, Scale), new PitchItem(25, Scale, false), + new PitchItem(20, Scale), + new PitchItem(15, Scale, false), new PitchItem(10, Scale), new PitchItem(5, Scale, false), + new PitchItem(0, Scale), + new PitchItem(-5, Scale, false), new PitchItem(-10, Scale), new PitchItem(-15, Scale, false), + new PitchItem(-20, Scale), + new PitchItem(-25, Scale, false), new PitchItem(-30, Scale), new PitchItem(-35, Scale, false), + new PitchItem(-40, Scale), + new PitchItem(-45, Scale, false), new PitchItem(-50, Scale), new PitchItem(-55, Scale, false), + new PitchItem(-60, Scale), + new PitchItem(-65, Scale, false), new PitchItem(-70, Scale), new PitchItem(-75, Scale, false), + new PitchItem(-80, Scale), + new PitchItem(-85, Scale, false), new PitchItem(-90, Scale), new PitchItem(-95, Scale, false), + new PitchItem(-100, Scale), + new PitchItem(-105, Scale, false), new PitchItem(-110, Scale), new PitchItem(-115, Scale, false), + new PitchItem(-120, Scale), + new PitchItem(-125, Scale, false), new PitchItem(-130, Scale), new PitchItem(-135, Scale, false) + ); + + var velocityControlLength = smallerSide * VelocityControlLengthPrc; + var velocityItemLength = velocityControlLength / (VelocityItemCount - 1); + VelocityItems = new AvaloniaList(Enumerable.Range(0, VelocityItemCount).Select(_ => + new ScaleItem(0, VelocityValueRange, _, VelocityItemCount, velocityControlLength + velocityItemLength, + velocityControlLength, showNegative: false))); + + var altitudeControlLength = smallerSide * AltitudeControlLengthPrc; + var altitudeItemLength = altitudeControlLength / (AltitudeItemCount - 1); + AltitudeItems = new AvaloniaList(Enumerable.Range(0, AltitudeItemCount).Select(_ => + new ScaleItem(0, AltitudeValueRange, _, AltitudeItemCount, altitudeControlLength + altitudeItemLength, + altitudeControlLength))); + + var headingControlLength = smallerSide * HeadingControlLengthPrc; + var headingItemLength = headingControlLength / (HeadingItemCount - 1); + HeadingItems = new AvaloniaList(Enumerable.Range(0, HeadingItemCount).Select(_ => + new HeadingScaleItem(0, HeadingValueRange, _, HeadingItemCount, + headingControlLength + headingItemLength, headingControlLength))); + + var headingItemStep = (headingControlLength + headingItemLength) / + (HeadingItemCount % 2 != 0 ? HeadingItemCount - 1 : HeadingItemCount); + _headingPositionStep = -1 * headingItemStep / HeadingValueRange; + _headingCenterPosition = headingControlLength / 2; + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + if (change.Property == VibrationXProperty) + { + UpdateColorX(change.Sender); + } + else if (change.Property == VibrationYProperty) + { + UpdateColorY(change.Sender); + } + else if (change.Property == VibrationZProperty) + { + UpdateColorZ(change.Sender); + } + else if (change.Property == VelocityProperty) + { + UpdateVelocityItems(change.Sender); + } + else if (change.Property == RollAngleProperty) + { + UpdateRollAngle(change.Sender); + } + else if (change.Property == PitchAngleProperty) + { + UpdateAngle(change.Sender); + } + else if (change.Property == AltitudeProperty) + { + UpdateAltitudeItems(change.Sender); + } + else if (change.Property == HeadingProperty) + { + UpdateHeadingItems(change.Sender); + } + else if (change.Property == HomeAzimuthProperty) + { + UpdateHomeAzimuthPosition(change.Sender); + } + else if (change.Property == HomeAzimuthProperty) + { + UpdateHomeAzimuthPosition(change.Sender); + } + } + + private static void UpdateColorX(AvaloniaObject source) + { + if (source is not AttitudeIndicator indicator) return; + + if (indicator.VibrationX < 30) + { + indicator.BrushVibrationX = Colors.Red; + } + else if (indicator.VibrationX > 30 & indicator.VibrationX < 60) + { + indicator.BrushVibrationX = Colors.Yellow; + } + else if (indicator.VibrationX > 60) + { + indicator.BrushVibrationX = Colors.GreenYellow; + } + } + + private static void UpdateColorY(AvaloniaObject source) + { + if (source is not AttitudeIndicator indicator) return; + + if (indicator.VibrationY < 30) + { + indicator.BrushVibrationY = Colors.Red; + } + else if (indicator.VibrationY > 30 & indicator.VibrationY < 60) + { + indicator.BrushVibrationY = Colors.Yellow; + } + else if (indicator.VibrationY > 60) + { + indicator.BrushVibrationY = Colors.GreenYellow; + } + } + + private static void UpdateColorZ(AvaloniaObject source) + { + if (source is not AttitudeIndicator indicator) return; + + if (indicator.VibrationZ < 30) + { + indicator.BrushVibrationZ = Colors.Red; + } + else if (indicator.VibrationZ > 30 & indicator.VibrationZ < 60) + { + indicator.BrushVibrationZ = Colors.Yellow; + } + else if (indicator.VibrationZ > 60) + { + indicator.BrushVibrationZ = Colors.GreenYellow; + } + } + + private static void UpdateAngle(AvaloniaObject source) + { + if (source is not AttitudeIndicator indicator) return; + var pitch = indicator.PitchAngle; + UpdateRollAngle(source); + foreach (var item in indicator.PitchItems) + { + item.UpdateVisibility(pitch); + } + } + + private static void UpdateRollAngle(AvaloniaObject source) + { + if (source is not AttitudeIndicator indicator) return; + var roll = indicator.RollAngle; + var pitch = indicator.PitchAngle; + indicator.PitchTranslateX = -pitch * indicator.Scale * Math.Cos((roll - 90.0) * Math.PI / 180.0); + indicator.PitchTranslateY = pitch * indicator.Scale * Math.Sin((90 - roll) * Math.PI / 180.0); + } + + private static void UpdateVelocityItems(AvaloniaObject source) + { + if (source is not AttitudeIndicator indicator) return; + var velocity = indicator.Velocity; + foreach (var item in indicator.VelocityItems) + { + item.UpdateValue(velocity); + } + } + + private static void UpdateAltitudeItems(AvaloniaObject source) + { + if (source is not AttitudeIndicator indicator) return; + var altitude = indicator.Altitude; + foreach (var item in indicator.AltitudeItems) + { + item.UpdateValue(altitude); + } + } + + private static void UpdateHeadingItems(AvaloniaObject source) + { + if (source is not AttitudeIndicator indicator) return; + var heading = indicator.Heading; + foreach (var item in indicator.HeadingItems) + { + item.UpdateValue(heading); + } + + indicator.HomeAzimuthPosition = GetHomeAzimuthPosition(indicator.HomeAzimuth, indicator.Heading); + } + + private static void UpdateHomeAzimuthPosition(AvaloniaObject source) + { + if (source is not AttitudeIndicator indicator) return; + indicator.HomeAzimuthPosition = GetHomeAzimuthPosition(indicator.HomeAzimuth, indicator.Heading); + } + + private static double GetHomeAzimuthPosition(double? value, double headingValue) + { + if (value == null) return -100; + + var distance = (headingValue - value.Value) % 360; + if (distance < -180) + distance += 360; + else if (distance > 179) + distance -= 360; + + return _headingCenterPosition + distance * _headingPositionStep; + } + } + + + public class ScaleItem : AvaloniaObject + { + private readonly double _valueRange; + private readonly bool _showNegative; + private readonly double _startPosition; + private readonly double _positionStep; + private readonly double _valueOffset; + private readonly bool _isFixedTitle; + private string _title; + private double _value; + private double _position; + private bool _isVisible; + + public ScaleItem(double value, double valueRange, int index, int itemCount, double fullLength, double length, + bool isInverse = false, bool showNegative = true, string fixedTitle = null) + { + _valueRange = valueRange; + _showNegative = showNegative; + _isFixedTitle = fixedTitle != null; + var step = fullLength / (itemCount % 2 != 0 ? itemCount - 1 : itemCount); + _positionStep = step / valueRange; + + if (!isInverse) + { + _startPosition = (length - fullLength) / 2.0 + step * index; + } + else + { + _startPosition = (length - fullLength) / 2.0 + step * (itemCount - index); + _positionStep *= -1; + } + + var centerIndex = itemCount % 2 == 0 ? itemCount / 2 : itemCount / 2 + 1; + + var indexOffset = index - centerIndex; + _valueOffset = -1 * valueRange * indexOffset; + + if (_isFixedTitle) Title = fixedTitle; + UpdateValue(value); + } + + public void UpdateValue(double value) + { + Value = GetValue(value); + Position = GetPosition(value); + if (!_isFixedTitle) + Title = GetTitle(Value); + IsVisible = _showNegative || Value >= 0; + } + + protected virtual string GetTitle(double value) + { + return Math.Round(value).ToString("F0"); + } + + private double GetValue(double value) + { + return Math.Round(value) - Math.Round(value) % _valueRange + _valueOffset; + } + + private double GetPosition(double value) + { + return _startPosition + _positionStep * (Math.Round(value) % _valueRange); + } + + public static readonly DirectProperty IsVisibleProperty = + AvaloniaProperty.RegisterDirect(nameof(IsVisible), _ => _.IsVisible, + (_, value) => _.IsVisible = value); + + public bool IsVisible + { + get => _isVisible; + set => SetAndRaise(IsVisibleProperty, ref _isVisible, value); + } + + public static readonly DirectProperty TitleProperty = + AvaloniaProperty.RegisterDirect(nameof(Title), _ => _.Title, + (_, value) => _.Title = value); + + public string Title + { + get => _title; + set => SetAndRaise(TitleProperty, ref _title, value); + } + + public static readonly DirectProperty ValueProperty = + AvaloniaProperty.RegisterDirect(nameof(Value), _ => _.Value, + (_, value) => _.Value = value); + + public double Value + { + get => _value; + set => SetAndRaise(ValueProperty, ref _value, value); + } + + public static readonly DirectProperty PositionProperty = + AvaloniaProperty.RegisterDirect(nameof(Position), _ => _.Position, + (_, value) => _.Position = value); + + public double Position + { + get => _position; + set => SetAndRaise(PositionProperty, ref _position, value); + } + } + + public class HeadingScaleItem : ScaleItem + { + public HeadingScaleItem(double value, double valueRange, int index, int itemCount, double fullLength, + double length) : base(value, valueRange, index, itemCount, fullLength, length, true) + { + } + + protected override string GetTitle(double value) + { + var v = value < 0 ? ((int)Math.Round(value) % 360) + 360 : (int)Math.Round(value) % 360; + return v switch + { + 0 => "N", + 45 => "NE", + 90 => "E", + 135 => "SE", + 180 => "S", + 225 => "SW", + 270 => "W", + 315 => "NW", + 360 => "N", + _ => v.ToString("F0") + }; + } + } + + public class RollItem : AvaloniaObject + { + public RollItem(int angle) + { + Value = angle; + Title = Math.Abs(angle) > 180 ? (360 - Math.Abs(angle)).ToString() : Math.Abs(angle).ToString(); + } + + public static readonly DirectProperty TitleProperty = + AvaloniaProperty.RegisterDirect(nameof(Title), _ => _.Title, + (_, value) => _.Title = value); + + private string _title; + private double _value; + + public string Title + { + get => _title; + set => SetAndRaise(TitleProperty, ref _title, value); + } + + public static readonly DirectProperty ValueProperty = + AvaloniaProperty.RegisterDirect(nameof(Value), _ => _.Value, + (_, value) => _.Value = value); + + public double Value + { + get => _value; + set => SetAndRaise(ValueProperty, ref _value, value); + } + } + + public class PitchItem : AvaloniaObject + { + private readonly int _pitch; + private string _title; + private double _value; + private Point _startLine; + private Point _stopLine; + private bool _isVisible; + + public PitchItem(int pitch, double scale, bool titleIsVisible = true, double controlHeight = 284) + { + _pitch = pitch; + Value = (controlHeight / 2 - pitch) * scale; + if (titleIsVisible) + { + Title = pitch.ToString(); + StartLine = new Point(0 * scale, 0 * scale); + StopLine = new Point(20 * scale, 0 * scale); + } + else + { + Title = null; + StartLine = new Point(4 * scale, 0 * scale); + StopLine = new Point(16 * scale, 0 * scale); + } + + IsVisible = Math.Abs(pitch) <= 20; + } + + public static readonly DirectProperty TitleProperty = + AvaloniaProperty.RegisterDirect(nameof(Title), _ => _.Title, + (_, value) => _.Title = value); + + public string Title + { + get => _title; + set => SetAndRaise(TitleProperty, ref _title, value); + } + + public static readonly DirectProperty ValueProperty = + AvaloniaProperty.RegisterDirect(nameof(Value), _ => _.Value, + (_, value) => _.Value = value); + + public double Value + { + get => _value; + set => SetAndRaise(ValueProperty, ref _value, value); + } + + public static readonly DirectProperty IsVisibleProperty = + AvaloniaProperty.RegisterDirect(nameof(IsVisible), _ => _.IsVisible, + (_, value) => _.IsVisible = value); + + + public bool IsVisible + { + get => _isVisible; + set => SetAndRaise(IsVisibleProperty, ref _isVisible, value); + } + + public static readonly DirectProperty StartLineProperty = + AvaloniaProperty.RegisterDirect(nameof(StartLine), _ => _.StartLine, + (_, value) => _.StartLine = value); + + public Point StartLine + { + get => _startLine; + set => SetAndRaise(StartLineProperty, ref _startLine, value); + } + + public static readonly DirectProperty StopLineProperty = + AvaloniaProperty.RegisterDirect(nameof(StopLine), _ => _.StopLine, + (_, value) => _.StopLine = value); + + public Point StopLine + { + get => _stopLine; + set => SetAndRaise(StopLineProperty, ref _stopLine, value); + } + + public void UpdateVisibility(double pitch) + { + IsVisible = pitch >= _pitch - 20 && pitch <= _pitch + 20; + } + } +} \ No newline at end of file diff --git a/src/Asv.Drones.Gui.Api/Tools/Controls/HierarchicalStore/HierarchicalStoreEntryTagViewModel.cs b/src/Asv.Drones.Gui.Api/Tools/Controls/HierarchicalStore/HierarchicalStoreEntryTagViewModel.cs new file mode 100644 index 0000000..a00f6f7 --- /dev/null +++ b/src/Asv.Drones.Gui.Api/Tools/Controls/HierarchicalStore/HierarchicalStoreEntryTagViewModel.cs @@ -0,0 +1,14 @@ +using System.Windows.Input; +using Avalonia.Media; +using Material.Icons; +using ReactiveUI; + +namespace Asv.Drones.Gui.Api; + +public class HierarchicalStoreEntryTagViewModel : ReactiveObject +{ + public MaterialIconKind Icon { get; set; } = MaterialIconKind.Tag; + public IBrush Color { get; set; } + public string Name { get; set; } + public ICommand? Remove { get; set; } +} \ No newline at end of file diff --git a/src/Asv.Drones.Gui.Api/Tools/Controls/HierarchicalStore/HierarchicalStoreEntryViewModel.cs b/src/Asv.Drones.Gui.Api/Tools/Controls/HierarchicalStore/HierarchicalStoreEntryViewModel.cs new file mode 100644 index 0000000..0797647 --- /dev/null +++ b/src/Asv.Drones.Gui.Api/Tools/Controls/HierarchicalStore/HierarchicalStoreEntryViewModel.cs @@ -0,0 +1,207 @@ +using System.Collections.ObjectModel; +using System.Reactive; +using Asv.Common; +using Asv.Mavlink; +using DynamicData; +using DynamicData.Binding; +using ReactiveUI; +using ReactiveUI.Fody.Helpers; + +namespace Asv.Drones.Gui.Api; + +public enum HierarchicalStoreEntryAction +{ + Rename, + Delete +} + +public class HierarchicalStoreEntryViewModel : DisposableReactiveObject +{ + public HierarchicalStoreEntryViewModel() + { + BeginEditName = ReactiveCommand.Create(() => { IsInEditNameMode = true; }).DisposeItWith(Disposable); + ; + EndEditName = ReactiveCommand.Create(() => + { + IsInEditNameMode = false; + Rename(Name); + }).DisposeItWith(Disposable); + EndEditName.ThrownExceptions + .Subscribe(ex => OnError(HierarchicalStoreEntryAction.Rename, ex)) + .DisposeItWith(Disposable); + DeleteEntry = ReactiveCommand.Create(Delete) + .DisposeItWith(Disposable); + DeleteEntry.ThrownExceptions + .Subscribe(ex => OnError(HierarchicalStoreEntryAction.Delete, ex)) + .DisposeItWith(Disposable); + + BeginMove = ReactiveCommand.Create(() => { IsInMoveMode = true; }).DisposeItWith(Disposable); + CancelMove = ReactiveCommand.Create(() => { IsInMoveMode = false; }).DisposeItWith(Disposable); + EndMove = ReactiveCommand.Create(() => + { + IsInMoveMode = false; + Move(); + }).DisposeItWith(Disposable); + + this.WhenValueChanged(x => x.IsInMoveMode) + .Subscribe(x => IsNotInMoveMode = !x) + .DisposeItWith(Disposable); + this.WhenValueChanged(x => x.IsNotInMoveMode) + .Subscribe(x => IsInMoveMode = !x) + .DisposeItWith(Disposable); + } + + + [Reactive] public bool IsInMoveMode { get; set; } + [Reactive] public bool IsNotInMoveMode { get; set; } + public object Id { get; set; } + public object ParentId { get; set; } + public bool IsFolder { get; set; } + public bool IsFile { get; set; } + public FolderStoreEntryType Type { get; set; } + [Reactive] public string Name { get; set; } + [Reactive] public bool IsExpanded { get; set; } + [Reactive] public bool IsSelected { get; set; } + + public virtual ReadOnlyObservableCollection Items { get; set; } + [Reactive] public bool IsInEditNameMode { get; set; } = false; + + public ReactiveCommand DeleteEntry { get; } + public ReactiveCommand BeginEditName { get; } + public ReactiveCommand EndEditName { get; } + public ReactiveCommand BeginMove { get; } + public ReactiveCommand EndMove { get; } + public ReactiveCommand CancelMove { get; } + + public virtual ReadOnlyObservableCollection Tags { get; set; } + + [Reactive] public string Description { get; set; } + + + protected virtual void OnError(HierarchicalStoreEntryAction action, Exception? exception) + { + } + + protected virtual void Rename(string? name) + { + } + + protected virtual void Delete() + { + } + + protected virtual void Move() + { + } + + public virtual void Refresh() + { + } + + public HierarchicalStoreEntryViewModel? FindAndSelect(object id) + { + if (id.Equals(Id)) + { + IsExpanded = true; + IsSelected = true; + return this; + } + else + { + foreach (var item in Items) + { + var find = item.FindAndSelect(id); + if (find == null) continue; + IsExpanded = true; + return find; + } + } + + return null; + } +} + +public class HierarchicalStoreEntryViewModel : HierarchicalStoreEntryViewModel + where TKey : notnull + where TFile : IDisposable +{ + private readonly Node, TKey> _node; + private readonly HierarchicalStoreViewModel _context; + private readonly ILogService _log; + private readonly ReadOnlyObservableCollection _items; + private ReadOnlyObservableCollection _tags; + private readonly SourceCache _tagsSource; + private bool _isAlreadyFillTags; + + public HierarchicalStoreEntryViewModel(Node, TKey> node, + HierarchicalStoreViewModel context, ILogService log) + { + _node = node; + _context = context; + _log = log; + node.Children.Connect() + .Transform(x => + (HierarchicalStoreEntryViewModel)new HierarchicalStoreEntryViewModel(x, context, log)) + .Bind(out _items) + .DisposeMany() + .Subscribe() + .DisposeItWith(Disposable); + _tagsSource = new SourceCache(x => x.Name) + .DisposeItWith(Disposable); + _tagsSource.Connect() + .Bind(out _tags) + .Subscribe() + .DisposeItWith(Disposable); + IsFolder = node.Item.Type == FolderStoreEntryType.Folder; + IsFile = node.Item.Type == FolderStoreEntryType.File; + Name = node.Item.Name ?? ""; + Type = node.Item.Type; + Id = node.Item.Id; + Description = context.GetEntryDescription(node.Item); + ParentId = node.Item.ParentId; + } + + protected override void OnError(HierarchicalStoreEntryAction action, Exception? ex) + { + base.OnError(action, ex); + _log.Error("Store", $"Error to '{action}' entry", ex); + } + + protected override void Rename(string? name) + { + base.Rename(name); + if (name != null) + { + _context.RenameEntryImpl(_node.Item.Id, name); + } + } + + protected override void Delete() + { + base.Delete(); + _context.DeleteEntryImpl(_node.Item.Id); + } + + protected override void Move() + { + base.Move(); + if (_context.SelectedItemMoveTo == null) return; + if (_context.SelectedItem == null) return; + _context.MoveEntryImpl((TKey)_context.SelectedItem.Id, (TKey)_context.SelectedItemMoveTo.Id); + } + + public override void Refresh() + { + base.Refresh(); + if (_isAlreadyFillTags == false) + { + _tagsSource.Clear(); + _tagsSource.AddOrUpdate(_context.GetEntryTags((TKey)Id)); + _isAlreadyFillTags = true; + } + } + + public override ReadOnlyObservableCollection Items => _items; + + public override ReadOnlyObservableCollection Tags => _tags; +} \ No newline at end of file diff --git a/src/Asv.Drones.Gui.Api/Tools/Controls/HierarchicalStore/HierarchicalStoreView.axaml b/src/Asv.Drones.Gui.Api/Tools/Controls/HierarchicalStore/HierarchicalStoreView.axaml new file mode 100644 index 0000000..0578b0c --- /dev/null +++ b/src/Asv.Drones.Gui.Api/Tools/Controls/HierarchicalStore/HierarchicalStoreView.axaml @@ -0,0 +1,278 @@ + + + + + + 18 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Asv.Drones.Gui.Api/Tools/Controls/HierarchicalStore/HierarchicalStoreView.axaml.cs b/src/Asv.Drones.Gui.Api/Tools/Controls/HierarchicalStore/HierarchicalStoreView.axaml.cs new file mode 100644 index 0000000..e6e3a50 --- /dev/null +++ b/src/Asv.Drones.Gui.Api/Tools/Controls/HierarchicalStore/HierarchicalStoreView.axaml.cs @@ -0,0 +1,11 @@ +using Avalonia.ReactiveUI; + +namespace Asv.Drones.Gui.Api; + +public partial class HierarchicalStoreView : ReactiveUserControl +{ + public HierarchicalStoreView() + { + InitializeComponent(); + } +} \ No newline at end of file diff --git a/src/Asv.Drones.Gui.Api/Tools/Controls/HierarchicalStore/HierarchicalStoreViewModel.cs b/src/Asv.Drones.Gui.Api/Tools/Controls/HierarchicalStore/HierarchicalStoreViewModel.cs new file mode 100644 index 0000000..b536afa --- /dev/null +++ b/src/Asv.Drones.Gui.Api/Tools/Controls/HierarchicalStore/HierarchicalStoreViewModel.cs @@ -0,0 +1,370 @@ +using System.Collections.ObjectModel; +using System.Globalization; +using System.Reactive; +using System.Reactive.Linq; +using System.Reactive.Subjects; +using Asv.Common; +using Asv.Mavlink; +using Avalonia.Media; +using DynamicData; +using DynamicData.Binding; +using ReactiveUI; +using ReactiveUI.Fody.Helpers; + +namespace Asv.Drones.Gui.Api; + +public class HierarchicalStoreViewModel : ViewModelBase +{ + protected HierarchicalStoreViewModel(Uri id) : base(id) + { + CreateNewFile = ReactiveCommand.Create(CreateNewFileImpl) + .DisposeItWith(Disposable); + CreateNewFile.ThrownExceptions + .Subscribe(ex => OnError(HierarchicalStoreAction.CreateFile, ex)) + .DisposeItWith(Disposable); + + CreateNewFolder = ReactiveCommand.Create(CreateNewFolderImpl) + .DisposeItWith(Disposable); + CreateNewFolder.ThrownExceptions + .Subscribe(ex => OnError(HierarchicalStoreAction.CreateFolder, ex)) + .DisposeItWith(Disposable); + + Refresh = ReactiveCommand.Create(() => + { + var selectedId = SelectedItem?.Id; + var selectedParentId = SelectedItem?.ParentId; + RefreshImpl(); + if (TrySelect(selectedId) == false) + { + TrySelect(selectedParentId); + } + }) + .DisposeItWith(Disposable); + Refresh.ThrownExceptions + .Subscribe(ex => OnError(HierarchicalStoreAction.Refresh, ex)) + .DisposeItWith(Disposable); + } + + + public HierarchicalStoreViewModel() : this(WellKnownUri.UndefinedUri) + { + DesignTime.ThrowIfNotDesignMode(); + Items = new ReadOnlyObservableCollection( + new ObservableCollection(new List + { + new() + { + Id = Guid.NewGuid(), + Name = "Record1", + Tags = new ReadOnlyObservableCollection( + new ObservableCollection( + new List + { + new() + { + Color = Brushes.CornflowerBlue, + Name = "Latitude: 55.1234567", + }, + new() + { + Color = Brushes.DarkOrange, + Name = "Short", + }, + new() + { + Color = new SolidColorBrush(Color.Parse("#FBC02D")), + Name = "Longitude: 66.1234567", + }, + new() + { + Color = new SolidColorBrush(Color.Parse("#FE8256")), + Name = "Longitude: 66.1234567", + }, + new() + { + Color = new SolidColorBrush(Color.Parse("#ACC865")), + Name = "ACC865:66.1234567", + }, + new() + { + Color = new SolidColorBrush(Color.Parse("#CD91B6")), + Name = "ShortShort", + }, + })), + IsFile = true, + }, + new() + { + Id = Guid.NewGuid(), + Name = "Folder", + IsFolder = true, + Items = new ReadOnlyObservableCollection( + new ObservableCollection( + new List + { + new() + { + Id = Guid.NewGuid(), + Name = "Record 2", + IsFile = true, + }, + })) + }, + })); + FolderItems = new ReadOnlyObservableCollection( + new ObservableCollection(new List + { + new() + { + Id = Guid.NewGuid(), + Name = "Folder", + IsFolder = true, + Items = new ReadOnlyObservableCollection( + new ObservableCollection( + new List + { + new() + { + Id = Guid.NewGuid(), + Name = "Folder 2", + IsFolder = true, + }, + })) + }, + })); + } + + [Reactive] public string SearchText { get; set; } + [Reactive] public string DisplayName { get; set; } + public virtual ReadOnlyObservableCollection Items { get; } + public virtual ReadOnlyObservableCollection FolderItems { get; } + [Reactive] public HierarchicalStoreEntryViewModel? SelectedItem { get; set; } + public ReactiveCommand CreateNewFolder { get; set; } + public ReactiveCommand CreateNewFile { get; set; } + public ReactiveCommand Refresh { get; set; } + public bool IsHeaderVisible { get; set; } = true; + + public bool TrySelect(object? selectedId) + { + if (selectedId == null) return false; + foreach (var item in Items) + { + var find = item.FindAndSelect(selectedId); + if (find == null) continue; + find.IsSelected = true; + return true; + } + + return false; + } + + protected virtual void RefreshImpl() + { + } + + protected virtual void CreateNewFileImpl() + { + } + + protected virtual void CreateNewFolderImpl() + { + } + + protected virtual void OnError(HierarchicalStoreAction action, Exception? ex) + { + } + + [Reactive] public bool IsCreateFolderAvailable { get; set; } = true; + [Reactive] public bool IsCreateFileAvailable { get; set; } = true; + + [Reactive] public HierarchicalStoreEntryViewModel? SelectedItemMoveTo { get; set; } +} + +public abstract class HierarchicalStoreViewModel : HierarchicalStoreViewModel + where TFile : IDisposable where TKey : notnull +{ + private readonly IHierarchicalStore _store; + private readonly ILogService _log; + private readonly SourceCache, TKey> _source; + private readonly ReadOnlyObservableCollection _tree; + private readonly ReadOnlyObservableCollection _treeFolder; + + + public HierarchicalStoreViewModel() : base(WellKnownUri.UndefinedUri) + { + DesignTime.ThrowIfNotDesignMode(); + } + + public HierarchicalStoreViewModel(Uri id, IHierarchicalStore store, ILogService log) : base(id) + { + _store = store; + _log = log; + _source = new SourceCache, TKey>(x => x.Id) + .DisposeItWith(Disposable); + var filterPipe = new Subject, bool>>() + .DisposeItWith(Disposable); + this.WhenValueChanged(x => x.SearchText) + .Throttle(TimeSpan.FromMilliseconds(500), RxApp.MainThreadScheduler) + .Subscribe(search => filterPipe.OnNext(item => search.IsNullOrWhiteSpace() || item.Name.Contains(search))) + .DisposeItWith(Disposable); + + _source + .Connect() + .Filter(filterPipe) + .TransformToTree(x => x.ParentId) + .Transform(x => + (HierarchicalStoreEntryViewModel)new HierarchicalStoreEntryViewModel(x, this, log)) + .DisposeMany() + .Bind(out _tree) + .Subscribe() + .DisposeItWith(Disposable); + + _source + .Connect() + .Filter(x => x.Type == FolderStoreEntryType.Folder) + .TransformToTree(x => x.ParentId) + .Transform(x => + (HierarchicalStoreEntryViewModel)new HierarchicalStoreEntryViewModel(x, this, log)) + .DisposeMany() + .Bind(out _treeFolder) + .Subscribe() + .DisposeItWith(Disposable); + + this.WhenValueChanged(x => x.SelectedItem) + .Subscribe(x => x?.Refresh()).DisposeItWith(Disposable); + } + + protected override void OnError(HierarchicalStoreAction action, Exception? ex) + { + _log.Error(DisplayName, $"Error to '{action:G}'", ex); + } + + protected override void CreateNewFolderImpl() + { + TKey parentId; + if (SelectedItem != null) + { + parentId = + (TKey)(SelectedItem.Type == FolderStoreEntryType.Folder ? SelectedItem.Id : SelectedItem.ParentId); + } + else + { + parentId = _store.RootFolderId; + } + + var attempt = 0; + start: + var name = $"New folder {++attempt}"; + try + { + _store.CreateFolder(GenerateNewId(), name, parentId); + } + catch (HierarchicalStoreFolderAlreadyExistException) + { + goto start; + } + + Refresh.Execute().Subscribe(_ => { }); + } + + protected abstract TKey GenerateNewId(); + + protected override void CreateNewFileImpl() + { + TKey parentId; + if (SelectedItem != null) + { + parentId = + (TKey)(SelectedItem.Type == FolderStoreEntryType.Folder ? SelectedItem.Id : SelectedItem.ParentId); + } + else + { + parentId = _store.RootFolderId; + } + + var attempt = 0; + start: + var name = $"New file {++attempt}"; + try + { + using var file = _store.CreateFile(GenerateNewId(), name, parentId); + } + catch (HierarchicalStoreFolderAlreadyExistException) + { + goto start; + } + + Refresh.Execute().Subscribe(_ => { }); + } + + protected override void RefreshImpl() + { + _source.Clear(); + _source.AddOrUpdate(_store.GetEntries()); + } + + + public override ReadOnlyObservableCollection Items => _tree; + public override ReadOnlyObservableCollection FolderItems => _treeFolder; + + public void DeleteEntryImpl(TKey itemId) + { + _store.DeleteEntry(itemId); + Refresh.Execute().Subscribe(); + } + + public void RenameEntryImpl(TKey itemId, string name) + { + _store.RenameEntry(itemId, name); + Refresh.Execute().Subscribe(); + } + + public void MoveEntryImpl(TKey id, TKey parentId) + { + _store.MoveEntry(id, parentId); + Refresh.Execute().Subscribe(); + } + + public IReadOnlyCollection GetEntryTags(TKey id) + { + var item = _source.Lookup(id); + return !item.HasValue + ? ArraySegment.Empty + : InternalGetEntryTags(item.Value); + } + + protected virtual IReadOnlyCollection InternalGetEntryTags( + IHierarchicalStoreEntry itemValue) + { + return ArraySegment.Empty; + } + + + public virtual string GetEntryDescription(IHierarchicalStoreEntry nodeItem) + { + var entry = nodeItem as FileSystemHierarchicalStoreEntry; + + if (entry == null) return string.Empty; + + if (nodeItem.Type == FolderStoreEntryType.Folder) + { + var info = new DirectoryInfo(entry.FullPath); + return info.CreationTime.ToString(CultureInfo.CurrentCulture); + } + + if (nodeItem.Type == FolderStoreEntryType.File) + { + var info = new FileInfo(entry.FullPath); + return info.CreationTime.ToString(CultureInfo.CurrentCulture); + } + + return string.Empty; + } +} + +public enum HierarchicalStoreAction +{ + Refresh, + CreateFile, + CreateFolder +} \ No newline at end of file diff --git a/src/Asv.Drones.Gui.Api/Tools/Controls/Indicators/BatteryIndicator.axaml b/src/Asv.Drones.Gui.Api/Tools/Controls/Indicators/BatteryIndicator.axaml new file mode 100644 index 0000000..da0c179 --- /dev/null +++ b/src/Asv.Drones.Gui.Api/Tools/Controls/Indicators/BatteryIndicator.axaml @@ -0,0 +1,48 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/src/Asv.Drones.Gui.Api/Tools/Controls/Indicators/BatteryIndicator.axaml.cs b/src/Asv.Drones.Gui.Api/Tools/Controls/Indicators/BatteryIndicator.axaml.cs new file mode 100644 index 0000000..ee0575f --- /dev/null +++ b/src/Asv.Drones.Gui.Api/Tools/Controls/Indicators/BatteryIndicator.axaml.cs @@ -0,0 +1,102 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Metadata; +using Material.Icons; + +namespace Asv.Drones.Gui.Api; + +[PseudoClasses(":critical", ":warning", ":normal", ":unknown")] +public class BatteryIndicator : IndicatorBase +{ + #region Styled Props + + public static readonly StyledProperty CriticalValueProperty = + AvaloniaProperty.Register( + nameof(CriticalValue), 20); + + public double CriticalValue + { + get => GetValue(CriticalValueProperty); + set => SetValue(CriticalValueProperty, value); + } + + public static readonly StyledProperty WarningValueProperty = + AvaloniaProperty.Register( + nameof(WarningValue), 50); + + public double WarningValue + { + get => GetValue(WarningValueProperty); + set => SetValue(WarningValueProperty, value); + } + + public static readonly StyledProperty MaxValueProperty = + AvaloniaProperty.Register( + nameof(MaxValue), 100); + + public double MaxValue + { + get => GetValue(MaxValueProperty); + set => SetValue(MaxValueProperty, value); + } + + public static readonly StyledProperty ValueProperty = AvaloniaProperty.Register( + nameof(Value), default(double?)); + + public double? Value + { + get => GetValue(ValueProperty); + set => SetValue(ValueProperty, value); + } + + #endregion + + public BatteryIndicator() + { + } + + private static void SetPseudoClass(BatteryIndicator indicator) + { + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + if (change.Property == ValueProperty || change.Property == MaxValueProperty || + change.Property == CriticalValueProperty || change.Property == WarningValueProperty) + { + var value = Value; + PseudoClasses.Set(":unknown", value == null || double.IsFinite(value.Value) == false || value > MaxValue); + PseudoClasses.Set(":critical", value <= CriticalValue); + PseudoClasses.Set(":warning", value > CriticalValue & value <= WarningValue); + PseudoClasses.Set(":normal", value > WarningValue & value <= MaxValue); + if (MaxValue == 0 || double.IsFinite(MaxValue) == false) + { + } + + IconKind = GetIcon(Value / MaxValue); + } + } + + private static MaterialIconKind GetIcon(double? normalizedValue) + { + return (normalizedValue ?? double.NaN) switch + { + (< 0 or > 1 + or double.NegativeInfinity + or double.PositiveInfinity + or double.NaN) => MaterialIconKind.BatteryUnknown, + (0) => MaterialIconKind.Battery0, + (> 0 and <= 0.10) => MaterialIconKind.Battery10, + (> 0.10 and <= 0.20) => MaterialIconKind.Battery20, + (> 0.20 and <= 0.30) => MaterialIconKind.Battery30, + (> 0.30 and <= 0.40) => MaterialIconKind.Battery40, + (> 0.40 and <= 0.50) => MaterialIconKind.Battery50, + (> 0.50 and <= 0.60) => MaterialIconKind.Battery60, + (> 0.60 and <= 0.70) => MaterialIconKind.Battery70, + (> 0.70 and <= 0.80) => MaterialIconKind.Battery80, + (> 0.80 and <= 0.90) => MaterialIconKind.Battery90, + (> 0.90 and <= 1) => MaterialIconKind.Battery100 + }; + } +} \ No newline at end of file diff --git a/src/Asv.Drones.Gui.Api/Tools/Controls/Indicators/ConnectionQuality.axaml b/src/Asv.Drones.Gui.Api/Tools/Controls/Indicators/ConnectionQuality.axaml new file mode 100644 index 0000000..83c8972 --- /dev/null +++ b/src/Asv.Drones.Gui.Api/Tools/Controls/Indicators/ConnectionQuality.axaml @@ -0,0 +1,49 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Asv.Drones.Gui.Api/Tools/Controls/Indicators/ConnectionQuality.axaml.cs b/src/Asv.Drones.Gui.Api/Tools/Controls/Indicators/ConnectionQuality.axaml.cs new file mode 100644 index 0000000..db217ad --- /dev/null +++ b/src/Asv.Drones.Gui.Api/Tools/Controls/Indicators/ConnectionQuality.axaml.cs @@ -0,0 +1,100 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Metadata; +using Material.Icons; + +namespace Asv.Drones.Gui.Api; + +[PseudoClasses(":critical", ":warning", ":normal", ":unknown")] +public class ConnectionQuality : IndicatorBase +{ + #region Styled Props + + public static readonly StyledProperty CriticalValueProperty = + AvaloniaProperty.Register( + nameof(CriticalValue), 0.2); + + public double CriticalValue + { + get => GetValue(CriticalValueProperty); + set => SetValue(CriticalValueProperty, value); + } + + public static readonly StyledProperty WarningValueProperty = + AvaloniaProperty.Register( + nameof(WarningValue), 0.5); + + public double WarningValue + { + get => GetValue(WarningValueProperty); + set => SetValue(WarningValueProperty, value); + } + + public static readonly StyledProperty MaxValueProperty = + AvaloniaProperty.Register( + nameof(MaxValue), 1); + + public double MaxValue + { + get => GetValue(MaxValueProperty); + set => SetValue(MaxValueProperty, value); + } + + public static readonly StyledProperty ValueProperty = + AvaloniaProperty.Register( + nameof(Value), default(double?)); + + public double? Value + { + get => GetValue(ValueProperty); + set => SetValue(ValueProperty, value); + } + + public static readonly StyledProperty IconKindProperty = + AvaloniaProperty.Register( + nameof(IconKind), MaterialIconKind.WifiStrengthAlertOutline); + + public MaterialIconKind IconKind + { + get => GetValue(IconKindProperty); + set => SetValue(IconKindProperty, value); + } + + #endregion + + public ConnectionQuality() + { + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + if (change.Property == ValueProperty) + { + var value = Value; + PseudoClasses.Set(":unknown", value == null || double.IsFinite(value.Value) == false || value > MaxValue); + PseudoClasses.Set(":critical", value <= CriticalValue); + PseudoClasses.Set(":warning", value > CriticalValue & value <= WarningValue); + PseudoClasses.Set(":normal", value > WarningValue & value <= MaxValue); + IconKind = GetIcon(Value / MaxValue); + } + } + + + private static MaterialIconKind GetIcon(double? normalizedValue) + { + return (normalizedValue ?? double.NaN) switch + { + (< 0 or > 1 + or double.NegativeInfinity + or double.PositiveInfinity + or double.NaN) => MaterialIconKind.WifiStrengthAlertOutline, + (0) => MaterialIconKind.WifiStrength0, + (> 0 and <= 0.2) => MaterialIconKind.WifiStrength0, + (> 0.2 and <= 0.4) => MaterialIconKind.WifiStrength1, + (> 0.4 and <= 0.6) => MaterialIconKind.WifiStrength2, + (> 0.6 and <= 0.8) => MaterialIconKind.WifiStrength3, + (> 0.8 and <= 1.0) => MaterialIconKind.WifiStrength4, + }; + } +} \ No newline at end of file diff --git a/src/Asv.Drones.Gui.Api/Tools/Controls/Indicators/DigitIndicator.axaml b/src/Asv.Drones.Gui.Api/Tools/Controls/Indicators/DigitIndicator.axaml new file mode 100644 index 0000000..03d08f3 --- /dev/null +++ b/src/Asv.Drones.Gui.Api/Tools/Controls/Indicators/DigitIndicator.axaml @@ -0,0 +1,28 @@ + + + + + + + \ No newline at end of file diff --git a/src/Asv.Drones.Gui.Api/Tools/Controls/Indicators/DigitIndicator.axaml.cs b/src/Asv.Drones.Gui.Api/Tools/Controls/Indicators/DigitIndicator.axaml.cs new file mode 100644 index 0000000..2b8c181 --- /dev/null +++ b/src/Asv.Drones.Gui.Api/Tools/Controls/Indicators/DigitIndicator.axaml.cs @@ -0,0 +1,119 @@ +using Avalonia; + +namespace Asv.Drones.Gui.Api; + +public class DigitIndicator : IndicatorBase +{ + #region Direct properties + + private string _title; + + public static readonly DirectProperty TitleProperty = + AvaloniaProperty.RegisterDirect( + nameof(Title), o => o.Title, (o, v) => o.Title = v); + + public string Title + { + get => _title; + set => SetAndRaise(TitleProperty, ref _title, value); + } + + private string _units; + + public static readonly DirectProperty UnitsProperty = + AvaloniaProperty.RegisterDirect( + nameof(Units), o => o.Units, (o, v) => o.Units = v); + + public string Units + { + get => _units; + set => SetAndRaise(UnitsProperty, ref _units, value); + } + + private double _value; + + public static readonly DirectProperty ValueProperty = + AvaloniaProperty.RegisterDirect( + nameof(Value), o => o.Value, (o, v) => o.Value = v); + + public double Value + { + get => _value; + set + { + if (Math.Abs(value - _value) < double.Epsilon) + { + IsDecreased = false; + IsIncreased = false; + } + else if (value > _value) + { + IsDecreased = false; + IsIncreased = true; + } + else if (value < _value) + { + IsDecreased = true; + IsIncreased = false; + } + + SetAndRaise(ValueProperty, ref _value, value); + FormatedValue = Value.ToString(FormatString); + } + } + + private string _formatString; + + public static readonly DirectProperty FormatStringProperty = + AvaloniaProperty.RegisterDirect( + nameof(FormatString), o => o.FormatString, (o, v) => o.FormatString = v); + + public string FormatString + { + get => _formatString; + set => SetAndRaise(FormatStringProperty, ref _formatString, value); + } + + private bool _isIncreased; + + public static readonly DirectProperty IsIncreasedProperty = + AvaloniaProperty.RegisterDirect( + nameof(IsIncreased), o => o.IsIncreased, (o, v) => o.IsIncreased = v); + + public bool IsIncreased + { + get => _isIncreased; + set => SetAndRaise(IsIncreasedProperty, ref _isIncreased, value); + } + + private bool _isDecreased; + + public static readonly DirectProperty IsDecreasedProperty = + AvaloniaProperty.RegisterDirect( + nameof(IsDecreased), o => o.IsDecreased, (o, v) => o.IsDecreased = v); + + public bool IsDecreased + { + get => _isDecreased; + set => SetAndRaise(IsDecreasedProperty, ref _isDecreased, value); + } + + #endregion + + private string _formatedValue; + + public static readonly DirectProperty FormatedValueProperty = + AvaloniaProperty.RegisterDirect( + nameof(FormatedValue), o => o.FormatedValue, (o, v) => o.FormatedValue = v); + + public string FormatedValue + { + get => _formatedValue; + set => SetAndRaise(FormatedValueProperty, ref _formatedValue, value); + } + + public DigitIndicator() + { + FormatString = ""; + } +} \ No newline at end of file diff --git a/src/Asv.Drones.Gui.Api/Tools/Controls/Indicators/GpsStatusIndicator.axaml b/src/Asv.Drones.Gui.Api/Tools/Controls/Indicators/GpsStatusIndicator.axaml new file mode 100644 index 0000000..e1515b5 --- /dev/null +++ b/src/Asv.Drones.Gui.Api/Tools/Controls/Indicators/GpsStatusIndicator.axaml @@ -0,0 +1,48 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/src/Asv.Drones.Gui.Api/Tools/Controls/Indicators/GpsStatusIndicator.axaml.cs b/src/Asv.Drones.Gui.Api/Tools/Controls/Indicators/GpsStatusIndicator.axaml.cs new file mode 100644 index 0000000..6587f2d --- /dev/null +++ b/src/Asv.Drones.Gui.Api/Tools/Controls/Indicators/GpsStatusIndicator.axaml.cs @@ -0,0 +1,91 @@ +using Asv.Mavlink; +using Asv.Mavlink.V2.Common; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Metadata; +using Material.Icons; + +namespace Asv.Drones.Gui.Api; + +[PseudoClasses(":critical", ":warning", ":normal", ":unknown")] +public class GpsStatusIndicator : IndicatorBase +{ + #region Styled Props + + public static readonly StyledProperty FixTypeProperty = + AvaloniaProperty.Register( + nameof(FixType), GpsFixType.GpsFixTypeNoGps, coerce: UpdateValue); + + public GpsFixType FixType + { + get => GetValue(FixTypeProperty); + set => SetValue(FixTypeProperty, value); + } + + public static readonly StyledProperty ToolTipTextProperty = + AvaloniaProperty.Register( + nameof(ToolTipText), string.Empty); + + public string ToolTipText + { + get => GetValue(ToolTipTextProperty); + set => SetValue(ToolTipTextProperty, value); + } + + public static readonly StyledProperty DopStatusProperty = + AvaloniaProperty.Register( + nameof(DopStatus), DopStatusEnum.Unknown, coerce: UpdateValue); + + private static DopStatusEnum UpdateValue(AvaloniaObject arg1, DopStatusEnum arg2) + { + // TODO: AVALONIA11 => implement this + return DopStatusEnum.Excellent; + } + + public DopStatusEnum DopStatus + { + get => GetValue(DopStatusProperty); + set => SetValue(DopStatusProperty, value); + } + + #endregion + + public static void SetPseudoClass(GpsStatusIndicator indicator) + { + var dopStatus = indicator.DopStatus; + + indicator.PseudoClasses.Set(":unknown", dopStatus == DopStatusEnum.Unknown); + indicator.PseudoClasses.Set(":critical", dopStatus is DopStatusEnum.Fair or DopStatusEnum.Poor); + indicator.PseudoClasses.Set(":warning", dopStatus == DopStatusEnum.Moderate); + indicator.PseudoClasses.Set(":normal", + dopStatus is DopStatusEnum.Ideal or DopStatusEnum.Excellent or DopStatusEnum.Good); + + indicator.IconKind = GetIcon(indicator.FixType); + indicator.ToolTipText = indicator.FixType.GetShortDisplayName(); + } + + private static GpsFixType UpdateValue(AvaloniaObject avaloniaObject, GpsFixType gpsFixType) + { + // TODO: AVALONIA11 => implement this + if (avaloniaObject is not GpsStatusIndicator indicator) return GpsFixType.GpsFixType2dFix; + SetPseudoClass(indicator); + return GpsFixType.GpsFixType2dFix; + } + + private static MaterialIconKind GetIcon(GpsFixType fixType) + { + return fixType switch + { + GpsFixType.GpsFixTypeNoGps => MaterialIconKind.CrosshairsQuestion, + GpsFixType.GpsFixTypeNoFix => MaterialIconKind.Crosshairs, + GpsFixType.GpsFixType2dFix => MaterialIconKind.Crosshairs, + GpsFixType.GpsFixType3dFix => MaterialIconKind.Crosshairs, + GpsFixType.GpsFixTypeDgps => MaterialIconKind.Crosshairs, + GpsFixType.GpsFixTypeRtkFloat => MaterialIconKind.CrosshairsGps, + GpsFixType.GpsFixTypeRtkFixed => MaterialIconKind.CrosshairsGps, + GpsFixType.GpsFixTypeStatic => MaterialIconKind.CrosshairsGps, + GpsFixType.GpsFixTypePpp => MaterialIconKind.CrosshairsGps, + _ => throw new ArgumentOutOfRangeException(nameof(fixType), fixType, null) + }; + } +} \ No newline at end of file diff --git a/src/Asv.Drones.Gui.Api/Tools/Controls/Indicators/IndicatorBase.cs b/src/Asv.Drones.Gui.Api/Tools/Controls/Indicators/IndicatorBase.cs new file mode 100644 index 0000000..fe10393 --- /dev/null +++ b/src/Asv.Drones.Gui.Api/Tools/Controls/Indicators/IndicatorBase.cs @@ -0,0 +1,60 @@ +using Avalonia; +using Avalonia.Controls.Primitives; +using Avalonia.Media; +using Material.Icons; + +namespace Asv.Drones.Gui.Api; + +public abstract class IndicatorBase : TemplatedControl +{ + #region Brushes + + public static readonly StyledProperty UnknownBrushProperty = AvaloniaProperty.Register( + "UnknownBrush"); + + public Brush UnknownBrush + { + get => GetValue(UnknownBrushProperty); + set => SetValue(UnknownBrushProperty, value); + } + + public static readonly StyledProperty CriticalBrushProperty = + AvaloniaProperty.Register( + "CriticalBrush"); + + public Brush CriticalBrush + { + get => GetValue(CriticalBrushProperty); + set => SetValue(CriticalBrushProperty, value); + } + + public static readonly StyledProperty WarningBrushProperty = AvaloniaProperty.Register( + "WarningBrush"); + + public Brush WarningBrush + { + get => GetValue(WarningBrushProperty); + set => SetValue(WarningBrushProperty, value); + } + + public static readonly StyledProperty NormalBrushProperty = AvaloniaProperty.Register( + "NormalBrush"); + + public Brush NormalBrush + { + get => GetValue(NormalBrushProperty); + set => SetValue(NormalBrushProperty, value); + } + + #endregion + + public static readonly StyledProperty IconKindProperty = + AvaloniaProperty.Register( + nameof(IconKind), MaterialIconKind.BatteryUnknown); + + public MaterialIconKind IconKind + { + get => GetValue(IconKindProperty); + set => SetValue(IconKindProperty, value); + } +} \ No newline at end of file diff --git a/src/Asv.Drones.Gui.Api/Tools/Controls/Indicators/LevelIndicator.axaml b/src/Asv.Drones.Gui.Api/Tools/Controls/Indicators/LevelIndicator.axaml new file mode 100644 index 0000000..0bffa2e --- /dev/null +++ b/src/Asv.Drones.Gui.Api/Tools/Controls/Indicators/LevelIndicator.axaml @@ -0,0 +1,137 @@ + + + + + + + \ No newline at end of file diff --git a/src/Asv.Drones.Gui.Api/Tools/Controls/Indicators/LevelIndicator.axaml.cs b/src/Asv.Drones.Gui.Api/Tools/Controls/Indicators/LevelIndicator.axaml.cs new file mode 100644 index 0000000..be5d36c --- /dev/null +++ b/src/Asv.Drones.Gui.Api/Tools/Controls/Indicators/LevelIndicator.axaml.cs @@ -0,0 +1,119 @@ +using Avalonia; +using Avalonia.Controls.Primitives; + +namespace Asv.Drones.Gui.Api; + +public class LevelIndicator : TemplatedControl +{ + public static readonly StyledProperty ValueFromProperty = AvaloniaProperty.Register( + nameof(ValueFrom), 100); + + public double ValueFrom + { + get => GetValue(ValueFromProperty); + set => SetValue(ValueFromProperty, value); + } + + public static readonly StyledProperty ValueToProperty = AvaloniaProperty.Register( + nameof(ValueTo), 100); + + public double ValueTo + { + get => GetValue(ValueToProperty); + set => SetValue(ValueToProperty, value); + } + + public static readonly StyledProperty LevelProperty = AvaloniaProperty.Register( + nameof(Level)); + + public double? Level + { + get => GetValue(LevelProperty); + set => SetValue(LevelProperty, value); + } + + public static readonly StyledProperty ValueProperty = AvaloniaProperty.Register( + nameof(Value), default(double?)); + + public double? Value + { + get => GetValue(ValueProperty); + set => SetValue(ValueProperty, value); + } + + private static void UpdateValueLimits(AvaloniaObject source) + { + if (source is not LevelIndicator indicator) return; + + if (indicator.ValueTo <= indicator.ValueFrom || double.IsFinite(indicator.ValueTo) == false || + double.IsFinite(indicator.ValueTo) == false) + { + indicator.Value = indicator.ValueFrom; + } + else + { + UpdateValue(source); + } + } + + private static void UpdateValue(AvaloniaObject source) + { + if (source is not LevelIndicator indicator) return; + + if (double.IsFinite(indicator.ValueTo) == false || double.IsFinite(indicator.ValueTo) == false) + { + indicator.Level = 0; + return; + } + + if (indicator.Value < indicator.ValueFrom) + { + indicator.Level = 0; + return; + } + + if (indicator.Value > indicator.ValueTo) + { + indicator.Level = 350; + return; + } + + indicator.Level = (indicator.Value - indicator.ValueFrom) / (indicator.ValueTo - indicator.ValueFrom) * 350.0; + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + if (change.Property == ValueFromProperty || change.Property == ValueToProperty) + { + UpdateValueLimits(change.Sender); + } + else if (change.Property == ValueProperty) + { + UpdateValue(change.Sender); + } + } + + private Random _random = new Random(); + + public LevelIndicator() + { + // if (Design.IsDesignMode) + // { + // Width = 350; + // Height = 41; + // Foreground = Brushes.DarkBlue; + // BorderBrush = Brushes.Transparent; + // BorderThickness = new Thickness(0); + // + // ValueFrom = -120; + // ValueTo = 20; + // Value = -60; + // + // Observable.Timer(TimeSpan.Zero, TimeSpan.FromSeconds(1),RxApp.MainThreadScheduler).Subscribe(_ => + // { + // Value = _random.NextDouble() * 140 + ValueFrom; + // }); + // } + } +} \ No newline at end of file diff --git a/src/Asv.Drones.Gui.Api/Tools/Controls/Indicators/StringIndicator.axaml b/src/Asv.Drones.Gui.Api/Tools/Controls/Indicators/StringIndicator.axaml new file mode 100644 index 0000000..5dc7388 --- /dev/null +++ b/src/Asv.Drones.Gui.Api/Tools/Controls/Indicators/StringIndicator.axaml @@ -0,0 +1,18 @@ + + + + + + + \ No newline at end of file diff --git a/src/Asv.Drones.Gui.Api/Tools/Controls/Indicators/StringIndicator.axaml.cs b/src/Asv.Drones.Gui.Api/Tools/Controls/Indicators/StringIndicator.axaml.cs new file mode 100644 index 0000000..7f1cdee --- /dev/null +++ b/src/Asv.Drones.Gui.Api/Tools/Controls/Indicators/StringIndicator.axaml.cs @@ -0,0 +1,34 @@ +using Avalonia; + +namespace Asv.Drones.Gui.Api; + +public class StringIndicator : IndicatorBase +{ + #region Directed properties + + private string _title; + + public static readonly DirectProperty TitleProperty = + AvaloniaProperty.RegisterDirect( + nameof(Title), o => o.Title, (o, v) => o.Title = v); + + public string Title + { + get => _title; + set => SetAndRaise(TitleProperty, ref _title, value); + } + + private string _value; + + public static readonly DirectProperty ValueProperty = + AvaloniaProperty.RegisterDirect( + nameof(Value), o => o.Value, (o, v) => o.Value = v); + + public string Value + { + get => _value; + set => SetAndRaise(ValueProperty, ref _value, value); + } + + #endregion +} \ No newline at end of file diff --git a/src/Asv.Drones.Gui.Api/Tools/Controls/Map/Actions/IMapAction.cs b/src/Asv.Drones.Gui.Api/Tools/Controls/Map/Actions/IMapAction.cs new file mode 100644 index 0000000..d251736 --- /dev/null +++ b/src/Asv.Drones.Gui.Api/Tools/Controls/Map/Actions/IMapAction.cs @@ -0,0 +1,27 @@ +using Avalonia.Controls; + +namespace Asv.Drones.Gui.Api; + +/// +/// IMapAction provides a contract for a Map Action. +/// It's part of the Asv.Drones.Gui.Core project which uses AvaloniaUI for the User Interface. +/// +public interface IMapAction : IViewModel +{ + /// + /// Position on the control where an element/border should be docked. + /// + Dock Dock { get; } + + /// + /// Represents the order or sequence of the map actions. + /// + int Order { get; } + + /// + /// Initializes a map action using the provided context. + /// + /// An instance of IMap to be used for initiation. + /// An instance of IMapAction. + IMapAction Init(IMap context); +} \ No newline at end of file diff --git a/src/Asv.Drones.Gui.Api/Tools/Controls/Map/Actions/MapActionBase.cs b/src/Asv.Drones.Gui.Api/Tools/Controls/Map/Actions/MapActionBase.cs new file mode 100644 index 0000000..9043e70 --- /dev/null +++ b/src/Asv.Drones.Gui.Api/Tools/Controls/Map/Actions/MapActionBase.cs @@ -0,0 +1,31 @@ +using Avalonia.Controls; +using ReactiveUI.Fody.Helpers; + +namespace Asv.Drones.Gui.Api; + +public class MapActionBase : ViewModelBase, IMapAction +{ + protected MapActionBase(Uri id) : base(id) + { + } + + protected MapActionBase(string id) : base(id) + { + } + + [Reactive] public Dock Dock { get; set; } = Dock.Right; + public int Order { get; set; } + public IMap? Map { get; private set; } + + public virtual IMapAction Init(IMap context) + { + Map = context; + InternalWhenMapLoaded(context); + return this; + } + + protected virtual void InternalWhenMapLoaded(IMap context) + { + // do nothing + } +} \ No newline at end of file diff --git a/src/Asv.Drones.Gui.Api/Tools/Controls/Map/Actions/Mover/MapMoverActionView.axaml b/src/Asv.Drones.Gui.Api/Tools/Controls/Map/Actions/Mover/MapMoverActionView.axaml new file mode 100644 index 0000000..6c80d1d --- /dev/null +++ b/src/Asv.Drones.Gui.Api/Tools/Controls/Map/Actions/Mover/MapMoverActionView.axaml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Asv.Drones.Gui.Api/Tools/Controls/Map/Actions/Mover/MapMoverActionView.axaml.cs b/src/Asv.Drones.Gui.Api/Tools/Controls/Map/Actions/Mover/MapMoverActionView.axaml.cs new file mode 100644 index 0000000..4def5ff --- /dev/null +++ b/src/Asv.Drones.Gui.Api/Tools/Controls/Map/Actions/Mover/MapMoverActionView.axaml.cs @@ -0,0 +1,12 @@ +using Avalonia.ReactiveUI; + +namespace Asv.Drones.Gui.Api; + +[ExportView(typeof(MapMoverActionViewModel))] +public partial class MapMoverActionView : ReactiveUserControl +{ + public MapMoverActionView() + { + InitializeComponent(); + } +} \ No newline at end of file diff --git a/src/Asv.Drones.Gui.Api/Tools/Controls/Map/Actions/Mover/MapMoverActionViewModel.cs b/src/Asv.Drones.Gui.Api/Tools/Controls/Map/Actions/Mover/MapMoverActionViewModel.cs new file mode 100644 index 0000000..1ff9250 --- /dev/null +++ b/src/Asv.Drones.Gui.Api/Tools/Controls/Map/Actions/Mover/MapMoverActionViewModel.cs @@ -0,0 +1,31 @@ +using Avalonia.Controls; + +namespace Asv.Drones.Gui.Api; + +public class MapMoverActionViewModel : ViewModelBase, IMapAction +{ + private IMap _map; + + public MapMoverActionViewModel() : base(WellKnownUri.UndefinedUri) + { + DesignTime.ThrowIfNotDesignMode(); + } + + protected MapMoverActionViewModel(Uri id) : base(id) + { + } + + protected MapMoverActionViewModel(string id) : base(id) + { + } + + public IMapAction Init(IMap context) + { + _map = context; + return this; + } + + public IMap Map => _map; + public Dock Dock { get; } + public int Order => 0; +} \ No newline at end of file diff --git a/src/Asv.Drones.Gui.Api/Tools/Controls/Map/Actions/Ruler/MapRulerActionView.axaml b/src/Asv.Drones.Gui.Api/Tools/Controls/Map/Actions/Ruler/MapRulerActionView.axaml new file mode 100644 index 0000000..f9399ea --- /dev/null +++ b/src/Asv.Drones.Gui.Api/Tools/Controls/Map/Actions/Ruler/MapRulerActionView.axaml @@ -0,0 +1,29 @@ + + + + + + + + + + \ No newline at end of file diff --git a/src/Asv.Drones.Gui.Api/Tools/Controls/Map/Actions/Ruler/MapRulerActionView.axaml.cs b/src/Asv.Drones.Gui.Api/Tools/Controls/Map/Actions/Ruler/MapRulerActionView.axaml.cs new file mode 100644 index 0000000..13223fa --- /dev/null +++ b/src/Asv.Drones.Gui.Api/Tools/Controls/Map/Actions/Ruler/MapRulerActionView.axaml.cs @@ -0,0 +1,12 @@ +using Avalonia.ReactiveUI; + +namespace Asv.Drones.Gui.Api; + +[ExportView(typeof(MapRulerActionViewModel))] +public partial class MapRulerActionView : ReactiveUserControl +{ + public MapRulerActionView() + { + InitializeComponent(); + } +} \ No newline at end of file diff --git a/src/Asv.Drones.Gui.Api/Tools/Controls/Map/Actions/Ruler/MapRulerActionViewModel.cs b/src/Asv.Drones.Gui.Api/Tools/Controls/Map/Actions/Ruler/MapRulerActionViewModel.cs new file mode 100644 index 0000000..88c2faf --- /dev/null +++ b/src/Asv.Drones.Gui.Api/Tools/Controls/Map/Actions/Ruler/MapRulerActionViewModel.cs @@ -0,0 +1,188 @@ +using System.Collections.ObjectModel; +using System.Reactive.Disposables; +using System.Reactive.Linq; +using Asv.Avalonia.Map; +using Asv.Common; +using Asv.Mavlink.Vehicle; +using Avalonia.Collections; +using Avalonia.Media; +using DynamicData; +using DynamicData.Binding; +using Material.Icons; +using ReactiveUI; +using ReactiveUI.Fody.Helpers; + +namespace Asv.Drones.Gui.Api; + +public class MapRulerActionViewModel : MapActionBase +{ + private readonly ILocalizationService _loc; + private CancellationTokenSource? _tokenSource; + private RulerStartAnchor? _startAnchor; + private RulerStopAnchor? _stopAnchor; + private RulerPolygon? _rulerPolygon; + + public MapRulerActionViewModel() : base(WellKnownUri.UndefinedUri) + { + DesignTime.ThrowIfNotDesignMode(); + } + + public MapRulerActionViewModel(string id, ILocalizationService loc) : base(id) + { + _loc = loc; + this.WhenValueChanged(m => m.IsRulerEnabled, false) + .Subscribe(SetUpRuler) + .DisposeItWith(Disposable); + Disposable.AddAction(() => { SetUpRuler(false); }); + } + + private async void SetUpRuler(bool isEnabled) + { + if (Map == null) return; + if (isEnabled) + { + _tokenSource?.Cancel(false); + _tokenSource?.Dispose(); + _tokenSource = new CancellationTokenSource(); + try + { + var startPoint = await Map.ShowTargetDialog("Select ruler starting point", _tokenSource.Token); + if (double.IsNaN(startPoint.Altitude)) + { + IsRulerEnabled = false; + return; + } + + _startAnchor = new RulerStartAnchor(new Uri(Id, "start"), startPoint); + Map.AdditionalAnchorsSource.AddOrUpdate(_startAnchor); + var stopPoint = await Map.ShowTargetDialog("Select ruler stopping point", _tokenSource.Token); + if (double.IsNaN(stopPoint.Altitude)) + { + IsRulerEnabled = false; + return; + } + + _stopAnchor = new RulerStopAnchor(new Uri(Id, "stop"), _startAnchor, _loc, stopPoint); + Map.AdditionalAnchorsSource.AddOrUpdate(_stopAnchor); + + _rulerPolygon = new RulerPolygon(new Uri(Id, "ruler"), _startAnchor, _stopAnchor); + Map.AdditionalAnchorsSource.AddOrUpdate(_rulerPolygon); + } + catch (TaskCanceledException) + { + DisableRulerOnClick(); + } + } + else + { + DisableRulerOnClick(); + } + } + + private void DisableRulerOnClick() + { + _tokenSource?.Cancel(); + + if (Map == null) return; + + if (_startAnchor != null) + { + Map.AdditionalAnchorsSource.Remove(_startAnchor.Id); + _startAnchor.Dispose(); + _startAnchor = null; + } + + if (_stopAnchor != null) + { + Map.AdditionalAnchorsSource.Remove(_stopAnchor.Id); + _stopAnchor.Dispose(); + _stopAnchor = null; + } + + if (_rulerPolygon != null) + { + Map.AdditionalAnchorsSource.Remove(_rulerPolygon.Id); + _rulerPolygon.Dispose(); + _rulerPolygon = null; + } + + IsRulerEnabled = false; + } + + [Reactive] public bool IsRulerEnabled { get; set; } +} + +public class RulerStartAnchor : MapAnchorBase +{ + public RulerStartAnchor(Uri id, GeoPoint startPoint) : base(id) + { + Size = 48; + BaseSize = 48; + OffsetX = OffsetXEnum.Center; + OffsetY = OffsetYEnum.Bottom; + StrokeThickness = 1; + IconBrush = Brushes.Indigo; + Stroke = Brushes.White; + IsVisible = true; + Icon = MaterialIconKind.MapMarker; + IsEditable = true; + Location = startPoint; + } +} + +public class RulerStopAnchor : MapAnchorBase +{ + public RulerStopAnchor(Uri id, RulerStartAnchor start, ILocalizationService loc, GeoPoint stopPoint) : base(id) + { + Size = 48; + BaseSize = 48; + OffsetX = OffsetXEnum.Center; + OffsetY = OffsetYEnum.Bottom; + StrokeThickness = 1; + IconBrush = Brushes.Indigo; + Stroke = Brushes.White; + IsVisible = true; + Icon = MaterialIconKind.MapMarker; + IsEditable = true; + Location = stopPoint; + this.WhenValueChanged(x => x.Location, false) + .Merge(start.WhenValueChanged(x => x.Location, false)) + .Throttle(TimeSpan.FromMilliseconds(50), RxApp.MainThreadScheduler) + .Subscribe(x => + { + Title = loc.Distance.FromSiToStringWithUnits(GeoMath.Distance(start.Location, Location)); + }).DisposeItWith(Disposable); + Title = loc.Distance.FromSiToStringWithUnits(GeoMath.Distance(start.Location, Location)); + } +} + +public class RulerPolygon : MapAnchorBase +{ + private readonly ReadOnlyObservableCollection _path; + + public RulerPolygon(Uri id, RulerStartAnchor start, RulerStopAnchor stop) : base(id) + { + ZOrder = -1000; + OffsetX = 0; + OffsetY = 0; + PathOpacity = 0.6; + StrokeThickness = 5; + Stroke = Brushes.Purple; + IsVisible = true; + StrokeDashArray = new AvaloniaList(2, 2); + + var cache = new SourceList().DisposeItWith(Disposable); + cache.Add(new GeoPoint(0, 0, 0)); + cache.Add(new GeoPoint(0, 0, 0)); + + start.WhenValueChanged(x => x.Location).Subscribe(x => cache.ReplaceAt(0, x)).DisposeItWith(Disposable); + stop.WhenValueChanged(x => x.Location).Subscribe(x => cache.ReplaceAt(1, x)).DisposeItWith(Disposable); + + cache.Connect() + .Bind(out _path) + .Subscribe() + .DisposeWith(Disposable); + } + + public override ReadOnlyObservableCollection Path => _path; +} \ No newline at end of file diff --git a/src/Asv.Drones.Gui.Api/Tools/Controls/Map/Actions/Zoom/MapZoomActionView.axaml b/src/Asv.Drones.Gui.Api/Tools/Controls/Map/Actions/Zoom/MapZoomActionView.axaml new file mode 100644 index 0000000..828a33f --- /dev/null +++ b/src/Asv.Drones.Gui.Api/Tools/Controls/Map/Actions/Zoom/MapZoomActionView.axaml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Asv.Drones.Gui.Api/Tools/Controls/Map/Widgets/AnchorEditor/AnchorsEditorView.axaml.cs b/src/Asv.Drones.Gui.Api/Tools/Controls/Map/Widgets/AnchorEditor/AnchorsEditorView.axaml.cs new file mode 100644 index 0000000..b03beac --- /dev/null +++ b/src/Asv.Drones.Gui.Api/Tools/Controls/Map/Widgets/AnchorEditor/AnchorsEditorView.axaml.cs @@ -0,0 +1,35 @@ +using Avalonia.Layout; +using Avalonia.ReactiveUI; +using DynamicData.Binding; + +namespace Asv.Drones.Gui.Api; + +[ExportView(typeof(AnchorsEditorViewModel))] +public partial class AnchorsEditorView : ReactiveUserControl +{ + private IDisposable? _subscribe; + private double? _lastWidth = null; + + public AnchorsEditorView() + { + InitializeComponent(); + this.WhenValueChanged(x => x.ViewModel).Subscribe(OnViewModel); + } + + + private void OnViewModel(AnchorsEditorViewModel? vm) + { + if (vm == null) return; + _subscribe?.Dispose(); + } + + private void Layoutable_OnEffectiveViewportChanged(object? sender, EffectiveViewportChangedEventArgs e) + { + ViewModel.IsCompactMode = PART_Grid.Bounds.Width switch + { + < 500 => true, + > 500 => false, + _ => ViewModel.IsCompactMode + }; + } +} \ No newline at end of file diff --git a/src/Asv.Drones.Gui.Api/Tools/Controls/Map/Widgets/AnchorEditor/AnchorsEditorViewModel.cs b/src/Asv.Drones.Gui.Api/Tools/Controls/Map/Widgets/AnchorEditor/AnchorsEditorViewModel.cs new file mode 100644 index 0000000..ceed994 --- /dev/null +++ b/src/Asv.Drones.Gui.Api/Tools/Controls/Map/Widgets/AnchorEditor/AnchorsEditorViewModel.cs @@ -0,0 +1,279 @@ +using System.Globalization; +using System.Reactive; +using Asv.Avalonia.Map; +using Asv.Common; +using Avalonia.Controls; +using DynamicData.Binding; +using Material.Icons; +using ReactiveUI; +using ReactiveUI.Fody.Helpers; +using ReactiveUI.Validation.Extensions; + +namespace Asv.Drones.Gui.Api; + +public class AnchorsEditorViewModel : MapWidgetBase +{ + private readonly ILocalizationService _loc; + private bool _internalChange; + private IDisposable? _locationSubscription; + private IMapAnchor? _prevAnchor; + #region ifDEBUG + public AnchorsEditorViewModel() : base(WellKnownUri.UndefinedUri) + { + DesignTime.ThrowIfNotDesignMode(); + if (Design.IsDesignMode) + { + IsVisible = true; + IsEditable = true; + IsCompactMode = true; + Actions = new[] + { + new MapAnchorActionViewModel() + { + Icon = MaterialIconKind.Accelerometer, + Title = "TakeOff" + }, + new MapAnchorActionViewModel() + { + Icon = MaterialIconKind.Accelerometer, + Title = "ROI" + }, + new MapAnchorActionViewModel() + { + Icon = MaterialIconKind.Accelerometer, + Title = "Land" + }, + new MapAnchorActionViewModel() + { + Icon = MaterialIconKind.Accelerometer, + Title = "Reboot" + }, + new MapAnchorActionViewModel() + { + Icon = MaterialIconKind.Accelerometer, + Title = "Start Mission" + }, + new MapAnchorActionViewModel() + { + Icon = MaterialIconKind.Accelerometer, + Title = "" + }, + new MapAnchorActionViewModel() + { + Icon = MaterialIconKind.Accelerometer, + Title = "Action" + }, + new MapAnchorActionViewModel() + { + Icon = MaterialIconKind.Accelerometer, + Title = ":Some" + }, + new MapAnchorActionViewModel() + { + Icon = MaterialIconKind.Accelerometer, + Title = ":Action" + }, + new MapAnchorActionViewModel() + { + Icon = MaterialIconKind.Accelerometer, + Title = "SuperAction" + }, + }; + } + } + #endregion + + protected AnchorsEditorViewModel(Uri id, ILocalizationService loc) : base(id) + { + _loc = loc; + Title = RS.AnchorsEditorViewModel_Title; + Icon = MaterialIconKind.MapMarker; + Location = WidgetLocation.Bottom; + Disposable.AddAction(() => { _locationSubscription?.Dispose(); }); + Order = 0; + + CopyCommand = ReactiveCommand.Create(CopyGeoPoint).DisposeItWith(Disposable); + PasteCommand = ReactiveCommand.Create(PasteGeoPoint).DisposeItWith(Disposable); + + this.WhenAnyValue(_ => _.Latitude) + .Subscribe(UpdateLatitude) + .DisposeItWith(Disposable); + this.WhenAnyValue(_ => _.Longitude) + .Subscribe(UpdateLongitude) + .DisposeItWith(Disposable); + this.WhenAnyValue(_ => _.Altitude) + .Subscribe(UpdateAltitude) + .DisposeItWith(Disposable); + + this.ValidationRule(x => x.Latitude, + _ => _loc.Latitude.IsValid(_), + _ => _loc.Latitude.GetErrorMessage(_) ?? string.Empty) + .DisposeItWith(Disposable); + + this.ValidationRule(x => x.Longitude, + _ => _loc.Longitude.IsValid(_), + _ => _loc.Longitude.GetErrorMessage(_) ?? string.Empty) + .DisposeItWith(Disposable); + + this.ValidationRule(x => x.Altitude, + _ => _loc.Altitude.IsValid(_), + _ => _loc.Altitude.GetErrorMessage(_) ?? string.Empty) + .DisposeItWith(Disposable); + } + + protected AnchorsEditorViewModel(string id, ILocalizationService loc) : this(new Uri(id), loc) + { + } + [Reactive] public bool IsTitleCompactMode { get; set; } + [Reactive] public bool IsCompactMode { get; set; } + + [Reactive] public bool IsVisible { get; set; } + + [Reactive] public string Latitude { get; set; } + + [Reactive] public string LatitudeUnits { get; set; } + + [Reactive] public string Longitude { get; set; } + + [Reactive] public string LongitudeUnits { get; set; } + + [Reactive] public string Altitude { get; set; } + + [Reactive] public string AltitudeUnits { get; set; } + + [Reactive] public bool IsEditable { get; set; } + + [Reactive] public ReactiveCommand CopyCommand { get; set; } + [Reactive] public ReactiveCommand PasteCommand { get; set; } + + [Reactive] private GeoPoint CopiedPoint { get; set; } + [Reactive] public IEnumerable Actions { get; set; } + + + + private void CopyGeoPoint() + { + CopiedPoint = new GeoPoint(_loc.Latitude.ConvertToSi(Latitude), _loc.Longitude.ConvertToSi(Longitude), + _loc.Altitude.ConvertToSi(Altitude)); + } + + private void PasteGeoPoint() + { + Latitude = CopiedPoint.Latitude.ToString(CultureInfo.InvariantCulture); + Longitude = CopiedPoint.Longitude.ToString(CultureInfo.InvariantCulture); + Altitude = CopiedPoint.Altitude.ToString(CultureInfo.InvariantCulture); + } + + + private void UpdateLatitude(string latitude) + { + if (_internalChange) return; + if (!string.IsNullOrWhiteSpace(latitude) && _loc.Latitude.IsValid(latitude)) + { + var prevLocation = _prevAnchor.Location; + var newLocation = _prevAnchor.Location = new GeoPoint( + _loc.Latitude.CurrentUnit.Value.ConvertToSi(latitude), + prevLocation.Longitude, prevLocation.Altitude); + // if new location is equal to previous location then we need to update longitude string + if (prevLocation.Equals(newLocation)) + { + _internalChange = true; + Latitude = _loc.Longitude.FromSiToString(prevLocation.Latitude); + _internalChange = false; + } + else + { + _prevAnchor.Location = newLocation; + } + } + } + + private void UpdateLongitude(string longitude) + { + if (_internalChange) return; + if (!string.IsNullOrWhiteSpace(longitude) && _loc.Longitude.IsValid(longitude)) + { + var prevLocation = _prevAnchor.Location; + var newLocation = new GeoPoint(prevLocation.Latitude, + _loc.Longitude.CurrentUnit.Value.ConvertToSi(longitude), + prevLocation.Altitude); + // if new location is equal to previous location then we need to update longitude string + if (prevLocation.Equals(newLocation)) + { + _internalChange = true; + Longitude = _loc.Longitude.FromSiToString(prevLocation.Longitude); + _internalChange = false; + } + else + { + _prevAnchor.Location = newLocation; + } + } + } + + private void UpdateAltitude(string altitude) + { + if (_internalChange) return; + if (!string.IsNullOrWhiteSpace(altitude) && _loc.Altitude.IsValid(altitude)) + { + var prevLocation = _prevAnchor.Location; + var newLocation = _prevAnchor.Location = new GeoPoint(prevLocation.Latitude, + prevLocation.Longitude, + _loc.Altitude.CurrentUnit.Value.ConvertToSi(altitude)); + // if new location is equal to previous location then we need to update string + if (prevLocation.Equals(newLocation)) + { + _internalChange = true; + Altitude = _loc.Altitude.FromSiToString(prevLocation.Altitude); + _internalChange = false; + } + else + { + _prevAnchor.Location = newLocation; + } + } + } + + protected override void InternalAfterMapInit(IMap context) + { + LatitudeUnits = _loc.Latitude.CurrentUnit.Value.Unit; + LongitudeUnits = _loc.Longitude.CurrentUnit.Value.Unit; + AltitudeUnits = _loc.Altitude.CurrentUnit.Value.Unit; + context.WhenValueChanged(_ => _.SelectedItem) + .Subscribe(_ => + { + Actions = null; + if (_prevAnchor != null && _prevAnchor.IsInEditMode) + { + UpdateLatitude(Latitude); + UpdateLongitude(Longitude); + UpdateAltitude(Altitude); + } + + _locationSubscription?.Dispose(); + _locationSubscription = null; + + if (_ != null) + { + if (_.Actions is not null) Actions = _.Actions.OrderByDescending(_=>_.Title.Length); + _prevAnchor = _; + IsVisible = true; + IsEditable = _.IsEditable; + _locationSubscription = _.WhenAnyValue(_ => _.Location) + .Subscribe(_ => + { + _internalChange = true; + Latitude = _loc.Latitude.FromSiToString(_.Latitude); + Longitude = _loc.Longitude.FromSiToString(_.Longitude); + Altitude = _loc.Altitude.FromSiToString(_.Altitude); + _internalChange = false; + }); + } + else + { + IsVisible = false; + } + }) + .DisposeItWith(Disposable); + } +} \ No newline at end of file diff --git a/src/Asv.Drones.Gui.Api/Tools/Controls/Map/Widgets/IMapWidget.cs b/src/Asv.Drones.Gui.Api/Tools/Controls/Map/Widgets/IMapWidget.cs new file mode 100644 index 0000000..136fa13 --- /dev/null +++ b/src/Asv.Drones.Gui.Api/Tools/Controls/Map/Widgets/IMapWidget.cs @@ -0,0 +1,47 @@ +using Material.Icons; + +namespace Asv.Drones.Gui.Api; + +/// +/// Enumeration to specify the location of a widget on the map. +/// +public enum WidgetLocation +{ + Left, + Right, + Bottom +} + +/// +/// An interface that describes the structure and behavior of a Map Widget. +/// Map Widget represents an interface element on the map. +/// +public interface IMapWidget : IViewModel +{ + /// + /// Gets the location of the widget on the map. + /// + WidgetLocation Location { get; } + + /// + /// Gets the title of the widget. + /// + string Title { get; } + + /// + /// Gets the order of the widget. + /// + int Order { get; } + + /// + /// Gets the icon associated with the widget. + /// + MaterialIconKind Icon { get; } + + /// + /// Initializes the Map Widget with a reference to a Map it belongs to. + /// + /// The map the widget is associated with. + /// The initialized MapWidget. + IMapWidget Init(IMap context); +} \ No newline at end of file diff --git a/src/Asv.Drones.Gui.Api/Tools/Controls/Map/Widgets/MapWidgetBase.cs b/src/Asv.Drones.Gui.Api/Tools/Controls/Map/Widgets/MapWidgetBase.cs new file mode 100644 index 0000000..67ebef0 --- /dev/null +++ b/src/Asv.Drones.Gui.Api/Tools/Controls/Map/Widgets/MapWidgetBase.cs @@ -0,0 +1,32 @@ +using Material.Icons; +using ReactiveUI.Fody.Helpers; + +namespace Asv.Drones.Gui.Api +{ + public abstract class MapWidgetBase : ViewModelBaseWithValidation, IMapWidget + { + protected MapWidgetBase(Uri id) : base(id) + { + } + + protected MapWidgetBase(string id) : base(id) + { + } + + [Reactive] public WidgetLocation Location { get; set; } + [Reactive] public string Title { get; set; } + public int Order { get; set; } + [Reactive] public MaterialIconKind Icon { get; set; } + + public IMapWidget Init(IMap context) + { + Map = context; + InternalAfterMapInit(context); + return this; + } + + protected abstract void InternalAfterMapInit(IMap context); + + public IMap Map { get; private set; } + } +} \ No newline at end of file diff --git a/src/Asv.Drones.Gui.Api/Tools/Controls/OptionsDisplayItem/OptionsDisplayItem.cs b/src/Asv.Drones.Gui.Api/Tools/Controls/OptionsDisplayItem/OptionsDisplayItem.cs new file mode 100644 index 0000000..e16b746 --- /dev/null +++ b/src/Asv.Drones.Gui.Api/Tools/Controls/OptionsDisplayItem/OptionsDisplayItem.cs @@ -0,0 +1,181 @@ +using System.Windows.Input; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Input; +using Avalonia.Interactivity; +using FAIconElement = FluentAvalonia.UI.Controls.FAIconElement; + +namespace Asv.Drones.Gui.Api; + +public class OptionsDisplayItem : TemplatedControl +{ + public static readonly StyledProperty HeaderProperty = + AvaloniaProperty.Register(nameof(Header)); + + public static readonly StyledProperty DescriptionProperty = + AvaloniaProperty.Register(nameof(Description)); + + public static readonly StyledProperty IconProperty = + AvaloniaProperty.Register(nameof(Icon)); + + public static readonly StyledProperty NavigatesProperty = + AvaloniaProperty.Register(nameof(Navigates)); + + public static readonly StyledProperty ActionButtonProperty = + AvaloniaProperty.Register(nameof(ActionButton)); + + public static readonly StyledProperty ExpandsProperty = + AvaloniaProperty.Register(nameof(Expands)); + + public static readonly StyledProperty ContentProperty = + ContentControl.ContentProperty.AddOwner(); + + public static readonly StyledProperty IsExpandedProperty = + Expander.IsExpandedProperty.AddOwner(); + + + public static readonly StyledProperty NavigationCommandProperty = + AvaloniaProperty.Register(nameof(NavigationCommand)); + + public string Header + { + get => GetValue(HeaderProperty); + set => SetValue(HeaderProperty, value); + } + + public string Description + { + get => GetValue(DescriptionProperty); + set => SetValue(DescriptionProperty, value); + } + + public FAIconElement Icon + { + get => GetValue(IconProperty); + set => SetValue(IconProperty, value); + } + + public bool Navigates + { + get => GetValue(NavigatesProperty); + set => SetValue(NavigatesProperty, value); + } + + public Control ActionButton + { + get => GetValue(ActionButtonProperty); + set => SetValue(ActionButtonProperty, value); + } + + public bool Expands + { + get => GetValue(ExpandsProperty); + set => SetValue(ExpandsProperty, value); + } + + public object Content + { + get => GetValue(ContentProperty); + set => SetValue(ContentProperty, value); + } + + public bool IsExpanded + { + get => GetValue(IsExpandedProperty); + set => SetValue(IsExpandedProperty, value); + } + + public ICommand NavigationCommand + { + get => GetValue(NavigationCommandProperty); + set => SetValue(NavigationCommandProperty, value); + } + + public static readonly RoutedEvent NavigationRequestedEvent = + RoutedEvent.Register(nameof(NavigationRequested), + RoutingStrategies.Bubble); + + public event EventHandler NavigationRequested + { + add => AddHandler(NavigationRequestedEvent, value); + remove => RemoveHandler(NavigationRequestedEvent, value); + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == NavigatesProperty) + { + if (Expands) + throw new InvalidOperationException("Control cannot both Navigate and Expand"); + + PseudoClasses.Set(":navigates", change.GetNewValue()); + } + else if (change.Property == ExpandsProperty) + { + if (Navigates) + throw new InvalidOperationException("Control cannot both Navigate and Expand"); + + PseudoClasses.Set(":expands", change.GetNewValue()); + } + else if (change.Property == IsExpandedProperty) + { + PseudoClasses.Set(":expanded", change.GetNewValue()); + } + else if (change.Property == IconProperty) + { + PseudoClasses.Set(":icon", change.NewValue != null); + } + } + + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) + { + base.OnApplyTemplate(e); + + _layoutRoot = e.NameScope.Find("LayoutRoot"); + _layoutRoot.PointerPressed += OnLayoutRootPointerPressed; + _layoutRoot.PointerReleased += OnLayoutRootPointerReleased; + _layoutRoot.PointerCaptureLost += OnLayoutRootPointerCaptureLost; + } + + private void OnLayoutRootPointerPressed(object sender, PointerPressedEventArgs e) + { + if (e.GetCurrentPoint(this).Properties.PointerUpdateKind == PointerUpdateKind.LeftButtonPressed) + { + _isPressed = true; + PseudoClasses.Set(":pressed", true); + } + } + + private void OnLayoutRootPointerReleased(object sender, PointerReleasedEventArgs e) + { + var pt = e.GetCurrentPoint(this); + if (_isPressed && pt.Properties.PointerUpdateKind == PointerUpdateKind.LeftButtonReleased) + { + _isPressed = false; + + PseudoClasses.Set(":pressed", false); + + if (Expands) + IsExpanded = !IsExpanded; + + if (Navigates) + { + RaiseEvent(new RoutedEventArgs(NavigationRequestedEvent, this)); + NavigationCommand?.Execute(null); + } + } + } + + private void OnLayoutRootPointerCaptureLost(object sender, PointerCaptureLostEventArgs e) + { + _isPressed = false; + PseudoClasses.Set(":pressed", false); + } + + private bool _isPressed; + private bool _isExpanded; + private Border _layoutRoot; +} \ No newline at end of file diff --git a/src/Asv.Drones.Gui.Api/Tools/Controls/OptionsDisplayItem/OptionsDisplayItemStyles.axaml b/src/Asv.Drones.Gui.Api/Tools/Controls/OptionsDisplayItem/OptionsDisplayItemStyles.axaml new file mode 100644 index 0000000..ca21914 --- /dev/null +++ b/src/Asv.Drones.Gui.Api/Tools/Controls/OptionsDisplayItem/OptionsDisplayItemStyles.axaml @@ -0,0 +1,195 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Asv.Drones.Gui.Api/Tools/Controls/Params/ParamItemView.axaml b/src/Asv.Drones.Gui.Api/Tools/Controls/Params/ParamItemView.axaml new file mode 100644 index 0000000..1a6bd59 --- /dev/null +++ b/src/Asv.Drones.Gui.Api/Tools/Controls/Params/ParamItemView.axaml @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Asv.Drones.Gui.Api/Tools/Controls/Params/ParamPageView.axaml.cs b/src/Asv.Drones.Gui.Api/Tools/Controls/Params/ParamPageView.axaml.cs new file mode 100644 index 0000000..67a97cf --- /dev/null +++ b/src/Asv.Drones.Gui.Api/Tools/Controls/Params/ParamPageView.axaml.cs @@ -0,0 +1,18 @@ +using Avalonia.Markup.Xaml; +using Avalonia.ReactiveUI; + +namespace Asv.Drones.Gui.Api; + +[ExportView(typeof(ParamPageViewModel))] +public partial class ParamPageView : ReactiveUserControl +{ + public ParamPageView() + { + InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } +} \ No newline at end of file diff --git a/src/Asv.Drones.Gui.Api/Tools/Controls/Params/ParamPageViewModel.cs b/src/Asv.Drones.Gui.Api/Tools/Controls/Params/ParamPageViewModel.cs new file mode 100644 index 0000000..f4b4a4c --- /dev/null +++ b/src/Asv.Drones.Gui.Api/Tools/Controls/Params/ParamPageViewModel.cs @@ -0,0 +1,289 @@ +using System.Collections.ObjectModel; +using System.Collections.Specialized; +using System.Reactive; +using System.Reactive.Linq; +using System.Reactive.Subjects; +using Asv.Cfg; +using Asv.Common; +using Asv.Mavlink; +using DynamicData; +using DynamicData.Binding; +using FluentAvalonia.UI.Controls; +using ReactiveUI; +using ReactiveUI.Fody.Helpers; + +namespace Asv.Drones.Gui.Api; + +public class ParamsConfig +{ + public List Params { get; set; } = new(); +} + +public class ParamPageViewModel : ShellPage +{ + private readonly IMavlinkDevicesService _svc; + private readonly ILogService _log; + private readonly IConfiguration _cfg; + private CancellationTokenSource _cancellationTokenSource; + private readonly ReadOnlyObservableCollection _viewedParams; + private ObservableAsPropertyHelper _isRefreshing; + private readonly SourceList _viewedParamsList; + private ParamItemViewModel _selectedItem; + private readonly Subject _canClearSearchText = new(); + private readonly ParamsConfig _config; + private IParamsClientEx _paramsIfc; + + #region Uri + + public static Uri GenerateUri(string baseUri, ushort deviceFullId, DeviceClass @class) => + new($"{baseUri}?id={deviceFullId}&class={@class:G}"); + + #endregion + + public ParamPageViewModel() : base(WellKnownUri.UndefinedUri) + { + DesignTime.ThrowIfNotDesignMode(); + DeviceName = "Params"; + } + + protected ParamPageViewModel(string id, IMavlinkDevicesService svc, ILogService log, IConfiguration cfg) : base(id) + { + _svc = svc ?? throw new ArgumentNullException(nameof(svc)); + _log = log ?? throw new ArgumentNullException(nameof(log)); + _cfg = cfg ?? throw new ArgumentNullException(nameof(cfg)); + _config = _cfg.Get(); + FilterPipe.DisposeItWith(Disposable); + this.WhenAnyValue(_ => _.SearchText, _ => _.ShowStaredOnly) + .Throttle(TimeSpan.FromMilliseconds(100), RxApp.MainThreadScheduler) + .Subscribe(_ => FilterPipe.OnNext(item => item.Filter(SearchText, ShowStaredOnly))) + .DisposeItWith(Disposable); + _viewedParamsList = new SourceList().DisposeItWith(Disposable); + _cancellationTokenSource = new CancellationTokenSource().DisposeItWith(Disposable); + _viewedParamsList.Connect() + .Bind(out _viewedParams) + .Subscribe() + .DisposeItWith(Disposable); + this.WhenValueChanged(_ => _.SearchText) + .Subscribe(_ => { _canClearSearchText.OnNext(!string.IsNullOrWhiteSpace(_)); }) + .DisposeItWith(Disposable); + + Clear = ReactiveCommand.Create(() => { SearchText = string.Empty; }, _canClearSearchText) + .DisposeItWith(Disposable); + + Disposable.AddAction(() => + { + _config.Params = _config.Params.Where(_ => _.IsStarred).ToList(); + _cfg.Set(_config); + }); + } + + public override void SetArgs(NameValueCollection args) + { + base.SetArgs(args); + if (ushort.TryParse(args["id"], out var id) == false) return; + if (Enum.TryParse(args["class"], true, out var deviceClass) == false) return; + var ifc = GetParamsClient(_svc, id, deviceClass); + if (ifc == null) return; + + Icon = MavlinkHelper.GetIcon(deviceClass); + + switch (deviceClass) + { + case DeviceClass.Plane: + var plane = _svc.GetVehicleByFullId(id); + if (plane == null) break; + DeviceName = plane.Name.Value; + break; + case DeviceClass.Copter: + var copter = _svc.GetVehicleByFullId(id); + if (copter == null) break; + DeviceName = copter.Name.Value; + break; + case DeviceClass.GbsRtk: + var gbs = _svc.GetGbsByFullId(id); + if (gbs == null) break; + DeviceName = gbs.Name.Value; + break; + case DeviceClass.SdrPayload: + var sdr = _svc.GetPayloadsByFullId(id); + if (sdr == null) break; + DeviceName = sdr.Name.Value; + break; + case DeviceClass.Adsb: + var adsb = _svc.GetAdsbVehicleByFullId(id); + if (adsb == null) break; + DeviceName = adsb.Name.Value; + break; + } + + InternalInit(ifc); + } + + public virtual IParamsClientEx? GetParamsClient(IMavlinkDevicesService svc, ushort fullId, DeviceClass @class) + { + return null; + } + + protected void InternalInit(IParamsClientEx paramsIfc) + { + @paramsIfc.RemoteCount.Where(_ => _.HasValue).Subscribe(_ => Total = _.Value).DisposeItWith(Disposable); + var inputPipe = @paramsIfc.Items + .Transform(_ => new ParamItemViewModel(Id, _, _log)) + .DisposeMany() + .RefCount(); + inputPipe + .Bind(out var allItems) + .Subscribe(_ => + { + foreach (var item in _config.Params) + { + var existItem = _.FirstOrDefault(_ => _.Current.Name == item.Name); + if (existItem == null) continue; + existItem.Current?.SetConfig(item); + } + }) + .DisposeItWith(Disposable); + inputPipe + .Filter(FilterPipe) + .SortBy(_ => _.Name) + .AutoRefresh(v => v.IsSynced) + .Bind(out var leftItems) + .Subscribe() + .DisposeItWith(Disposable); + inputPipe + .AutoRefresh(_ => _.IsStarred) + .Filter(_ => _.IsStarred) + .Subscribe(_ => + { + foreach (var item in _) + { + var existItem = _config.Params.FirstOrDefault(__ => __.Name == item.Current.Name); + + if (existItem != null) _config.Params.Remove(existItem); + + _config.Params.Add(new ParamItemViewModelConfig + { + IsStarred = item.Current.IsStarred, + Name = item.Current.Name + }); + } + }) + .DisposeItWith(Disposable); + + FilterPipe.OnNext(_ => true); + Params = leftItems; + + UpdateParams = ReactiveCommand.CreateFromTask(async cancel => + { + _cancellationTokenSource = new CancellationTokenSource().DisposeItWith(Disposable); + var viewed = _viewedParamsList.Items.Select(_ => _.GetConfig()).ToArray(); + _viewedParamsList.Clear(); + try + { + await paramsIfc.ReadAll(new Progress(_ => Progress = _), _cancellationTokenSource.Token); + } + catch (TaskCanceledException) + { + _log.Info("User", "Canceled updating params"); + } + finally + { + foreach (var item in viewed) + { + var existItem = allItems.FirstOrDefault(_ => _.Name == item.Name); + if (existItem == null) continue; + existItem.SetConfig(item); + _viewedParamsList.Add(existItem); + } + } + }).DisposeItWith(Disposable); + UpdateParams.IsExecuting.ToProperty(this, _ => _.IsRefreshing, out _isRefreshing).DisposeItWith(Disposable); + UpdateParams.ThrownExceptions.Subscribe(OnRefreshError).DisposeItWith(Disposable); + StopUpdateParams = ReactiveCommand.Create(() => { _cancellationTokenSource.Cancel(); }); + RemoveAllPins = ReactiveCommand.Create(() => + { + _viewedParamsList.Edit(_ => + { + foreach (var item in _) + { + item.IsPinned = false; + } + }); + }).DisposeItWith(Disposable); + } + + public override async Task TryClose() + { + var notSyncedParams = _viewedParamsList.Items.Where(_ => !_.IsSynced).ToArray(); + + if (notSyncedParams.Any()) + { + var dialog = new ContentDialog() + { + Title = RS.ParamPageViewModel_DataLossDialog_Title, + Content = RS.ParamPageViewModel_DataLossDialog_Content, + IsSecondaryButtonEnabled = true, + PrimaryButtonText = RS.ParamPageViewModel_DataLossDialog_PrimaryButtonText, + SecondaryButtonText = RS.ParamPageViewModel_DataLossDialog_SecondaryButtonText, + CloseButtonText = RS.ParamPageViewModel_DataLossDialog_CloseButtonText + }; + + var result = await dialog.ShowAsync(); + + if (result == ContentDialogResult.Primary) + { + foreach (var param in notSyncedParams) + { + param.WriteParamData(); + param.IsSynced = true; + } + + return true; + } + + if (result == ContentDialogResult.Secondary) return true; + + if (result == ContentDialogResult.None) return false; + } + + return true; + } + + private void OnRefreshError(Exception? ex) + { + _log.Error("Params view", "Error to read all params items", ex); + } + + public bool IsRefreshing => _isRefreshing.Value; + [Reactive] public bool ShowStaredOnly { get; set; } + [Reactive] public double Progress { get; set; } + [Reactive] public ReactiveCommand Clear { get; set; } + [Reactive] public ReactiveCommand UpdateParams { get; set; } + [Reactive] public ReactiveCommand StopUpdateParams { get; set; } + [Reactive] public ReactiveCommand RemoveAllPins { get; set; } + public Subject> FilterPipe { get; } = new(); + [Reactive] public ReadOnlyObservableCollection Params { get; set; } + public ReadOnlyObservableCollection ViewedParams => _viewedParams; + [Reactive] public string DeviceName { get; set; } + [Reactive] public string SearchText { get; set; } + [Reactive] public int Total { get; set; } + [Reactive] public int Loaded { get; set; } + + public ParamItemViewModel SelectedItem + { + get => _selectedItem; + set + { + var itemsToDelete = _viewedParamsList.Items.Where(_ => _.IsPinned == false).ToArray(); + _viewedParamsList.RemoveMany(itemsToDelete); + this.RaiseAndSetIfChanged(ref _selectedItem, value); + if (value != null) + { + if (_viewedParamsList.Items.Contains(value) == false) + { + _viewedParamsList.Add(value); + } + } + } + } +} \ No newline at end of file diff --git a/src/Asv.Drones.Gui.Api/Tools/Controls/TreePage/DesignTime/TreePageExampleView.axaml b/src/Asv.Drones.Gui.Api/Tools/Controls/TreePage/DesignTime/TreePageExampleView.axaml new file mode 100644 index 0000000..293646c --- /dev/null +++ b/src/Asv.Drones.Gui.Api/Tools/Controls/TreePage/DesignTime/TreePageExampleView.axaml @@ -0,0 +1,8 @@ + + Welcome to Avalonia! + \ No newline at end of file diff --git a/src/Asv.Drones.Gui.Api/Tools/Controls/TreePage/DesignTime/TreePageExampleView.axaml.cs b/src/Asv.Drones.Gui.Api/Tools/Controls/TreePage/DesignTime/TreePageExampleView.axaml.cs new file mode 100644 index 0000000..4481049 --- /dev/null +++ b/src/Asv.Drones.Gui.Api/Tools/Controls/TreePage/DesignTime/TreePageExampleView.axaml.cs @@ -0,0 +1,12 @@ +using Avalonia.Controls; + +namespace Asv.Drones.Gui.Api; + +[ExportView(typeof(TreePageExampleViewModel))] +public partial class TreePageExampleView : UserControl +{ + public TreePageExampleView() + { + InitializeComponent(); + } +} \ No newline at end of file diff --git a/src/Asv.Drones.Gui.Api/Tools/Controls/TreePage/DesignTime/TreePageExampleViewModel.cs b/src/Asv.Drones.Gui.Api/Tools/Controls/TreePage/DesignTime/TreePageExampleViewModel.cs new file mode 100644 index 0000000..5a25166 --- /dev/null +++ b/src/Asv.Drones.Gui.Api/Tools/Controls/TreePage/DesignTime/TreePageExampleViewModel.cs @@ -0,0 +1,69 @@ +using System.Collections.ObjectModel; +using Avalonia.Input; +using Material.Icons; +using ReactiveUI; + +namespace Asv.Drones.Gui.Api; + +public class TreePageExampleViewModel : TreePageViewModel +{ + private static int _instanceCounter = 0; + + public TreePageExampleViewModel() : base(WellKnownUri.UndefinedUri) + { + } + + public TreePageExampleViewModel(ITreePageContext treePageContext, string id) : base(id) + { + var collection = new ObservableCollection + { + new MenuItem("asv:1") + { + Header = "Action 1", + Icon = MaterialIconKind.AccessPoint, + Command = ReactiveCommand.Create(() => { }), + Order = 1 + }, + + new MenuItem("asv:2") + { + Header = "Action 2", + Icon = MaterialIconKind.AccessPoint, + Command = ReactiveCommand.Create(() => { }), + Order = 2, + Items = new ReadOnlyObservableCollection(new ObservableCollection + { + new MenuItem("asv:2.1") + { + Header = "Action 2.1", + Icon = MaterialIconKind.AccessPoint, + Command = ReactiveCommand.Create(() => { }), + Order = 2, + HotKey = new KeyGesture(Key.A, KeyModifiers.Alt) + }, + new MenuItem("asv:2.2") + { + Header = "Action 2.2", + Icon = MaterialIconKind.Create, + Command = ReactiveCommand.Create(() => { }), + Order = 2, + HotKey = new KeyGesture(Key.A, KeyModifiers.Alt), + }, + }) + }, + }; + _instanceCounter++; + for (int i = 0; i < Math.Min(_instanceCounter, 3); i++) + { + collection.Add(new MenuItem($"asv:{3 + i}") + { + Header = $"Action {3 + i}", + Icon = (MaterialIconKind)i, + Command = ReactiveCommand.Create(() => { }), + Order = i + }); + } + + Actions = new ReadOnlyObservableCollection(collection); + } +} \ No newline at end of file diff --git a/src/Asv.Drones.Gui.Api/Tools/Controls/TreePage/DesignTime/TreePageExplorerDesignTime.cs b/src/Asv.Drones.Gui.Api/Tools/Controls/TreePage/DesignTime/TreePageExplorerDesignTime.cs new file mode 100644 index 0000000..b27bccf --- /dev/null +++ b/src/Asv.Drones.Gui.Api/Tools/Controls/TreePage/DesignTime/TreePageExplorerDesignTime.cs @@ -0,0 +1,47 @@ +using DynamicData; +using Material.Icons; + +namespace Asv.Drones.Gui.Api; + +public class TreePageExplorerDesignTime : ViewModelProviderBase, ITreePageContext +{ + public static TreePageExplorerDesignTime Instance { get; } = new(); + public static TreePageExplorerDesignTime[] Instances { get; } = [Instance]; + + public BreadCrumbItem[] BreadCrumbs { get; } + + private TreePageExplorerDesignTime() + { + const int rootCnt = 10; + const int subRootCnt = 10; + var maxIconIndex = Enum.GetValues().Length; + var icons = Enum.GetValues(); + for (var i = 0; i < rootCnt; i++) + { + var root = new TreePageCallbackMenuItem($"asv:{i}", + $"Settings {i}", + icons[Random.Shared.Next(0, maxIconIndex)], + $"Settings {i} description", + rootCnt - i) + { + Status = i % 3 == 0 ? null : i.ToString() + }; + Source.AddOrUpdate(root); + for (var j = 0; j < subRootCnt; j++) + { + Source.AddOrUpdate(new TreePageCallbackMenuItem($"asv:{i}{j}", + $"Settings {i}", + icons[Random.Shared.Next(0, maxIconIndex)], + $"Settings {i} {j} description", + rootCnt - i, + root.Id, x => new TreePageExampleViewModel(x, $"asv:{i}{j}")) + { + Status = i % 3 == 0 ? null : i.ToString() + }); + } + } + + BreadCrumbs = Source.Items.Take(2).Select((x, i) => new BreadCrumbItem(i == 0, x)).ToArray(); + } + +} \ No newline at end of file diff --git a/src/Asv.Drones.Gui.Api/Tools/Controls/TreePage/ITreePage.cs b/src/Asv.Drones.Gui.Api/Tools/Controls/TreePage/ITreePage.cs new file mode 100644 index 0000000..8d0fc63 --- /dev/null +++ b/src/Asv.Drones.Gui.Api/Tools/Controls/TreePage/ITreePage.cs @@ -0,0 +1,45 @@ +using System.Collections.ObjectModel; + +namespace Asv.Drones.Gui.Api; + +public interface ITreePage : IViewModel +{ + ReadOnlyObservableCollection? Actions { get; } + Task TryClose(); + +} + +public class TreePageViewModel : ViewModelBase, ITreePage +{ + protected TreePageViewModel(Uri id) : base(id) + { + } + + protected TreePageViewModel(string id) : base(id) + { + } + + public ReadOnlyObservableCollection? Actions { get; set; } + public virtual Task TryClose() + { + return Task.FromResult(true); + } +} + +public class TreePageWithValidationViewModel : ViewModelBaseWithValidation, ITreePage +{ + protected TreePageWithValidationViewModel(Uri id) : base(id) + { + } + + protected TreePageWithValidationViewModel(string id) : base(id) + { + } + + public ReadOnlyObservableCollection? Actions { get; set; } + + public virtual Task TryClose() + { + return Task.FromResult(true); + } +} \ No newline at end of file diff --git a/src/Asv.Drones.Gui.Api/Tools/Controls/TreePage/ITreePageContext.cs b/src/Asv.Drones.Gui.Api/Tools/Controls/TreePage/ITreePageContext.cs new file mode 100644 index 0000000..892d9bb --- /dev/null +++ b/src/Asv.Drones.Gui.Api/Tools/Controls/TreePage/ITreePageContext.cs @@ -0,0 +1,6 @@ +namespace Asv.Drones.Gui.Api; + +public interface ITreePageContext +{ + +} \ No newline at end of file diff --git a/src/Asv.Drones.Gui.Api/Tools/Controls/TreePage/ITreePageExplorer.cs b/src/Asv.Drones.Gui.Api/Tools/Controls/TreePage/ITreePageExplorer.cs new file mode 100644 index 0000000..a5dee62 --- /dev/null +++ b/src/Asv.Drones.Gui.Api/Tools/Controls/TreePage/ITreePageExplorer.cs @@ -0,0 +1,9 @@ +namespace Asv.Drones.Gui.Api; + +public interface ITreePageExplorer +{ + Task GoTo(Uri pageId); + ITreePageMenuItem? SelectedMenu { get; } + ITreePage? CurrentPage { get; } + bool IsCompactMode { get; set; } +} \ No newline at end of file diff --git a/src/Asv.Drones.Gui.Api/Tools/Controls/TreePage/ITreePageMenuItem.cs b/src/Asv.Drones.Gui.Api/Tools/Controls/TreePage/ITreePageMenuItem.cs new file mode 100644 index 0000000..c3316f9 --- /dev/null +++ b/src/Asv.Drones.Gui.Api/Tools/Controls/TreePage/ITreePageMenuItem.cs @@ -0,0 +1,68 @@ +using Material.Icons; +using ReactiveUI.Fody.Helpers; + +namespace Asv.Drones.Gui.Api; + +public interface ITreePageMenuItem : IViewModel +{ + Uri ParentId { get; } + string? Name { get; } + string? Description { get; } + MaterialIconKind Icon { get; } + int Order { get; } + public string? Status { get; } + ITreePage? CreatePage(ITreePageContext context); +} + +public abstract class TreePageMenuItem : ViewModelBase, ITreePageMenuItem +{ + public abstract Uri ParentId { get; } + public abstract string? Name { get; } + public abstract string? Description { get; } + public abstract MaterialIconKind Icon { get; } + public abstract int Order { get; } + [Reactive] public string? Status { get; set; } + + public abstract ITreePage? CreatePage(ITreePageContext context); + + protected TreePageMenuItem(Uri id) : base(id) + { + } + + protected TreePageMenuItem(string id) : base(id) + { + } +} + +public class TreePageCallbackMenuItem : TreePageMenuItem +{ + private readonly Func? _factory; + + public TreePageCallbackMenuItem(Uri id, string name, MaterialIconKind icon, string? desc = null, int order = 0, + Uri? parentId = null, Func? factory = null) : base(id) + { + ParentId = parentId ?? WellKnownUri.UndefinedUri; + Name = name; + Description = desc; + Icon = icon; + Order = order; + _factory = factory; + } + + public TreePageCallbackMenuItem(string id, string name, MaterialIconKind icon, string? desc = null, int order = 0, + Uri? parentId = null, Func? factory = null) : this(new Uri(id), name, icon, desc, + order, parentId, factory) + { + } + + public override Uri ParentId { get; } + public override string? Name { get; } + public override string? Description { get; } + public override MaterialIconKind Icon { get; } + public override int Order { get; } + + public override ITreePage? CreatePage(ITreePageContext context) + { + return _factory?.Invoke(context); + } +} \ No newline at end of file diff --git a/src/Asv.Drones.Gui.Api/Tools/Controls/TreePage/TreeGroupView.axaml b/src/Asv.Drones.Gui.Api/Tools/Controls/TreePage/TreeGroupView.axaml new file mode 100644 index 0000000..3821f95 --- /dev/null +++ b/src/Asv.Drones.Gui.Api/Tools/Controls/TreePage/TreeGroupView.axaml @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Asv.Drones.Gui.Api/Tools/Controls/TreePage/TreeGroupView.axaml.cs b/src/Asv.Drones.Gui.Api/Tools/Controls/TreePage/TreeGroupView.axaml.cs new file mode 100644 index 0000000..0c5a097 --- /dev/null +++ b/src/Asv.Drones.Gui.Api/Tools/Controls/TreePage/TreeGroupView.axaml.cs @@ -0,0 +1,12 @@ +using Avalonia.Controls; + +namespace Asv.Drones.Gui.Api; + +[ExportView(typeof(TreeGroupViewModel))] +public partial class TreeGroupView : UserControl +{ + public TreeGroupView() + { + InitializeComponent(); + } +} \ No newline at end of file diff --git a/src/Asv.Drones.Gui.Api/Tools/Controls/TreePage/TreeGroupViewModel.cs b/src/Asv.Drones.Gui.Api/Tools/Controls/TreePage/TreeGroupViewModel.cs new file mode 100644 index 0000000..f6880e5 --- /dev/null +++ b/src/Asv.Drones.Gui.Api/Tools/Controls/TreePage/TreeGroupViewModel.cs @@ -0,0 +1,53 @@ +using System.Collections.ObjectModel; +using System.Reactive; +using Asv.Common; +using DynamicData; +using ReactiveUI; + +namespace Asv.Drones.Gui.Api; + +public class TreeGroupViewModel : ViewModelBase, ITreePage +{ + private readonly Func> _navigate; + + public TreeGroupViewModel() : base(TreePageExplorerDesignTime.Instance.BreadCrumbs.First().Item.Id) + { + var item = TreePageExplorerDesignTime.Instance.BreadCrumbs.First().Item; + var node = new Node(item, WellKnownUri.UndefinedUri); + + Items = new ReadOnlyObservableCollection( + new ObservableCollection( + new[] + { + new TreePartMenuItemContainer(new Node(item, WellKnownUri.UndefinedUri)), + new TreePartMenuItemContainer(new Node(item, WellKnownUri.UndefinedUri)) + })); + + + Item = new TreePartMenuItemContainer(node); + } + + public TreeGroupViewModel(TreePartMenuItemContainer menu, Func> navigate) : + base(menu.Base.Id) + { + _navigate = navigate; + Item = menu; + Items = menu.Items; + NavigateCommand = ReactiveCommand.Create(Navigate).DisposeItWith(Disposable); + } + + private Unit Navigate(TreePartMenuItemContainer arg) + { + _navigate(arg); + return Unit.Default; + } + + public TreePartMenuItemContainer Item { get; } + public ReadOnlyObservableCollection Items { get; } + public ReactiveCommand NavigateCommand { get; } + public ReadOnlyObservableCollection? Actions { get; } = null; + public virtual Task TryClose() + { + return Task.FromResult(true); + } +} \ No newline at end of file diff --git a/src/Asv.Drones.Gui.Api/Tools/Controls/TreePage/TreePageExplorerView.axaml b/src/Asv.Drones.Gui.Api/Tools/Controls/TreePage/TreePageExplorerView.axaml new file mode 100644 index 0000000..433e742 --- /dev/null +++ b/src/Asv.Drones.Gui.Api/Tools/Controls/TreePage/TreePageExplorerView.axaml @@ -0,0 +1,200 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Asv.Drones.Gui.Api/Tools/Controls/TreePage/TreePageExplorerView.axaml.cs b/src/Asv.Drones.Gui.Api/Tools/Controls/TreePage/TreePageExplorerView.axaml.cs new file mode 100644 index 0000000..057f11c --- /dev/null +++ b/src/Asv.Drones.Gui.Api/Tools/Controls/TreePage/TreePageExplorerView.axaml.cs @@ -0,0 +1,56 @@ +using Avalonia.Controls; +using Avalonia.Layout; +using Avalonia.ReactiveUI; +using DynamicData.Binding; + +namespace Asv.Drones.Gui.Api; + +public partial class TreePageExplorerView : ReactiveUserControl +{ + private IDisposable? _subscribe; + private double? _lastWidth = null; + + public TreePageExplorerView() + { + InitializeComponent(); + this.WhenValueChanged(x => x.ViewModel).Subscribe(OnViewModel); + } + + private void OnViewModel(TreePageExplorerViewModel? vm) + { + if (vm == null) return; + _subscribe?.Dispose(); + _subscribe = vm.WhenValueChanged(x => x.IsCompactMode, false) + .Subscribe(OnIsCompactModeChanged); + } + + private void OnIsCompactModeChanged(bool compactMode) + { + var column = PART_Grid.ColumnDefinitions.FirstOrDefault(); + if (compactMode && column != null) + { + column.Width = new GridLength(0, GridUnitType.Auto); + } + } + + private void Layoutable_OnEffectiveViewportChanged(object? sender, EffectiveViewportChangedEventArgs e) + { + if (_lastWidth == null) + { + if (PART_TitleGrid.ColumnDefinitions[2].ActualWidth < 8) + { + if (ViewModel != null) ViewModel.IsTitleCompactMode = true; + // replace large control + _lastWidth = PART_TitleGrid.Bounds.Width; + } + } + else + { + if (PART_TitleGrid.Bounds.Width > _lastWidth) + { + if (ViewModel != null) ViewModel.IsTitleCompactMode = false; + _lastWidth = null; + } + } + } +} \ No newline at end of file diff --git a/src/Asv.Drones.Gui.Api/Tools/Controls/TreePage/TreePageExplorerViewModel.cs b/src/Asv.Drones.Gui.Api/Tools/Controls/TreePage/TreePageExplorerViewModel.cs new file mode 100644 index 0000000..2919cfd --- /dev/null +++ b/src/Asv.Drones.Gui.Api/Tools/Controls/TreePage/TreePageExplorerViewModel.cs @@ -0,0 +1,264 @@ +using System.Collections.ObjectModel; +using System.Composition; +using System.Reactive; +using System.Reactive.Linq; +using System.Reflection; +using Asv.Common; +using DynamicData; +using DynamicData.Binding; +using Material.Icons; +using ReactiveUI; +using ReactiveUI.Fody.Helpers; + +namespace Asv.Drones.Gui.Api; + +public class TreePageExplorerViewModel : DisposableReactiveObject,ITreePageExplorer +{ + private readonly ITreePageContext _context; + private readonly ILogService _log; + private readonly ReadOnlyObservableCollection _tree; + private readonly CircularBuffer2 _backwardHistory = new(10); + private readonly CircularBuffer2 _forwardHistory = new(10); + private bool _addToHistory = true; + private TreePartMenuItemContainer? _lastNavigationItem; + private int _isNavigationInProgress; + + public TreePageExplorerViewModel() : this(TreePageExplorerDesignTime.Instances, TreePageExplorerDesignTime.Instance, + NullLogService.Instance) + { + DesignTime.ThrowIfNotDesignMode(); + Title = "Tree view"; + GoBack = ReactiveCommand.Create(() => { }); + GoForward = ReactiveCommand.Create(() => { }); + BreadCrumb.AddRange(TreePageExplorerDesignTime.Instance.BreadCrumbs); + } + + + public TreePageExplorerViewModel(IEnumerable> items, ITreePageContext context, + ILogService log) + { + _context = context; + _log = log; + items.Select(x => x.Items) + .MergeChangeSets() + .TransformToTree(x => x.ParentId) + .Transform(x => new TreePartMenuItemContainer(x)) + .SortBy(x => x.Base.Order) + .Bind(out _tree) + .DisposeMany() + .Subscribe() + .DisposeItWith(Disposable); + + + this.WhenValueChanged(x => x.SelectedMenuContainer, notifyOnInitialValue:false) + .Where(x=>x != null) + .Do(AddToHistory) + .Do(x=>SelectedMenu = x?.Base) + .Subscribe(x=>Navigate(x).Wait()) + .DisposeItWith(Disposable); + + Disposable.AddAction(() => + { + if (CurrentPage != null && CurrentPage.GetType().GetCustomAttribute() == null) + { + CurrentPage.Dispose(); + } + }); + + GoForward = ReactiveCommand.Create(GoForwardImpl, this.WhenAnyValue(x => x.CanGoForward)) + .DisposeItWith(Disposable); + GoBack = ReactiveCommand.Create(GoBackImpl, this.WhenAnyValue(x => x.CanGoBack)).DisposeItWith(Disposable); + BreadCrumb.CollectionChanged += (sender, args) => + { + + }; + } + + [Reactive] public bool IsCompactMode { get; set; } + [Reactive] public bool IsTitleCompactMode { get; set; } + + public Task GoTo(Uri pageId) + { + ArgumentNullException.ThrowIfNull(pageId); + if (pageId.Scheme.Equals(WellKnownUri.UriScheme) == false) + { + _log.Error(Title, $"Unknown uri scheme. Want {WellKnownUri.UriScheme}. Got:{pageId.Scheme}"); + return Task.FromResult(false); + } + + return Navigate(Find(pageId)); + } + + [Reactive] + public ITreePageMenuItem? SelectedMenu { get; set; } + + private TreePartMenuItemContainer? Find(Uri pageId, TreePartMenuItemContainer? root = null) + { + if (root != null) + { + if (root.Id == pageId) return root; + } + var items = root?.Items ?? _tree; + foreach (var item in items) + { + var result = Find(pageId, item); + if (result != null) return result; + } + return null; + } + + private async Task Navigate(TreePartMenuItemContainer? menu) + { + if (menu == null) return false; + if (Interlocked.CompareExchange(ref _isNavigationInProgress, 1, 0) != 0) + { + // recursive or fast change navigation will be ignored + return false; + } + + try + { + var currentPage = CurrentPage; + if (currentPage != null) + { + var canClose = await currentPage.TryClose(); + if (canClose == false) return false; // can't close, it's busy now + if (currentPage.GetType().GetCustomAttribute() == null) + { + currentPage.Dispose(); + } + } + + var part = menu.Base.CreatePage(_context) ?? new TreeGroupViewModel(menu, Navigate); + + + CurrentPage = part; + + var stack = new Stack(); + stack.Push(menu.Base); + var current = menu.Node.Parent; + while (current.HasValue) + { + stack.Push(current.Value.Item); + current = current.Value.Parent; + } + + BreadCrumb.Clear(); + while (stack.Count > 0) + { + var item = stack.Pop(); + BreadCrumb.Add(new BreadCrumbItem(BreadCrumb.Count == 0, item)); + } + + menu.IsSelected = true; + return true; + } + catch (Exception? e) + { + _log.Error(Title, $"Can't create page {menu.Base.Name}:{e.Message}", e); + return false; + } + finally + { + Interlocked.Exchange(ref _isNavigationInProgress, 0); + } + } + + [Reactive] public string Title { get; set; } + [Reactive] public MaterialIconKind Icon { get; set; } + + [Reactive] public TreePartMenuItemContainer? SelectedMenuContainer { get; set; } + + [Reactive] public ITreePage? CurrentPage { get; set; } + + public ReadOnlyObservableCollection Items => _tree; + + public ObservableCollectionExtended BreadCrumb { get; } = []; + + #region Navigation + + private void AddToHistory(TreePartMenuItemContainer? menu) + { + if (_addToHistory && _lastNavigationItem != null) + { + _backwardHistory.PushFront(_lastNavigationItem); + _forwardHistory.Clear(); + CanGoForward = false; + CanGoBack = true; + } + + _lastNavigationItem = menu; + } + + [Reactive] public bool CanGoForward { get; set; } + public ReactiveCommand GoForward { get; } + + private async void GoForwardImpl() + { + if (_forwardHistory.IsEmpty) return; + var item = _forwardHistory[0]; + _forwardHistory.PopFront(); + CanGoForward = _forwardHistory.IsEmpty == false; + _addToHistory = false; + await Navigate(item); + _addToHistory = true; + } + + [Reactive] public bool CanGoBack { get; set; } + public ReactiveCommand GoBack { get; } + + + private async void GoBackImpl() + { + if (_backwardHistory.IsEmpty) return; + var item = _backwardHistory[0]; + _backwardHistory.PopFront(); + CanGoBack = _backwardHistory.IsEmpty == false; + if (SelectedMenuContainer != null) + { + _forwardHistory.PushFront(SelectedMenuContainer); + } + + CanGoForward = true; + _addToHistory = false; + await Navigate(item); + _addToHistory = true; + } + + #endregion +} + +public class BreadCrumbItem(bool isFirst, ITreePageMenuItem item) +{ + public bool IsFirst { get; } = isFirst; + public ITreePageMenuItem Item { get; } = item; +} + +public class TreePartMenuItemContainer : ViewModelBase +{ + private readonly ReadOnlyObservableCollection _items; + + public TreePartMenuItemContainer(Node node) : base(node.Item.Id) + { + Node = node; + node.Children.Connect() + .Transform(x => new TreePartMenuItemContainer(x)) + .SortBy(x => x.Base.Order) + .Bind(out _items) + .DisposeMany() + .Subscribe() + .DisposeItWith(Disposable); + + Base = node.Item; + } + + public Node Node { get; } + + public ITreePageMenuItem Base { get; set; } + + public ReadOnlyObservableCollection Items => _items; + + [Reactive] public bool IsExpanded { get; set; } = true; + + [Reactive] public bool IsSelected { get; set; } +} \ No newline at end of file diff --git a/src/Asv.Drones.Gui.Api/Tools/Converters/AddDoubleConverter.cs b/src/Asv.Drones.Gui.Api/Tools/Converters/AddDoubleConverter.cs new file mode 100644 index 0000000..3daf0d4 --- /dev/null +++ b/src/Asv.Drones.Gui.Api/Tools/Converters/AddDoubleConverter.cs @@ -0,0 +1,30 @@ +using System.Globalization; +using Avalonia.Data.Converters; + +namespace Asv.Drones.Gui.Api +{ + public class AddDoubleConverter : IValueConverter + { + public static IValueConverter Instance { get; } = new AddDoubleConverter(); + + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + if (parameter is string p && double.TryParse(p, out var add) && value is double v) + { + return add + v; + } + + return value; + } + + public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + { + if (parameter is string p && double.TryParse(p, out var add) && value is double v) + { + return add - v; + } + + return value; + } + } +} \ No newline at end of file diff --git "a/src/Asv.Drones.Gui.Api/Tools/Converters/AddPer\321\201entDoubleConverter.cs" "b/src/Asv.Drones.Gui.Api/Tools/Converters/AddPer\321\201entDoubleConverter.cs" new file mode 100644 index 0000000..b552bdc --- /dev/null +++ "b/src/Asv.Drones.Gui.Api/Tools/Converters/AddPer\321\201entDoubleConverter.cs" @@ -0,0 +1,30 @@ +using System.Globalization; +using Avalonia.Data.Converters; + +namespace Asv.Drones.Gui.Api +{ + public class AddPerсentDoubleConverter : IValueConverter + { + public static IValueConverter Instance { get; } = new AddPerсentDoubleConverter(); + + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + if (parameter is string p && double.TryParse(p, out var add) && value is double v) + { + return v + v * add / 100; + } + + return value; + } + + public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + { + if (parameter is string p && double.TryParse(p, out var add) && value is double v) + { + return v - v * add / 100; + } + + return value; + } + } +} \ No newline at end of file diff --git a/src/Asv.Drones.Gui.Api/Tools/Converters/EnumToBooleanConverter.cs b/src/Asv.Drones.Gui.Api/Tools/Converters/EnumToBooleanConverter.cs new file mode 100644 index 0000000..7ab3538 --- /dev/null +++ b/src/Asv.Drones.Gui.Api/Tools/Converters/EnumToBooleanConverter.cs @@ -0,0 +1,18 @@ +using System.Globalization; +using Avalonia.Data; +using Avalonia.Data.Converters; + +namespace Asv.Drones.Gui.Api; + +public class EnumToBooleanConverter : IValueConverter +{ + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + return value?.Equals(parameter); + } + + public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + { + return value?.Equals(true) == true ? parameter : BindingOperations.DoNothing; + } +} \ No newline at end of file diff --git a/src/Asv.Drones.Gui.Api/Tools/Converters/MaterialIconConverter.cs b/src/Asv.Drones.Gui.Api/Tools/Converters/MaterialIconConverter.cs new file mode 100644 index 0000000..233c5f0 --- /dev/null +++ b/src/Asv.Drones.Gui.Api/Tools/Converters/MaterialIconConverter.cs @@ -0,0 +1,41 @@ +using System.Globalization; +using Avalonia.Data.Converters; +using Material.Icons; +using Material.Icons.Avalonia; + +namespace Asv.Drones.Gui.Api +{ + public class MaterialIconConverter : IValueConverter + { + public static IValueConverter Instance { get; } = new MaterialIconConverter(); + + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + if (value is MaterialIconKind kind) + { + return new MaterialIcon + { + Kind = kind + }; + } + + if (value is string str) + { + if (Enum.TryParse(str, true, out kind)) + { + return new MaterialIcon + { + Kind = kind + }; + } + } + + return new MaterialIcon(); + } + + public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + { + return value; + } + } +} \ No newline at end of file diff --git a/src/Asv.Drones.Gui.Api/Tools/Converters/MultipleIsNotNullConverter.cs b/src/Asv.Drones.Gui.Api/Tools/Converters/MultipleIsNotNullConverter.cs new file mode 100644 index 0000000..82a311f --- /dev/null +++ b/src/Asv.Drones.Gui.Api/Tools/Converters/MultipleIsNotNullConverter.cs @@ -0,0 +1,19 @@ +using System.Globalization; +using Avalonia.Data.Converters; + +namespace Asv.Drones.Gui.Api; + +public class MultipleIsNotNullConverter : IMultiValueConverter +{ + public static IMultiValueConverter Instance { get; } = new MultipleIsNotNullConverter(); + + public object Convert(IList values, Type targetType, object? parameter, CultureInfo culture) + { + return values.All(value => value != null); + } + + public object ConvertBack(IList values, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/src/Asv.Drones.Gui.Api/Tools/Converters/StringToDecimalConverter.cs b/src/Asv.Drones.Gui.Api/Tools/Converters/StringToDecimalConverter.cs new file mode 100644 index 0000000..8fbc7f5 --- /dev/null +++ b/src/Asv.Drones.Gui.Api/Tools/Converters/StringToDecimalConverter.cs @@ -0,0 +1,22 @@ +using Avalonia.Data.Converters; +using System.Globalization; + +namespace Asv.Drones.Gui.Api +{ + public class StringToDecimalConverter : IValueConverter + { + public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + if (value is string stringValue && decimal.TryParse(stringValue, NumberStyles.Number, CultureInfo.InvariantCulture, out var decimalValue)) + return decimalValue; + return 0m; + } + + public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + { + if (value is decimal decimalValue) + return decimalValue.ToString("0.00", CultureInfo.InvariantCulture); + return string.Empty; + } + } +} \ No newline at end of file diff --git a/src/Asv.Drones.Gui.Api/Tools/DesignTime.cs b/src/Asv.Drones.Gui.Api/Tools/DesignTime.cs new file mode 100644 index 0000000..3817ca5 --- /dev/null +++ b/src/Asv.Drones.Gui.Api/Tools/DesignTime.cs @@ -0,0 +1,14 @@ +using Avalonia.Controls; + +namespace Asv.Drones.Gui.Api; + +public static class DesignTime +{ + public static void ThrowIfNotDesignMode() + { + if (Design.IsDesignMode == false) + throw new InvalidOperationException("This method is for design mode only"); + } + + public static ILogService Log => NullLogService.Instance; +} \ No newline at end of file diff --git a/src/Asv.Drones.Gui.Api/Tools/ExportViewAttribute.cs b/src/Asv.Drones.Gui.Api/Tools/ExportViewAttribute.cs new file mode 100644 index 0000000..e082493 --- /dev/null +++ b/src/Asv.Drones.Gui.Api/Tools/ExportViewAttribute.cs @@ -0,0 +1,28 @@ +using System.Composition; +using Avalonia.Controls; + +namespace Asv.Drones.Gui.Api +{ + /// + /// This attribute is used to find a matching View for the ViewModel in ViewLocator + /// + [MetadataAttribute] + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] + public class ExportViewAttribute : ExportAttribute, IViewMetadata + { + public ExportViewAttribute(Type viewModelType) + : base(null, typeof(Control)) + { + if (viewModelType.IsSubclassOf(typeof(Control))) + throw new ArgumentException("ViewModelType cannot be a View type", nameof(viewModelType)); + this.ViewModelType = viewModelType; + } + + public Type ViewModelType { get; } + } + + public interface IViewMetadata + { + Type ViewModelType { get; } + } +} \ No newline at end of file diff --git a/src/Asv.Drones.Gui.Api/Tools/ProgressMessage.cs b/src/Asv.Drones.Gui.Api/Tools/ProgressMessage.cs new file mode 100644 index 0000000..5a58484 --- /dev/null +++ b/src/Asv.Drones.Gui.Api/Tools/ProgressMessage.cs @@ -0,0 +1,7 @@ +namespace Asv.Drones.Gui.Api; + +public class ProgressMessage(double progress, string message) +{ + public string Message => message; + public double Progress => progress; +} \ No newline at end of file diff --git a/src/Asv.Drones.Gui.Api/Tools/PropertyComparer.cs b/src/Asv.Drones.Gui.Api/Tools/PropertyComparer.cs new file mode 100644 index 0000000..f49eb6f --- /dev/null +++ b/src/Asv.Drones.Gui.Api/Tools/PropertyComparer.cs @@ -0,0 +1,20 @@ +namespace Asv.Drones.Gui.Api; + +public class PropertyComparer : IComparer + where TField : IComparable +{ + private readonly Func _callback; + + public PropertyComparer(Func callback) + { + _callback = callback; + } + + public int Compare(T? x, T? y) + { + if (ReferenceEquals(x, y)) return 0; + if (ReferenceEquals(null, y)) return 1; + if (ReferenceEquals(null, x)) return -1; + return _callback(x).CompareTo(_callback(y)); + } +} \ No newline at end of file diff --git a/src/Asv.Drones.Gui.Api/Tools/ViewModel/DisposableReactiveObject.cs b/src/Asv.Drones.Gui.Api/Tools/ViewModel/DisposableReactiveObject.cs new file mode 100644 index 0000000..17cee27 --- /dev/null +++ b/src/Asv.Drones.Gui.Api/Tools/ViewModel/DisposableReactiveObject.cs @@ -0,0 +1,76 @@ +using System.Reactive.Disposables; +using ReactiveUI; + +namespace Asv.Drones.Gui.Api +{ + public class DisposableReactiveObject : ReactiveObject, IDisposable + { + private const int Disposed = 1; + private const int NotDisposed = 0; + private int _disposeFlag; + private CancellationTokenSource? _cancel; + private CompositeDisposable? _dispose; + private readonly object _sync1 = new(); + private readonly object _sync2 = new(); + + #region Disposing + + protected bool IsDisposed => Thread.VolatileRead(ref _disposeFlag) > 0; + + protected void ThrowIfDisposed() + { + if (IsDisposed) throw new ObjectDisposedException(GetType().Name); + } + + protected CancellationToken DisposeCancel + { + get + { + if (_cancel != null) + { + return IsDisposed ? CancellationToken.None : _cancel.Token; + } + + lock (_sync2) + { + if (_cancel != null) + { + return IsDisposed ? CancellationToken.None : _cancel.Token; + } + + _cancel = new(); + return _cancel.Token; + } + } + } + + protected CompositeDisposable Disposable + { + get + { + if (_dispose != null) return _dispose; + lock (_sync1) + { + return _dispose ??= new CompositeDisposable(); + } + } + } + + protected virtual void InternalDisposeOnce() + { + if (_cancel?.Token.CanBeCanceled == true) + _cancel.Cancel(false); + _cancel?.Dispose(); + _dispose?.Dispose(); + } + + #endregion + + public void Dispose() + { + if (Interlocked.CompareExchange(ref _disposeFlag, Disposed, NotDisposed) != NotDisposed) return; + InternalDisposeOnce(); + GC.SuppressFinalize(this); + } + } +} \ No newline at end of file diff --git a/src/Asv.Drones.Gui.Api/Tools/ViewModel/DisposableReactiveObjectWithValidation.cs b/src/Asv.Drones.Gui.Api/Tools/ViewModel/DisposableReactiveObjectWithValidation.cs new file mode 100644 index 0000000..d81e8f0 --- /dev/null +++ b/src/Asv.Drones.Gui.Api/Tools/ViewModel/DisposableReactiveObjectWithValidation.cs @@ -0,0 +1,76 @@ +using System.Reactive.Disposables; +using ReactiveUI.Validation.Helpers; + +namespace Asv.Drones.Gui.Api +{ + public class DisposableReactiveObjectWithValidation : ReactiveValidationObject, IDisposable + { + private const int Disposed = 1; + private const int NotDisposed = 0; + private int _disposeFlag; + private CancellationTokenSource? _cancel; + private CompositeDisposable? _dispose; + private readonly object _sync1 = new(); + private readonly object _sync2 = new(); + + #region Disposing + + protected bool IsDisposed => Thread.VolatileRead(ref _disposeFlag) > 0; + + protected void ThrowIfDisposed() + { + if (IsDisposed) throw new ObjectDisposedException(GetType().Name); + } + + protected CancellationToken DisposeCancel + { + get + { + if (_cancel != null) + { + return IsDisposed ? CancellationToken.None : _cancel.Token; + } + + lock (_sync2) + { + if (_cancel != null) + { + return IsDisposed ? CancellationToken.None : _cancel.Token; + } + + _cancel = new(); + return _cancel.Token; + } + } + } + + protected CompositeDisposable Disposable + { + get + { + if (_dispose != null) return _dispose; + lock (_sync1) + { + return _dispose ??= new CompositeDisposable(); + } + } + } + + protected virtual void InternalDisposeOnce() + { + if (_cancel?.Token.CanBeCanceled == true) + _cancel.Cancel(false); + _cancel?.Dispose(); + _dispose?.Dispose(); + } + + #endregion + + public void Dispose() + { + if (Interlocked.CompareExchange(ref _disposeFlag, Disposed, NotDisposed) != NotDisposed) return; + InternalDisposeOnce(); + GC.SuppressFinalize(this); + } + } +} \ No newline at end of file diff --git a/src/Asv.Drones.Gui.Api/Tools/ViewModel/IViewModelProvider.cs b/src/Asv.Drones.Gui.Api/Tools/ViewModel/IViewModelProvider.cs new file mode 100644 index 0000000..d5699d2 --- /dev/null +++ b/src/Asv.Drones.Gui.Api/Tools/ViewModel/IViewModelProvider.cs @@ -0,0 +1,60 @@ +using Asv.Common; +using DynamicData; +using ReactiveUI; + +namespace Asv.Drones.Gui.Api +{ + public interface IViewModelProvider : IDisposable + { + IObservable> Items { get; } + } + + public abstract class ViewModelProviderBase : DisposableOnceWithCancel, IViewModelProvider + where TView : IViewModel + { + private readonly SourceCache _sourceCache; + + protected ViewModelProviderBase() + { + _sourceCache = new SourceCache(model => model.Id) + .DisposeItWith(Disposable); + } + + protected ISourceCache Source => _sourceCache; + public virtual IObservable> Items => Source.Connect().DisposeMany(); + } + + + public interface IViewModel : IReactiveObject, IDisposable + { + Uri Id { get; } + } + + public class ViewModelBase : DisposableReactiveObject, IViewModel + { + protected ViewModelBase(Uri id) + { + Id = id; + } + + protected ViewModelBase(string id) : this(new Uri(id)) + { + } + + public Uri Id { get; } + } + + public class ViewModelBaseWithValidation : DisposableReactiveObjectWithValidation, IViewModel + { + protected ViewModelBaseWithValidation(Uri id) + { + Id = id; + } + + protected ViewModelBaseWithValidation(string id) : this(new Uri(id)) + { + } + + public Uri Id { get; } + } +} \ No newline at end of file diff --git a/src/Asv.Drones.Gui.Api/Tools/ViewModel/RemoteLogMessageProxy.cs b/src/Asv.Drones.Gui.Api/Tools/ViewModel/RemoteLogMessageProxy.cs new file mode 100644 index 0000000..3813a02 --- /dev/null +++ b/src/Asv.Drones.Gui.Api/Tools/ViewModel/RemoteLogMessageProxy.cs @@ -0,0 +1,47 @@ +using Material.Icons; +using Microsoft.Extensions.Logging; + +namespace Asv.Drones.Gui.Api; + +public class RemoteLogMessageProxy +{ + public RemoteLogMessageProxy(LogMessage textMessage) + { + switch (textMessage.LogLevel) + { + case LogLevel.Information: + IsInfo = true; + Icon = MaterialIconKind.InformationCircle; + break; + + case LogLevel.Warning: + IsWarning = true; + Icon = MaterialIconKind.Warning; + break; + + case LogLevel.Error: + IsError = true; + Icon = MaterialIconKind.Warning; + break; + + case LogLevel.Trace: + IsTrace = true; + Icon = MaterialIconKind.Exclamation; + break; + } + + DateTime = textMessage.Timestamp; + Sender = textMessage.Category; + Message = textMessage.Message; + } + + public bool IsError { get; } + public bool IsWarning { get; } + public bool IsTrace { get; } + public bool IsInfo { get; } + + public DateTime DateTime { get; } + public MaterialIconKind Icon { get; } + public string Sender { get; } + public string Message { get; } +} \ No newline at end of file diff --git a/src/Asv.Drones.Gui.Api/Tools/WindowHelper.cs b/src/Asv.Drones.Gui.Api/Tools/WindowHelper.cs new file mode 100644 index 0000000..30464db --- /dev/null +++ b/src/Asv.Drones.Gui.Api/Tools/WindowHelper.cs @@ -0,0 +1,157 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.VisualTree; + +namespace Asv.Drones.Gui.Api +{ + public class WindowHelper + { + public static readonly AttachedProperty EnableDragProperty = + AvaloniaProperty.RegisterAttached("EnableDrag"); + + public static readonly AttachedProperty DoubleTappedWindowStateProperty = + AvaloniaProperty.RegisterAttached("DoubleTappedWindowState"); + + public static readonly AttachedProperty IgnoreDragProperty = + AvaloniaProperty.RegisterAttached("IgnoreDrag"); + + static WindowHelper() + { + EnableDragProperty.Changed.Subscribe(OnChangedEnableDrag); + DoubleTappedWindowStateProperty.Changed.Subscribe(OnChangedDoubleClickWindowState); + } + + #region IgnoreDragProperty + + public static void SetIgnoreDrag(AvaloniaObject element, bool commandValue) + { + element.SetValue(IgnoreDragProperty, commandValue); + } + + public static bool GetIgnoreDrag(AvaloniaObject element) + { + return element.GetValue(IgnoreDragProperty); + } + + #endregion + + #region DoubleTappedWindowStateProperty + + private static void OnChangedDoubleClickWindowState(AvaloniaPropertyChangedEventArgs source) + { + if (source.Sender is InputElement uiElement) + { + if (source.OldValue == false && source.NewValue.Value) + { + uiElement.DoubleTapped += DoubleTappedHandler; + } + else + { + uiElement.PointerPressed -= DoubleTappedHandler; + } + } + } + + private static void DoubleTappedHandler(object? sender, RoutedEventArgs e) + { + if (sender is not Visual uiElement) return; + if (uiElement is AvaloniaObject avalonia) + { + if (GetIgnoreDrag(avalonia)) return; + } + + var parent = uiElement; + var avoidInfiniteLoop = 0; + // Search up the visual tree to find the first parent window. + while (parent is Window == false) + { + parent = parent.GetVisualParent(); + avoidInfiniteLoop++; + if (avoidInfiniteLoop == 1000) + { + // Something is wrong - we could not find the parent window. + return; + } + } + + var window = parent as Window; + window.WindowState = window.WindowState switch + { + WindowState.Normal => WindowState.Maximized, + WindowState.Minimized => WindowState.Maximized, + WindowState.Maximized => WindowState.Normal, + _ => throw new ArgumentOutOfRangeException() + }; + } + + public static void SetDoubleTappedWindowState(AvaloniaObject element, bool commandValue) + { + element.SetValue(DoubleTappedWindowStateProperty, commandValue); + } + + public static bool GetDoubleTappedWindowState(AvaloniaObject element) + { + return element.GetValue(DoubleTappedWindowStateProperty); + } + + #endregion + + #region EnableDrag + + private static void OnChangedEnableDrag(AvaloniaPropertyChangedEventArgs source) + { + if (source.Sender is IInputElement uiElement) + { + if (source.OldValue == false && source.NewValue.Value) + { + uiElement.PointerPressed += MouseDownHandler; + } + else + { + uiElement.PointerPressed -= MouseDownHandler; + } + } + } + + private static void MouseDownHandler(object sender, PointerPressedEventArgs e) + { + if (sender is not Visual uiElement) return; + var parent = uiElement; + var avoidInfiniteLoop = 0; + // Search up the visual tree to find the first parent window. + while (parent is Window == false) + { + parent = parent.GetVisualParent(); + avoidInfiniteLoop++; + if (avoidInfiniteLoop == 1000) + { + // Something is wrong - we could not find the parent window. + return; + } + } + + var window = parent as Window; + window.BeginMoveDrag(e); + } + + /// + /// Accessor for Attached property . + /// + public static void SetEnableDrag(AvaloniaObject element, bool commandValue) + { + element.SetValue(EnableDragProperty, commandValue); + } + + /// + /// Accessor for Attached property . + /// + public static bool GetEnableDrag(AvaloniaObject element) + { + return element.GetValue(EnableDragProperty); + } + + #endregion + } +} \ No newline at end of file diff --git a/src/Asv.Drones.Gui.Api/WellKnownUri.cs b/src/Asv.Drones.Gui.Api/WellKnownUri.cs new file mode 100644 index 0000000..4ee8861 --- /dev/null +++ b/src/Asv.Drones.Gui.Api/WellKnownUri.cs @@ -0,0 +1,158 @@ +namespace Asv.Drones.Gui.Api; + +public static class WellKnownUri +{ + /// + /// This is Scheme for URI in this application + /// + public const string UriScheme = "asv"; + + /// + /// This simple non empty URI + /// + public const string Undefined = $"{UriScheme}:null"; + + public static readonly Uri UndefinedUri = new(Undefined); + + /// + /// This is base URI for all shell controls + /// + public const string Shell = $"{UriScheme}:shell"; + + /// + /// This base URI for left menu tms with shell pages SHELL=>MENU controls + /// + public const string ShellMenu = $"{Shell}.menu"; + + /// + /// This base URI for SHELL=>HEADER controls + /// + public const string ShellHeader = $"{Shell}.header"; + + /// + /// This is base uri for SHELL=>HEADER=>MENU + /// + public const string ShellHeaderMenu = $"{ShellHeader}.menu"; + + /// + /// This is base URI for SHELL=>STATUS controls + /// + public const string ShellStatus = $"{Shell}.status"; + + /// + /// This is base URI for SHELL=>PAGES + /// + public const string ShellPage = $"{Shell}.page"; + + + public const string ShellStatusTextMessage = $"{ShellStatus}.text-message"; + public const string ShellStatusMapCache = $"{ShellStatus}.map-cache"; + public const string ShellStatusPorts = $"{ShellStatus}.ports"; + + + public const string ShellMenuMapFlight = $"{ShellMenu}.flight"; + public static readonly Uri ShellMenuMapFlightUri = new(ShellMenuMapFlight); + public const string ShellPageMapFlight = $"{ShellPage}.flight"; + public static readonly Uri ShellPageMapFlightUri = new(ShellPageMapFlight); + public const string ShellPageMapFlightAction = $"{ShellPageMapFlight}.action"; + public const string ShellPageMapFlightActionZoom = $"{ShellPageMapFlightAction}.zoom"; + public const string ShellPageMapFlightActionRuler = $"{ShellPageMapFlightAction}.ruler"; + public const string ShellPageMapFlightActionMover = $"{ShellPageMapFlightAction}.mover"; + public const string ShellPageMapFlightWidget = $"{ShellPageMapFlight}.widget"; + public const string ShellPageMapFlightWidgetAnchorEditor = $"{ShellPageMapFlightWidget}.editor"; + public const string ShellPageMapFlightWidgetUav = $"{ShellPageMapFlightWidget}.uav"; + public const string ShellPageMapFlightWidgetLogger = $"{ShellPageMapFlightWidget}.logger"; + public const string ShellPageMapFlightAnchor = $"{ShellPageMapFlight}.layer"; + + + public const string ShellMenuMapPlaning = $"{ShellMenu}.planing"; + public static readonly Uri ShellMenuMapPlaningUri = new(ShellMenuMapPlaning); + public const string ShellPageMapPlaning = $"{ShellPage}.planing"; + public static readonly Uri ShellPageMapPlaningUri = new(ShellPageMapPlaning); + public const string ShellPageMapPlaningAction = $"{ShellPageMapPlaning}.action"; + public const string ShellPageMapPlaningActionZoom = $"{ShellPageMapPlaningAction}.zoom"; + public const string ShellPageMapPlaningActionRuler = $"{ShellPageMapPlaningAction}.ruler"; + public const string ShellPageMapPlaningActionMover = $"{ShellPageMapPlaningAction}.mover"; + public const string ShellPageMapPlaningMissionBrowser = $"{ShellPageMapPlaning}.browser"; + public const string ShellPageMapPlaningMissionSavingBrowser = $"{ShellPageMapPlaning}.browser"; + public static readonly Uri ShellPageMapPlaningMissionBrowserUri = new(ShellPageMapPlaningMissionBrowser); + public static readonly Uri ShellPageMapPlaningMissionSavingBrowserUri = new(ShellPageMapPlaningMissionSavingBrowser); + public const string ShellPageMapPlaningWidget = $"{ShellPageMapPlaning}.widget"; + public const string ShellPageMapPlaningWidgetAnchorEditor = $"{ShellPageMapPlaningWidget}.editor"; + public const string ShellPageMapPlaningWidgetEditor = $"{ShellPageMapPlaningWidget}.mission-editor"; + public const string ShellPageMapPlaningWidgetItemEditor = $"{ShellPageMapPlaningWidget}.item-editor"; + + public const string ShellPageMapPlaningWidgetEditorUploadMissionDialog = + $"{ShellPageMapPlaningWidgetEditor}.upload-mission-dialog"; + + public const string ShellPageMapPlaningWidgetEditorDownloadMissionDialog = + $"{ShellPageMapPlaningWidgetEditor}.download-mission-dialog"; + + public const string ShellPagePacketViewer = $"{ShellPage}.packets"; + public static readonly Uri ShellPagePacketViewerUri = new(ShellPagePacketViewer); + public const string ShellMenuPacketViewer = $"{ShellMenu}.packets"; + + + public const string ShellMenuSettings = $"{ShellMenu}.settings"; + public static readonly Uri ShellMenuSettingsUri = new(ShellPageSettings); + public const string ShellPageSettings = $"{ShellPage}.settings"; + public static readonly Uri ShellPageSettingsUri = new(ShellPageSettings); + + public const string ShellPageSettingsConnections = $"{ShellPageSettings}.connection"; + public static readonly Uri ShellPageSettingsConnectionsUri = new(ShellPageSettingsConnections); + + public const string ShellPageSettingsConnectionsIdentify = $"{ShellPageSettingsConnections}.id"; + public static readonly Uri ShellPageSettingsConnectionsIdentifyUri = new(ShellPageSettingsConnectionsIdentify); + + public const string ShellPageSettingsConnectionsDevices = $"{ShellPageSettingsConnections}.devices"; + public static readonly Uri ShellPageSettingsConnectionsDevicesUri = new(ShellPageSettingsConnectionsDevices); + + public const string ShellPageSettingsConnectionsPorts = $"{ShellPageSettingsConnections}.ports"; + public static readonly Uri ShellPageSettingsConnectionsPortsUri = new(ShellPageSettingsConnectionsPorts); + + public const string ShellPageSettingsAppearance = $"{ShellPageSettings}.appearance"; + public static readonly Uri ShellPageSettingsAppearanceUri = new Uri(ShellPageSettingsAppearance); + + public const string ShellPageSettingsMeasure = $"{ShellPageSettings}.measure"; + public static readonly Uri ShellPageSettingsMeasureUri = new(ShellPageSettingsMeasure); + + public const string ShellPageSettingsPlugins = $"{ShellPageSettings}.plugins"; + public static readonly Uri ShellPageSettingsPluginsUri = new(ShellPageSettingsPlugins); + + public const string ShellPageSettingsPluginsMarket = $"{ShellPageSettingsPlugins}.market"; + public static readonly Uri ShellPageSettingsPluginsMarketUri = new(ShellPageSettingsPluginsMarket); + + public const string ShellPageSettingsPluginsLocal = $"{ShellPageSettingsPlugins}.local"; + public static readonly Uri ShellPageSettingsPluginsLocalUri = new(ShellPageSettingsPluginsLocal); + + public const string ShellPageSettingsPluginsSource = $"{ShellPageSettingsPlugins}.source"; + public static readonly Uri ShellPageSettingsPluginsSourceUri = new(ShellPageSettingsPluginsSource); + + public const string ShellMenuParamsVehicle = $"{ShellMenu}.params-vehicle"; + public const string ShellPageParamsVehicle = $"{ShellPage}.params-vehicle"; + + public const string ShellMenuQuickParamsVehicle = $"{ShellMenu}.quick-params-vehicle"; + + public const string ShellPageQuickParams = $"{ShellPage}.quick-params"; + + public const string ShellPageQuickParamsArduCopterVehicle = $"{ShellPageQuickParams}.ardu-copter"; + + public const string ShellPageQuickParamsArduPlaneVehicle = $"{ShellPageQuickParams}.ardu-plane"; + + public const string ShellPageQuickParamsPx4CopterVehicle = $"{ShellPageQuickParams}.px-4-copter"; + + public const string ShellPageQuickParamsPx4PlaneVehicle = $"{ShellPageQuickParams}.px-4-plane"; + + public const string ShellPageQuickParamsArduCopterVehicleStandard = $"{ShellPageQuickParamsArduCopterVehicle}.standard-params"; + + public const string ShellPageQuickParamsArduPlaneVehicleStandard = $"{ShellPageQuickParamsArduPlaneVehicle}.standard-params"; + + public const string ShellPageQuickParamsPx4CopterVehicleStandard = $"{ShellPageQuickParamsPx4CopterVehicle}.standard-params"; + + public const string ShellPageQuickParamsPx4PlaneVehicleStandard = $"{ShellPageQuickParamsPx4PlaneVehicle}.standard-params"; + + public const string ShellMenuLogViewer = $"{ShellMenu}.log-viewer"; + public static readonly Uri ShellMenuLogViewerUri = new(ShellMenuLogViewer); + public const string ShellPageLogViewer = $"{ShellPage}.log-viewer"; + public static readonly Uri ShellPageLogViewerUri = new(ShellPageLogViewer); +} \ No newline at end of file diff --git a/src/Directory.Build.props b/src/Directory.Build.props new file mode 100644 index 0000000..fce3195 --- /dev/null +++ b/src/Directory.Build.props @@ -0,0 +1,19 @@ + + + 2.0.0-dev.1 + 1.0.3-dev + 3.0.0-dev.2 + 2.5.5 + 11.1.0 + 1.0.1 + 2.0.6 + 3.10.4-dev.1 + NuGet + 2.0.5 + 19.5.41 + 6.0.0 + 2.0.1 + 3.1.7 + 8.0.0 + +