Meta - Replace WPF projects with Avalonia projects

This commit is contained in:
Robert committed 2022-03-27 11:55:26 +02:00
1 parent fcf0376a9a
commit d7f3ee1190
1110 files changed
+10872 -52240

No files matched your search

@@ -0,0 +1,3 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=plugins/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=services_005Cwindowservice/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
@@ -0,0 +1,506 @@
<?xml version="1.0"?>
<doc>
<assembly>
<name>Artemis.UI.Shared</name>
</assembly>
<members>
<member name="F:Artemis.UI.Avalonia.Shared.Controls.ArtemisIcon.IconProperty">
<summary>
Gets or sets the currently displayed icon as either a <see cref="T:Material.Icons.MaterialIconKind" /> or an <see cref="T:System.Uri" /> pointing
to an SVG
</summary>
</member>
<member name="P:Artemis.UI.Avalonia.Shared.Controls.ArtemisIcon.Icon">
<summary>
Gets or sets the currently displayed icon as either a <see cref="T:Material.Icons.MaterialIconKind" /> or an <see cref="T:System.Uri" /> pointing
to an SVG
</summary>
</member>
<member name="T:Artemis.UI.Avalonia.Shared.Controls.DeviceVisualizer">
<summary>
Visualizes an <see cref="T:Artemis.Core.ArtemisDevice" /> with optional per-LED colors
</summary>
</member>
<member name="M:Artemis.UI.Avalonia.Shared.Controls.DeviceVisualizer.#ctor">
<inheritdoc />
</member>
<member name="M:Artemis.UI.Avalonia.Shared.Controls.DeviceVisualizer.Render(Avalonia.Media.DrawingContext)">
<inheritdoc />
</member>
<member name="E:Artemis.UI.Avalonia.Shared.Controls.DeviceVisualizer.LedClicked">
<summary>
Occurs when a LED of the device has been clicked
</summary>
</member>
<member name="M:Artemis.UI.Avalonia.Shared.Controls.DeviceVisualizer.OnLedClicked(Artemis.UI.Avalonia.Shared.Events.LedClickedEventArgs)">
<summary>
Invokes the <see cref="E:Artemis.UI.Avalonia.Shared.Controls.DeviceVisualizer.LedClicked" /> event
</summary>
<param name="e"></param>
</member>
<member name="F:Artemis.UI.Avalonia.Shared.Controls.DeviceVisualizer.DeviceProperty">
<summary>
Gets or sets the <see cref="T:Artemis.Core.ArtemisDevice" /> to display
</summary>
</member>
<member name="P:Artemis.UI.Avalonia.Shared.Controls.DeviceVisualizer.Device">
<summary>
Gets or sets the <see cref="T:Artemis.Core.ArtemisDevice" /> to display
</summary>
</member>
<member name="F:Artemis.UI.Avalonia.Shared.Controls.DeviceVisualizer.ShowColorsProperty">
<summary>
Gets or sets boolean indicating whether or not to show per-LED colors
</summary>
</member>
<member name="P:Artemis.UI.Avalonia.Shared.Controls.DeviceVisualizer.ShowColors">
<summary>
Gets or sets a boolean indicating whether or not to show per-LED colors
</summary>
</member>
<member name="F:Artemis.UI.Avalonia.Shared.Controls.DeviceVisualizer.HighlightedLedsProperty">
<summary>
Gets or sets a list of LEDs to highlight
</summary>
</member>
<member name="P:Artemis.UI.Avalonia.Shared.Controls.DeviceVisualizer.HighlightedLeds">
<summary>
Gets or sets a list of LEDs to highlight
</summary>
</member>
<member name="M:Artemis.UI.Avalonia.Shared.Controls.DeviceVisualizer.OnAttachedToLogicalTree(Avalonia.LogicalTree.LogicalTreeAttachmentEventArgs)">
<inheritdoc />
</member>
<member name="M:Artemis.UI.Avalonia.Shared.Controls.DeviceVisualizer.OnDetachedFromLogicalTree(Avalonia.LogicalTree.LogicalTreeAttachmentEventArgs)">
<inheritdoc />
</member>
<member name="M:Artemis.UI.Avalonia.Shared.Controls.DeviceVisualizer.MeasureOverride(Avalonia.Size)">
<inheritdoc />
</member>
<member name="F:Artemis.UI.Avalonia.Shared.Controls.EnumComboBox.ValueProperty">
<summary>
Gets or sets the currently selected value
</summary>
</member>
<member name="P:Artemis.UI.Avalonia.Shared.Controls.EnumComboBox.Value">
<summary>
Gets or sets the currently selected value
</summary>
</member>
<member name="M:Artemis.UI.Avalonia.Shared.Controls.EnumComboBox.OnAttachedToLogicalTree(Avalonia.LogicalTree.LogicalTreeAttachmentEventArgs)">
<inheritdoc />
</member>
<member name="M:Artemis.UI.Avalonia.Shared.Controls.EnumComboBox.OnDetachedFromLogicalTree(Avalonia.LogicalTree.LogicalTreeAttachmentEventArgs)">
<inheritdoc />
</member>
<member name="F:Artemis.UI.Avalonia.Shared.Controls.ProfileConfigurationIcon.ConfigurationIconProperty">
<summary>
Gets or sets the <see cref="T:Artemis.Core.ProfileConfigurationIcon" /> to display
</summary>
</member>
<member name="P:Artemis.UI.Avalonia.Shared.Controls.ProfileConfigurationIcon.ConfigurationIcon">
<summary>
Gets or sets the <see cref="T:Artemis.Core.ProfileConfigurationIcon" /> to display
</summary>
</member>
<member name="T:Artemis.UI.Avalonia.Shared.Controls.SelectionRectangle">
<summary>
Visualizes an <see cref="T:Artemis.Core.ArtemisDevice" /> with optional per-LED colors
</summary>
</member>
<member name="F:Artemis.UI.Avalonia.Shared.Controls.SelectionRectangle.BackgroundProperty">
<summary>
Defines the <see cref="P:Artemis.UI.Avalonia.Shared.Controls.SelectionRectangle.Background" /> property.
</summary>
</member>
<member name="F:Artemis.UI.Avalonia.Shared.Controls.SelectionRectangle.BorderBrushProperty">
<summary>
Defines the <see cref="P:Artemis.UI.Avalonia.Shared.Controls.SelectionRectangle.BorderBrush" /> property.
</summary>
</member>
<member name="F:Artemis.UI.Avalonia.Shared.Controls.SelectionRectangle.BorderThicknessProperty">
<summary>
Defines the <see cref="P:Artemis.UI.Avalonia.Shared.Controls.SelectionRectangle.BorderBrush" /> property.
</summary>
</member>
<member name="F:Artemis.UI.Avalonia.Shared.Controls.SelectionRectangle.InputElementProperty">
<summary>
Defines the <see cref="M:Artemis.UI.Avalonia.Shared.Controls.SelectionRectangle.get_InputElement" /> property.
</summary>
</member>
<member name="M:Artemis.UI.Avalonia.Shared.Controls.SelectionRectangle.#ctor">
<inheritdoc />
</member>
<member name="P:Artemis.UI.Avalonia.Shared.Controls.SelectionRectangle.Background">
<summary>
Gets or sets a brush used to paint the control's background.
</summary>
</member>
<member name="P:Artemis.UI.Avalonia.Shared.Controls.SelectionRectangle.BorderBrush">
<summary>
Gets or sets a brush used to paint the control's border
</summary>
</member>
<member name="P:Artemis.UI.Avalonia.Shared.Controls.SelectionRectangle.BorderThickness">
<summary>
Gets or sets the width of the control's border
</summary>
</member>
<member name="M:Artemis.UI.Avalonia.Shared.Controls.SelectionRectangle.OnAttachedToVisualTree(Avalonia.VisualTreeAttachmentEventArgs)">
<inheritdoc />
</member>
<member name="M:Artemis.UI.Avalonia.Shared.Controls.SelectionRectangle.OnDetachedFromVisualTree(Avalonia.VisualTreeAttachmentEventArgs)">
<inheritdoc />
</member>
<member name="T:Artemis.UI.Avalonia.Shared.Converters.ColorToSKColorConverter">
<summary>
Converts <see cref="T:Avalonia.Media.Color" /> into <see cref="T:SkiaSharp.SKColor" />.
</summary>
</member>
<member name="M:Artemis.UI.Avalonia.Shared.Converters.ColorToSKColorConverter.Convert(System.Object,System.Type,System.Object,System.Globalization.CultureInfo)">
<inheritdoc />
</member>
<member name="M:Artemis.UI.Avalonia.Shared.Converters.ColorToSKColorConverter.ConvertBack(System.Object,System.Type,System.Object,System.Globalization.CultureInfo)">
<inheritdoc />
</member>
<member name="T:Artemis.UI.Avalonia.Shared.Converters.SKColorToColorConverter">
<summary>
Converts <see cref="T:SkiaSharp.SKColor" /> into <see cref="T:Avalonia.Media.Color" />.
</summary>
</member>
<member name="M:Artemis.UI.Avalonia.Shared.Converters.SKColorToColorConverter.Convert(System.Object,System.Type,System.Object,System.Globalization.CultureInfo)">
<inheritdoc />
</member>
<member name="M:Artemis.UI.Avalonia.Shared.Converters.SKColorToColorConverter.ConvertBack(System.Object,System.Type,System.Object,System.Globalization.CultureInfo)">
<inheritdoc />
</member>
<member name="T:Artemis.UI.Avalonia.Shared.Events.DataModelInputDynamicEventArgs">
<summary>
Provides data about selection events raised by <see cref="!:DataModelDynamicViewModel" />
</summary>
</member>
<member name="P:Artemis.UI.Avalonia.Shared.Events.DataModelInputDynamicEventArgs.DataModelPath">
<summary>
Gets the data model path that was selected
</summary>
</member>
<member name="T:Artemis.UI.Avalonia.Shared.Events.DataModelInputStaticEventArgs">
<summary>
Provides data about submit events raised by <see cref="!:DataModelStaticViewModel" />
</summary>
</member>
<member name="P:Artemis.UI.Avalonia.Shared.Events.DataModelInputStaticEventArgs.Value">
<summary>
The value that was submitted
</summary>
</member>
<member name="T:Artemis.UI.Avalonia.Shared.Events.LedClickedEventArgs">
<summary>
Provides data on LED click events raised by the device visualizer
</summary>
</member>
<member name="P:Artemis.UI.Avalonia.Shared.Events.LedClickedEventArgs.Device">
<summary>
The device that was clicked
</summary>
</member>
<member name="P:Artemis.UI.Avalonia.Shared.Events.LedClickedEventArgs.Led">
<summary>
The LED that was clicked
</summary>
</member>
<member name="T:Artemis.UI.Avalonia.Shared.Events.ProfileConfigurationEventArgs">
<summary>
Provides data on profile related events raised by the profile editor
</summary>
</member>
<member name="P:Artemis.UI.Avalonia.Shared.Events.ProfileConfigurationEventArgs.ProfileConfiguration">
<summary>
Gets the profile the event was raised for
</summary>
</member>
<member name="P:Artemis.UI.Avalonia.Shared.Events.ProfileConfigurationEventArgs.PreviousProfileConfiguration">
<summary>
If applicable, the previous active profile before the event was raised
</summary>
</member>
<member name="T:Artemis.UI.Avalonia.Shared.Events.RenderProfileElementEventArgs">
<summary>
Provides data on profile element related events raised by the profile editor
</summary>
</member>
<member name="P:Artemis.UI.Avalonia.Shared.Events.RenderProfileElementEventArgs.RenderProfileElement">
<summary>
Gets the profile element the event was raised for
</summary>
</member>
<member name="P:Artemis.UI.Avalonia.Shared.Events.RenderProfileElementEventArgs.PreviousRenderProfileElement">
<summary>
If applicable, the previous active profile element before the event was raised
</summary>
</member>
<member name="T:Artemis.UI.Avalonia.Shared.Exceptions.ArtemisSharedUIException">
<summary>
Represents errors that occur within the Artemis Shared UI library
</summary>
</member>
<member name="T:Artemis.UI.Avalonia.Shared.Ninject.SharedUIModule">
<summary>
The main <see cref="T:Ninject.Modules.NinjectModule" /> of the Artemis Shared UI toolkit that binds all services
</summary>
</member>
<member name="M:Artemis.UI.Avalonia.Shared.Ninject.SharedUIModule.Load">
<inheritdoc />
</member>
<member name="T:Artemis.UI.Avalonia.Shared.PluginConfigurationDialog`1">
<inheritdoc />
</member>
<member name="P:Artemis.UI.Avalonia.Shared.PluginConfigurationDialog`1.Type">
<inheritdoc />
</member>
<member name="T:Artemis.UI.Avalonia.Shared.PluginConfigurationDialog">
<summary>
Describes a configuration dialog for a specific plugin
</summary>
</member>
<member name="P:Artemis.UI.Avalonia.Shared.PluginConfigurationDialog.Type">
<inheritdoc />
</member>
<member name="T:Artemis.UI.Avalonia.Shared.PluginConfigurationViewModel">
<summary>
Represents a view model for a plugin configuration window
</summary>
</member>
<member name="M:Artemis.UI.Avalonia.Shared.PluginConfigurationViewModel.#ctor(Artemis.Core.Plugin)">
<summary>
Creates a new instance of the <see cref="T:Artemis.UI.Avalonia.Shared.PluginConfigurationViewModel" /> class
</summary>
<param name="plugin"></param>
</member>
<member name="P:Artemis.UI.Avalonia.Shared.PluginConfigurationViewModel.Plugin">
<summary>
Gets the plugin this configuration view model is associated with
</summary>
</member>
<member name="M:Artemis.UI.Avalonia.Shared.PluginConfigurationViewModel.Close">
<summary>
Closes the window hosting the view model
</summary>
</member>
<member name="E:Artemis.UI.Avalonia.Shared.PluginConfigurationViewModel.CloseRequested">
<summary>
Occurs when the the window hosting the view model should close
</summary>
</member>
<member name="T:Artemis.UI.Avalonia.Shared.Services.Builders.FileDialogFilterBuilder">
<summary>
Represents a builder that can create a <see cref="T:Avalonia.Controls.FileDialogFilter" />.
</summary>
</member>
<member name="M:Artemis.UI.Avalonia.Shared.Services.Builders.FileDialogFilterBuilder.WithName(System.String)">
<summary>
Sets the name of the filter
</summary>
</member>
<member name="M:Artemis.UI.Avalonia.Shared.Services.Builders.FileDialogFilterBuilder.WithExtension(System.String)">
<summary>
Adds the provided extension to the filter
</summary>
</member>
<member name="M:Artemis.UI.Avalonia.Shared.Services.Builders.NotificationBuilder.HavingButton(System.Action{Artemis.UI.Avalonia.Shared.Services.Builders.NotificationButtonBuilder})">
<summary>
Add a filter to the dialog
</summary>
</member>
<member name="T:Artemis.UI.Avalonia.Shared.Services.Builders.OpenFileDialogBuilder">
<summary>
Represents a builder that can create a <see cref="T:Avalonia.Controls.OpenFileDialog" />.
</summary>
</member>
<member name="M:Artemis.UI.Avalonia.Shared.Services.Builders.OpenFileDialogBuilder.WithAllowMultiple">
<summary>
Indicate that the user can select multiple files.
</summary>
</member>
<member name="M:Artemis.UI.Avalonia.Shared.Services.Builders.OpenFileDialogBuilder.WithTitle(System.String)">
<summary>
Set the title of the dialog
</summary>
</member>
<member name="M:Artemis.UI.Avalonia.Shared.Services.Builders.OpenFileDialogBuilder.WithDirectory(System.String)">
<summary>
Set the initial directory of the dialog
</summary>
</member>
<member name="M:Artemis.UI.Avalonia.Shared.Services.Builders.OpenFileDialogBuilder.WithInitialFileName(System.String)">
<summary>
Set the initial file name of the dialog
</summary>
</member>
<member name="M:Artemis.UI.Avalonia.Shared.Services.Builders.OpenFileDialogBuilder.HavingFilter(System.Action{Artemis.UI.Avalonia.Shared.Services.Builders.FileDialogFilterBuilder})">
<summary>
Add a filter to the dialog
</summary>
</member>
<member name="M:Artemis.UI.Avalonia.Shared.Services.Builders.OpenFileDialogBuilder.ShowAsync">
<summary>
Shows the file dialog
</summary>
<returns>
A task that on completion returns an array containing the full path to the selected
files, or null if the dialog was canceled.
</returns>
</member>
<member name="T:Artemis.UI.Avalonia.Shared.Services.Builders.SaveFileDialogBuilder">
<summary>
Represents a builder that can create a <see cref="T:Avalonia.Controls.SaveFileDialog" />.
</summary>
</member>
<member name="M:Artemis.UI.Avalonia.Shared.Services.Builders.SaveFileDialogBuilder.WithTitle(System.String)">
<summary>
Set the title of the dialog
</summary>
</member>
<member name="M:Artemis.UI.Avalonia.Shared.Services.Builders.SaveFileDialogBuilder.WithDirectory(System.String)">
<summary>
Set the initial directory of the dialog
</summary>
</member>
<member name="M:Artemis.UI.Avalonia.Shared.Services.Builders.SaveFileDialogBuilder.WithInitialFileName(System.String)">
<summary>
Set the initial file name of the dialog
</summary>
</member>
<member name="M:Artemis.UI.Avalonia.Shared.Services.Builders.SaveFileDialogBuilder.WithDefaultExtension(System.String)">
<summary>
Set the default extension of the dialog
</summary>
</member>
<member name="M:Artemis.UI.Avalonia.Shared.Services.Builders.SaveFileDialogBuilder.HavingFilter(System.Action{Artemis.UI.Avalonia.Shared.Services.Builders.FileDialogFilterBuilder})">
<summary>
Add a filter to the dialog
</summary>
</member>
<member name="M:Artemis.UI.Avalonia.Shared.Services.Builders.SaveFileDialogBuilder.ShowAsync">
<summary>
Shows the save file dialog.
</summary>
<returns>
A task that on completion contains the full path of the save location, or null if the
dialog was canceled.
</returns>
</member>
<member name="T:Artemis.UI.Avalonia.Shared.Services.Interfaces.IArtemisSharedUIService">
<summary>
Represents a service provided by the Artemis Shared UI library
</summary>
</member>
<member name="M:Artemis.UI.Avalonia.Shared.Services.Interfaces.IWindowService.ShowWindow``1(System.ValueTuple{System.String,System.Object}[])">
<summary>
Creates a view model instance of type <typeparamref name="TViewModel" /> and shows its corresponding View as a window
</summary>
<typeparam name="TViewModel">The type of view model to create</typeparam>
<returns>The created view model</returns>
</member>
<member name="M:Artemis.UI.Avalonia.Shared.Services.Interfaces.IWindowService.ShowWindow(System.Object)">
<summary>
Given a ViewModel, show its corresponding View as a window
</summary>
<param name="viewModel">ViewModel to show the View for</param>
</member>
<member name="M:Artemis.UI.Avalonia.Shared.Services.Interfaces.IWindowService.ShowExceptionDialog(System.String,System.Exception)">
<summary>
Shows a dialog displaying the given exception
</summary>
<param name="title">The title of the dialog</param>
<param name="exception">The exception to display</param>
</member>
<member name="M:Artemis.UI.Avalonia.Shared.Services.Interfaces.IWindowService.ShowDialogAsync``1(Artemis.UI.Avalonia.Shared.DialogViewModelBase{``0})">
<summary>
Given an existing ViewModel, show its corresponding View as a Dialog
</summary>
<typeparam name="TResult">The return type</typeparam>
<param name="viewModel">ViewModel to show the View for</param>
<returns>A task containing the return value of type <typeparamref name="TResult" /></returns>
</member>
<member name="M:Artemis.UI.Avalonia.Shared.Services.Interfaces.IWindowService.ShowDialogAsync``2(System.ValueTuple{System.String,System.Object}[])">
<summary>
Creates a view model instance of type <typeparamref name="TViewModel"/> and shows its corresponding View as a Dialog
</summary>
<typeparam name="TViewModel">The view model type</typeparam>
<typeparam name="TResult">The return type</typeparam>
<returns>A task containing the return value of type <typeparamref name="TResult" /></returns>
</member>
<member name="M:Artemis.UI.Avalonia.Shared.Services.Interfaces.IWindowService.ShowConfirmContentDialog(System.String,System.String,System.String,System.String)">
<summary>
Shows a content dialog asking the user to confirm an action
</summary>
<param name="title">The title of the dialog</param>
<param name="message">The message of the dialog</param>
<param name="confirm">The text of the confirm button</param>
<param name="cancel">The text of the cancel button, if <see langword="null"/> the cancel button will not be shown</param>
<returns>A task containing the result of the dialog, <see langword="true"/> if confirmed; otherwise <see langword="false"/></returns>
</member>
<member name="M:Artemis.UI.Avalonia.Shared.Services.Interfaces.IWindowService.CreateOpenFileDialog">
<summary>
Creates an open file dialog, use the fluent API to configure it
</summary>
<returns>The builder that can be used to configure the dialog</returns>
</member>
<member name="M:Artemis.UI.Avalonia.Shared.Services.Interfaces.IWindowService.CreateSaveFileDialog">
<summary>
Creates a save file dialog, use the fluent API to configure it
</summary>
<returns>The builder that can be used to configure the dialog</returns>
</member>
<member name="T:Artemis.UI.Avalonia.Shared.ViewModelBase">
<summary>
Represents the base class for Artemis view models
</summary>
</member>
<member name="P:Artemis.UI.Avalonia.Shared.ViewModelBase.DisplayName">
<summary>
Gets or sets the display name of the view model
</summary>
</member>
<member name="T:Artemis.UI.Avalonia.Shared.ActivatableViewModelBase">
<summary>
Represents the base class for Artemis view models that are interested in the activated event
</summary>
</member>
<member name="M:Artemis.UI.Avalonia.Shared.ActivatableViewModelBase.#ctor">
<inheritdoc />
</member>
<member name="M:Artemis.UI.Avalonia.Shared.ActivatableViewModelBase.Dispose(System.Boolean)">
<summary>
Releases the unmanaged resources used by the object and optionally releases the managed resources.
</summary>
<param name="disposing">
<see langword="true" /> to release both managed and unmanaged resources;
<see langword="false" /> to release only unmanaged resources.
</param>
</member>
<member name="P:Artemis.UI.Avalonia.Shared.ActivatableViewModelBase.Activator">
<inheritdoc />
</member>
<member name="M:Artemis.UI.Avalonia.Shared.ActivatableViewModelBase.Dispose">
<inheritdoc />
</member>
<member name="T:Artemis.UI.Avalonia.Shared.DialogViewModelBase`1">
<summary>
Represents the base class for Artemis view models used to drive dialogs
</summary>
</member>
<member name="M:Artemis.UI.Avalonia.Shared.DialogViewModelBase`1.Close(`0)">
<summary>
Closes the dialog with the given <paramref name="result" />
</summary>
<param name="result">The result of the dialog</param>
</member>
<member name="M:Artemis.UI.Avalonia.Shared.DialogViewModelBase`1.Cancel">
<summary>
Closes the dialog without a result
</summary>
</member>
</members>
</doc>
+36 -62
View File
@@ -1,76 +1,50 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0-windows</TargetFramework>
<PreserveCompilationContext>false</PreserveCompilationContext>
<ProjectTypeGuids>{60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
<ShouldIncludeNativeSkiaSharp>false</ShouldIncludeNativeSkiaSharp>
<AssemblyTitle>Artemis.UI.Shared</AssemblyTitle>
<Company>Artemis.UI.Shared</Company>
<Product>Artemis.UI.Shared</Product>
<Copyright>Copyright © Robert Beekman - 2020</Copyright>
<OutputType>Library</OutputType>
<TargetFramework>net6.0</TargetFramework>
<Nullable>enable</Nullable>
<ApplicationIcon />
<StartupObject />
<OutputPath>bin\</OutputPath>
<UseWPF>true</UseWPF>
<PlatformTarget>x64</PlatformTarget>
<Platforms>x64</Platforms>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<PlatformTarget>x64</PlatformTarget>
<DocumentationFile>bin\Artemis.UI.Shared.xml</DocumentationFile>
</PropertyGroup>
<PropertyGroup>
<NrtRevisionFormat>2.0-{chash:6}</NrtRevisionFormat>
<NrtResolveSimpleAttributes>true</NrtResolveSimpleAttributes>
<NrtResolveInformationalAttribute>true</NrtResolveInformationalAttribute>
<NrtResolveCopyright>true</NrtResolveCopyright>
<NrtTagMatch>v[0-9]*</NrtTagMatch>
<NrtRemoveTagV>true</NrtRemoveTagV>
<NrtRequiredVcs>git</NrtRequiredVcs>
<NrtShowRevision>true</NrtShowRevision>
<Version>2.0.0</Version>
<Nullable>enable</Nullable>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<DocumentationFile>bin\Artemis.UI.Shared.xml</DocumentationFile>
<DocumentationFile>bin\Artemis.UI.Avalonia.Shared.xml</DocumentationFile>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Humanizer.Core" Version="2.11.10" />
<PackageReference Include="MaterialDesignExtensions" Version="3.3.0" />
<PackageReference Include="MaterialDesignThemes" Version="4.1.0" />
<PackageReference Include="Microsoft.Xaml.Behaviors.Wpf" Version="1.1.31" />
<PackageReference Include="Ninject" Version="3.3.4" />
<PackageReference Include="Ninject.Extensions.Conventions" Version="3.3.0" />
<None Remove="Artemis.UI.Avalonia.Shared.csproj.DotSettings" />
<None Remove="Artemis.UI.Shared.csproj.DotSettings" />
<None Remove="DefaultTypes\DataModel\Display\DefaultDataModelDisplayView.xaml" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Avalonia" Version="0.10.13" />
<PackageReference Include="Avalonia.ReactiveUI" Version="0.10.13" />
<PackageReference Include="Avalonia.Svg.Skia" Version="0.10.12" />
<PackageReference Include="Avalonia.Xaml.Behaviors" Version="0.10.13.2" />
<PackageReference Include="DynamicData" Version="7.5.4" />
<PackageReference Include="FluentAvaloniaUI" Version="1.3.0" />
<PackageReference Include="Material.Icons.Avalonia" Version="1.0.2" />
<PackageReference Include="ReactiveUI" Version="17.1.50" />
<PackageReference Include="ReactiveUI.Validation" Version="2.2.1" />
<PackageReference Include="RGB.NET.Core" Version="1.0.0-prerelease7" />
<PackageReference Include="SharpVectors.Reloaded" Version="1.7.5" />
<PackageReference Include="SkiaSharp" Version="2.88.0-preview.178" />
<PackageReference Include="SkiaSharp.Views.WPF" Version="2.80.3" />
<PackageReference Include="Stylet" Version="1.3.6" />
<PackageReference Include="System.Buffers" Version="4.5.1" />
<PackageReference Include="System.Numerics.Vectors" Version="4.5.0" />
<PackageReference Include="Unclassified.NetRevisionTask" Version="0.4.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<Page Include="DefaultTypes\DataModel\Display\DefaultDataModelDisplayView.xaml">
<XamlRuntime>$(DefaultXamlRuntime)</XamlRuntime>
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Compile Remove="obj\x64\Debug\ColorPicker.g.cs" />
<Compile Remove="obj\x64\Debug\ColorPicker.g.i.cs" />
<Compile Remove="obj\x64\Debug\DraggableDouble.g.i.cs" />
<Compile Remove="obj\x64\Debug\DraggableFloat.g.cs" />
<Compile Remove="obj\x64\Debug\DraggableFloat.g.i.cs" />
<Compile Remove="obj\x64\Debug\DraggableInt.g.i.cs" />
<Compile Remove="obj\x64\Debug\GeneratedInternalTypeHelper.g.cs" />
<Compile Remove="obj\x64\Debug\GeneratedInternalTypeHelper.g.i.cs" />
<Compile Remove="obj\x64\Debug\UserControl1.g.i.cs" />
<ProjectReference Include="..\Artemis.Core\Artemis.Core.csproj" />
</ItemGroup>
<ItemGroup>
<None Remove="Resources\Fonts\RobotoMono-Regular.ttf" />
<ItemGroup>
<Compile Update="Controls\HotkeyBox.axaml.cs">
<DependentUpon>HotkeyBox.axaml</DependentUpon>
</Compile>
<Compile Update="Services\Window\ExceptionDialogView.axaml.cs">
<DependentUpon>%(Filename)</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Artemis.Core\Artemis.Core.csproj">
<Private>false</Private>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<Resource Include="Resources\Fonts\RobotoMono-Regular.ttf" />
</ItemGroup>
</Project>
</Project>
@@ -1,20 +1,6 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=behaviors/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=controls/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=converters/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=datamodelvisualization/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=datamodelvisualization_005Cshared/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=dependencyproperties/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=events/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=exceptions/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=extensions/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=ninject/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=ninject_005Cfactories/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=plugins/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=propertyinput/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=services_005Cdatamodelvisualization/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=services_005Cdialog/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=services_005Cinterfaces/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=services_005Cmessage/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=services_005Cwindow/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=utilities/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=services_005Cwindows/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=services_005Cwindowservice/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
@@ -0,0 +1,55 @@
using System.Windows.Input;
using Avalonia;
using FluentAvalonia.UI.Controls;
namespace Artemis.UI.Shared.AttachedProperties;
/// <summary>
/// Helper properties for working with NumberBoxes.
/// </summary>
public class NumberBoxAssist : AvaloniaObject
{
/// <summary>
/// Identifies the <seealso cref="SuffixTextProperty" /> Avalonia attached property.
/// </summary>
/// <value>Provide an <see cref="ICommand" /> derived object or binding.</value>
public static readonly AttachedProperty<string> SuffixTextProperty = AvaloniaProperty.RegisterAttached<NumberBoxAssist, string>("SuffixText", typeof(NumberBox));
/// <summary>
/// Identifies the <seealso cref="PrefixTextProperty" /> Avalonia attached property.
/// </summary>
/// <value>Provide an <see cref="ICommand" /> derived object or binding.</value>
public static readonly AttachedProperty<string> PrefixTextProperty = AvaloniaProperty.RegisterAttached<NumberBoxAssist, string>("PrefixText", typeof(NumberBox));
/// <summary>
/// Accessor for Attached property <see cref="SuffixTextProperty" />.
/// </summary>
public static void SetSuffixText(AvaloniaObject element, string value)
{
element.SetValue(SuffixTextProperty, value);
}
/// <summary>
/// Accessor for Attached property <see cref="SuffixTextProperty" />.
/// </summary>
public static string GetSuffixText(AvaloniaObject element)
{
return element.GetValue(SuffixTextProperty);
}
/// <summary>
/// Accessor for Attached property <see cref="PrefixTextProperty" />.
/// </summary>
public static void SetPrefixText(AvaloniaObject element, string value)
{
element.SetValue(PrefixTextProperty, value);
}
/// <summary>
/// Accessor for Attached property <see cref="PrefixTextProperty" />.
/// </summary>
public static string GetPrefixText(AvaloniaObject element)
{
return element.GetValue(PrefixTextProperty);
}
}
@@ -0,0 +1,65 @@
using System.Windows.Input;
using Avalonia;
using Avalonia.Controls;
namespace Artemis.UI.Shared.AttachedProperties;
/// <summary>
/// Helper properties for working with TextBoxes.
/// </summary>
public class TextBoxAssist : AvaloniaObject
{
/// <summary>
/// Identifies the <seealso cref="SuffixTextProperty" /> Avalonia attached property.
/// </summary>
/// <value>Provide an <see cref="ICommand" /> derived object or binding.</value>
public static readonly AttachedProperty<string> SuffixTextProperty = AvaloniaProperty.RegisterAttached<TextBoxAssist, string>("SuffixText", typeof(TextBox));
/// <summary>
/// Identifies the <seealso cref="PrefixTextProperty" /> Avalonia attached property.
/// </summary>
/// <value>Provide an <see cref="ICommand" /> derived object or binding.</value>
public static readonly AttachedProperty<string> PrefixTextProperty = AvaloniaProperty.RegisterAttached<TextBoxAssist, string>("PrefixText", typeof(TextBox));
/// <summary>
/// Accessor for Attached property <see cref="SuffixTextProperty" />.
/// </summary>
public static void SetSuffixText(AvaloniaObject element, string value)
{
element.SetValue(SuffixTextProperty, value);
if (!string.IsNullOrWhiteSpace(value) && !((TextBox)element).Classes.Contains("suffixed"))
((TextBox)element).Classes.Add("suffixed");
else
((TextBox)element).Classes.Remove("suffixed");
}
/// <summary>
/// Accessor for Attached property <see cref="SuffixTextProperty" />.
/// </summary>
public static string GetSuffixText(AvaloniaObject element)
{
return element.GetValue(SuffixTextProperty);
}
/// <summary>
/// Accessor for Attached property <see cref="PrefixTextProperty" />.
/// </summary>
public static void SetPrefixText(AvaloniaObject element, string value)
{
element.SetValue(PrefixTextProperty, value);
if (!string.IsNullOrWhiteSpace(value) && !((TextBox)element).Classes.Contains("prefixed"))
((TextBox)element).Classes.Add("prefixed");
else
((TextBox)element).Classes.Remove("prefixed");
}
/// <summary>
/// Accessor for Attached property <see cref="PrefixTextProperty" />.
/// </summary>
public static string GetPrefixText(AvaloniaObject element)
{
return element.GetValue(PrefixTextProperty);
}
}
@@ -0,0 +1,56 @@
using System;
using Avalonia;
using Avalonia.Data;
using Avalonia.Interactivity;
using Avalonia.Xaml.Interactivity;
using FluentAvalonia.UI.Controls;
namespace Artemis.UI.Shared.Behaviors;
/// <summary>
/// Represents a behavior that can be used to make a text box only update it's binding on focus loss.
/// </summary>
public class LostFocusNumberBoxBindingBehavior : Behavior<NumberBox>
{
public static readonly StyledProperty<double> ValueProperty = AvaloniaProperty.Register<LostFocusTextBoxBindingBehavior, double>(
nameof(Value), defaultBindingMode: BindingMode.TwoWay);
static LostFocusNumberBoxBindingBehavior()
{
ValueProperty.Changed.Subscribe(e => ((LostFocusNumberBoxBindingBehavior) e.Sender).OnBindingValueChanged());
}
public double Value
{
get => GetValue(ValueProperty);
set => SetValue(ValueProperty, value);
}
/// <inheritdoc />
protected override void OnAttached()
{
if (AssociatedObject != null)
AssociatedObject.LostFocus += OnLostFocus;
base.OnAttached();
}
/// <inheritdoc />
protected override void OnDetaching()
{
if (AssociatedObject != null)
AssociatedObject.LostFocus -= OnLostFocus;
base.OnDetaching();
}
private void OnLostFocus(object? sender, RoutedEventArgs e)
{
if (AssociatedObject != null)
Value = AssociatedObject.Value;
}
private void OnBindingValueChanged()
{
if (AssociatedObject != null)
AssociatedObject.Value = Value;
}
}
@@ -0,0 +1,56 @@
using System;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Data;
using Avalonia.Interactivity;
using Avalonia.Xaml.Interactivity;
namespace Artemis.UI.Shared.Behaviors;
/// <summary>
/// Represents a behavior that can be used to make a text box only update it's binding on focus loss.
/// </summary>
public class LostFocusTextBoxBindingBehavior : Behavior<TextBox>
{
public static readonly StyledProperty<string> TextProperty = AvaloniaProperty.Register<LostFocusTextBoxBindingBehavior, string>(
"Text", defaultBindingMode: BindingMode.TwoWay);
static LostFocusTextBoxBindingBehavior()
{
TextProperty.Changed.Subscribe(e => ((LostFocusTextBoxBindingBehavior) e.Sender).OnBindingValueChanged());
}
public string Text
{
get => GetValue(TextProperty);
set => SetValue(TextProperty, value);
}
/// <inheritdoc />
protected override void OnAttached()
{
if (AssociatedObject != null)
AssociatedObject.LostFocus += OnLostFocus;
base.OnAttached();
}
/// <inheritdoc />
protected override void OnDetaching()
{
if (AssociatedObject != null)
AssociatedObject.LostFocus -= OnLostFocus;
base.OnDetaching();
}
private void OnLostFocus(object? sender, RoutedEventArgs e)
{
if (AssociatedObject != null)
Text = AssociatedObject.Text;
}
private void OnBindingValueChanged()
{
if (AssociatedObject != null)
AssociatedObject.Text = Text;
}
}
@@ -1,41 +0,0 @@
using System.Windows.Documents;
using System.Windows.Navigation;
using Artemis.Core;
using Microsoft.Xaml.Behaviors;
namespace Artemis.UI.Shared
{
/// <summary>
/// Represents a behavior that opens the URI of the hyperlink in the browser when requested
/// </summary>
public class OpenInBrowser : Behavior<Hyperlink>
{
private Hyperlink? _hyperLink;
/// <inheritdoc />
protected override void OnAttached()
{
base.OnAttached();
_hyperLink = AssociatedObject;
if (_hyperLink == null)
return;
_hyperLink.RequestNavigate += HyperLinkOnRequestNavigate;
}
/// <inheritdoc />
protected override void OnDetaching()
{
if (_hyperLink == null) return;
_hyperLink.RequestNavigate -= HyperLinkOnRequestNavigate;
base.OnDetaching();
}
private void HyperLinkOnRequestNavigate(object sender, RequestNavigateEventArgs e)
{
Utilities.OpenUrl(e.Uri.AbsoluteUri);
}
}
}
@@ -1,40 +0,0 @@
using System.Windows;
using System.Windows.Controls;
using Microsoft.Xaml.Behaviors;
namespace Artemis.UI.Shared
{
/// <summary>
/// Represents a behavior that puts the cursor at the end of a text box when it receives focus
/// </summary>
public class PutCursorAtEndTextBox : Behavior<UIElement>
{
private TextBox? _textBox;
/// <inheritdoc />
protected override void OnAttached()
{
base.OnAttached();
_textBox = AssociatedObject as TextBox;
if (_textBox == null) return;
_textBox.GotFocus += TextBoxGotFocus;
}
/// <inheritdoc />
protected override void OnDetaching()
{
if (_textBox == null) return;
_textBox.GotFocus -= TextBoxGotFocus;
base.OnDetaching();
}
private void TextBoxGotFocus(object sender, RoutedEventArgs routedEventArgs)
{
if (_textBox == null) return;
_textBox.CaretIndex = _textBox.Text.Length;
}
}
}
@@ -1,60 +0,0 @@
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using Microsoft.Xaml.Behaviors;
namespace Artemis.UI.Shared
{
/// <summary>
/// A behavior that makes a scroll viewer bubble up its events if it is at its scroll limit
/// </summary>
public class ScrollParentWhenAtMax : Behavior<FrameworkElement>
{
/// <inheritdoc />
protected override void OnAttached()
{
base.OnAttached();
AssociatedObject.PreviewMouseWheel += PreviewMouseWheel;
}
/// <inheritdoc />
protected override void OnDetaching()
{
AssociatedObject.PreviewMouseWheel -= PreviewMouseWheel;
base.OnDetaching();
}
private void PreviewMouseWheel(object sender, MouseWheelEventArgs e)
{
ScrollViewer? scrollViewer = GetVisualChild<ScrollViewer>(AssociatedObject);
if (scrollViewer == null)
return;
double scrollPos = scrollViewer.ContentVerticalOffset;
// ReSharper disable once CompareOfFloatsByEqualityOperator
if (scrollPos == scrollViewer.ScrollableHeight && e.Delta < 0 || scrollPos == 0 && e.Delta > 0)
{
e.Handled = true;
MouseWheelEventArgs e2 = new(e.MouseDevice, e.Timestamp, e.Delta);
e2.RoutedEvent = UIElement.MouseWheelEvent;
AssociatedObject.RaiseEvent(e2);
}
}
private static T? GetVisualChild<T>(DependencyObject parent) where T : Visual
{
T? child = default;
int numVisuals = VisualTreeHelper.GetChildrenCount(parent);
for (int i = 0; i < numVisuals; i++)
{
Visual v = (Visual) VisualTreeHelper.GetChild(parent, i);
child = v as T;
if (child == null) child = GetVisualChild<T>(v);
if (child != null) break;
}
return child;
}
}
}
-34
View File
@@ -1,34 +0,0 @@
using Artemis.UI.Shared.Controls;
using Artemis.UI.Shared.Services;
using Ninject;
namespace Artemis.UI.Shared
{
/// <summary>
/// Represents the main entry point for the shared UI library
/// <para>The Artemis UI calls this so there's no need to deal with this in a plugin</para>
/// </summary>
public static class Bootstrapper
{
/// <summary>
/// Gets a boolean indicating whether or not the shared UI library has been initialized
/// </summary>
public static bool Initialized { get; private set; }
/// <summary>
/// Initializes the shared UI library
/// </summary>
/// <param name="kernel"></param>
public static void Initialize(IKernel kernel)
{
if (Initialized)
return;
GradientPicker.ColorPickerService = kernel.Get<IColorPickerService>();
ColorPicker.ColorPickerService = kernel.Get<IColorPickerService>();
DataModelPicker.DataModelUIService = kernel.Get<IDataModelUIService>();
Initialized = true;
}
}
}
@@ -0,0 +1,8 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Shared.Controls.ArtemisIcon">
Welcome to Avalonia!
</UserControl>
@@ -0,0 +1,115 @@
using System;
using Avalonia;
using Avalonia.Controls;
using Avalonia.LogicalTree;
using Avalonia.Markup.Xaml;
using Avalonia.Media.Imaging;
using Avalonia.Svg.Skia;
using Material.Icons;
using Material.Icons.Avalonia;
namespace Artemis.UI.Shared.Controls
{
/// <summary>
/// Represents a control that can display an arbitrary kind of icon.
/// </summary>
public class ArtemisIcon : UserControl
{
/// <summary>
/// Creates a new instance of the <see cref="ArtemisIcon" /> class.
/// </summary>
public ArtemisIcon()
{
InitializeComponent();
DetachedFromLogicalTree += OnDetachedFromLogicalTree;
LayoutUpdated += OnLayoutUpdated;
}
private static void IconChanging(IAvaloniaObject sender, bool before)
{
if (before)
((ArtemisIcon) sender).Update();
}
private void Update()
{
try
{
// First look for an enum value instead of a string
if (Icon is MaterialIconKind materialIcon)
{
Content = new MaterialIcon {Kind = materialIcon, Width = Bounds.Width, Height = Bounds.Height};
}
// If it's a string there are several options
else if (Icon is string iconString)
{
// An enum defined as a string
if (Enum.TryParse(iconString, true, out MaterialIconKind parsedIcon))
{
Content = new MaterialIcon {Kind = parsedIcon, Width = Bounds.Width, Height = Bounds.Height};
}
// An URI pointing to an SVG
else if (iconString.EndsWith(".svg"))
{
SvgSource source = new();
source.Load(iconString);
Content = new SvgImage {Source = source};
}
// An URI pointing to a different kind of image
else
{
Content = new Image {Source = new Bitmap(iconString), Width = Bounds.Width, Height = Bounds.Height};
}
}
}
catch
{
Content = new MaterialIcon {Kind = MaterialIconKind.QuestionMark, Width = Bounds.Width, Height = Bounds.Height};
}
}
private void OnLayoutUpdated(object? sender, EventArgs e)
{
if (Content is Control contentControl)
{
contentControl.Width = Bounds.Width;
contentControl.Height = Bounds.Height;
}
}
private void OnDetachedFromLogicalTree(object? sender, LogicalTreeAttachmentEventArgs e)
{
if (Content is SvgImage svgImage)
svgImage.Source?.Dispose();
else if (Content is Image image)
((Bitmap) image.Source).Dispose();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
#region Properties
/// <summary>
/// Gets or sets the currently displayed icon as either a <see cref="MaterialIconKind" /> or an <see cref="Uri" />
/// pointing to an SVG
/// </summary>
public static readonly StyledProperty<object?> IconProperty =
AvaloniaProperty.Register<ArtemisIcon, object?>(nameof(Icon), notifying: IconChanging);
/// <summary>
/// Gets or sets the currently displayed icon as either a <see cref="MaterialIconKind" /> or an <see cref="Uri" />
/// pointing to an SVG
/// </summary>
public object? Icon
{
get => GetValue(IconProperty);
set => SetValue(IconProperty, value);
}
#endregion
}
}
@@ -1,29 +0,0 @@
<UserControl x:Class="Artemis.UI.Shared.ArtemisIcon"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
xmlns:system="clr-namespace:System;assembly=System.Runtime"
xmlns:svgc="http://sharpvectors.codeplex.com/svgc/"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<ContentControl Content="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type UserControl}}, Path=Icon}" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Focusable="False">
<ContentControl.Resources>
<DataTemplate DataType="{x:Type system:Uri}">
<Rectangle Fill="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type UserControl}}, Path=Foreground}">
<Rectangle.OpacityMask>
<VisualBrush Stretch="Uniform">
<VisualBrush.Visual>
<Image Source="{Binding Converter={svgc:SvgImageConverter}, Mode=OneWay}"/>
</VisualBrush.Visual>
</VisualBrush>
</Rectangle.OpacityMask>
</Rectangle>
</DataTemplate>
<DataTemplate DataType="{x:Type materialDesign:PackIconKind}">
<materialDesign:PackIcon Kind="{Binding}" Width="Auto" Height="Auto"/>
</DataTemplate>
</ContentControl.Resources>
</ContentControl>
</UserControl>
@@ -1,101 +0,0 @@
using System;
using System.Windows;
using System.Windows.Controls;
using MaterialDesignThemes.Wpf;
namespace Artemis.UI.Shared
{
/// <summary>
/// Interaction logic for ArtemisIcon.xaml
/// </summary>
public partial class ArtemisIcon : UserControl
{
/// <summary>
/// Gets or sets the currently displayed icon as either a <see cref="PackIconKind" /> or an <see cref="Uri" /> pointing
/// to an SVG
/// </summary>
public static readonly DependencyProperty IconProperty = DependencyProperty.Register(nameof(Icon), typeof(object), typeof(ArtemisIcon),
new FrameworkPropertyMetadata(IconPropertyChangedCallback));
/// <summary>
/// Gets or sets the <see cref="PackIconKind" />
/// </summary>
public static readonly DependencyProperty PackIconProperty = DependencyProperty.Register(nameof(PackIcon), typeof(PackIconKind?), typeof(ArtemisIcon),
new FrameworkPropertyMetadata(IconPropertyChangedCallback));
/// <summary>
/// Gets or sets the <see cref="Uri" /> pointing to the SVG
/// </summary>
public static readonly DependencyProperty SvgSourceProperty = DependencyProperty.Register(nameof(SvgSource), typeof(Uri), typeof(ArtemisIcon),
new FrameworkPropertyMetadata(IconPropertyChangedCallback));
private bool _inCallback;
/// <summary>
/// Creates a new instance of the <see cref="ArtemisIcon"/> class
/// </summary>
public ArtemisIcon()
{
InitializeComponent();
}
/// <summary>
/// Gets or sets the currently displayed icon as either a <see cref="PackIconKind" /> or an <see cref="Uri" /> pointing
/// to an SVG
/// </summary>
public object? Icon
{
get => GetValue(IconProperty);
set => SetValue(IconProperty, value);
}
/// <summary>
/// Gets or sets the <see cref="PackIconKind" />
/// </summary>
public PackIconKind? PackIcon
{
get => (PackIconKind?) GetValue(PackIconProperty);
set => SetValue(PackIconProperty, value);
}
/// <summary>
/// Gets or sets the <see cref="Uri" /> pointing to the SVG
/// </summary>
public Uri SvgSource
{
get => (Uri) GetValue(SvgSourceProperty);
set => SetValue(SvgSourceProperty, value);
}
private static void IconPropertyChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
ArtemisIcon artemisIcon = (ArtemisIcon) d;
if (artemisIcon._inCallback)
return;
try
{
artemisIcon._inCallback = true;
if (artemisIcon.PackIcon != null)
{
artemisIcon.Icon = artemisIcon.PackIcon;
}
else if (artemisIcon.SvgSource != null)
{
artemisIcon.Icon = artemisIcon.SvgSource;
}
else if (artemisIcon.Icon is string iconString)
{
if (Uri.TryCreate(iconString, UriKind.Absolute, out Uri? uriResult))
artemisIcon.Icon = uriResult;
else if (Enum.TryParse(typeof(PackIconKind), iconString, true, out object? result))
artemisIcon.Icon = result;
}
}
finally
{
artemisIcon._inCallback = false;
}
}
}
}
@@ -1,159 +0,0 @@
<UserControl
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
xmlns:shared="clr-namespace:Artemis.UI.Shared"
x:Class="Artemis.UI.Shared.ColorPicker"
mc:Ignorable="d"
d:DesignHeight="101.848" d:DesignWidth="242.956">
<UserControl.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="pack://application:,,,/Artemis.UI.Shared;component/Resources/ArtemisShared.xaml" />
<ResourceDictionary Source="pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesignTheme.ColorPicker.xaml" />
</ResourceDictionary.MergedDictionaries>
<shared:ColorToStringConverter x:Key="ColorToStringConverter" />
<shared:ColorToSolidColorConverter x:Key="ColorToSolidColorConverter" />
<ControlTemplate x:Key="MaterialDesignOpacitySlider" TargetType="{x:Type Slider}">
<Border
x:Name="border"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
SnapsToDevicePixels="True">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" MinHeight="{TemplateBinding MinHeight}" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Border Grid.Row="1" Height="8" CornerRadius="4" Background="{StaticResource Checkerboard}">
<Border Height="8" CornerRadius="4">
<Border.Background>
<LinearGradientBrush StartPoint="0,0" EndPoint="1,0">
<GradientStop Offset="0" Color="Transparent" />
<GradientStop Offset="1.0"
Color="{Binding Color, Mode=OneWay, RelativeSource={RelativeSource AncestorType={x:Type UserControl}}, Converter={StaticResource ColorToSolidColorConverter}}" />
</LinearGradientBrush>
</Border.Background>
</Border>
</Border>
<Track x:Name="PART_Track"
Grid.Row="1"
OpacityMask="{x:Null}">
<Track.DecreaseRepeatButton>
<RepeatButton Command="{x:Static Slider.DecreaseLarge}" Style="{StaticResource MaterialDesignRepeatButton}" />
</Track.DecreaseRepeatButton>
<Track.IncreaseRepeatButton>
<RepeatButton Command="{x:Static Slider.IncreaseLarge}" Style="{StaticResource MaterialDesignRepeatButton}" />
</Track.IncreaseRepeatButton>
<Track.Thumb>
<Thumb x:Name="Thumb" Width="20" Height="20" VerticalAlignment="Center" Focusable="False" OverridesDefaultStyle="True"
Template="{DynamicResource MaterialDesignColorSliderThumb}">
<Thumb.Foreground>
<SolidColorBrush
Color="{Binding Color, RelativeSource={RelativeSource AncestorType={x:Type UserControl}}, Mode=OneWay, Converter={StaticResource ColorToSolidColorConverter}}" />
</Thumb.Foreground>
</Thumb>
</Track.Thumb>
</Track>
</Grid>
</Border>
</ControlTemplate>
</ResourceDictionary>
</UserControl.Resources>
<Grid HorizontalAlignment="Stretch">
<TextBox x:Name="ColorCodeTextBox"
materialDesign:TextFieldAssist.TextBoxViewMargin="0 2 20 0"
materialDesign:HintAssist.IsFloating="False"
Style="{Binding TextBoxStyle, RelativeSource={RelativeSource AncestorType={x:Type UserControl}}}"
Text="{Binding Color, RelativeSource={RelativeSource AncestorType={x:Type UserControl}}, Converter={StaticResource ColorToStringConverter}}"
MinWidth="95"
MaxLength="9"
Margin="0"
HorizontalAlignment="Stretch"
FontFamily="Consolas"
CharacterCasing="Upper">
<materialDesign:TextFieldAssist.CharacterCounterStyle>
<Style TargetType="TextBlock" />
</materialDesign:TextFieldAssist.CharacterCounterStyle>
</TextBox>
<Border Width="15"
Height="15"
CornerRadius="15"
Margin="0 0 2 0"
VerticalAlignment="Center"
HorizontalAlignment="Right"
Background="{StaticResource Checkerboard}">
<Ellipse Stroke="{DynamicResource NormalBorderBrush}" Cursor="Hand" MouseUp="UIElement_OnMouseUp">
<Ellipse.Fill>
<SolidColorBrush Color="{Binding Color, RelativeSource={RelativeSource AncestorType={x:Type UserControl}}, Mode=OneWay}" />
</Ellipse.Fill>
</Ellipse>
</Border>
<Popup AllowsTransparency="True"
Placement="Bottom"
CustomPopupPlacementCallback="{x:Static materialDesign:CustomPopupPlacementCallbackHelper.LargePopupCallback}"
PlacementTarget="{Binding ElementName=ColorCodeTextBox}"
StaysOpen="{Binding StaysOpen, RelativeSource={RelativeSource AncestorType={x:Type UserControl}}}"
PopupAnimation="Fade"
IsOpen="{Binding PopupOpen, RelativeSource={RelativeSource AncestorType={x:Type UserControl}}}">
<materialDesign:Card Width="200" Height="260" Margin="30" materialDesign:ShadowAssist.ShadowDepth="Depth4">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<materialDesign:ColorPicker Grid.Row="0"
Color="{Binding Color, RelativeSource={RelativeSource AncestorType={x:Type UserControl}}}"
MouseUp="ColorGradient_OnMouseUp"
PreviewMouseDown="Slider_OnMouseDown"
PreviewMouseUp="Slider_OnMouseUp" />
<Slider Grid.Row="1" Margin="8 2 8 8"
IsMoveToPointEnabled="True"
Orientation="Horizontal"
Style="{DynamicResource MaterialDesignColorSlider}"
Template="{StaticResource MaterialDesignOpacitySlider}"
Value="{Binding ColorOpacity, RelativeSource={RelativeSource AncestorType={x:Type UserControl}}}"
PreviewMouseDown="Slider_OnMouseDown"
PreviewMouseUp="Slider_OnMouseUp"
Maximum="255" />
<ItemsControl Grid.Row="2" x:Name="RecentColorsContainer" Margin="10 0" HorizontalAlignment="Left">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border CornerRadius="4" Background="{StaticResource Checkerboard}" Width="16" Height="16" Margin="2">
<Rectangle RadiusX="4" RadiusY="4" Cursor="Hand" MouseLeftButtonDown="SelectRecentColor" ToolTip="{Binding}">
<Rectangle.Fill>
<SolidColorBrush Color="{Binding}" />
</Rectangle.Fill>
</Rectangle>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
<CheckBox Grid.Row="3"
x:Name="PreviewCheckBox"
Margin="10 5 0 10"
VerticalAlignment="Center"
Style="{StaticResource MaterialDesignCheckBox}"
Click="PreviewCheckBoxClick">
Preview on devices
</CheckBox>
</Grid>
</materialDesign:Card>
</Popup>
</Grid>
</UserControl>
@@ -1,303 +0,0 @@
using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Windows;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Shapes;
using Artemis.UI.Shared.Services;
namespace Artemis.UI.Shared
{
/// <summary>
/// Interaction logic for ColorPicker.xaml
/// </summary>
public partial class ColorPicker : INotifyPropertyChanged
{
private static IColorPickerService? _colorPickerService;
/// <summary>
/// Gets or sets the color
/// </summary>
public static readonly DependencyProperty ColorProperty = DependencyProperty.Register(nameof(Color), typeof(Color), typeof(ColorPicker),
new FrameworkPropertyMetadata(default(Color), FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, ColorPropertyChangedCallback));
/// <summary>
/// Gets or sets a boolean indicating that the popup containing the color picker is open
/// </summary>
public static readonly DependencyProperty PopupOpenProperty = DependencyProperty.Register(nameof(PopupOpen), typeof(bool), typeof(ColorPicker),
new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, PopupOpenPropertyChangedCallback));
/// <summary>
/// Gets or sets a boolean indicating whether the popup should stay open when clicked outside of it
/// </summary>
public static readonly DependencyProperty StaysOpenProperty = DependencyProperty.Register(nameof(StaysOpen), typeof(bool), typeof(ColorPicker),
new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, StaysOpenPropertyChangedCallback));
/// <summary>
/// Gets or sets the style used on the text box
/// </summary>
public static readonly DependencyProperty TextBoxStyleProperty = DependencyProperty.Register(nameof(TextBoxStyle), typeof(Style), typeof(ColorPicker),
new FrameworkPropertyMetadata(Application.Current.Resources["MaterialDesignTextBox"]));
internal static readonly DependencyProperty ColorOpacityProperty = DependencyProperty.Register(nameof(ColorOpacity), typeof(byte), typeof(ColorPicker),
new FrameworkPropertyMetadata((byte) 255, FrameworkPropertyMetadataOptions.AffectsArrange | FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, ColorOpacityPropertyChangedCallback));
/// <summary>
/// Occurs when the selected color has changed
/// </summary>
public static readonly RoutedEvent ColorChangedEvent =
EventManager.RegisterRoutedEvent(
nameof(Color),
RoutingStrategy.Bubble,
typeof(RoutedPropertyChangedEventHandler<Color>),
typeof(ColorPicker));
/// <summary>
/// Occurs when the popup opens or closes
/// </summary>
public static readonly RoutedEvent PopupOpenChangedEvent =
EventManager.RegisterRoutedEvent(
nameof(PopupOpen),
RoutingStrategy.Bubble,
typeof(RoutedPropertyChangedEventHandler<bool>),
typeof(ColorPicker));
private bool _inCallback;
/// <summary>
/// Creates a new instance of the <see cref="ColorPicker" /> class
/// </summary>
public ColorPicker()
{
InitializeComponent();
Loaded += OnLoaded;
Unloaded += OnUnloaded;
}
/// <summary>
/// Gets or sets the color
/// </summary>
public Color Color
{
get => (Color) GetValue(ColorProperty);
set => SetValue(ColorProperty, value);
}
/// <summary>
/// Gets or sets a boolean indicating that the popup containing the color picker is open
/// </summary>
public bool PopupOpen
{
get => (bool) GetValue(PopupOpenProperty);
set => SetValue(PopupOpenProperty, value);
}
/// <summary>
/// Gets or sets a boolean indicating whether the popup should stay open when clicked outside of it
/// </summary>
public bool StaysOpen
{
get => (bool) GetValue(StaysOpenProperty);
set => SetValue(StaysOpenProperty, value);
}
/// <summary>
/// Gets or sets the style used on the text box
/// </summary>
public Style TextBoxStyle
{
get => (Style) GetValue(TextBoxStyleProperty);
set => SetValue(TextBoxStyleProperty, value);
}
/// <summary>
/// Used by the gradient picker to load saved gradients, do not touch or it'll just throw an exception
/// </summary>
internal static IColorPickerService ColorPickerService
{
set
{
if (_colorPickerService != null)
throw new AccessViolationException("This is not for you to touch");
_colorPickerService = value;
}
}
internal byte ColorOpacity
{
get => (byte) GetValue(ColorOpacityProperty);
set => SetValue(ColorOpacityProperty, value);
}
/// <summary>
/// Occurs when dragging the color picker has started
/// </summary>
public event EventHandler? DragStarted;
/// <summary>
/// Occurs when dragging the color picker has ended
/// </summary>
public event EventHandler? DragEnded;
/// <summary>
/// Invokes the <see cref="PropertyChanged" /> event
/// </summary>
/// <param name="propertyName"></param>
protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
/// <summary>
/// Invokes the <see cref="DragStarted" /> event
/// </summary>
protected virtual void OnDragStarted()
{
DragStarted?.Invoke(this, EventArgs.Empty);
}
/// <summary>
/// Invokes the <see cref="DragEnded" /> event
/// </summary>
protected virtual void OnDragEnded()
{
DragEnded?.Invoke(this, EventArgs.Empty);
}
private static void ColorPropertyChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
ColorPicker colorPicker = (ColorPicker) d;
if (colorPicker._inCallback)
return;
colorPicker._inCallback = true;
colorPicker.SetCurrentValue(ColorOpacityProperty, ((Color) e.NewValue).A);
colorPicker.OnPropertyChanged(nameof(Color));
_colorPickerService?.UpdateColorDisplay(colorPicker.Color);
colorPicker._inCallback = false;
}
private static void PopupOpenPropertyChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
ColorPicker colorPicker = (ColorPicker) d;
if (colorPicker._inCallback)
return;
colorPicker._inCallback = true;
colorPicker.OnPropertyChanged(nameof(PopupOpen));
if ((bool) e.NewValue)
colorPicker.PopupOpened();
else
colorPicker.PopupClosed();
colorPicker._inCallback = false;
}
private static void StaysOpenPropertyChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
ColorPicker colorPicker = (ColorPicker) d;
if (colorPicker._inCallback)
return;
colorPicker._inCallback = true;
colorPicker.OnPropertyChanged(nameof(PopupOpen));
colorPicker._inCallback = false;
}
private static void ColorOpacityPropertyChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
ColorPicker colorPicker = (ColorPicker) d;
if (colorPicker._inCallback)
return;
colorPicker._inCallback = true;
Color color = colorPicker.Color;
if (e.NewValue is byte opacity)
color = Color.FromArgb(opacity, color.R, color.G, color.B);
colorPicker.SetCurrentValue(ColorProperty, color);
colorPicker.OnPropertyChanged(nameof(ColorOpacity));
_colorPickerService?.UpdateColorDisplay(colorPicker.Color);
colorPicker._inCallback = false;
}
private void UIElement_OnMouseUp(object sender, MouseButtonEventArgs e)
{
PopupOpen = !PopupOpen;
e.Handled = true;
}
private void ColorGradient_OnMouseUp(object sender, MouseButtonEventArgs e)
{
e.Handled = true;
}
private void Slider_OnMouseDown(object sender, MouseButtonEventArgs e)
{
if (_colorPickerService == null) return;
OnDragStarted();
if (_colorPickerService.PreviewSetting.Value)
_colorPickerService.StartColorDisplay();
}
private void Slider_OnMouseUp(object sender, MouseButtonEventArgs e)
{
if (_colorPickerService == null) return;
OnDragEnded();
_colorPickerService.StopColorDisplay();
}
private void PreviewCheckBoxClick(object sender, RoutedEventArgs e)
{
if (_colorPickerService == null) return;
_colorPickerService.PreviewSetting.Value = PreviewCheckBox.IsChecked ?? false;
}
private void OnLoaded(object sender, RoutedEventArgs e)
{
if (_colorPickerService == null) return;
PreviewCheckBox.IsChecked = _colorPickerService.PreviewSetting.Value;
_colorPickerService.PreviewSetting.SettingChanged += PreviewSettingOnSettingChanged;
}
private void OnUnloaded(object sender, RoutedEventArgs e)
{
if (_colorPickerService == null) return;
_colorPickerService.PreviewSetting.SettingChanged -= PreviewSettingOnSettingChanged;
}
private void PreviewSettingOnSettingChanged(object? sender, EventArgs e)
{
if (_colorPickerService == null) return;
PreviewCheckBox.IsChecked = _colorPickerService.PreviewSetting.Value;
}
private void PopupClosed()
{
_colorPickerService?.QueueRecentColor(Color);
}
private void PopupOpened()
{
if (_colorPickerService != null)
RecentColorsContainer.ItemsSource = new ObservableCollection<Color>(_colorPickerService.RecentColors);
}
private void SelectRecentColor(object sender, MouseButtonEventArgs e)
{
Color = (Color) ((Rectangle) sender).DataContext;
PopupOpen = false;
}
/// <inheritdoc />
public event PropertyChangedEventHandler? PropertyChanged;
}
}
@@ -1,75 +0,0 @@
<UserControl x:Class="Artemis.UI.Shared.Controls.DataModelPicker"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:Artemis.UI.Shared.Controls"
xmlns:s="https://github.com/canton7/Stylet"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
xmlns:shared="clr-namespace:Artemis.UI.Shared"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<UserControl.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="pack://application:,,,/Artemis.UI.Shared;component/ResourceDictionaries/DataModelConditions.xaml" />
<ResourceDictionary>
<shared:NullToVisibilityConverter x:Key="NullToVisibilityConverter" />
<shared:BindingProxy x:Key="DataContextProxy" Data="{Binding RelativeSource={RelativeSource AncestorType={x:Type UserControl}}}" />
<DataTemplate x:Key="DataModelDataTemplate">
<Control x:Name="TemplateControl" Focusable="False" Template="{StaticResource DataModelSelectionTemplate}" />
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding Data.ShowDataModelValues, Source={StaticResource DataContextProxy}}" Value="True">
<Setter TargetName="TemplateControl" Property="Template" Value="{StaticResource DataModelSelectionTemplateWithValues}" />
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
</ResourceDictionary>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</UserControl.Resources>
<Button Background="{Binding ButtonBrush, RelativeSource={RelativeSource AncestorType={x:Type UserControl}}}"
BorderBrush="{Binding ButtonBrush, RelativeSource={RelativeSource AncestorType={x:Type UserControl}}}"
Style="{StaticResource DataModelConditionButton}"
IsEnabled="{Binding IsEnabled, RelativeSource={RelativeSource AncestorType={x:Type UserControl}}}"
HorizontalAlignment="Left"
Click="PropertyButton_OnClick"
x:Name="DataModelButton">
<Button.ContextMenu>
<ContextMenu>
<ContextMenu.Resources>
<Style TargetType="{x:Type MenuItem}" BasedOn="{StaticResource MaterialDesignMenuItem}">
<Setter Property="ItemsSource" Value="{Binding Children}" />
<Setter Property="Command" Value="{Binding Data.SelectPropertyCommand, Source={StaticResource DataContextProxy}}" />
<Setter Property="CommandParameter" Value="{Binding}" />
<Setter Property="CommandTarget" Value="{Binding}" />
<Setter Property="IsEnabled" Value="{Binding IsMatchingFilteredTypes}" />
<Setter Property="IsSubmenuOpen" Value="{Binding IsVisualizationExpanded, Mode=TwoWay}" />
<Setter Property="HeaderTemplate" Value="{StaticResource DataModelDataTemplate}" />
</Style>
<shared:IntToVisibilityConverter x:Key="IntToVisibilityConverter" />
</ContextMenu.Resources>
<ContextMenu.ItemsSource>
<CompositeCollection>
<CollectionContainer Collection="{Binding Data.ExtraDataModelViewModels, Source={StaticResource DataContextProxy}}" />
<Separator Visibility="{Binding Data.ExtraDataModelViewModels.Count, Source={StaticResource DataContextProxy}, Converter={StaticResource IntToVisibilityConverter}, Mode=OneWay}" />
<CollectionContainer Collection="{Binding Data.DataModelViewModel.Children, Source={StaticResource DataContextProxy}}" />
</CompositeCollection>
</ContextMenu.ItemsSource>
</ContextMenu>
</Button.ContextMenu>
<Grid>
<Grid x:Name="ValueDisplay">
<TextBlock x:Name="ValueDisplayTextBlock"
Visibility="{Binding DataModelPath, Converter={StaticResource NullToVisibilityConverter}, RelativeSource={RelativeSource AncestorType={x:Type UserControl}}}" />
<TextBlock FontStyle="Italic"
Visibility="{Binding DataModelPath, Converter={StaticResource NullToVisibilityConverter}, ConverterParameter=Inverted, RelativeSource={RelativeSource AncestorType={x:Type UserControl}}}">
<Run Text="« " /><Run Text="{Binding Placeholder, RelativeSource={RelativeSource AncestorType={x:Type UserControl}}}" /><Run Text=" »" />
</TextBlock>
</Grid>
<TextBlock x:Name="ValuePlaceholder" FontStyle="Italic">
« Invalid »
</TextBlock>
</Grid>
</Button>
</UserControl>
@@ -1,343 +0,0 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Threading;
using Artemis.Core;
using Artemis.Core.Modules;
using Artemis.UI.Shared.Services;
namespace Artemis.UI.Shared.Controls
{
/// <summary>
/// Interaction logic for DataModelPicker.xaml
/// </summary>
public partial class DataModelPicker : INotifyPropertyChanged
{
private static IDataModelUIService _dataModelUIService;
/// <summary>
/// Gets or sets data model path
/// </summary>
public static readonly DependencyProperty DataModelPathProperty = DependencyProperty.Register(
nameof(DataModelPath), typeof(DataModelPath), typeof(DataModelPicker),
new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, DataModelPathPropertyChangedCallback)
);
/// <summary>
/// Gets or sets the brush to use when drawing the button
/// </summary>
public static readonly DependencyProperty PlaceholderProperty = DependencyProperty.Register(
nameof(Placeholder), typeof(string), typeof(DataModelPicker),
new FrameworkPropertyMetadata("Click to select")
);
/// <summary>
/// Gets or sets the brush to use when drawing the button
/// </summary>
public static readonly DependencyProperty ShowDataModelValuesProperty = DependencyProperty.Register(nameof(ShowDataModelValues), typeof(bool), typeof(DataModelPicker));
/// <summary>
/// Gets or sets the brush to use when drawing the button
/// </summary>
public static readonly DependencyProperty ShowFullPathProperty = DependencyProperty.Register(
nameof(ShowFullPath), typeof(bool), typeof(DataModelPicker),
new FrameworkPropertyMetadata(ShowFullPathPropertyCallback)
);
/// <summary>
/// Gets or sets the brush to use when drawing the button
/// </summary>
public static readonly DependencyProperty ButtonBrushProperty = DependencyProperty.Register(nameof(ButtonBrush), typeof(Brush), typeof(DataModelPicker));
/// <summary>
/// A list of extra modules to show data models of
/// </summary>
public static readonly DependencyProperty ModulesProperty = DependencyProperty.Register(
nameof(Modules), typeof(ObservableCollection<Module>), typeof(DataModelPicker),
new FrameworkPropertyMetadata(new ObservableCollection<Module>(), FrameworkPropertyMetadataOptions.None, ModulesPropertyChangedCallback)
);
/// <summary>
/// The data model view model to show, if not provided one will be retrieved by the control
/// </summary>
public static readonly DependencyProperty DataModelViewModelProperty = DependencyProperty.Register(
nameof(DataModelViewModel), typeof(DataModelPropertiesViewModel), typeof(DataModelPicker),
new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.None, DataModelViewModelPropertyChangedCallback)
);
/// <summary>
/// A list of data model view models to show
/// </summary>
public static readonly DependencyProperty ExtraDataModelViewModelsProperty = DependencyProperty.Register(
nameof(ExtraDataModelViewModels), typeof(ObservableCollection<DataModelPropertiesViewModel>), typeof(DataModelPicker),
new FrameworkPropertyMetadata(new ObservableCollection<DataModelPropertiesViewModel>(), FrameworkPropertyMetadataOptions.None, ExtraDataModelViewModelsPropertyChangedCallback)
);
/// <summary>
/// A list of data model view models to show
/// </summary>
public static readonly DependencyProperty FilterTypesProperty = DependencyProperty.Register(
nameof(FilterTypes), typeof(ObservableCollection<Type>), typeof(DataModelPicker),
new FrameworkPropertyMetadata(new ObservableCollection<Type>())
);
public DataModelPicker()
{
SelectPropertyCommand = new DelegateCommand(ExecuteSelectPropertyCommand);
Unloaded += OnUnloaded;
InitializeComponent();
GetDataModel();
UpdateValueDisplay();
}
/// <summary>
/// Gets or sets the brush to use when drawing the button
/// </summary>
public DataModelPath? DataModelPath
{
get => (DataModelPath) GetValue(DataModelPathProperty);
set => SetValue(DataModelPathProperty, value);
}
/// <summary>
/// Gets or sets the brush to use when drawing the button
/// </summary>
public string Placeholder
{
get => (string) GetValue(PlaceholderProperty);
set => SetValue(PlaceholderProperty, value);
}
/// <summary>
/// Gets or sets the brush to use when drawing the button
/// </summary>
public bool ShowDataModelValues
{
get => (bool) GetValue(ShowDataModelValuesProperty);
set => SetValue(ShowDataModelValuesProperty, value);
}
/// <summary>
/// Gets or sets the brush to use when drawing the button
/// </summary>
public bool ShowFullPath
{
get => (bool) GetValue(ShowFullPathProperty);
set => SetValue(ShowFullPathProperty, value);
}
/// <summary>
/// Gets or sets the brush to use when drawing the button
/// </summary>
public Brush ButtonBrush
{
get => (Brush) GetValue(ButtonBrushProperty);
set => SetValue(ButtonBrushProperty, value);
}
/// <summary>
/// Gets or sets a list of extra modules to show data models of
/// </summary>
public ObservableCollection<Module>? Modules
{
get => (ObservableCollection<Module>) GetValue(ModulesProperty);
set => SetValue(ModulesProperty, value);
}
/// <summary>
/// Gets or sets the data model view model to show
/// </summary>
public DataModelPropertiesViewModel? DataModelViewModel
{
get => (DataModelPropertiesViewModel) GetValue(DataModelViewModelProperty);
set => SetValue(DataModelViewModelProperty, value);
}
/// <summary>
/// Gets or sets a list of data model view models to show
/// </summary>
public ObservableCollection<DataModelPropertiesViewModel>? ExtraDataModelViewModels
{
get => (ObservableCollection<DataModelPropertiesViewModel>) GetValue(ExtraDataModelViewModelsProperty);
set => SetValue(ExtraDataModelViewModelsProperty, value);
}
/// <summary>
/// Gets or sets the types of properties this view model will allow to be selected
/// </summary>
public ObservableCollection<Type>? FilterTypes
{
get => (ObservableCollection<Type>) GetValue(FilterTypesProperty);
set => SetValue(FilterTypesProperty, value);
}
/// <summary>
/// Command used by view
/// </summary>
public DelegateCommand SelectPropertyCommand { get; }
internal static IDataModelUIService DataModelUIService
{
set
{
if (_dataModelUIService != null)
throw new AccessViolationException("This is not for you to touch");
_dataModelUIService = value;
}
}
/// <summary>
/// Occurs when a new path has been selected
/// </summary>
public event EventHandler<DataModelSelectedEventArgs>? DataModelPathSelected;
/// <summary>
/// Invokes the <see cref="DataModelPathSelected" /> event
/// </summary>
/// <param name="e"></param>
protected virtual void OnDataModelPathSelected(DataModelSelectedEventArgs e)
{
DataModelPathSelected?.Invoke(this, e);
}
private void GetDataModel()
{
ChangeDataModel(_dataModelUIService.GetPluginDataModelVisualization(Modules?.ToList() ?? new List<Module>(), true));
}
private void ChangeDataModel(DataModelPropertiesViewModel? dataModel)
{
if (DataModelViewModel != null)
{
DataModelViewModel.Dispose();
DataModelViewModel.UpdateRequested -= DataModelOnUpdateRequested;
}
DataModelViewModel = dataModel;
if (DataModelViewModel != null)
DataModelViewModel.UpdateRequested += DataModelOnUpdateRequested;
}
private void UpdateValueDisplay()
{
ValueDisplay.Visibility = DataModelPath == null || DataModelPath.IsValid ? Visibility.Visible : Visibility.Collapsed;
ValuePlaceholder.Visibility = DataModelPath == null || DataModelPath.IsValid ? Visibility.Collapsed : Visibility.Visible;
string? formattedPath = null;
if (DataModelPath != null && DataModelPath.IsValid)
formattedPath = string.Join(" › ", DataModelPath.Segments.Where(s => s.GetPropertyDescription() != null).Select(s => s.GetPropertyDescription()!.Name));
DataModelButton.ToolTip = formattedPath;
ValueDisplayTextBlock.Text = ShowFullPath
? formattedPath
: DataModelPath?.Segments.LastOrDefault()?.GetPropertyDescription()?.Name ?? DataModelPath?.Segments.LastOrDefault()?.Identifier;
}
private void DataModelOnUpdateRequested(object? sender, EventArgs e)
{
DataModelViewModel?.ApplyTypeFilter(true, FilterTypes?.ToArray() ?? Type.EmptyTypes);
if (ExtraDataModelViewModels == null) return;
foreach (DataModelPropertiesViewModel extraDataModelViewModel in ExtraDataModelViewModels)
extraDataModelViewModel.ApplyTypeFilter(true, FilterTypes?.ToArray() ?? Type.EmptyTypes);
}
private void ExecuteSelectPropertyCommand(object? context)
{
if (context is not DataModelVisualizationViewModel selected)
return;
if (selected.DataModelPath == null)
return;
if (selected.DataModelPath.Equals(DataModelPath))
return;
DataModelPath = new DataModelPath(selected.DataModelPath);
OnDataModelPathSelected(new DataModelSelectedEventArgs(DataModelPath));
}
private void PropertyButton_OnClick(object sender, RoutedEventArgs e)
{
// DataContext is not set when using left button, I don't know why but there it is
if (sender is Button button && button.ContextMenu != null)
{
button.ContextMenu.DataContext = button.DataContext;
button.ContextMenu.IsOpen = true;
}
}
private static void DataModelPathPropertyChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is not DataModelPicker dataModelPicker)
return;
if (e.OldValue is DataModelPath oldPath)
{
oldPath.PathInvalidated -= dataModelPicker.PathValidationChanged;
oldPath.PathValidated -= dataModelPicker.PathValidationChanged;
oldPath.Dispose();
}
dataModelPicker.UpdateValueDisplay();
if (e.NewValue is DataModelPath newPath)
{
newPath.PathInvalidated += dataModelPicker.PathValidationChanged;
newPath.PathValidated += dataModelPicker.PathValidationChanged;
}
}
private static void ShowFullPathPropertyCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is not DataModelPicker dataModelPicker)
return;
dataModelPicker.UpdateValueDisplay();
}
private static void ModulesPropertyChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is not DataModelPicker dataModelPicker)
return;
dataModelPicker.GetDataModel();
}
private static void DataModelViewModelPropertyChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is not DataModelPicker dataModelPicker)
return;
if (e.OldValue is DataModelPropertiesViewModel vm)
vm.Dispose();
}
private static void ExtraDataModelViewModelsPropertyChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is not DataModelPicker dataModelPicker)
return;
}
private void PathValidationChanged(object? sender, EventArgs e)
{
Dispatcher.Invoke(UpdateValueDisplay, DispatcherPriority.DataBind);
}
private void OnUnloaded(object o, RoutedEventArgs routedEventArgs)
{
if (DataModelPath != null)
{
DataModelPath.PathInvalidated -= PathValidationChanged;
DataModelPath.PathValidated -= PathValidationChanged;
}
DataModelViewModel?.Dispose();
}
/// <inheritdoc />
public event PropertyChangedEventHandler? PropertyChanged;
}
}
@@ -0,0 +1,300 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Reactive;
using Artemis.Core;
using Artemis.Core.Modules;
using Artemis.UI.Shared.DataModelVisualization.Shared;
using Artemis.UI.Shared.Events;
using Artemis.UI.Shared.Extensions;
using Artemis.UI.Shared.Services.Interfaces;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Data;
using Material.Icons.Avalonia;
using ReactiveUI;
namespace Artemis.UI.Shared.Controls.DataModelPicker;
/// <summary>
/// Represents a data model picker picker that can be used to select a data model path.
/// </summary>
public class DataModelPicker : TemplatedControl
{
/// <summary>
/// The data model UI service this picker should use.
/// </summary>
public static IDataModelUIService? DataModelUIService;
/// <summary>
/// Gets or sets data model path.
/// </summary>
public static readonly StyledProperty<DataModelPath?> DataModelPathProperty =
AvaloniaProperty.Register<DataModelPicker, DataModelPath?>(nameof(DataModelPath), defaultBindingMode: BindingMode.TwoWay);
/// <summary>
/// Gets or sets a boolean indicating whether the data model picker should show current values when selecting a path.
/// </summary>
public static readonly StyledProperty<bool> ShowDataModelValuesProperty =
AvaloniaProperty.Register<DataModelPicker, bool>(nameof(ShowDataModelValues));
/// <summary>
/// A list of extra modules to show data models of.
/// </summary>
public static readonly StyledProperty<ObservableCollection<Module>?> ModulesProperty =
AvaloniaProperty.Register<DataModelPicker, ObservableCollection<Module>?>(nameof(Modules), new ObservableCollection<Module>());
/// <summary>
/// The data model view model to show, if not provided one will be retrieved by the control.
/// </summary>
public static readonly StyledProperty<DataModelPropertiesViewModel?> DataModelViewModelProperty =
AvaloniaProperty.Register<DataModelPicker, DataModelPropertiesViewModel?>(nameof(DataModelViewModel));
/// <summary>
/// A list of data model view models to show
/// </summary>
public static readonly StyledProperty<ObservableCollection<DataModelPropertiesViewModel>?> ExtraDataModelViewModelsProperty =
AvaloniaProperty.Register<DataModelPicker, ObservableCollection<DataModelPropertiesViewModel>?>(nameof(ExtraDataModelViewModels), new ObservableCollection<DataModelPropertiesViewModel>());
/// <summary>
/// A list of types to filter the selectable paths on.
/// </summary>
public static readonly StyledProperty<ObservableCollection<Type>?> FilterTypesProperty =
AvaloniaProperty.Register<DataModelPicker, ObservableCollection<Type>?>(nameof(FilterTypes), new ObservableCollection<Type>());
private MaterialIcon? _currentPathIcon;
private TextBlock? _currentPathDisplay;
private TextBlock? _currentPathDescription;
private TreeView? _dataModelTreeView;
static DataModelPicker()
{
ModulesProperty.Changed.Subscribe(ModulesChanged);
DataModelPathProperty.Changed.Subscribe(DataModelPathPropertyChanged);
DataModelViewModelProperty.Changed.Subscribe(DataModelViewModelPropertyChanged);
ExtraDataModelViewModelsProperty.Changed.Subscribe(ExtraDataModelViewModelsChanged);
}
/// <summary>
/// Creates a new instance of the <see cref="DataModelPicker" /> class.
/// </summary>
public DataModelPicker()
{
SelectPropertyCommand = ReactiveCommand.Create<DataModelVisualizationViewModel>(selected => ExecuteSelectPropertyCommand(selected));
}
/// <summary>
/// Gets a command that selects the path by it's view model.
/// </summary>
public ReactiveCommand<DataModelVisualizationViewModel, Unit> SelectPropertyCommand { get; }
/// <summary>
/// Gets or sets data model path.
/// </summary>
public DataModelPath? DataModelPath
{
get => GetValue(DataModelPathProperty);
set => SetValue(DataModelPathProperty, value);
}
/// <summary>
/// Gets or sets a boolean indicating whether the data model picker should show current values when selecting a path.
/// </summary>
public bool ShowDataModelValues
{
get => GetValue(ShowDataModelValuesProperty);
set => SetValue(ShowDataModelValuesProperty, value);
}
/// <summary>
/// A list of extra modules to show data models of.
/// </summary>
public ObservableCollection<Module>? Modules
{
get => GetValue(ModulesProperty);
set => SetValue(ModulesProperty, value);
}
/// <summary>
/// The data model view model to show, if not provided one will be retrieved by the control.
/// </summary>
public DataModelPropertiesViewModel? DataModelViewModel
{
get => GetValue(DataModelViewModelProperty);
private set => SetValue(DataModelViewModelProperty, value);
}
/// <summary>
/// A list of data model view models to show.
/// </summary>
public ObservableCollection<DataModelPropertiesViewModel>? ExtraDataModelViewModels
{
get => GetValue(ExtraDataModelViewModelsProperty);
set => SetValue(ExtraDataModelViewModelsProperty, value);
}
/// <summary>
/// A list of types to filter the selectable paths on.
/// </summary>
public ObservableCollection<Type>? FilterTypes
{
get => GetValue(FilterTypesProperty);
set => SetValue(FilterTypesProperty, value);
}
/// <summary>
/// Occurs when a new path has been selected
/// </summary>
public event EventHandler<DataModelSelectedEventArgs>? DataModelPathSelected;
/// <summary>
/// Invokes the <see cref="DataModelPathSelected" /> event
/// </summary>
/// <param name="e"></param>
protected virtual void OnDataModelPathSelected(DataModelSelectedEventArgs e)
{
DataModelPathSelected?.Invoke(this, e);
}
#region Overrides of TemplatedControl
/// <inheritdoc />
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
if (_dataModelTreeView != null)
_dataModelTreeView.SelectionChanged -= DataModelTreeViewOnSelectionChanged;
_currentPathIcon = e.NameScope.Find<MaterialIcon>("CurrentPathIcon");
_currentPathDisplay = e.NameScope.Find<TextBlock>("CurrentPathDisplay");
_currentPathDescription = e.NameScope.Find<TextBlock>("CurrentPathDescription");
_dataModelTreeView = e.NameScope.Find<TreeView>("DataModelTreeView");
if (_dataModelTreeView != null)
_dataModelTreeView.SelectionChanged += DataModelTreeViewOnSelectionChanged;
}
#region Overrides of Visual
/// <inheritdoc />
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
GetDataModel();
UpdateCurrentPath(true);
}
/// <inheritdoc />
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
{
DataModelViewModel?.Dispose();
}
#endregion
#endregion
private static void ModulesChanged(AvaloniaPropertyChangedEventArgs<ObservableCollection<Module>?> e)
{
if (e.Sender is DataModelPicker dataModelPicker)
dataModelPicker.GetDataModel();
}
private static void DataModelPathPropertyChanged(AvaloniaPropertyChangedEventArgs<DataModelPath?> e)
{
if (e.Sender is DataModelPicker dataModelPicker)
dataModelPicker.UpdateCurrentPath(false);
}
private static void DataModelViewModelPropertyChanged(AvaloniaPropertyChangedEventArgs<DataModelPropertiesViewModel?> e)
{
if (e.Sender is DataModelPicker && e.OldValue.Value != null)
e.OldValue.Value.Dispose();
}
private static void ExtraDataModelViewModelsChanged(AvaloniaPropertyChangedEventArgs<ObservableCollection<DataModelPropertiesViewModel>?> e)
{
// TODO, the original did nothing here either and I can't remember why
}
private void ExecuteSelectPropertyCommand(DataModelVisualizationViewModel selected)
{
if (selected.DataModelPath == null)
return;
if (selected.DataModelPath.Equals(DataModelPath))
return;
DataModelPath = new DataModelPath(selected.DataModelPath);
OnDataModelPathSelected(new DataModelSelectedEventArgs(DataModelPath));
}
private void GetDataModel()
{
if (DataModelUIService == null)
return;
ChangeDataModel(DataModelUIService.GetPluginDataModelVisualization(Modules?.ToList() ?? new List<Module>(), true));
}
private void ChangeDataModel(DataModelPropertiesViewModel? dataModel)
{
if (DataModelViewModel != null)
{
DataModelViewModel.Dispose();
DataModelViewModel.UpdateRequested -= DataModelOnUpdateRequested;
}
DataModelViewModel = dataModel;
if (DataModelViewModel != null)
DataModelViewModel.UpdateRequested += DataModelOnUpdateRequested;
}
private void DataModelOnUpdateRequested(object? sender, EventArgs e)
{
DataModelViewModel?.ApplyTypeFilter(true, FilterTypes?.ToArray() ?? Type.EmptyTypes);
if (ExtraDataModelViewModels == null) return;
foreach (DataModelPropertiesViewModel extraDataModelViewModel in ExtraDataModelViewModels)
extraDataModelViewModel.ApplyTypeFilter(true, FilterTypes?.ToArray() ?? Type.EmptyTypes);
}
private void DataModelTreeViewOnSelectionChanged(object? sender, SelectionChangedEventArgs e)
{
// Multi-select isn't a think so grab the first one
object? selected = _dataModelTreeView?.SelectedItems[0];
if (selected == null)
return;
if (selected is DataModelPropertyViewModel property && property.DataModelPath != null)
DataModelPath = new DataModelPath(property.DataModelPath);
else if (selected is DataModelListViewModel list && list.DataModelPath != null)
DataModelPath = new DataModelPath(list.DataModelPath);
else if (selected is DataModelEventViewModel dataModelEvent && dataModelEvent.DataModelPath != null)
DataModelPath = new DataModelPath(dataModelEvent.DataModelPath);
}
private void UpdateCurrentPath(bool selectCurrentPath)
{
if (DataModelPath == null)
return;
if (_dataModelTreeView != null && selectCurrentPath)
{
// Expand the path
DataModel? start = DataModelPath.Target;
DataModelVisualizationViewModel? root = DataModelViewModel?.Children.FirstOrDefault(c => c.DataModel == start);
if (root != null)
{
root.ExpandToPath(DataModelPath);
_dataModelTreeView.SelectedItem = root.GetViewModelForPath(DataModelPath);
}
}
if (_currentPathDisplay != null)
_currentPathDisplay.Text = string.Join(" › ", DataModelPath.Segments.Where(s => s.GetPropertyDescription() != null).Select(s => s.GetPropertyDescription()!.Name));
if (_currentPathDescription != null)
_currentPathDescription.Text = DataModelPath.GetPropertyDescription()?.Description;
if (_currentPathIcon != null)
_currentPathIcon.Kind = DataModelPath.GetPropertyType().GetTypeIcon();
}
}
@@ -0,0 +1,338 @@
using System;
using System.Collections.ObjectModel;
using System.Linq;
using Artemis.Core;
using Artemis.Core.Modules;
using Artemis.UI.Shared.Controls.Flyouts;
using Artemis.UI.Shared.DataModelVisualization.Shared;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Data;
using Avalonia.Interactivity;
using Avalonia.Threading;
using FluentAvalonia.Core;
namespace Artemis.UI.Shared.Controls.DataModelPicker;
/// <summary>
/// Represents a button that can be used to pick a data model path in a flyout.
/// </summary>
public class DataModelPickerButton : TemplatedControl
{
/// <summary>
/// Gets or sets the placeholder to show when nothing is selected.
/// </summary>
public static readonly StyledProperty<string> PlaceholderProperty =
AvaloniaProperty.Register<DataModelPicker, string>(nameof(Placeholder), "Click to select");
/// <summary>
/// Gets or sets a boolean indicating whether the data model picker should show the full path of the selected value.
/// </summary>
public static readonly StyledProperty<bool> ShowFullPathProperty =
AvaloniaProperty.Register<DataModelPicker, bool>(nameof(ShowFullPath));
/// <summary>
/// Gets or sets a boolean indicating whether the button should show the icon of the first provided filter type.
/// </summary>
public static readonly StyledProperty<bool> ShowTypeIconProperty =
AvaloniaProperty.Register<DataModelPicker, bool>(nameof(ShowTypeIcon));
/// <summary>
/// Gets a boolean indicating whether the data model picker has a value.
/// </summary>
public static readonly StyledProperty<bool> HasValueProperty =
AvaloniaProperty.Register<DataModelPicker, bool>(nameof(HasValue));
/// <summary>
/// Gets or sets the desired flyout placement.
/// </summary>
public static readonly StyledProperty<FlyoutPlacementMode> PlacementProperty =
AvaloniaProperty.Register<FlyoutBase, FlyoutPlacementMode>(nameof(Placement));
/// <summary>
/// Gets or sets data model path.
/// </summary>
public static readonly StyledProperty<DataModelPath?> DataModelPathProperty =
AvaloniaProperty.Register<DataModelPicker, DataModelPath?>(nameof(DataModelPath), defaultBindingMode: BindingMode.TwoWay);
/// <summary>
/// Gets or sets a boolean indicating whether the data model picker should show current values when selecting a path.
/// </summary>
public static readonly StyledProperty<bool> ShowDataModelValuesProperty =
AvaloniaProperty.Register<DataModelPicker, bool>(nameof(ShowDataModelValues));
/// <summary>
/// A list of extra modules to show data models of.
/// </summary>
public static readonly StyledProperty<ObservableCollection<Module>?> ModulesProperty =
AvaloniaProperty.Register<DataModelPicker, ObservableCollection<Module>?>(nameof(Modules), new ObservableCollection<Module>());
/// <summary>
/// A list of data model view models to show
/// </summary>
public static readonly StyledProperty<ObservableCollection<DataModelPropertiesViewModel>?> ExtraDataModelViewModelsProperty =
AvaloniaProperty.Register<DataModelPicker, ObservableCollection<DataModelPropertiesViewModel>?>(nameof(ExtraDataModelViewModels), new ObservableCollection<DataModelPropertiesViewModel>());
/// <summary>
/// A list of types to filter the selectable paths on.
/// </summary>
public static readonly StyledProperty<ObservableCollection<Type>?> FilterTypesProperty =
AvaloniaProperty.Register<DataModelPicker, ObservableCollection<Type>?>(nameof(FilterTypes), new ObservableCollection<Type>());
private bool _attached;
private bool _flyoutActive;
private Button? _button;
private TextBlock? _label;
private DataModelPickerFlyout? _flyout;
static DataModelPickerButton()
{
ShowFullPathProperty.Changed.Subscribe(ShowFullPathChanged);
DataModelPathProperty.Changed.Subscribe(DataModelPathChanged);
}
/// <summary>
/// Gets or sets the placeholder to show when nothing is selected.
/// </summary>
public string Placeholder
{
get => GetValue(PlaceholderProperty);
set => SetValue(PlaceholderProperty, value);
}
/// <summary>
/// Gets or sets a boolean indicating whether the data model picker should show the full path of the selected value.
/// </summary>
public bool ShowFullPath
{
get => GetValue(ShowFullPathProperty);
set => SetValue(ShowFullPathProperty, value);
}
/// <summary>
/// Gets or sets a boolean indicating whether the button should show the icon of the first provided filter type.
/// </summary>
public bool ShowTypeIcon
{
get => GetValue(ShowTypeIconProperty);
set => SetValue(ShowTypeIconProperty, value);
}
/// <summary>
/// Gets a boolean indicating whether the data model picker has a value.
/// </summary>
public bool HasValue
{
get => GetValue(HasValueProperty);
private set => SetValue(HasValueProperty, value);
}
/// <summary>
/// Gets or sets the desired flyout placement.
/// </summary>
public FlyoutPlacementMode Placement
{
get => GetValue(PlacementProperty);
set => SetValue(PlacementProperty, value);
}
/// <summary>
/// Gets or sets data model path.
/// </summary>
public DataModelPath? DataModelPath
{
get => GetValue(DataModelPathProperty);
set => SetValue(DataModelPathProperty, value);
}
/// <summary>
/// Gets or sets a boolean indicating whether the data model picker should show current values when selecting a path.
/// </summary>
public bool ShowDataModelValues
{
get => GetValue(ShowDataModelValuesProperty);
set => SetValue(ShowDataModelValuesProperty, value);
}
/// <summary>
/// A list of extra modules to show data models of.
/// </summary>
public ObservableCollection<Module>? Modules
{
get => GetValue(ModulesProperty);
set => SetValue(ModulesProperty, value);
}
/// <summary>
/// A list of data model view models to show.
/// </summary>
public ObservableCollection<DataModelPropertiesViewModel>? ExtraDataModelViewModels
{
get => GetValue(ExtraDataModelViewModelsProperty);
set => SetValue(ExtraDataModelViewModelsProperty, value);
}
/// <summary>
/// A list of types to filter the selectable paths on.
/// </summary>
public ObservableCollection<Type>? FilterTypes
{
get => GetValue(FilterTypesProperty);
set => SetValue(FilterTypesProperty, value);
}
/// <summary>
/// Raised when the flyout opens.
/// </summary>
public event TypedEventHandler<DataModelPickerButton, EventArgs>? FlyoutOpened;
/// <summary>
/// Raised when the flyout closes.
/// </summary>
public event TypedEventHandler<DataModelPickerButton, EventArgs>? FlyoutClosed;
private static void DataModelPathChanged(AvaloniaPropertyChangedEventArgs<DataModelPath?> e)
{
if (e.Sender is not DataModelPickerButton self || !self._attached)
return;
if (e.OldValue.Value != null)
{
e.OldValue.Value.PathInvalidated -= self.PathValidationChanged;
e.OldValue.Value.PathValidated -= self.PathValidationChanged;
e.OldValue.Value.Dispose();
}
if (e.NewValue.Value != null)
{
e.NewValue.Value.PathInvalidated += self.PathValidationChanged;
e.NewValue.Value.PathValidated += self.PathValidationChanged;
}
self.UpdateValueDisplay();
}
private static void ShowFullPathChanged(AvaloniaPropertyChangedEventArgs<bool> e)
{
if (e.Sender is DataModelPickerButton self)
self.UpdateValueDisplay();
}
private void PathValidationChanged(object? sender, EventArgs e)
{
Dispatcher.UIThread.InvokeAsync(UpdateValueDisplay, DispatcherPriority.DataBind);
}
private void UpdateValueDisplay()
{
HasValue = DataModelPath != null && DataModelPath.IsValid;
if (_button == null || _label == null)
return;
if (!HasValue)
{
ToolTip.SetTip(_button, null);
_label.Text = Placeholder;
}
else
{
string? formattedPath = null;
if (DataModelPath != null && DataModelPath.IsValid)
formattedPath = string.Join(" › ", DataModelPath.Segments.Where(s => s.GetPropertyDescription() != null).Select(s => s.GetPropertyDescription()!.Name));
ToolTip.SetTip(_button, formattedPath);
_label.Text = ShowFullPath
? formattedPath
: DataModelPath?.Segments.LastOrDefault()?.GetPropertyDescription()?.Name ?? DataModelPath?.Segments.LastOrDefault()?.Identifier;
}
}
private void OnButtonClick(object? sender, RoutedEventArgs e)
{
if (_flyout == null)
return;
// Logic here is taken from Fluent Avalonia's ColorPicker which also reuses the same control since it's large
_flyout.DataModelPicker.DataModelPath = DataModelPath;
_flyout.DataModelPicker.ExtraDataModelViewModels = ExtraDataModelViewModels;
_flyout.DataModelPicker.FilterTypes = FilterTypes;
_flyout.DataModelPicker.Modules = Modules;
_flyout.DataModelPicker.ShowDataModelValues = ShowDataModelValues;
_flyout.Placement = Placement;
_flyout.ShowAt(_button != null ? _button : this);
_flyoutActive = true;
FlyoutOpened?.Invoke(this, EventArgs.Empty);
}
#region Overrides of TemplatedControl
/// <inheritdoc />
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
if (_button != null)
_button.Click -= OnButtonClick;
base.OnApplyTemplate(e);
_button = e.NameScope.Find<Button>("MainButton");
_label = e.NameScope.Find<TextBlock>("MainButtonLabel");
if (_button != null)
_button.Click += OnButtonClick;
UpdateValueDisplay();
}
/// <inheritdoc />
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
_attached = true;
if (DataModelPath != null)
{
DataModelPath.PathInvalidated += PathValidationChanged;
DataModelPath.PathValidated += PathValidationChanged;
}
_flyout ??= new DataModelPickerFlyout();
_flyout.Confirmed += FlyoutOnConfirmed;
_flyout.Closed += OnFlyoutClosed;
}
private void FlyoutOnConfirmed(DataModelPickerFlyout sender, object args)
{
if (_flyoutActive)
{
FlyoutClosed?.Invoke(this, EventArgs.Empty);
_flyoutActive = false;
if (_flyout != null)
DataModelPath = _flyout.DataModelPicker.DataModelPath;
}
}
private void OnFlyoutClosed(object? sender, EventArgs e)
{
if (_flyoutActive)
{
FlyoutClosed?.Invoke(this, EventArgs.Empty);
_flyoutActive = false;
}
}
/// <inheritdoc />
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
{
if (DataModelPath != null)
{
DataModelPath.PathInvalidated -= PathValidationChanged;
DataModelPath.PathValidated -= PathValidationChanged;
}
if (_flyout != null)
_flyout.Closed -= OnFlyoutClosed;
}
#endregion
}
+208 -273
View File
@@ -1,94 +1,93 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Timers;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Threading;
using System.Threading.Tasks;
using Artemis.Core;
using Artemis.UI.Shared.Events;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.LogicalTree;
using Avalonia.Media;
using Avalonia.Media.Imaging;
using Avalonia.Platform;
using Avalonia.Rendering;
using Avalonia.Threading;
using Avalonia.Visuals.Media.Imaging;
namespace Artemis.UI.Shared
namespace Artemis.UI.Shared.Controls
{
/// <summary>
/// Visualizes an <see cref="ArtemisDevice" /> with optional per-LED colors
/// </summary>
public class DeviceVisualizer : FrameworkElement
public class DeviceVisualizer : Control
{
/// <summary>
/// The device to visualize
/// </summary>
public static readonly DependencyProperty DeviceProperty = DependencyProperty.Register(nameof(Device), typeof(ArtemisDevice), typeof(DeviceVisualizer),
new FrameworkPropertyMetadata(default(ArtemisDevice), FrameworkPropertyMetadataOptions.AffectsRender, DevicePropertyChangedCallback));
/// <summary>
/// Whether or not to show per-LED colors
/// </summary>
public static readonly DependencyProperty ShowColorsProperty = DependencyProperty.Register(nameof(ShowColors), typeof(bool), typeof(DeviceVisualizer),
new FrameworkPropertyMetadata(default(bool), FrameworkPropertyMetadataOptions.AffectsRender, ShowColorsPropertyChangedCallback));
/// <summary>
/// A list of LEDs to highlight
/// </summary>
public static readonly DependencyProperty HighlightedLedsProperty = DependencyProperty.Register(nameof(HighlightedLeds), typeof(ObservableCollection<ArtemisLed>), typeof(DeviceVisualizer),
new FrameworkPropertyMetadata(default(ObservableCollection<ArtemisLed>), HighlightedLedsPropertyChanged));
private readonly DrawingGroup _backingStore;
private const double UpdateFrameRate = 25.0;
private readonly List<DeviceVisualizerLed> _deviceVisualizerLeds;
private readonly DispatcherTimer _timer;
private BitmapImage? _deviceImage;
private ArtemisDevice? _oldDevice;
private List<DeviceVisualizerLed> _highlightedLeds;
private List<DeviceVisualizerLed> _dimmedLeds;
/// <summary>
/// Creates a new instance of the <see cref="DeviceVisualizer" /> class
/// </summary>
private Rect _deviceBounds;
private RenderTargetBitmap? _deviceImage;
private List<DeviceVisualizerLed>? _dimmedLeds;
private List<DeviceVisualizerLed>? _highlightedLeds;
private ArtemisDevice? _oldDevice;
/// <inheritdoc />
public DeviceVisualizer()
{
_backingStore = new DrawingGroup();
_timer = new DispatcherTimer(DispatcherPriority.Render) {Interval = TimeSpan.FromMilliseconds(1000.0 / UpdateFrameRate)};
_deviceVisualizerLeds = new List<DeviceVisualizerLed>();
_dimmedLeds = new List<DeviceVisualizerLed>();
// Run an update timer at 25 fps
_timer = new DispatcherTimer(DispatcherPriority.Render) {Interval = TimeSpan.FromMilliseconds(40)};
MouseLeftButtonUp += OnMouseLeftButtonUp;
Loaded += OnLoaded;
Unloaded += OnUnloaded;
PointerReleased += OnPointerReleased;
}
/// <summary>
/// Gets or sets the device to visualize
/// </summary>
public ArtemisDevice? Device
/// <inheritdoc />
public override void Render(DrawingContext drawingContext)
{
get => (ArtemisDevice) GetValue(DeviceProperty);
set => SetValue(DeviceProperty, value);
}
if (Device == null)
return;
/// <summary>
/// Gets or sets whether or not to show per-LED colors
/// </summary>
public bool ShowColors
{
get => (bool) GetValue(ShowColorsProperty);
set => SetValue(ShowColorsProperty, value);
}
// Determine the scale required to fit the desired size of the control
double scale = Math.Min(Bounds.Width / _deviceBounds.Width, Bounds.Height / _deviceBounds.Height);
/// <summary>
/// Gets or sets a list of LEDs to highlight
/// </summary>
public ObservableCollection<ArtemisLed>? HighlightedLeds
{
get => (ObservableCollection<ArtemisLed>) GetValue(HighlightedLedsProperty);
set => SetValue(HighlightedLedsProperty, value);
DrawingContext.PushedState? boundsPush = null;
try
{
// Scale the visualization in the desired bounding box
if (Bounds.Width > 0 && Bounds.Height > 0)
boundsPush = drawingContext.PushPreTransform(Matrix.CreateScale(scale, scale));
// Apply device rotation
using DrawingContext.PushedState translationPush = drawingContext.PushPreTransform(Matrix.CreateTranslation(0 - _deviceBounds.Left, 0 - _deviceBounds.Top));
using DrawingContext.PushedState rotationPush = drawingContext.PushPreTransform(Matrix.CreateRotation(Matrix.ToRadians(Device.Rotation)));
// Apply device scale
using DrawingContext.PushedState scalePush = drawingContext.PushPreTransform(Matrix.CreateScale(Device.Scale, Device.Scale));
// Render device and LED images
if (_deviceImage != null)
{
drawingContext.DrawImage(
_deviceImage,
new Rect(_deviceImage.Size),
new Rect(0, 0, Device.RgbDevice.ActualSize.Width, Device.RgbDevice.ActualSize.Height),
RenderOptions.GetBitmapInterpolationMode(this)
);
}
if (!ShowColors)
return;
foreach (DeviceVisualizerLed deviceVisualizerLed in _deviceVisualizerLeds)
deviceVisualizerLed.RenderGeometry(drawingContext, false);
}
finally
{
boundsPush?.Dispose();
}
}
/// <summary>
@@ -96,54 +95,6 @@ namespace Artemis.UI.Shared
/// </summary>
public event EventHandler<LedClickedEventArgs>? LedClicked;
/// <inheritdoc />
protected override void OnRender(DrawingContext drawingContext)
{
if (Device == null)
return;
// Determine the scale required to fit the desired size of the control
Size measureSize = MeasureDevice();
double scale = Math.Min(RenderSize.Width / measureSize.Width, RenderSize.Height / measureSize.Height);
// Scale the visualization in the desired bounding box
if (RenderSize.Width > 0 && RenderSize.Height > 0)
drawingContext.PushTransform(new ScaleTransform(scale, scale));
// Determine the offset required to rotate within bounds
Rect rotationRect = new(0, 0, Device.RgbDevice.ActualSize.Width, Device.RgbDevice.ActualSize.Height);
rotationRect.Transform(new RotateTransform(Device.Rotation).Value);
// Apply device rotation
drawingContext.PushTransform(new TranslateTransform(0 - rotationRect.Left, 0 - rotationRect.Top));
drawingContext.PushTransform(new RotateTransform(Device.Rotation));
// Apply device scale
drawingContext.PushTransform(new ScaleTransform(Device.Scale, Device.Scale));
// Render device and LED images
if (_deviceImage != null)
drawingContext.DrawImage(_deviceImage, new Rect(0, 0, Device.RgbDevice.Size.Width, Device.RgbDevice.Size.Height));
foreach (DeviceVisualizerLed deviceVisualizerLed in _deviceVisualizerLeds)
deviceVisualizerLed.RenderImage(drawingContext);
drawingContext.DrawDrawing(_backingStore);
}
/// <inheritdoc />
protected override Size MeasureOverride(Size availableSize)
{
if (Device == null)
return Size.Empty;
Size deviceSize = MeasureDevice();
if (deviceSize.Width <= 0 || deviceSize.Height <= 0)
return Size.Empty;
return ResizeKeepAspect(deviceSize, availableSize.Width, availableSize.Height);
}
/// <summary>
/// Invokes the <see cref="LedClicked" /> event
/// </summary>
@@ -153,72 +104,37 @@ namespace Artemis.UI.Shared
LedClicked?.Invoke(this, e);
}
/// <summary>
/// Releases the unmanaged resources used by the object and optionally releases the managed resources.
/// </summary>
/// <param name="disposing">
/// <see langword="true" /> to release both managed and unmanaged resources;
/// <see langword="false" /> to release only unmanaged resources.
/// </param>
protected virtual void Dispose(bool disposing)
private void Update()
{
if (disposing)
_timer.Stop();
InvalidateVisual();
}
private static Size ResizeKeepAspect(Size src, double maxWidth, double maxHeight)
{
double scale;
if (double.IsPositiveInfinity(maxWidth) && !double.IsPositiveInfinity(maxHeight))
scale = maxHeight / src.Height;
else if (!double.IsPositiveInfinity(maxWidth) && double.IsPositiveInfinity(maxHeight))
scale = maxWidth / src.Width;
else if (double.IsPositiveInfinity(maxWidth) && double.IsPositiveInfinity(maxHeight))
return src;
else
scale = Math.Min(maxWidth / src.Width, maxHeight / src.Height);
return new Size(src.Width * scale, src.Height * scale);
}
private Size MeasureDevice()
private Rect MeasureDevice()
{
if (Device == null)
return Size.Empty;
return Rect.Empty;
Rect rotationRect = new(0, 0, Device.RgbDevice.ActualSize.Width, Device.RgbDevice.ActualSize.Height);
rotationRect.Transform(new RotateTransform(Device.Rotation).Value);
Rect deviceRect = new(0, 0, Device.RgbDevice.ActualSize.Width, Device.RgbDevice.ActualSize.Height);
Geometry geometry = new RectangleGeometry(deviceRect);
geometry.Transform = new RotateTransform(Device.Rotation);
return rotationRect.Size;
return geometry.Bounds;
}
private void OnUnloaded(object? sender, RoutedEventArgs e)
private void TimerOnTick(object? sender, EventArgs e)
{
_timer.Stop();
_timer.Tick -= TimerOnTick;
if (HighlightedLeds != null)
HighlightedLeds.CollectionChanged -= HighlightedLedsChanged;
if (_oldDevice != null)
{
if (Device != null)
{
Device.RgbDevice.PropertyChanged -= DevicePropertyChanged;
Device.DeviceUpdated -= DeviceUpdated;
}
_oldDevice = null;
}
if (ShowColors && IsVisible && Opacity > 0)
Update();
}
private void OnMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
private void OnPointerReleased(object? sender, PointerReleasedEventArgs e)
{
if (Device == null)
return;
Point position = e.GetPosition(this);
double x = position.X / RenderSize.Width;
double y = position.Y / RenderSize.Height;
double x = position.X / Bounds.Width;
double y = position.Y / Bounds.Height;
Point scaledPosition = new(x * Device.Rectangle.Width, y * Device.Rectangle.Height);
DeviceVisualizerLed? deviceVisualizerLed = _deviceVisualizerLeds.FirstOrDefault(l => l.HitTest(scaledPosition));
@@ -226,154 +142,173 @@ namespace Artemis.UI.Shared
OnLedClicked(new LedClickedEventArgs(deviceVisualizerLed.Led.Device, deviceVisualizerLed.Led));
}
private void OnLoaded(object? sender, RoutedEventArgs e)
private void DevicePropertyChanged(object? sender, PropertyChangedEventArgs e)
{
_timer.Start();
_timer.Tick += TimerOnTick;
Dispatcher.UIThread.Post(SetupForDevice, DispatcherPriority.Background);
}
private void TimerOnTick(object? sender, EventArgs e)
private void DeviceUpdated(object? sender, EventArgs e)
{
if (ShowColors && Visibility == Visibility.Visible)
Render();
Dispatcher.UIThread.Post(SetupForDevice, DispatcherPriority.Background);
}
private void Render()
#region Properties
/// <summary>
/// Gets or sets the <see cref="ArtemisDevice" /> to display
/// </summary>
public static readonly StyledProperty<ArtemisDevice?> DeviceProperty =
AvaloniaProperty.Register<DeviceVisualizer, ArtemisDevice?>(nameof(Device), notifying: DeviceUpdated);
private static void DeviceUpdated(IAvaloniaObject sender, bool before)
{
DrawingContext drawingContext = _backingStore.Append();
if (_highlightedLeds.Any())
{
foreach (DeviceVisualizerLed deviceVisualizerLed in _highlightedLeds)
deviceVisualizerLed.RenderColor(_backingStore, drawingContext, false);
foreach (DeviceVisualizerLed deviceVisualizerLed in _dimmedLeds)
deviceVisualizerLed.RenderColor(_backingStore, drawingContext, true);
}
else
{
foreach (DeviceVisualizerLed deviceVisualizerLed in _deviceVisualizerLeds)
deviceVisualizerLed.RenderColor(_backingStore, drawingContext, false);
}
drawingContext.Close();
if (!before)
((DeviceVisualizer) sender).SetupForDevice();
}
private void UpdateTransform()
/// <summary>
/// Gets or sets the <see cref="ArtemisDevice" /> to display
/// </summary>
public ArtemisDevice? Device
{
InvalidateVisual();
InvalidateMeasure();
get => GetValue(DeviceProperty);
set => SetValue(DeviceProperty, value);
}
private void SetupForDevice()
/// <summary>
/// Gets or sets boolean indicating whether or not to show per-LED colors
/// </summary>
public static readonly StyledProperty<bool> ShowColorsProperty =
AvaloniaProperty.Register<DeviceVisualizer, bool>(nameof(ShowColors));
/// <summary>
/// Gets or sets a boolean indicating whether or not to show per-LED colors
/// </summary>
public bool ShowColors
{
get => GetValue(ShowColorsProperty);
set => SetValue(ShowColorsProperty, value);
}
/// <summary>
/// Gets or sets a list of LEDs to highlight
/// </summary>
public static readonly StyledProperty<ObservableCollection<ArtemisLed>?> HighlightedLedsProperty =
AvaloniaProperty.Register<DeviceVisualizer, ObservableCollection<ArtemisLed>?>(nameof(HighlightedLeds));
/// <summary>
/// Gets or sets a list of LEDs to highlight
/// </summary>
public ObservableCollection<ArtemisLed>? HighlightedLeds
{
get => GetValue(HighlightedLedsProperty);
set => SetValue(HighlightedLedsProperty, value);
}
#endregion
#region Lifetime management
/// <inheritdoc />
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
{
_deviceImage?.Dispose();
_deviceImage = null;
_deviceVisualizerLeds.Clear();
_highlightedLeds = new List<DeviceVisualizerLed>();
_dimmedLeds = new List<DeviceVisualizerLed>();
if (Device == null)
return;
if (_oldDevice != null)
if (Device != null)
{
Device.RgbDevice.PropertyChanged -= DevicePropertyChanged;
Device.DeviceUpdated -= DeviceUpdated;
}
base.OnDetachedFromVisualTree(e);
}
/// <inheritdoc />
protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e)
{
_timer.Start();
_timer.Tick += TimerOnTick;
base.OnAttachedToLogicalTree(e);
}
/// <inheritdoc />
protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e)
{
_timer.Stop();
_timer.Tick -= TimerOnTick;
base.OnDetachedFromLogicalTree(e);
}
private void SetupForDevice()
{
_deviceImage?.Dispose();
_deviceImage = null;
_deviceVisualizerLeds.Clear();
_highlightedLeds = new List<DeviceVisualizerLed>();
_dimmedLeds = new List<DeviceVisualizerLed>();
if (_oldDevice != null)
{
_oldDevice.RgbDevice.PropertyChanged -= DevicePropertyChanged;
_oldDevice.DeviceUpdated -= DeviceUpdated;
}
_oldDevice = Device;
if (Device == null)
return;
_deviceBounds = MeasureDevice();
Device.RgbDevice.PropertyChanged += DevicePropertyChanged;
Device.DeviceUpdated += DeviceUpdated;
UpdateTransform();
// Load the device main image
if (Device.Layout?.Image != null && File.Exists(Device.Layout.Image.LocalPath))
_deviceImage = new BitmapImage(Device.Layout.Image);
// Create all the LEDs
foreach (ArtemisLed artemisLed in Device.Leds)
_deviceVisualizerLeds.Add(new DeviceVisualizerLed(artemisLed));
if (!ShowColors)
// Load the device main image on a background thread
ArtemisDevice? device = Device;
Task.Run(() =>
{
InvalidateMeasure();
return;
}
if (device.Layout?.Image == null || !File.Exists(device.Layout.Image.LocalPath))
return;
// Create the opacity drawing group
DrawingGroup opacityDrawingGroup = new();
DrawingContext drawingContext = opacityDrawingGroup.Open();
foreach (DeviceVisualizerLed deviceVisualizerLed in _deviceVisualizerLeds)
deviceVisualizerLed.RenderOpacityMask(drawingContext);
drawingContext.Close();
try
{
// Create a bitmap that'll be used to render the device and LED images just once
RenderTargetBitmap renderTargetBitmap = new(new PixelSize((int) device.RgbDevice.Size.Width * 4, (int) device.RgbDevice.Size.Height * 4));
// Render the store as a bitmap
DrawingImage drawingImage = new(opacityDrawingGroup);
Image image = new() {Source = drawingImage};
RenderTargetBitmap bitmap = new(
Math.Max(1, (int) (opacityDrawingGroup.Bounds.Width * 2.5)),
Math.Max(1, (int) (opacityDrawingGroup.Bounds.Height * 2.5)),
96,
96,
PixelFormats.Pbgra32
);
image.Arrange(new Rect(0, 0, bitmap.Width, bitmap.Height));
bitmap.Render(image);
bitmap.Freeze();
using IDrawingContextImpl context = renderTargetBitmap.CreateDrawingContext(new ImmediateRenderer(this));
using Bitmap bitmap = new(device.Layout.Image.LocalPath);
context.DrawBitmap(bitmap.PlatformImpl, 1, new Rect(bitmap.Size), new Rect(renderTargetBitmap.Size), BitmapInterpolationMode.HighQuality);
foreach (DeviceVisualizerLed deviceVisualizerLed in _deviceVisualizerLeds)
deviceVisualizerLed.DrawBitmap(context);
// Set the bitmap as the opacity mask for the colors backing store
ImageBrush bitmapBrush = new(bitmap);
bitmapBrush.Freeze();
_backingStore.OpacityMask = bitmapBrush;
_backingStore.Children.Clear();
_deviceImage = renderTargetBitmap;
InvalidateMeasure();
Dispatcher.UIThread.Post(InvalidateMeasure);
}
catch
{
// ignored
}
});
}
private void DeviceUpdated(object? sender, EventArgs e)
/// <inheritdoc />
protected override Size MeasureOverride(Size availableSize)
{
Dispatcher.Invoke(SetupForDevice);
if (_deviceBounds.Width <= 0 || _deviceBounds.Height <= 0)
return new Size(0, 0);
double availableWidth = double.IsInfinity(availableSize.Width) ? _deviceBounds.Width : availableSize.Width;
double availableHeight = double.IsInfinity(availableSize.Height) ? _deviceBounds.Height : availableSize.Height;
double bestRatio = Math.Min(availableWidth / _deviceBounds.Width, availableHeight / _deviceBounds.Height);
return new Size(_deviceBounds.Width * bestRatio, _deviceBounds.Height * bestRatio);
}
private void DevicePropertyChanged(object? sender, PropertyChangedEventArgs e)
{
Dispatcher.Invoke(SetupForDevice);
}
private static void DevicePropertyChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
DeviceVisualizer deviceVisualizer = (DeviceVisualizer) d;
deviceVisualizer.Dispatcher.Invoke(() => { deviceVisualizer.SetupForDevice(); });
}
private static void ShowColorsPropertyChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
DeviceVisualizer deviceVisualizer = (DeviceVisualizer) d;
deviceVisualizer.Dispatcher.Invoke(() => { deviceVisualizer.SetupForDevice(); });
}
private static void HighlightedLedsPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
DeviceVisualizer deviceVisualizer = (DeviceVisualizer) d;
if (e.OldValue is ObservableCollection<ArtemisLed> oldCollection)
oldCollection.CollectionChanged -= deviceVisualizer.HighlightedLedsChanged;
if (e.NewValue is ObservableCollection<ArtemisLed> newCollection)
newCollection.CollectionChanged += deviceVisualizer.HighlightedLedsChanged;
}
private void HighlightedLedsChanged(object? sender, NotifyCollectionChangedEventArgs notifyCollectionChangedEventArgs)
{
if (HighlightedLeds != null)
{
_highlightedLeds = _deviceVisualizerLeds.Where(l => HighlightedLeds.Contains(l.Led)).ToList();
_dimmedLeds = _deviceVisualizerLeds.Where(l => !HighlightedLeds.Contains(l.Led)).ToList();
}
else
{
_highlightedLeds = new List<DeviceVisualizerLed>();
_dimmedLeds = new List<DeviceVisualizerLed>();
}
}
#endregion
}
}
@@ -1,24 +1,23 @@
using System;
using System.IO;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using Artemis.Core;
using Avalonia;
using Avalonia.Media;
using Avalonia.Media.Imaging;
using Avalonia.Platform;
using Avalonia.Visuals.Media.Imaging;
using RGB.NET.Core;
using Color = System.Windows.Media.Color;
using Point = System.Windows.Point;
using SolidColorBrush = System.Windows.Media.SolidColorBrush;
using Color = Avalonia.Media.Color;
using Point = Avalonia.Point;
using SolidColorBrush = Avalonia.Media.SolidColorBrush;
namespace Artemis.UI.Shared
namespace Artemis.UI.Shared.Controls
{
internal class DeviceVisualizerLed
{
private const byte Dimmed = 100;
private const byte NonDimmed = 255;
private GeometryDrawing? _geometryDrawing;
private DrawingGroup? _lastBackingStore;
private Color _renderColor;
private SolidColorBrush? _renderColorBrush;
private readonly SolidColorBrush _penBrush;
private readonly SolidColorBrush _fillBrush;
private readonly Pen _pen;
public DeviceVisualizerLed(ArtemisLed led)
{
@@ -30,83 +29,63 @@ namespace Artemis.UI.Shared
Led.RgbLed.Size.Height
);
if (Led.Layout?.Image != null && File.Exists(Led.Layout.Image.LocalPath))
LedImage = new BitmapImage(Led.Layout.Image);
_fillBrush = new SolidColorBrush();
_penBrush = new SolidColorBrush();
_pen = new Pen(_penBrush) {LineJoin = PenLineJoin.Round};
CreateLedGeometry();
}
public ArtemisLed Led { get; }
public Rect LedRect { get; set; }
public BitmapImage? LedImage { get; set; }
public Geometry? DisplayGeometry { get; private set; }
public void RenderColor(DrawingGroup backingStore, DrawingContext drawingContext, bool isDimmed)
public void DrawBitmap(IDrawingContextImpl drawingContext)
{
if (DisplayGeometry == null || backingStore == null)
if (Led.Layout?.Image == null || !File.Exists(Led.Layout.Image.LocalPath))
return;
_renderColorBrush ??= new SolidColorBrush();
_geometryDrawing ??= new GeometryDrawing(_renderColorBrush, null, new RectangleGeometry(LedRect));
try
{
using Bitmap bitmap = new(Led.Layout.Image.LocalPath);
drawingContext.DrawBitmap(
bitmap.PlatformImpl,
1,
new Rect(bitmap.Size),
new Rect(Led.RgbLed.Location.X * 4, Led.RgbLed.Location.Y * 4, Led.RgbLed.Size.Width * 4, Led.RgbLed.Size.Height * 4),
BitmapInterpolationMode.HighQuality
);
}
catch
{
// ignored
}
}
public void RenderGeometry(DrawingContext drawingContext, bool dimmed)
{
byte r = Led.RgbLed.Color.GetR();
byte g = Led.RgbLed.Color.GetG();
byte b = Led.RgbLed.Color.GetB();
_renderColor.A = isDimmed ? Dimmed : NonDimmed;
_renderColor.R = r;
_renderColor.G = g;
_renderColor.B = b;
_renderColorBrush.Color = _renderColor;
if (_lastBackingStore != backingStore)
if (dimmed)
{
backingStore.Children.Add(_geometryDrawing);
_lastBackingStore = backingStore;
_fillBrush.Color = new Color(50, r, g, b);
_penBrush.Color = new Color(100, r, g, b);
}
else
{
_fillBrush.Color = new Color(100, r, g, b);
_penBrush.Color = new Color(255, r, g, b);
}
}
public void RenderImage(DrawingContext drawingContext)
{
if (LedImage == null)
return;
drawingContext.DrawImage(LedImage, LedRect);
}
public void RenderOpacityMask(DrawingContext drawingContext)
{
if (DisplayGeometry == null)
return;
SolidColorBrush fillBrush = new(Color.FromArgb(100, 255, 255, 255));
fillBrush.Freeze();
SolidColorBrush penBrush = new(Color.FromArgb(255, 255, 255, 255));
penBrush.Freeze();
// Create transparent pixels covering the entire LedRect so the image size matched the LedRect size
drawingContext.DrawRectangle(new SolidColorBrush(Colors.Transparent), new Pen(new SolidColorBrush(Colors.Transparent), 1), LedRect);
// Translate to the top-left of the LedRect
drawingContext.PushTransform(new TranslateTransform(LedRect.X, LedRect.Y));
// Render the LED geometry
drawingContext.DrawGeometry(fillBrush, new Pen(penBrush, 1) {LineJoin = PenLineJoin.Round}, DisplayGeometry.GetOutlinedPathGeometry());
// Restore the drawing context
drawingContext.Pop();
drawingContext.DrawGeometry(_fillBrush, _pen, DisplayGeometry);
}
public bool HitTest(Point position)
{
if (DisplayGeometry == null)
return false;
PathGeometry translatedGeometry = Geometry.Combine(
Geometry.Empty,
DisplayGeometry,
GeometryCombineMode.Union,
new TranslateTransform(Led.RgbLed.Location.X, Led.RgbLed.Location.Y)
);
return translatedGeometry.FillContains(position);
return DisplayGeometry != null && DisplayGeometry.FillContains(position);
}
private void CreateLedGeometry()
@@ -139,17 +118,17 @@ namespace Artemis.UI.Shared
private void CreateRectangleGeometry()
{
DisplayGeometry = new RectangleGeometry(new Rect(0.5, 0.5, Led.RgbLed.Size.Width - 1, Led.RgbLed.Size.Height - 1));
DisplayGeometry = new RectangleGeometry(new Rect(Led.RgbLed.Location.X + 0.5, Led.RgbLed.Location.Y + 0.5, Led.RgbLed.Size.Width - 1, Led.RgbLed.Size.Height - 1));
}
private void CreateCircleGeometry()
{
DisplayGeometry = new EllipseGeometry(new Rect(0.5, 0.5, Led.RgbLed.Size.Width - 1, Led.RgbLed.Size.Height - 1));
DisplayGeometry = new EllipseGeometry(new Rect(Led.RgbLed.Location.X + 0.5, Led.RgbLed.Location.Y + 0.5, Led.RgbLed.Size.Width - 1, Led.RgbLed.Size.Height - 1));
}
private void CreateKeyCapGeometry()
{
DisplayGeometry = new RectangleGeometry(new Rect(1, 1, Led.RgbLed.Size.Width - 2, Led.RgbLed.Size.Height - 2), 1.6, 1.6);
DisplayGeometry = new RectangleGeometry(new Rect(Led.RgbLed.Location.X + 1, Led.RgbLed.Location.Y + 1, Led.RgbLed.Size.Width - 2, Led.RgbLed.Size.Height - 2));
}
private void CreateCustomGeometry(double deflateAmount)
@@ -158,36 +137,17 @@ namespace Artemis.UI.Shared
{
double width = Led.RgbLed.Size.Width - deflateAmount;
double height = Led.RgbLed.Size.Height - deflateAmount;
// DisplayGeometry = Geometry.Parse(Led.RgbLed.ShapeData);
DisplayGeometry = Geometry.Combine(
Geometry.Empty,
Geometry.Parse(Led.RgbLed.ShapeData),
GeometryCombineMode.Union,
new TransformGroup
Geometry geometry = Geometry.Parse(Led.RgbLed.ShapeData);
geometry.Transform = new TransformGroup
{
Children = new Transforms
{
Children = new TransformCollection
{
new ScaleTransform(width, height),
new TranslateTransform(deflateAmount / 2, deflateAmount / 2)
}
new ScaleTransform(width, height),
new TranslateTransform(Led.RgbLed.Location.X + deflateAmount / 2, Led.RgbLed.Location.Y + deflateAmount / 2)
}
);
if (DisplayGeometry.Bounds.Width > width)
{
DisplayGeometry = Geometry.Combine(Geometry.Empty, DisplayGeometry, GeometryCombineMode.Union, new TransformGroup
{
Children = new TransformCollection {new ScaleTransform(width / DisplayGeometry.Bounds.Width, 1)}
});
}
if (DisplayGeometry.Bounds.Height > height)
{
DisplayGeometry = Geometry.Combine(Geometry.Empty, DisplayGeometry, GeometryCombineMode.Union, new TransformGroup
{
Children = new TransformCollection {new ScaleTransform(1, height / DisplayGeometry.Bounds.Height)}
});
}
};
DisplayGeometry = geometry;
}
catch (Exception)
{
@@ -1,71 +0,0 @@
<UserControl x:Class="Artemis.UI.Shared.DraggableFloat"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:shared="clr-namespace:Artemis.UI.Shared"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<UserControl.Style>
<Style TargetType="UserControl" >
<Setter Property="Validation.ErrorTemplate" Value="{StaticResource MaterialDesignValidationErrorTemplate}" />
<Style.Triggers>
<DataTrigger Binding="{Binding RelativeSource={RelativeSource Mode=Self}, Path=IsEnabled}" Value="False">
<Setter Property="Opacity" Value="0.5" />
</DataTrigger>
</Style.Triggers>
</Style>
</UserControl.Style>
<UserControl.Resources>
<Style TargetType="Rectangle" x:Key="RectStyle">
<Style.Triggers>
<DataTrigger Binding="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type UserControl}}, Path=IsEnabled}" Value="False">
<Setter Property="StrokeDashArray" Value="2 2" />
</DataTrigger>
</Style.Triggers>
</Style>
</UserControl.Resources>
<Grid>
<!-- Drag handle -->
<Border x:Name="DragHandle" BorderThickness="0,0,0,1" Height="19">
<Border.BorderBrush>
<VisualBrush>
<VisualBrush.Visual>
<Rectangle x:Name="BorderVisual"
Style="{StaticResource RectStyle}"
Stroke="{DynamicResource MaterialDesignTextBoxBorder}"
StrokeThickness="1"
Width="{Binding RelativeSource={RelativeSource AncestorType={x:Type Border}}, Path=ActualWidth}"
Height="{Binding RelativeSource={RelativeSource AncestorType={x:Type Border}}, Path=ActualHeight}">
</Rectangle>
</VisualBrush.Visual>
</VisualBrush>
</Border.BorderBrush>
<TextBlock Style="{x:Null}"
Width="60"
Height="17"
Padding="2 0"
Margin="0 3 0 0"
Text="{Binding InputValue, StringFormat=N3, RelativeSource={RelativeSource AncestorType={x:Type UserControl}}}"
Cursor="/Resources/Cursors/aero_drag_ew.cur"
MouseDown="InputMouseDown"
MouseUp="InputMouseUp"
MouseMove="InputMouseMove"
RequestBringIntoView="Input_OnRequestBringIntoView" />
</Border>
<!-- Input -->
<TextBox x:Name="DraggableFloatInputTextBox"
Width="60"
Height="21"
Padding="0 0 -2 0"
HorizontalAlignment="Left"
Text="{Binding InputValue, RelativeSource={RelativeSource AncestorType={x:Type UserControl}}}"
LostFocus="InputLostFocus"
KeyDown="InputKeyDown"
Visibility="Collapsed"
RequestBringIntoView="Input_OnRequestBringIntoView"
PreviewTextInput="Input_PreviewTextInput"
DataObject.Pasting="Input_OnPasting"/>
</Grid>
</UserControl>
@@ -1,303 +0,0 @@
using System;
using System.ComponentModel;
using System.Globalization;
using System.Runtime.CompilerServices;
using System.Text.RegularExpressions;
using System.Windows;
using System.Windows.Input;
namespace Artemis.UI.Shared
{
/// <summary>
/// Interaction logic for DraggableFloat.xaml
/// </summary>
public partial class DraggableFloat : INotifyPropertyChanged
{
/// <summary>
/// Gets or sets the current value
/// </summary>
public static readonly DependencyProperty ValueProperty = DependencyProperty.Register(nameof(Value), typeof(float), typeof(DraggableFloat),
new FrameworkPropertyMetadata(default(float), FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, FloatPropertyChangedCallback));
/// <summary>
/// Gets or sets the step size when dragging
/// </summary>
public static readonly DependencyProperty StepSizeProperty = DependencyProperty.Register(nameof(StepSize), typeof(float), typeof(DraggableFloat));
/// <summary>
/// Gets or sets the minimum value
/// </summary>
public static readonly DependencyProperty MinProperty = DependencyProperty.Register(nameof(Min), typeof(object), typeof(DraggableFloat));
/// <summary>
/// Gets or sets the maximum value
/// </summary>
public static readonly DependencyProperty MaxProperty = DependencyProperty.Register(nameof(Max), typeof(object), typeof(DraggableFloat));
/// <summary>
/// Occurs when the value has changed
/// </summary>
public static readonly RoutedEvent ValueChangedEvent =
EventManager.RegisterRoutedEvent(
nameof(Value),
RoutingStrategy.Bubble,
typeof(RoutedPropertyChangedEventHandler<float>),
typeof(DraggableFloat));
private readonly Regex _inputRegex = new("^[.][-|0-9]+$|^-?[0-9]*[.]{0,1}[0-9]*$");
private bool _calledDragStarted;
private bool _inCallback;
private Point _mouseDragStartPoint;
private float _startValue;
/// <summary>
/// Creates a new instance of the <see cref="DraggableFloat" /> class
/// </summary>
public DraggableFloat()
{
InitializeComponent();
}
/// <summary>
/// Gets or sets the current value
/// </summary>
public float Value
{
get => (float) GetValue(ValueProperty);
set => SetValue(ValueProperty, value);
}
/// <summary>
/// Gets or sets the current value as a string
/// </summary>
public string InputValue
{
get => Value.ToString("N3", CultureInfo.InvariantCulture);
set => UpdateValue(value);
}
/// <summary>
/// Gets or sets the step size when dragging
/// </summary>
public float StepSize
{
get => (float) GetValue(StepSizeProperty);
set => SetValue(StepSizeProperty, value);
}
/// <summary>
/// Gets or sets the minimum value
/// </summary>
public object? Min
{
get => (object?) GetValue(MinProperty);
set => SetValue(MinProperty, value);
}
/// <summary>
/// Gets or sets the maximum value
/// </summary>
public object? Max
{
get => (object?) GetValue(MaxProperty);
set => SetValue(MaxProperty, value);
}
private void UpdateValue(string value)
{
if (!float.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out float parsedResult))
return;
Value = parsedResult;
OnPropertyChanged(nameof(InputValue));
}
private void DisplayInput()
{
DragHandle.Visibility = Visibility.Collapsed;
DraggableFloatInputTextBox.Visibility = Visibility.Visible;
DraggableFloatInputTextBox.Focus();
DraggableFloatInputTextBox.SelectAll();
}
private void DisplayDragHandle()
{
DraggableFloatInputTextBox.Visibility = Visibility.Collapsed;
DragHandle.Visibility = Visibility.Visible;
}
/// <summary>
/// Rounds the provided decimal to the nearest value of x with a given threshold
/// Source: https://stackoverflow.com/a/25922075/5015269
/// </summary>
/// <param name="input">The value to round</param>
/// <param name="nearestOf">The value to round down towards</param>
private static decimal RoundToNearestOf(decimal input, decimal nearestOf)
{
return Math.Floor(input / nearestOf + 0.5m) * nearestOf;
}
#region Event handlers
private static void FloatPropertyChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
DraggableFloat draggableFloat = (DraggableFloat) d;
if (draggableFloat._inCallback)
return;
draggableFloat._inCallback = true;
draggableFloat.OnPropertyChanged(nameof(Value));
draggableFloat.OnPropertyChanged(nameof(InputValue));
draggableFloat._inCallback = false;
}
private void InputMouseDown(object sender, MouseButtonEventArgs e)
{
e.Handled = true;
_startValue = Value;
((IInputElement) sender).CaptureMouse();
_mouseDragStartPoint = e.GetPosition((IInputElement) sender);
}
private void InputMouseUp(object sender, MouseButtonEventArgs e)
{
e.Handled = true;
Point position = e.GetPosition((IInputElement) sender);
if (position == _mouseDragStartPoint)
{
DisplayInput();
}
else
{
OnDragEnded();
_calledDragStarted = false;
}
((IInputElement) sender).ReleaseMouseCapture();
}
private void InputMouseMove(object sender, MouseEventArgs e)
{
if (e.LeftButton != MouseButtonState.Pressed)
return;
e.Handled = true;
if (!_calledDragStarted)
{
OnDragStarted();
_calledDragStarted = true;
}
// Use decimals for everything to avoid floating point errors
decimal startValue = new(_startValue);
decimal startX = new(_mouseDragStartPoint.X);
decimal x = new(e.GetPosition((IInputElement) sender).X);
decimal stepSize = new(StepSize);
if (stepSize == 0)
stepSize = 0.1m;
if (Keyboard.IsKeyDown(Key.LeftShift) || Keyboard.IsKeyDown(Key.RightShift))
stepSize = stepSize * 10;
float value = (float) RoundToNearestOf(startValue + stepSize * (x - startX), stepSize);
if (Min != null && float.TryParse(Min.ToString(), out float minFloat))
value = Math.Max(value, minFloat);
if (Max != null && float.TryParse(Max.ToString(), out float maxFloat))
value = Math.Min(value, maxFloat);
Value = value;
}
private void InputLostFocus(object sender, RoutedEventArgs e)
{
DisplayDragHandle();
}
private void InputKeyDown(object sender, KeyEventArgs e)
{
if (e.Key == Key.Enter)
{
DisplayDragHandle();
}
else if (e.Key == Key.Escape)
{
DraggableFloatInputTextBox.Text = _startValue.ToString(CultureInfo.InvariantCulture);
DisplayDragHandle();
}
}
private void Input_OnRequestBringIntoView(object sender, RequestBringIntoViewEventArgs e)
{
e.Handled = true;
}
private void Input_PreviewTextInput(object sender, TextCompositionEventArgs e)
{
e.Handled = !_inputRegex.IsMatch(e.Text);
}
private void Input_OnPasting(object sender, DataObjectPastingEventArgs e)
{
if (e.DataObject.GetDataPresent(typeof(string)))
{
if (e.DataObject.GetData(typeof(string)) is string text && !_inputRegex.IsMatch(text))
e.CancelCommand();
}
else
{
e.CancelCommand();
}
}
#endregion
#region Events
/// <inheritdoc />
public event PropertyChangedEventHandler? PropertyChanged;
/// <summary>
/// Occurs when dragging has started
/// </summary>
public event EventHandler? DragStarted;
/// <summary>
/// Occurs when dragging has ended
/// </summary>
public event EventHandler? DragEnded;
/// <summary>
/// Invokes the <see cref="PropertyChanged" /> event
/// </summary>
protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
/// <summary>
/// Invokes the <see cref="DragStarted" /> event
/// </summary>
protected virtual void OnDragStarted()
{
DragStarted?.Invoke(this, EventArgs.Empty);
}
/// <summary>
/// Invokes the <see cref="DragEnded" /> event
/// </summary>
protected virtual void OnDragEnded()
{
DragEnded?.Invoke(this, EventArgs.Empty);
}
#endregion
}
}
@@ -0,0 +1,14 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Shared.Controls.EnumComboBox">
<ComboBox x:Name="EnumComboBox" HorizontalAlignment="Stretch">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding [0]}" />
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</UserControl>
@@ -0,0 +1,115 @@
using System;
using System.Collections.ObjectModel;
using System.Linq;
using Artemis.Core;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Data;
using Avalonia.LogicalTree;
using Avalonia.Markup.Xaml;
namespace Artemis.UI.Shared.Controls
{
/// <summary>
/// Represents a combobox that can display the values of an enum.
/// </summary>
public class EnumComboBox : UserControl
{
/// <summary>
/// Gets or sets the currently selected value
/// </summary>
public static readonly StyledProperty<object?> ValueProperty =
AvaloniaProperty.Register<EnumComboBox, object?>(nameof(Value), defaultBindingMode: BindingMode.TwoWay, notifying: ValueChanged);
private readonly ObservableCollection<(Enum, string)> _currentValues = new();
private ComboBox? _enumComboBox;
/// <summary>
/// Creates a new instance of the <see cref="EnumComboBox" /> class.
/// </summary>
public EnumComboBox()
{
InitializeComponent();
}
/// <summary>
/// Gets or sets the currently selected value
/// </summary>
public object? Value
{
get => GetValue(ValueProperty);
set => SetValue(ValueProperty, value);
}
private static void ValueChanged(IAvaloniaObject sender, bool before)
{
if (sender is EnumComboBox enumCombo && !before)
{
enumCombo.UpdateValues();
enumCombo.UpdateSelection();
}
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
private void OnSelectionChanged(object? sender, SelectionChangedEventArgs e)
{
if (_enumComboBox == null || _enumComboBox.SelectedIndex == -1)
return;
(Enum enumValue, _) = _currentValues[_enumComboBox.SelectedIndex];
if (!Equals(Value, enumValue))
Value = enumValue;
}
private void UpdateValues()
{
Type? newType = Value?.GetType();
if (_enumComboBox == null || _currentValues.Any() || newType is not {IsEnum: true})
return;
foreach ((Enum, string) valueDesc in EnumUtilities.GetAllValuesAndDescriptions(newType))
_currentValues.Add(valueDesc);
}
private void UpdateSelection()
{
if (_enumComboBox == null || Value is not Enum)
return;
(Enum, string) value = _currentValues.FirstOrDefault(v => v.Item1.Equals(Value));
if (!Equals(value.Item1, _enumComboBox.SelectedItem))
_enumComboBox.SelectedItem = value;
}
#region Overrides of TemplatedControl
/// <inheritdoc />
protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e)
{
_enumComboBox = this.Get<ComboBox>("EnumComboBox");
_enumComboBox.Items = _currentValues;
UpdateValues();
UpdateSelection();
_enumComboBox.SelectionChanged += OnSelectionChanged;
base.OnAttachedToLogicalTree(e);
}
/// <inheritdoc />
protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e)
{
if (_enumComboBox != null)
_enumComboBox.SelectionChanged -= OnSelectionChanged;
base.OnDetachedFromLogicalTree(e);
}
#endregion
}
}
@@ -0,0 +1,67 @@
using System;
using System.ComponentModel;
using Avalonia.Controls;
using FluentAvalonia.Core;
using FluentAvalonia.UI.Controls;
using FluentAvalonia.UI.Controls.Primitives;
namespace Artemis.UI.Shared.Controls.Flyouts;
/// <summary>
/// Defines a flyout that hosts a data model picker.
/// </summary>
public sealed class DataModelPickerFlyout : PickerFlyoutBase
{
private DataModelPicker.DataModelPicker? _picker;
/// <summary>
/// Gets the data model picker that the flyout hosts.
/// </summary>
public DataModelPicker.DataModelPicker DataModelPicker => _picker ??= new DataModelPicker.DataModelPicker();
/// <summary>
/// Raised when the Confirmed button is tapped indicating the new Color should be applied
/// </summary>
public event TypedEventHandler<DataModelPickerFlyout, object>? Confirmed;
/// <summary>
/// Raised when the Dismiss button is tapped, indicating the new color should not be applied
/// </summary>
public event TypedEventHandler<DataModelPickerFlyout, object>? Dismissed;
/// <inheritdoc />
protected override Control CreatePresenter()
{
_picker ??= new DataModelPicker.DataModelPicker();
PickerFlyoutPresenter presenter = new() {Content = DataModelPicker};
presenter.Confirmed += OnFlyoutConfirmed;
presenter.Dismissed += OnFlyoutDismissed;
return presenter;
}
/// <inheritdoc />
protected override void OnConfirmed()
{
Confirmed?.Invoke(this, EventArgs.Empty);
Hide();
}
/// <inheritdoc />
protected override void OnOpening(CancelEventArgs args)
{
base.OnOpening(args);
(Popup.Child as PickerFlyoutPresenter)?.Classes.Set(":acceptdismiss", true);
}
private void OnFlyoutDismissed(PickerFlyoutPresenter sender, object args)
{
Dismissed?.Invoke(this, EventArgs.Empty);
Hide();
}
private void OnFlyoutConfirmed(PickerFlyoutPresenter sender, object args)
{
OnConfirmed();
}
}
@@ -0,0 +1,24 @@
using Avalonia.Controls;
namespace Artemis.UI.Shared.Controls.Flyouts;
/// <summary>
/// Defines a flyout that hosts a gradient picker.
/// </summary>
public sealed class GradientPickerFlyout : Flyout
{
private GradientPicker.GradientPicker? _picker;
/// <summary>
/// Gets the gradient picker that this flyout hosts.
/// </summary>
public GradientPicker.GradientPicker GradientPicker => _picker ??= new GradientPicker.GradientPicker();
/// <inheritdoc />
protected override Control CreatePresenter()
{
_picker ??= new GradientPicker.GradientPicker();
FlyoutPresenter presenter = new() {Content = GradientPicker};
return presenter;
}
}
@@ -1,45 +0,0 @@
<UserControl x:Class="Artemis.UI.Shared.GradientPicker"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:shared="clr-namespace:Artemis.UI.Shared"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<UserControl.Resources>
<ResourceDictionary>
<shared:ColorGradientToGradientStopsConverter x:Key="ColorGradientToGradientStopsConverter" />
<VisualBrush x:Key="Checkerboard" TileMode="Tile" Stretch="Uniform" ViewportUnits="Absolute" Viewport="0,0,10,10">
<VisualBrush.Visual>
<Grid Width="10" Height="10">
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Rectangle Grid.Row="0" Grid.Column="0" Fill="Gray" />
<Rectangle Grid.Row="0" Grid.Column="1" Fill="White" />
<Rectangle Grid.Row="1" Grid.Column="0" Fill="White" />
<Rectangle Grid.Row="1" Grid.Column="1" Fill="Gray" />
</Grid>
</VisualBrush.Visual>
</VisualBrush>
</ResourceDictionary>
</UserControl.Resources>
<Border Height="15"
BorderBrush="{DynamicResource NormalBorderBrush}"
BorderThickness="1"
HorizontalAlignment="Stretch"
Background="{StaticResource Checkerboard}">
<Rectangle Stroke="{DynamicResource NormalBorderBrush}" Cursor="Hand" MouseUp="UIElement_OnMouseUp">
<Rectangle.Fill>
<LinearGradientBrush x:Name="GradientPreview" EndPoint="1,0" StartPoint="0,0" />
</Rectangle.Fill>
</Rectangle>
</Border>
</UserControl>
@@ -1,171 +0,0 @@
using System;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Windows;
using System.Windows.Input;
using System.Windows.Media;
using Artemis.Core;
using Artemis.UI.Shared.Properties;
using Artemis.UI.Shared.Services;
using Stylet;
namespace Artemis.UI.Shared
{
/// <summary>
/// Interaction logic for GradientPicker.xaml
/// </summary>
public partial class GradientPicker : INotifyPropertyChanged
{
private static IColorPickerService? _colorPickerService;
private bool _inCallback;
private ColorGradientToGradientStopsConverter _gradientConverter;
/// <summary>
/// Creates a new instance of the <see cref="GradientPicker" /> class
/// </summary>
public GradientPicker()
{
_gradientConverter = new ColorGradientToGradientStopsConverter();
InitializeComponent();
Unloaded += OnUnloaded;
}
/// <summary>
/// Gets or sets the color gradient
/// </summary>
public ColorGradient ColorGradient
{
get => (ColorGradient) GetValue(ColorGradientProperty);
set => SetValue(ColorGradientProperty, value);
}
/// <summary>
/// Gets or sets the dialog host in which to show the gradient dialog
/// </summary>
public string DialogHost
{
get => (string) GetValue(DialogHostProperty);
set => SetValue(DialogHostProperty, value);
}
/// <summary>
/// Used by the gradient picker to load saved gradients, do not touch or it'll just throw an exception
/// </summary>
internal static IColorPickerService ColorPickerService
{
set
{
if (_colorPickerService != null)
throw new AccessViolationException("This is not for you to touch");
_colorPickerService = value;
}
}
/// <summary>
/// Occurs when the dialog has opened
/// </summary>
public event EventHandler? DialogOpened;
/// <summary>
/// Occurs when the dialog has closed
/// </summary>
public event EventHandler? DialogClosed;
/// <summary>
/// Invokes the <see cref="PropertyChanged" /> event
/// </summary>
[NotifyPropertyChangedInvocator]
protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
/// <summary>
/// Invokes the <see cref="DialogOpened" /> event
/// </summary>
protected virtual void OnDialogOpened()
{
DialogOpened?.Invoke(this, EventArgs.Empty);
}
/// <summary>
/// Invokes the <see cref="DialogClosed" /> event
/// </summary>
protected virtual void OnDialogClosed()
{
DialogClosed?.Invoke(this, EventArgs.Empty);
}
private static void ColorGradientPropertyChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
GradientPicker gradientPicker = (GradientPicker) d;
if (gradientPicker._inCallback)
return;
gradientPicker._inCallback = true;
if (e.OldValue is ColorGradient oldGradient)
oldGradient.CollectionChanged -= gradientPicker.GradientChanged;
if (e.NewValue is ColorGradient newGradient)
newGradient.CollectionChanged += gradientPicker.GradientChanged;
gradientPicker.UpdateGradientStops();
gradientPicker.OnPropertyChanged(nameof(ColorGradient));
gradientPicker._inCallback = false;
}
private void GradientChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
Dispatcher.Invoke(UpdateGradientStops);
}
private void UpdateGradientStops()
{
GradientPreview.GradientStops = (GradientStopCollection) _gradientConverter.Convert(ColorGradient, null!, null!, null!);
}
private void UIElement_OnMouseUp(object sender, MouseButtonEventArgs e)
{
if (_colorPickerService == null)
return;
Execute.OnUIThread(async () =>
{
OnDialogOpened();
await _colorPickerService.ShowGradientPicker(ColorGradient, DialogHost);
OnDialogClosed();
});
}
private void OnUnloaded(object sender, RoutedEventArgs e)
{
ColorGradient.CollectionChanged -= GradientChanged;
}
/// <inheritdoc />
public event PropertyChangedEventHandler? PropertyChanged;
#region Static WPF fields
/// <summary>
/// Gets or sets the color gradient
/// </summary>
public static readonly DependencyProperty ColorGradientProperty = DependencyProperty.Register(nameof(ColorGradient), typeof(ColorGradient), typeof(GradientPicker),
new FrameworkPropertyMetadata(default(ColorGradient), FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, ColorGradientPropertyChangedCallback));
/// <summary>
/// Gets or sets the dialog host in which to show the gradient dialog
/// </summary>
public static readonly DependencyProperty DialogHostProperty = DependencyProperty.Register(nameof(DialogHost), typeof(string), typeof(GradientPicker),
new FrameworkPropertyMetadata(default(string)));
/// <summary>
/// Occurs when the color gradient has changed
/// </summary>
public static readonly RoutedEvent ColorGradientChangedEvent =
EventManager.RegisterRoutedEvent(nameof(ColorGradient), RoutingStrategy.Bubble, typeof(RoutedPropertyChangedEventHandler<ColorGradient>), typeof(GradientPicker));
#endregion
}
}
@@ -0,0 +1,325 @@
using System;
using System.Collections.Specialized;
using System.Linq;
using System.Windows.Input;
using Artemis.Core;
using Artemis.UI.Shared.Providers;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Data;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Media;
using FluentAvalonia.UI.Controls;
using FluentAvalonia.UI.Media;
using ReactiveUI;
using Button = Avalonia.Controls.Button;
namespace Artemis.UI.Shared.Controls.GradientPicker;
/// <summary>
/// Represents a gradient picker that can be used to edit a gradient.
/// </summary>
public class GradientPicker : TemplatedControl
{
/// <summary>
/// Gets or sets the color gradient.
/// </summary>
public static readonly StyledProperty<ColorGradient> ColorGradientProperty =
AvaloniaProperty.Register<GradientPicker, ColorGradient>(nameof(ColorGradient), notifying: ColorGradientChanged, defaultValue: ColorGradient.GetUnicornBarf());
/// <summary>
/// Gets or sets the currently selected color stop.
/// </summary>
public static readonly StyledProperty<ColorGradientStop?> SelectedColorStopProperty =
AvaloniaProperty.Register<GradientPicker, ColorGradientStop?>(nameof(SelectedColorStop), defaultBindingMode: BindingMode.TwoWay);
/// <summary>
/// Gets or sets a boolean indicating whether the gradient picker should be in compact mode or not.
/// </summary>
public static readonly StyledProperty<bool> IsCompactProperty =
AvaloniaProperty.Register<GradientPicker, bool>(nameof(IsCompact), defaultBindingMode: BindingMode.TwoWay);
/// <summary>
/// Gets or sets a storage provider to use for storing and loading gradients.
/// </summary>
public static readonly StyledProperty<IColorGradientStorageProvider?> StorageProviderProperty =
AvaloniaProperty.Register<GradientPicker, IColorGradientStorageProvider?>(nameof(StorageProvider), notifying: StorageProviderChanged);
/// <summary>
/// Gets the linear gradient brush representing the color gradient.
/// </summary>
public static readonly DirectProperty<GradientPicker, LinearGradientBrush> LinearGradientBrushProperty =
AvaloniaProperty.RegisterDirect<GradientPicker, LinearGradientBrush>(nameof(LinearGradientBrush), g => g.LinearGradientBrush);
/// <summary>
/// Gets the command to execute when deleting stops.
/// </summary>
public static readonly DirectProperty<GradientPicker, ICommand> DeleteStopProperty =
AvaloniaProperty.RegisterDirect<GradientPicker, ICommand>(nameof(DeleteStop), g => g.DeleteStop);
private readonly ICommand _deleteStop;
private bool _shiftDown;
private Button? _flipStops;
private Border? _gradient;
private Button? _rotateStops;
private Button? _spreadStops;
private Button? _toggleSeamless;
private Button? _randomize;
private ColorGradient? _lastColorGradient;
private ColorPicker? _colorPicker;
public GradientPicker()
{
_deleteStop = ReactiveCommand.Create<ColorGradientStop>(s =>
{
if (ColorGradient.Count <= 2)
return;
int index = ColorGradient.IndexOf(s);
ColorGradient.Remove(s);
if (index > ColorGradient.Count - 1)
index--;
SelectedColorStop = ColorGradient.ElementAtOrDefault(index);
});
}
/// <summary>
/// Gets or sets the color gradient.
/// </summary>
public ColorGradient ColorGradient
{
get => GetValue(ColorGradientProperty);
set => SetValue(ColorGradientProperty, value);
}
/// <summary>
/// Gets or sets the currently selected color stop.
/// </summary>
public ColorGradientStop? SelectedColorStop
{
get => GetValue(SelectedColorStopProperty);
set
{
if (_colorPicker != null && SelectedColorStop != null)
_colorPicker.PreviousColor = new Color2(SelectedColorStop.Color.Red, SelectedColorStop.Color.Green, SelectedColorStop.Color.Blue, SelectedColorStop.Color.Alpha);
SetValue(SelectedColorStopProperty, value);
}
}
/// <summary>
/// Gets or sets a boolean indicating whether the gradient picker should be in compact mode or not.
/// </summary>
public bool IsCompact
{
get => GetValue(IsCompactProperty);
set => SetValue(IsCompactProperty, value);
}
/// <summary>
/// Gets or sets a storage provider to use for storing and loading gradients.
/// </summary>
public IColorGradientStorageProvider? StorageProvider
{
get => GetValue(StorageProviderProperty);
set => SetValue(StorageProviderProperty, value);
}
/// <summary>
/// Gets the linear gradient brush representing the color gradient.
/// </summary>
public LinearGradientBrush LinearGradientBrush { get; } = new();
/// <summary>
/// Gets the command to execute when deleting stops.
/// </summary>
public ICommand DeleteStop
{
get => _deleteStop;
private init => SetAndRaise(DeleteStopProperty, ref _deleteStop, value);
}
/// <inheritdoc />
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
if (_gradient != null)
_gradient.PointerPressed -= GradientOnPointerPressed;
if (_spreadStops != null)
_spreadStops.Click -= SpreadStopsOnClick;
if (_toggleSeamless != null)
_toggleSeamless.Click -= ToggleSeamlessOnClick;
if (_flipStops != null)
_flipStops.Click -= FlipStopsOnClick;
if (_rotateStops != null)
_rotateStops.Click -= RotateStopsOnClick;
if (_randomize != null)
_randomize.Click -= RandomizeOnClick;
_colorPicker = e.NameScope.Find<ColorPicker>("ColorPicker");
_gradient = e.NameScope.Find<Border>("Gradient");
_spreadStops = e.NameScope.Find<Button>("SpreadStops");
_toggleSeamless = e.NameScope.Find<Button>("ToggleSeamless");
_flipStops = e.NameScope.Find<Button>("FlipStops");
_rotateStops = e.NameScope.Find<Button>("RotateStops");
_randomize = e.NameScope.Find<Button>("Randomize");
if (_gradient != null)
_gradient.PointerPressed += GradientOnPointerPressed;
if (_spreadStops != null)
_spreadStops.Click += SpreadStopsOnClick;
if (_toggleSeamless != null)
_toggleSeamless.Click += ToggleSeamlessOnClick;
if (_flipStops != null)
_flipStops.Click += FlipStopsOnClick;
if (_rotateStops != null)
_rotateStops.Click += RotateStopsOnClick;
if (_randomize != null)
_randomize.Click += RandomizeOnClick;
base.OnApplyTemplate(e);
}
/// <inheritdoc />
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
Subscribe();
KeyUp += OnKeyUp;
KeyDown += OnKeyDown;
}
/// <inheritdoc />
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
{
Unsubscribe();
KeyUp -= OnKeyUp;
KeyDown -= OnKeyDown;
_shiftDown = false;
}
private static void ColorGradientChanged(IAvaloniaObject sender, bool before)
{
(sender as GradientPicker)?.Subscribe();
}
private static void StorageProviderChanged(IAvaloniaObject sender, bool before)
{
}
private void Subscribe()
{
Unsubscribe();
ColorGradient.CollectionChanged += ColorGradientOnCollectionChanged;
ColorGradient.StopChanged += ColorGradientOnStopChanged;
SelectedColorStop = ColorGradient.FirstOrDefault();
UpdateGradient();
_lastColorGradient = ColorGradient;
}
private void Unsubscribe()
{
if (_lastColorGradient == null)
return;
_lastColorGradient.CollectionChanged -= ColorGradientOnCollectionChanged;
_lastColorGradient.StopChanged -= ColorGradientOnStopChanged;
_lastColorGradient = null;
}
private void ColorGradientOnCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
UpdateGradient();
}
private void ColorGradientOnStopChanged(object? sender, EventArgs e)
{
UpdateGradient();
}
private void UpdateGradient()
{
// Update the display gradient
GradientStops collection = new();
foreach (ColorGradientStop c in ColorGradient.OrderBy(s => s.Position))
collection.Add(new GradientStop(Color.FromArgb(c.Color.Alpha, c.Color.Red, c.Color.Green, c.Color.Blue), c.Position));
LinearGradientBrush.GradientStops = collection;
}
private void GradientOnPointerPressed(object? sender, PointerPressedEventArgs e)
{
if (_gradient == null)
return;
float position = (float) (e.GetPosition(_gradient).X / _gradient.Bounds.Width);
ColorGradientStop newStop = new(ColorGradient.GetColor(position), position);
ColorGradient.Add(newStop);
SelectedColorStop = newStop;
}
private void OnKeyDown(object? sender, KeyEventArgs e)
{
if (e.Key is Key.LeftShift or Key.RightShift)
_shiftDown = true;
}
private void OnKeyUp(object? sender, KeyEventArgs e)
{
if (e.Key is Key.LeftShift or Key.RightShift)
_shiftDown = false;
if (e.Key != Key.Delete || SelectedColorStop == null || ColorGradient.Count <= 2)
return;
int index = ColorGradient.IndexOf(SelectedColorStop);
ColorGradient.Remove(SelectedColorStop);
if (index > ColorGradient.Count - 1)
index--;
SelectedColorStop = ColorGradient.ElementAtOrDefault(index);
e.Handled = true;
}
private void SpreadStopsOnClick(object? sender, RoutedEventArgs e)
{
ColorGradient.SpreadStops();
}
private void ToggleSeamlessOnClick(object? sender, RoutedEventArgs e)
{
if (SelectedColorStop == null || ColorGradient.Count < 2)
return;
ColorGradient.ToggleSeamless();
}
private void FlipStopsOnClick(object? sender, RoutedEventArgs e)
{
if (SelectedColorStop == null || ColorGradient.Count < 2)
return;
ColorGradient.FlipStops();
}
private void RotateStopsOnClick(object? sender, RoutedEventArgs e)
{
if (SelectedColorStop == null || ColorGradient.Count < 2)
return;
ColorGradient.RotateStops(_shiftDown);
}
private void RandomizeOnClick(object? sender, RoutedEventArgs e)
{
ColorGradient.Randomize(6);
SelectedColorStop = ColorGradient.First();
}
}
@@ -0,0 +1,212 @@
using System;
using System.Collections.Specialized;
using System.Linq;
using Artemis.Core;
using Artemis.UI.Shared.Controls.Flyouts;
using Artemis.UI.Shared.Providers;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Data;
using Avalonia.Interactivity;
using Avalonia.Media;
using FluentAvalonia.Core;
using Button = FluentAvalonia.UI.Controls.Button;
namespace Artemis.UI.Shared.Controls.GradientPicker;
/// <summary>
/// Represents a gradient picker box that can be used to edit a gradient
/// </summary>
public class GradientPickerButton : TemplatedControl
{
/// <summary>
/// Gets or sets the color gradient.
/// </summary>
public static readonly StyledProperty<ColorGradient?> ColorGradientProperty =
AvaloniaProperty.Register<GradientPickerButton, ColorGradient?>(nameof(ColorGradient), notifying: ColorGradientChanged, defaultValue: ColorGradient.GetUnicornBarf());
/// <summary>
/// Gets or sets a boolean indicating whether the gradient picker should be in compact mode or not.
/// </summary>
public static readonly StyledProperty<bool> IsCompactProperty =
AvaloniaProperty.Register<GradientPickerButton, bool>(nameof(IsCompact), defaultBindingMode: BindingMode.TwoWay);
/// <summary>
/// Gets or sets a storage provider to use for storing and loading gradients.
/// </summary>
public static readonly StyledProperty<IColorGradientStorageProvider?> StorageProviderProperty =
AvaloniaProperty.Register<GradientPickerButton, IColorGradientStorageProvider?>(nameof(StorageProvider));
/// <summary>
/// Gets the linear gradient brush representing the color gradient.
/// </summary>
public static readonly DirectProperty<GradientPickerButton, LinearGradientBrush> LinearGradientBrushProperty =
AvaloniaProperty.RegisterDirect<GradientPickerButton, LinearGradientBrush>(nameof(LinearGradientBrush), g => g.LinearGradientBrush);
private ColorGradient? _lastColorGradient;
private Button? _button;
private GradientPickerFlyout? _flyout;
private bool _flyoutActive;
/// <summary>
/// Gets or sets the color gradient.
/// </summary>
public ColorGradient? ColorGradient
{
get => GetValue(ColorGradientProperty);
set => SetValue(ColorGradientProperty, value);
}
/// <summary>
/// Gets or sets a boolean indicating whether the gradient picker should be in compact mode or not.
/// </summary>
public bool IsCompact
{
get => GetValue(IsCompactProperty);
set => SetValue(IsCompactProperty, value);
}
/// <summary>
/// Gets or sets a storage provider to use for storing and loading gradients.
/// </summary>
public IColorGradientStorageProvider? StorageProvider
{
get => GetValue(StorageProviderProperty);
set => SetValue(StorageProviderProperty, value);
}
/// <summary>
/// Gets the linear gradient brush representing the color gradient.
/// </summary>
public LinearGradientBrush LinearGradientBrush { get; } = new();
/// <summary>
/// Raised when the flyout opens.
/// </summary>
public event TypedEventHandler<GradientPickerButton, EventArgs>? FlyoutOpened;
/// <summary>
/// Raised when the flyout closes.
/// </summary>
public event TypedEventHandler<GradientPickerButton, EventArgs>? FlyoutClosed;
#region Overrides of TemplatedControl
/// <inheritdoc />
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
if (_button != null)
_button.Click -= OnButtonClick;
base.OnApplyTemplate(e);
_button = e.NameScope.Find<Button>("MainButton");
if (_button != null)
_button.Click += OnButtonClick;
}
#endregion
private static void ColorGradientChanged(IAvaloniaObject sender, bool before)
{
(sender as GradientPickerButton)?.Subscribe();
}
private void Subscribe()
{
Unsubscribe();
if (ColorGradient != null)
{
ColorGradient.CollectionChanged += ColorGradientOnCollectionChanged;
ColorGradient.StopChanged += ColorGradientOnStopChanged;
}
UpdateGradient();
_lastColorGradient = ColorGradient;
}
private void Unsubscribe()
{
if (_lastColorGradient == null)
return;
_lastColorGradient.CollectionChanged -= ColorGradientOnCollectionChanged;
_lastColorGradient.StopChanged -= ColorGradientOnStopChanged;
_lastColorGradient = null;
}
private void ColorGradientOnCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
UpdateGradient();
}
private void ColorGradientOnStopChanged(object? sender, EventArgs e)
{
UpdateGradient();
}
private void OnButtonClick(object? sender, RoutedEventArgs e)
{
if (_flyout == null || ColorGradient == null)
return;
// Logic here is taken from Fluent Avalonia's ColorPicker which also reuses the same control since it's large
_flyout.GradientPicker.ColorGradient = ColorGradient;
_flyout.GradientPicker.IsCompact = IsCompact;
_flyout.GradientPicker.StorageProvider = StorageProvider;
_flyout.ShowAt(this);
_flyoutActive = true;
FlyoutOpened?.Invoke(this, EventArgs.Empty);
}
private void OnFlyoutClosed(object? sender, EventArgs e)
{
if (_flyoutActive)
{
FlyoutClosed?.Invoke(this, EventArgs.Empty);
_flyoutActive = false;
}
}
private void UpdateGradient()
{
// Update the display gradient
GradientStops collection = new();
if (ColorGradient != null)
{
foreach (ColorGradientStop c in ColorGradient.OrderBy(s => s.Position))
collection.Add(new GradientStop(Color.FromArgb(c.Color.Alpha, c.Color.Red, c.Color.Green, c.Color.Blue), c.Position));
}
LinearGradientBrush.GradientStops = collection;
}
#region Overrides of Visual
/// <inheritdoc />
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
Subscribe();
if (_flyout == null)
{
_flyout = new GradientPickerFlyout();
_flyout.FlyoutPresenterClasses.Add("gradient-picker-presenter");
}
_flyout.Closed += OnFlyoutClosed;
}
/// <inheritdoc />
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
{
Unsubscribe();
if (_flyout != null)
_flyout.Closed -= OnFlyoutClosed;
}
#endregion
}
@@ -0,0 +1,199 @@
using System;
using Artemis.Core;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Primitives;
using Avalonia.Input;
namespace Artemis.UI.Shared.Controls.GradientPicker;
public class GradientPickerColorStop : TemplatedControl
{
private static ColorGradientStop? _draggingStop;
private static IPointer? _dragPointer;
/// <summary>
/// Gets or sets the gradient picker.
/// </summary>
public static readonly StyledProperty<GradientPicker?> GradientPickerProperty =
AvaloniaProperty.Register<GradientPickerColorStop, GradientPicker?>(nameof(GradientPicker), notifying: Notifying);
private static void Notifying(IAvaloniaObject sender, bool before)
{
if (sender is not GradientPickerColorStop self)
return;
if (before && self.GradientPicker != null)
self.GradientPicker.PropertyChanged -= self.GradientPickerOnPropertyChanged;
else if (self.GradientPicker != null)
self.GradientPicker.PropertyChanged += self.GradientPickerOnPropertyChanged;
self.IsSelected = ReferenceEquals(self.GradientPicker?.SelectedColorStop, self.ColorStop);
}
/// <summary>
/// Gets or sets the color stop.
/// </summary>
public static readonly StyledProperty<ColorGradientStop> ColorStopProperty =
AvaloniaProperty.Register<GradientPickerColorStop, ColorGradientStop>(nameof(ColorStop));
/// <summary>
/// Gets or sets the position reference to use when positioning and dragging this color stop.
/// <para>If <see langword="null" /> then dragging is not enabled.</para>
/// </summary>
public static readonly StyledProperty<IControl?> PositionReferenceProperty =
AvaloniaProperty.Register<GradientPickerColorStop, IControl?>(nameof(PositionReference));
/// <summary>
/// Gets the linear gradient brush representing the color gradient.
/// </summary>
public static readonly DirectProperty<GradientPickerColorStop, bool> IsSelectedProperty =
AvaloniaProperty.RegisterDirect<GradientPickerColorStop, bool>(nameof(IsSelected), g => g.IsSelected);
private bool _isSelected;
private double _dragOffset;
/// <summary>
/// Gets or sets the gradient picker.
/// </summary>
public GradientPicker? GradientPicker
{
get => GetValue(GradientPickerProperty);
set => SetValue(GradientPickerProperty, value);
}
/// <summary>
/// Gets or sets the color stop.
/// </summary>
public ColorGradientStop ColorStop
{
get => GetValue(ColorStopProperty);
set => SetValue(ColorStopProperty, value);
}
/// <summary>
/// Gets or sets the position reference to use when positioning and dragging this color stop.
/// <para>If <see langword="null" /> then dragging is not enabled.</para>
/// </summary>
public IControl? PositionReference
{
get => GetValue(PositionReferenceProperty);
set => SetValue(PositionReferenceProperty, value);
}
/// <summary>
/// Gets the linear gradient brush representing the color gradient.
/// </summary>
public bool IsSelected
{
get => _isSelected;
private set
{
SetAndRaise(IsSelectedProperty, ref _isSelected, value);
if (IsSelected)
PseudoClasses.Add(":selected");
else
PseudoClasses.Remove(":selected");
}
}
private void GradientPickerOnPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
{
if (GradientPicker != null && e.Property == GradientPicker.SelectedColorStopProperty)
{
IsSelected = GradientPicker.SelectedColorStop == ColorStop;
}
}
private void OnPointerPressed(object? sender, PointerPressedEventArgs e)
{
if (!e.GetCurrentPoint(this).Properties.IsLeftButtonPressed || PositionReference == null)
return;
_dragOffset = e.GetCurrentPoint(PositionReference).Position.X - GetPixelPosition();
e.Pointer.Capture(this);
// Store these in case the control is being recreated due to an array resort
// it's a bit ugly but it gives us a way to pick up dragging again with the new control
_dragPointer = e.Pointer;
_draggingStop = ColorStop;
e.Handled = true;
if (GradientPicker != null)
GradientPicker.SelectedColorStop = ColorStop;
}
private void OnPointerMoved(object? sender, PointerEventArgs e)
{
if (!e.GetCurrentPoint(this).Properties.IsLeftButtonPressed || !ReferenceEquals(e.Pointer.Captured, this) || PositionReference == null)
{
if (_draggingStop != ColorStop)
return;
_dragOffset = e.GetCurrentPoint(PositionReference).Position.X - GetPixelPosition();
}
double position = e.GetCurrentPoint(PositionReference).Position.X - _dragOffset;
ColorStop.Position = MathF.Round((float) Math.Clamp(position / PositionReference.Bounds.Width, 0, 1), 3, MidpointRounding.AwayFromZero);
e.Handled = true;
}
private void OnPointerReleased(object? sender, PointerReleasedEventArgs e)
{
if (e.InitialPressMouseButton != MouseButton.Left)
return;
e.Pointer.Capture(null);
e.Handled = true;
_draggingStop = null;
}
private double GetPixelPosition()
{
if (PositionReference == null)
return 0;
return PositionReference.Bounds.Width * ColorStop.Position;
}
#region Overrides of Visual
/// <inheritdoc />
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
if (GradientPicker != null)
GradientPicker.PropertyChanged += GradientPickerOnPropertyChanged;
if (PositionReference != null)
{
PointerPressed += OnPointerPressed;
PointerMoved += OnPointerMoved;
PointerReleased += OnPointerReleased;
// If this stop was previously being dragged, carry on dragging again
// This can happen if the control was recreated due to an array sort
if (_draggingStop == ColorStop && _dragPointer != null)
{
_dragPointer.Capture(this);
IsSelected = true;
}
}
base.OnAttachedToVisualTree(e);
}
/// <inheritdoc />
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
{
if (GradientPicker != null)
GradientPicker.PropertyChanged -= GradientPickerOnPropertyChanged;
PointerPressed -= OnPointerPressed;
PointerMoved -= OnPointerMoved;
PointerReleased -= OnPointerReleased;
base.OnDetachedFromVisualTree(e);
}
#endregion
}
@@ -0,0 +1,26 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:controls="clr-namespace:Artemis.UI.Shared.Controls"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Shared.Controls.HotkeyBox">
<UserControl.Styles>
<Style Selector="TextBox#DisplayTextBox:focus:not(TextBox:empty)">
<Setter Property="InnerRightContent">
<Template>
<Button Classes="textBoxClearButton"
Click="Button_OnClick" />
</Template>
</Setter>
</Style>
</UserControl.Styles>
<controls:NoInputTextBox x:Name="DisplayTextBox"
Watermark="{Binding $parent.Watermark}"
UseFloatingWatermark="{Binding $parent.UseFloatingWatermark}"
Classes="clearButton"
IsReadOnly="True"
HorizontalAlignment="Stretch" />
</UserControl>
@@ -0,0 +1,138 @@
using System;
using System.Linq;
using Artemis.Core;
using Artemis.Core.Services;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Data;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Markup.Xaml;
using Humanizer;
using Material.Icons;
namespace Artemis.UI.Shared.Controls
{
/// <summary>
/// Represents a control that can be used to display or edit <see cref="Core.Hotkey"/> instances.
/// </summary>
public class HotkeyBox : UserControl
{
private readonly TextBox _displayTextBox;
/// <summary>
/// Creates a new instance of the <see cref="HotkeyBox"/> class
/// </summary>
public HotkeyBox()
{
InitializeComponent();
_displayTextBox = this.Find<TextBox>("DisplayTextBox");
_displayTextBox.KeyDown += DisplayTextBoxOnKeyDown;
_displayTextBox.KeyUp += DisplayTextBoxOnKeyUp;
UpdateDisplayTextBox();
}
private static void HotkeyChanging(IAvaloniaObject sender, bool before)
{
((HotkeyBox) sender).UpdateDisplayTextBox();
}
private void DisplayTextBoxOnKeyDown(object? sender, KeyEventArgs e)
{
if (e.Key >= Key.LeftShift && e.Key <= Key.RightAlt)
return;
Hotkey ??= new Hotkey();
Hotkey.Key = (KeyboardKey?) e.Key;
Hotkey.Modifiers = (KeyboardModifierKey?) e.KeyModifiers;
UpdateDisplayTextBox();
e.Handled = true;
}
private void DisplayTextBoxOnKeyUp(object? sender, KeyEventArgs e)
{
if (e.KeyModifiers == KeyModifiers.None)
FocusManager.Instance?.Focus(null);
e.Handled = true;
}
private void UpdateDisplayTextBox()
{
string? display = null;
if (Hotkey?.Modifiers != null)
display = string.Join("+", Enum.GetValues<KeyboardModifierKey>().Skip(1).Where(m => Hotkey.Modifiers.Value.HasFlag(m)).Select(v => v.Humanize()));
if (Hotkey?.Key != null)
display = string.IsNullOrEmpty(display) ? Hotkey.Key.ToString() : $"{display}+{Hotkey.Key}";
_displayTextBox.Text = display;
_displayTextBox.CaretIndex = _displayTextBox.Text?.Length ?? 0;
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
private void Button_OnClick(object? sender, RoutedEventArgs e)
{
Hotkey = null;
FocusManager.Instance?.Focus(null);
UpdateDisplayTextBox();
}
#region Properties
/// <summary>
/// Gets or sets the currently displayed icon as either a <see cref="MaterialIconKind" /> or an <see cref="Uri" />
/// pointing to an SVG
/// </summary>
public static readonly StyledProperty<Hotkey?> HotkeyProperty =
AvaloniaProperty.Register<HotkeyBox, Hotkey?>(nameof(Hotkey), defaultBindingMode: BindingMode.TwoWay, notifying: HotkeyChanging);
/// <summary>
/// Gets or sets the watermark of the hotkey box when it is empty.
/// </summary>
public static readonly StyledProperty<string?> WatermarkProperty =
AvaloniaProperty.Register<HotkeyBox, string?>(nameof(Watermark));
/// <summary>
/// Gets or sets a boolean indicating whether the watermark should float above the hotkey box when it is not empty.
/// </summary>
public static readonly StyledProperty<bool> UseFloatingWatermarkProperty =
AvaloniaProperty.Register<HotkeyBox, bool>(nameof(UseFloatingWatermark));
/// <summary>
/// Gets or sets the currently displayed icon as either a <see cref="MaterialIconKind" /> or an <see cref="Uri" />
/// pointing to an SVG
/// </summary>
public Hotkey? Hotkey
{
get => GetValue(HotkeyProperty);
set => SetValue(HotkeyProperty, value);
}
/// <summary>
/// Gets or sets the watermark of the hotkey box when it is empty.
/// </summary>
public string? Watermark
{
get => GetValue(WatermarkProperty);
set => SetValue(WatermarkProperty, value);
}
/// <summary>
/// Gets or sets a boolean indicating whether the watermark should float above the hotkey box when it is not empty.
/// </summary>
public bool UseFloatingWatermark
{
get => GetValue(UseFloatingWatermarkProperty);
set => SetValue(UseFloatingWatermarkProperty, value);
}
#endregion
}
}
@@ -1,32 +0,0 @@
using System.Windows;
using System.Windows.Controls.Primitives;
namespace Artemis.UI.Shared
{
/// <summary>
/// Represents a toggle button that can be locked using a property
/// </summary>
public class LockableToggleButton : ToggleButton
{
/// <summary>
/// Gets or sets a boolean indicating whether the toggle button is locked
/// </summary>
public static readonly DependencyProperty IsLockedProperty =
DependencyProperty.Register("IsLocked", typeof(bool), typeof(LockableToggleButton), new UIPropertyMetadata(false));
/// <summary>
/// Gets or sets a boolean indicating whether the toggle button is locked
/// </summary>
public bool IsLocked
{
get => (bool) GetValue(IsLockedProperty);
set => SetValue(IsLockedProperty, value);
}
/// <inheritdoc />
protected override void OnToggle()
{
if (!IsLocked) base.OnToggle();
}
}
}
@@ -0,0 +1,24 @@
using System;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Styling;
namespace Artemis.UI.Shared.Controls
{
internal class NoInputTextBox : TextBox, IStyleable
{
/// <inheritdoc />
protected override void OnKeyDown(KeyEventArgs e)
{
// Don't call the base method on purpose
}
/// <inheritdoc />
protected override void OnKeyUp(KeyEventArgs e)
{
// Don't call the base method on purpose
}
Type IStyleable.StyleKey => typeof(TextBox);
}
}
@@ -0,0 +1,7 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Shared.Controls.ProfileConfigurationIcon">
</UserControl>
@@ -0,0 +1,125 @@
using System;
using System.IO;
using Artemis.Core;
using Avalonia;
using Avalonia.Controls;
using Avalonia.LogicalTree;
using Avalonia.Markup.Xaml;
using Avalonia.Media.Imaging;
using Avalonia.Svg.Skia;
using Material.Icons;
using Material.Icons.Avalonia;
namespace Artemis.UI.Shared.Controls
{
/// <summary>
/// Represents a control that can display the icon of a specific <see cref="ProfileConfiguration"/>.
/// </summary>
public class ProfileConfigurationIcon : UserControl
{
/// <summary>
/// Creates a new instance of the <see cref="ProfileConfigurationIcon" /> class.
/// </summary>
public ProfileConfigurationIcon()
{
InitializeComponent();
DetachedFromLogicalTree += OnDetachedFromLogicalTree;
PropertyChanged += OnPropertyChanged;
}
private void Update()
{
if (ConfigurationIcon == null)
return;
try
{
if (ConfigurationIcon.IconType == ProfileConfigurationIconType.MaterialIcon)
{
Content = Enum.TryParse(ConfigurationIcon.IconName, true, out MaterialIconKind parsedIcon)
? new MaterialIcon {Kind = parsedIcon!}
: new MaterialIcon {Kind = MaterialIconKind.QuestionMark};
return;
}
Stream? stream = ConfigurationIcon.GetIconStream();
if (stream == null)
{
Content = new MaterialIcon {Kind = MaterialIconKind.QuestionMark};
return;
}
if (ConfigurationIcon.IconType == ProfileConfigurationIconType.SvgImage)
{
SvgSource source = new();
source.Load(stream);
Content = new Image {Source = new SvgImage {Source = source}};
}
else if (ConfigurationIcon.IconType == ProfileConfigurationIconType.BitmapImage)
{
Content = new Image {Source = new Bitmap(ConfigurationIcon.GetIconStream())};
}
else
{
Content = new MaterialIcon {Kind = MaterialIconKind.QuestionMark};
}
}
catch
{
Content = new MaterialIcon {Kind = MaterialIconKind.QuestionMark};
}
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
private void OnDetachedFromLogicalTree(object? sender, LogicalTreeAttachmentEventArgs e)
{
if (ConfigurationIcon != null)
ConfigurationIcon.IconUpdated -= ConfigurationIconOnIconUpdated;
if (Content is Image image && image.Source is IDisposable disposable)
disposable.Dispose();
}
private void OnPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
{
if (e.Property != ConfigurationIconProperty)
return;
if (e.OldValue is Core.ProfileConfigurationIcon oldIcon)
oldIcon.IconUpdated -= ConfigurationIconOnIconUpdated;
if (e.NewValue is Core.ProfileConfigurationIcon newIcon)
newIcon.IconUpdated += ConfigurationIconOnIconUpdated;
Update();
}
private void ConfigurationIconOnIconUpdated(object? sender, EventArgs e)
{
Update();
}
#region Properties
/// <summary>
/// Gets or sets the <see cref="Core.ProfileConfigurationIcon" /> to display
/// </summary>
public static readonly StyledProperty<Core.ProfileConfigurationIcon?> ConfigurationIconProperty =
AvaloniaProperty.Register<ProfileConfigurationIcon, Core.ProfileConfigurationIcon?>(nameof(ConfigurationIcon));
/// <summary>
/// Gets or sets the <see cref="Core.ProfileConfigurationIcon" /> to display
/// </summary>
public Core.ProfileConfigurationIcon? ConfigurationIcon
{
get => GetValue(ConfigurationIconProperty);
set => SetValue(ConfigurationIconProperty, value);
}
#endregion
}
}
@@ -1,65 +0,0 @@
<UserControl x:Class="Artemis.UI.Shared.ProfileConfigurationIcon"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:core="clr-namespace:Artemis.Core;assembly=Artemis.Core"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
xmlns:svgc="http://sharpvectors.codeplex.com/svgc/"
xmlns:shared="clr-namespace:Artemis.UI.Shared"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<UserControl.Resources>
<shared:StreamToBitmapImageConverter x:Key="StreamToBitmapImageConverter" />
<shared:StreamToSvgImageConverter x:Key="StreamToSvgImageConverter" />
</UserControl.Resources>
<ContentControl>
<ContentControl.Style>
<Style TargetType="ContentControl">
<Style.Triggers>
<DataTrigger Binding="{Binding ConfigurationIcon.IconType, RelativeSource={RelativeSource AncestorType={x:Type UserControl}}}"
Value="{x:Static core:ProfileConfigurationIconType.MaterialIcon}">
<Setter Property="ContentTemplate">
<Setter.Value>
<DataTemplate>
<materialDesign:PackIcon Kind="{Binding ConfigurationIcon.IconName, RelativeSource={RelativeSource AncestorType={x:Type UserControl}}}"
Width="Auto"
Height="Auto" />
</DataTemplate>
</Setter.Value>
</Setter>
</DataTrigger>
<DataTrigger Binding="{Binding ConfigurationIcon.IconType, RelativeSource={RelativeSource AncestorType={x:Type UserControl}}}"
Value="{x:Static core:ProfileConfigurationIconType.BitmapImage}">
<Setter Property="ContentTemplate">
<Setter.Value>
<DataTemplate>
<Image
Source="{Binding ConfigurationIcon.FileIcon, Converter={StaticResource StreamToBitmapImageConverter}, RelativeSource={RelativeSource AncestorType={x:Type UserControl}}}"
RenderOptions.BitmapScalingMode="HighQuality"
Width="Auto"
Height="Auto" />
</DataTemplate>
</Setter.Value>
</Setter>
</DataTrigger>
<DataTrigger Binding="{Binding ConfigurationIcon.IconType, RelativeSource={RelativeSource AncestorType={x:Type UserControl}}}"
Value="{x:Static core:ProfileConfigurationIconType.SvgImage}">
<Setter Property="ContentTemplate">
<Setter.Value>
<DataTemplate>
<Image
Source="{Binding ConfigurationIcon.FileIcon, Converter={StaticResource StreamToSvgImageConverter}, RelativeSource={RelativeSource AncestorType={x:Type UserControl}}}"
RenderOptions.BitmapScalingMode="HighQuality"
Width="Auto"
Height="Auto" />
</DataTemplate>
</Setter.Value>
</Setter>
</DataTrigger>
</Style.Triggers>
</Style>
</ContentControl.Style>
</ContentControl>
</UserControl>
@@ -1,35 +0,0 @@
using System.Windows;
using System.Windows.Controls;
namespace Artemis.UI.Shared
{
/// <summary>
/// Interaction logic for ProfileConfigurationIcon.xaml
/// </summary>
public partial class ProfileConfigurationIcon : UserControl
{
/// <summary>
/// Gets or sets the <see cref="Core.ProfileConfigurationIcon" /> to display
/// </summary>
public static readonly DependencyProperty ConfigurationIconProperty =
DependencyProperty.Register(nameof(ConfigurationIcon), typeof(Core.ProfileConfigurationIcon), typeof(ProfileConfigurationIcon));
/// <summary>
/// Creates a new instance of the <see cref="ProfileConfigurationIcon" /> class
/// </summary>
public ProfileConfigurationIcon()
{
InitializeComponent();
}
/// <summary>
/// Gets or sets the <see cref="Core.ProfileConfigurationIcon" /> to display
/// </summary>
public Core.ProfileConfigurationIcon ConfigurationIcon
{
get => (Core.ProfileConfigurationIcon) GetValue(ConfigurationIconProperty);
set => SetValue(ConfigurationIconProperty, value);
}
}
}
@@ -0,0 +1,275 @@
using System;
using Artemis.Core;
using Artemis.UI.Shared.Events;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Media;
namespace Artemis.UI.Shared.Controls;
/// <summary>
/// Visualizes an <see cref="ArtemisDevice" /> with optional per-LED colors
/// </summary>
public class SelectionRectangle : Control
{
/// <summary>
/// Defines the <see cref="Background" /> property.
/// </summary>
public static readonly StyledProperty<IBrush> BackgroundProperty =
AvaloniaProperty.Register<SelectionRectangle, IBrush>(nameof(Background), new SolidColorBrush(Colors.CadetBlue, 0.25));
/// <summary>
/// Defines the <see cref="BorderBrush" /> property.
/// </summary>
public static readonly StyledProperty<IBrush> BorderBrushProperty =
AvaloniaProperty.Register<SelectionRectangle, IBrush>(nameof(BorderBrush), new SolidColorBrush(Colors.CadetBlue));
/// <summary>
/// Defines the <see cref="BorderThickness" /> property.
/// </summary>
public static readonly StyledProperty<double> BorderThicknessProperty =
AvaloniaProperty.Register<SelectionRectangle, double>(nameof(BorderThickness), 1);
/// <summary>
/// Defines the <see cref="BorderRadius" /> property.
/// </summary>
public static readonly StyledProperty<double> BorderRadiusProperty =
AvaloniaProperty.Register<SelectionRectangle, double>(nameof(BorderRadius));
/// <summary>
/// Defines the <see cref="InputElement" /> property.
/// </summary>
public static readonly StyledProperty<IControl?> InputElementProperty =
AvaloniaProperty.Register<SelectionRectangle, IControl?>(nameof(InputElement), notifying: OnInputElementChanged);
/// <summary>
/// Defines the <see cref="ZoomRatio" /> property.
/// </summary>
public static readonly StyledProperty<double> ZoomRatioProperty =
AvaloniaProperty.Register<SelectionRectangle, double>(nameof(ZoomRatio), 1);
/// <summary>
/// Defines the read-only <see cref="IsSelecting" /> property.
/// </summary>
public static readonly DirectProperty<SelectionRectangle, bool> IsSelectingProperty = AvaloniaProperty.RegisterDirect<SelectionRectangle, bool>(nameof(IsSelecting), o => o.IsSelecting);
private Rect? _absoluteRect;
private Point _absoluteStartPosition;
private Rect? _displayRect;
private bool _isSelecting;
private IControl? _oldInputElement;
private Point _startPosition;
private Point _lastPosition;
/// <inheritdoc />
public SelectionRectangle()
{
AffectsRender<TextBlock>(BackgroundProperty, BorderBrushProperty, BorderThicknessProperty);
IsHitTestVisible = false;
}
/// <summary>
/// Gets or sets a brush used to paint the control's background.
/// </summary>
public IBrush Background
{
get => GetValue(BackgroundProperty);
set => SetValue(BackgroundProperty, value);
}
/// <summary>
/// Gets or sets a brush used to paint the control's border
/// </summary>
public IBrush BorderBrush
{
get => GetValue(BorderBrushProperty);
set => SetValue(BorderBrushProperty, value);
}
/// <summary>
/// Gets or sets the width of the control's border
/// </summary>
public double BorderThickness
{
get => GetValue(BorderThicknessProperty);
set => SetValue(BorderThicknessProperty, value);
}
/// <summary>
/// Gets or sets the radius of the control's border
/// </summary>
public double BorderRadius
{
get => GetValue(BorderRadiusProperty);
set => SetValue(BorderRadiusProperty, value);
}
/// <summary>
/// Gets or sets the element that captures input for the selection rectangle.
/// </summary>
public IControl? InputElement
{
get => GetValue(InputElementProperty);
set => SetValue(InputElementProperty, value);
}
/// <summary>
/// Gets or sets the zoom ratio to counteract when drawing
/// </summary>
public double ZoomRatio
{
get => GetValue(ZoomRatioProperty);
set => SetValue(ZoomRatioProperty, value);
}
/// <summary>
/// Gets a boolean indicating whether the selection rectangle is currently performing a selection.
/// </summary>
public bool IsSelecting
{
get => _isSelecting;
private set => SetAndRaise(IsSelectingProperty, ref _isSelecting, value);
}
/// <summary>
/// Occurs when the selection rect is being updated, indicating the user is dragging.
/// </summary>
public event EventHandler<SelectionRectangleEventArgs>? SelectionUpdated;
/// <summary>
/// Occurs when the selection has finished, indicating the user stopped dragging.
/// </summary>
public event EventHandler<SelectionRectangleEventArgs>? SelectionFinished;
/// <summary>
/// Invokes the <see cref="SelectionUpdated" /> event
/// </summary>
/// <param name="e"></param>
protected virtual void OnSelectionUpdated(SelectionRectangleEventArgs e)
{
SelectionUpdated?.Invoke(this, e);
}
/// <summary>
/// Invokes the <see cref="SelectionFinished" /> event
/// </summary>
/// <param name="e"></param>
protected virtual void OnSelectionFinished(SelectionRectangleEventArgs e)
{
SelectionFinished?.Invoke(this, e);
}
private static void OnInputElementChanged(IAvaloniaObject sender, bool before)
{
((SelectionRectangle) sender).SubscribeToInputElement();
}
private void ParentOnPointerMoved(object? sender, PointerEventArgs e)
{
// Point moved seems to trigger when the element under the mouse changes?
// I'm not sure why this is needed but this check makes sure the position really hasn't changed.
Point position = e.GetCurrentPoint(null).Position;
if (position == _lastPosition)
return;
_lastPosition = position;
if (!e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
return;
// Capture the pointer and initialize dragging the first time it moves
if (!ReferenceEquals(e.Pointer.Captured, this))
{
e.Pointer.Capture(this);
_startPosition = e.GetPosition(Parent);
_absoluteStartPosition = e.GetPosition(VisualRoot);
_displayRect = null;
}
Point currentPosition = e.GetPosition(Parent);
Point absoluteCurrentPosition = e.GetPosition(VisualRoot);
_displayRect = new Rect(
new Point(Math.Min(_startPosition.X, currentPosition.X), Math.Min(_startPosition.Y, currentPosition.Y)),
new Point(Math.Max(_startPosition.X, currentPosition.X), Math.Max(_startPosition.Y, currentPosition.Y))
);
_absoluteRect = new Rect(
new Point(Math.Min(_absoluteStartPosition.X, absoluteCurrentPosition.X), Math.Min(_absoluteStartPosition.Y, absoluteCurrentPosition.Y)),
new Point(Math.Max(_absoluteStartPosition.X, absoluteCurrentPosition.X), Math.Max(_absoluteStartPosition.Y, absoluteCurrentPosition.Y))
);
OnSelectionUpdated(new SelectionRectangleEventArgs(_displayRect.Value, _absoluteRect.Value, e.KeyModifiers));
InvalidateVisual();
IsSelecting = true;
}
private void ParentOnPointerReleased(object? sender, PointerReleasedEventArgs e)
{
if (!ReferenceEquals(e.Pointer.Captured, this))
return;
e.Pointer.Capture(null);
if (_displayRect != null && _absoluteRect != null)
{
OnSelectionFinished(new SelectionRectangleEventArgs(_displayRect.Value, _absoluteRect.Value, e.KeyModifiers));
e.Handled = true;
}
_displayRect = null;
InvalidateVisual();
IsSelecting = false;
}
private void SubscribeToInputElement()
{
if (_oldInputElement != null)
{
_oldInputElement.PointerMoved -= ParentOnPointerMoved;
_oldInputElement.PointerReleased -= ParentOnPointerReleased;
}
_oldInputElement = InputElement;
if (InputElement != null)
{
InputElement.PointerMoved += ParentOnPointerMoved;
InputElement.PointerReleased += ParentOnPointerReleased;
}
}
#region Overrides of Visual
/// <inheritdoc />
public override void Render(DrawingContext drawingContext)
{
if (_displayRect != null)
drawingContext.DrawRectangle(Background, new Pen(BorderBrush, BorderThickness / ZoomRatio), _displayRect.Value, BorderRadius / ZoomRatio, BorderRadius / ZoomRatio);
}
/// <inheritdoc />
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
SubscribeToInputElement();
base.OnAttachedToVisualTree(e);
}
/// <inheritdoc />
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
{
if (_oldInputElement != null)
{
_oldInputElement.PointerMoved -= ParentOnPointerMoved;
_oldInputElement.PointerReleased -= ParentOnPointerReleased;
_oldInputElement = null;
}
base.OnDetachedFromVisualTree(e);
}
#endregion
}
@@ -1,46 +1,41 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Windows.Data;
using System.Windows.Media;
using Artemis.Core;
using Avalonia.Data.Converters;
using Avalonia.Media;
using SkiaSharp;
namespace Artemis.UI.Shared
namespace Artemis.UI.Shared.Converters;
/// <summary>
/// Converts <see cref="T:Artemis.Core.ColorGradient" /> into a <see cref="T:Avalonia.Media.GradientStops" />.
/// </summary>
public class ColorGradientToGradientStopsConverter : IValueConverter
{
/// <inheritdoc />
/// <summary>
/// Converts <see cref="T:Artemis.Core.Models.Profile.ColorGradient" /> into a
/// <see cref="T:System.Windows.Media.GradientStopCollection" />.
/// </summary>
[ValueConversion(typeof(ColorGradient), typeof(GradientStopCollection))]
public class ColorGradientToGradientStopsConverter : IValueConverter
public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
/// <inheritdoc />
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
ColorGradient? colorGradient = value as ColorGradient;
GradientStopCollection collection = new();
if (colorGradient == null)
return collection;
foreach (ColorGradientStop c in colorGradient.OrderBy(s => s.Position))
collection.Add(new GradientStop(Color.FromArgb(c.Color.Alpha, c.Color.Red, c.Color.Green, c.Color.Blue), c.Position));
ColorGradient? colorGradient = value as ColorGradient;
GradientStops collection = new();
if (colorGradient == null)
return collection;
}
/// <inheritdoc />
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
GradientStopCollection? collection = value as GradientStopCollection;
ColorGradient colorGradients = new();
if (collection == null)
return colorGradients;
foreach (ColorGradientStop c in colorGradient.OrderBy(s => s.Position))
collection.Add(new GradientStop(Color.FromArgb(c.Color.Alpha, c.Color.Red, c.Color.Green, c.Color.Blue), c.Position));
return collection;
}
foreach (GradientStop c in collection.OrderBy(s => s.Offset))
colorGradients.Add(new ColorGradientStop(new SKColor(c.Color.R, c.Color.G, c.Color.B, c.Color.A), (float) c.Offset));
/// <inheritdoc />
public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
{
GradientStops? collection = value as GradientStops;
ColorGradient colorGradients = new();
if (collection == null)
return colorGradients;
}
foreach (GradientStop c in collection.OrderBy(s => s.Offset))
colorGradients.Add(new ColorGradientStop(new SKColor(c.Color.R, c.Color.G, c.Color.B, c.Color.A), (float) c.Offset));
return colorGradients;
}
}
@@ -1,30 +1,38 @@
using System;
using System.Globalization;
using System.Windows.Data;
using System.Windows.Media;
using Avalonia.Data.Converters;
using Avalonia.Media;
using FluentAvalonia.UI.Media;
using SkiaSharp;
namespace Artemis.UI.Shared
namespace Artemis.UI.Shared.Converters
{
/// <inheritdoc />
/// <summary>
/// Converts <see cref="T:SkiaSharp.SKColor" /> into a <see cref="T:System.Windows.Media.Color" />.
/// Converts <see cref="T:Avalonia.Media.Color" /> into <see cref="T:SkiaSharp.SKColor" />.
/// </summary>
[ValueConversion(typeof(Color), typeof(SKColor))]
public class SKColorToColorConverter : IValueConverter
public class ColorToSKColorConverter : IValueConverter
{
/// <inheritdoc />
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
SKColor skColor = (SKColor) value;
return Color.FromArgb(skColor.Alpha, skColor.Red, skColor.Green, skColor.Blue);
if (value is Color avaloniaColor)
return new SKColor(avaloniaColor.R, avaloniaColor.G, avaloniaColor.B, avaloniaColor.A);
if (value is Color2 fluentAvaloniaColor)
return new SKColor(fluentAvaloniaColor.R, fluentAvaloniaColor.G, fluentAvaloniaColor.B, fluentAvaloniaColor.A);
return SKColor.Empty;
}
/// <inheritdoc />
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
{
Color color = (Color) value;
return new SKColor(color.R, color.G, color.B, color.A);
Color result = new(0, 0, 0, 0);
if (value is SKColor skColor)
result = new Color(skColor.Alpha, skColor.Red, skColor.Green, skColor.Blue);
if (targetType == typeof(Color2))
return (Color2) result;
return result;
}
}
}
@@ -1,29 +0,0 @@
using System;
using System.Globalization;
using System.Windows.Data;
using System.Windows.Media;
namespace Artemis.UI.Shared
{
/// <inheritdoc />
/// <summary>
/// Converts <see cref="T:System.Windows.Media.Color" /> into a <see cref="T:System.Windows.Media.Color" /> with full
/// opacity.
/// </summary>
[ValueConversion(typeof(Color), typeof(string))]
public class ColorToSolidColorConverter : IValueConverter
{
/// <inheritdoc />
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
Color color = (Color) value;
return Color.FromRgb(color.R, color.G, color.B);
}
/// <inheritdoc />
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}
@@ -1,41 +0,0 @@
using System;
using System.Globalization;
using System.Windows.Data;
using System.Windows.Media;
namespace Artemis.UI.Shared
{
/// <inheritdoc />
/// <summary>
/// Converts <see cref="T:System.Windows.Media.Color" /> into <see cref="T:System.String" />.
/// </summary>
[ValueConversion(typeof(Color), typeof(string))]
public class ColorToStringConverter : IValueConverter
{
/// <inheritdoc />
public object? Convert(object? value, Type targetType, object parameter, CultureInfo culture)
{
return value?.ToString()?.ToUpper();
}
/// <inheritdoc />
public object? ConvertBack(object? value, Type targetType, object parameter, CultureInfo culture)
{
try
{
if (string.IsNullOrWhiteSpace(value as string))
return default(Color);
object? color = ColorConverter.ConvertFromString((string) value!);
if (color is Color c)
return c;
return default(Color);
}
catch (FormatException)
{
return default(Color);
}
}
}
}
@@ -0,0 +1,25 @@
using System;
using System.Globalization;
using Avalonia.Data;
using Avalonia.Data.Converters;
namespace Artemis.UI.Shared.Converters
{
/// <summary>
/// Converts an enum into a boolean.
/// </summary>
public class EnumToBooleanConverter : IValueConverter
{
/// <inheritdoc />
public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
return Equals(value, parameter);
}
/// <inheritdoc />
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
{
return value?.Equals(true) == true ? parameter : BindingOperations.DoNothing;
}
}
}
@@ -1,39 +0,0 @@
using System;
using System.Globalization;
using System.Windows;
using System.Windows.Data;
namespace Artemis.UI.Shared
{
/// <summary>
/// Converts <see cref="int"/> to <see cref="Visibility"/>
/// </summary>
public class IntToVisibilityConverter : IValueConverter
{
/// <inheritdoc />
public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
Parameters direction;
if (parameter == null)
direction = Parameters.Normal;
else
direction = (Parameters) Enum.Parse(typeof(Parameters), (string) parameter);
return direction == Parameters.Normal
? value is > 1 ? Visibility.Visible : Visibility.Collapsed
: value is > 1 ? Visibility.Collapsed : Visibility.Visible;
}
/// <inheritdoc />
public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
private enum Parameters
{
Normal,
Inverted
}
}
}
@@ -1,42 +0,0 @@
using System;
using System.Globalization;
using System.Windows;
using System.Windows.Data;
namespace Artemis.UI.Shared
{
/// <summary>
/// Converts <see langword="null"/> to <see cref="Visibility"/>
/// </summary>
public class NullToVisibilityConverter : IValueConverter
{
/// <inheritdoc />
public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
Parameters direction;
if (parameter == null)
direction = Parameters.Normal;
else
direction = (Parameters) Enum.Parse(typeof(Parameters), (string) parameter);
if (value is string stringValue && string.IsNullOrWhiteSpace(stringValue))
value = null;
if (direction == Parameters.Normal)
return value == null ? Visibility.Collapsed : Visibility.Visible;
return value == null ? Visibility.Visible : Visibility.Collapsed;
}
/// <inheritdoc />
public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
private enum Parameters
{
Normal,
Inverted
}
}
}
@@ -0,0 +1,34 @@
using System;
using System.Globalization;
using Avalonia.Controls;
using Avalonia.Data.Converters;
namespace Artemis.UI.Shared.Converters;
/// <summary>
/// Converts the width in percentage to a real number based on the width of the given parent
/// </summary>
public class ParentWidthPercentageConverter : IValueConverter
{
#region Implementation of IValueConverter
/// <inheritdoc />
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (parameter is not IControl parent || value is not double percentage)
return value;
return parent.Width / 100.0 * percentage;
}
/// <inheritdoc />
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (parameter is not IControl parent || value is not double real)
return value;
return real / parent.Width * 100.0;
}
#endregion
}
@@ -0,0 +1,29 @@
using System;
using System.Globalization;
using Avalonia.Data.Converters;
using Avalonia.Media;
using SkiaSharp;
namespace Artemis.UI.Shared.Converters;
/// <summary>
/// Converts <see cref="T:SkiaSharp.SKColor" /> into <see cref="T:Avalonia.Media.SolidColorBrush" />.
/// </summary>
public class SKColorToBrushConverter : IValueConverter
{
/// <inheritdoc />
public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (value is SKColor skColor)
return new SolidColorBrush(new Color(skColor.Alpha, skColor.Red, skColor.Green, skColor.Blue));
return new SolidColorBrush(Colors.Transparent);
}
/// <inheritdoc />
public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (value is SolidColorBrush brush)
return new SKColor(brush.Color.R, brush.Color.G, brush.Color.B, brush.Color.A);
return SKColor.Empty;
}
}
@@ -0,0 +1,38 @@
using System;
using System.Globalization;
using Avalonia.Data.Converters;
using Avalonia.Media;
using FluentAvalonia.UI.Media;
using SkiaSharp;
namespace Artemis.UI.Shared.Converters
{
/// <summary>
/// Converts <see cref="T:SkiaSharp.SKColor" /> into <see cref="T:Avalonia.Media.Color" />.
/// </summary>
public class SKColorToColorConverter : IValueConverter
{
/// <inheritdoc />
public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
Color result = new(0, 0, 0, 0);
if (value is SKColor skColor)
result = new Color(skColor.Alpha, skColor.Red, skColor.Green, skColor.Blue);
if (targetType == typeof(Color2))
return (Color2) result;
return result;
}
/// <inheritdoc />
public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (value is Color avaloniaColor)
return new SKColor(avaloniaColor.R, avaloniaColor.G, avaloniaColor.B, avaloniaColor.A);
if (value is Color2 fluentAvaloniaColor)
return new SKColor(fluentAvaloniaColor.R, fluentAvaloniaColor.G, fluentAvaloniaColor.B, fluentAvaloniaColor.A);
return SKColor.Empty;
}
}
}
@@ -1,31 +1,28 @@
using System;
using System.Globalization;
using System.Windows.Data;
using System.Windows.Media;
using Avalonia.Data.Converters;
using SkiaSharp;
namespace Artemis.UI.Shared
namespace Artemis.UI.Shared.Converters;
/// <inheritdoc />
/// <summary>
/// Converts <see cref="SKColor" />into <see cref="T:System.String" />.
/// </summary>
public class SKColorToStringConverter : IValueConverter
{
/// <inheritdoc />
/// <summary>
/// Converts <see cref="SKColor" />into <see cref="T:System.String" />.
/// </summary>
[ValueConversion(typeof(Color), typeof(string))]
public class SKColorToStringConverter : IValueConverter
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
/// <inheritdoc />
public object? Convert(object? value, Type targetType, object parameter, CultureInfo culture)
{
return value?.ToString();
}
return value?.ToString();
}
/// <inheritdoc />
public object ConvertBack(object? value, Type targetType, object parameter, CultureInfo culture)
{
if (string.IsNullOrWhiteSpace(value as string))
return SKColor.Empty;
/// <inheritdoc />
public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (string.IsNullOrWhiteSpace(value as string))
return SKColor.Empty;
return SKColor.TryParse((string) value!, out SKColor color) ? color : SKColor.Empty;
}
return SKColor.TryParse((string) value!, out SKColor color) ? color : SKColor.Empty;
}
}
@@ -1,42 +0,0 @@
using System;
using System.Globalization;
using System.IO;
using System.Windows.Data;
using System.Windows.Media.Imaging;
namespace Artemis.UI.Shared
{
/// <inheritdoc />
/// <summary>
/// Converts bitmap file in the form of a <see cref="T:Stream" /> into <see cref="T:BitmapImage" />.
/// </summary>
[ValueConversion(typeof(Stream), typeof(BitmapImage))]
public class StreamToBitmapImageConverter : IValueConverter
{
/// <inheritdoc />
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is not Stream stream)
return Binding.DoNothing;
stream.Position = 0;
BitmapImage selectedBitmap = new();
selectedBitmap.BeginInit();
selectedBitmap.StreamSource = stream;
selectedBitmap.CacheOption = BitmapCacheOption.OnLoad;
selectedBitmap.EndInit();
selectedBitmap.Freeze();
stream.Position = 0;
return selectedBitmap;
}
/// <inheritdoc />
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
return Binding.DoNothing;
}
}
}
@@ -1,48 +0,0 @@
using System;
using System.Globalization;
using System.IO;
using System.Windows.Data;
using System.Windows.Media.Imaging;
using SharpVectors.Converters;
using SharpVectors.Renderers.Wpf;
namespace Artemis.UI.Shared
{
/// <inheritdoc />
/// <summary>
/// Converts SVG file in the form of a <see cref="T:Stream" /> into <see cref="T:BitmapImage" />.
/// </summary>
[ValueConversion(typeof(Stream), typeof(BitmapImage))]
public class StreamToSvgImageConverter : IValueConverter
{
/// <inheritdoc />
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is not Stream stream)
return Binding.DoNothing;
stream.Position = 0;
StreamSvgConverter converter = new(new WpfDrawingSettings());
using MemoryStream imageStream = new();
converter.Convert(stream, imageStream);
BitmapImage selectedBitmap = new();
selectedBitmap.BeginInit();
selectedBitmap.StreamSource = imageStream;
selectedBitmap.CacheOption = BitmapCacheOption.OnLoad;
selectedBitmap.EndInit();
selectedBitmap.Freeze();
stream.Position = 0;
return selectedBitmap;
}
/// <inheritdoc />
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
return Binding.DoNothing;
}
}
}
@@ -1,27 +0,0 @@
using System;
using System.Globalization;
using System.Windows.Data;
using Artemis.Core;
namespace Artemis.UI.Shared
{
/// <summary>
/// Converts <see cref="T:System.String" /> into <see cref="Numeric" />.
/// </summary>
[ValueConversion(typeof(string), typeof(Numeric))]
public class StringToNumericConverter : IValueConverter
{
/// <inheritdoc />
public object? Convert(object? value, Type targetType, object parameter, CultureInfo culture)
{
return value?.ToString();
}
/// <inheritdoc />
public object ConvertBack(object? value, Type targetType, object parameter, CultureInfo culture)
{
Numeric.TryParse(value as string, out Numeric result);
return result;
}
}
}
@@ -1,9 +1,9 @@
using System;
using System.Globalization;
using System.Windows.Data;
using Artemis.Core;
using Avalonia.Data.Converters;
namespace Artemis.UI.Shared
namespace Artemis.UI.Shared.Converters
{
/// <summary>
/// Converts <see cref="T:System.Type" /> into <see cref="T:System.String" />.
@@ -11,7 +11,7 @@ namespace Artemis.UI.Shared
public class TypeToStringConverter : IValueConverter
{
/// <inheritdoc />
public object? Convert(object value, Type targetType, object? parameter, CultureInfo culture)
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
bool humanizeProvided = bool.TryParse(parameter?.ToString(), out bool humanize);
if (value is Type type)
@@ -21,7 +21,7 @@ namespace Artemis.UI.Shared
}
/// <inheritdoc />
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
@@ -0,0 +1,29 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using Avalonia.Controls;
using Avalonia.Data.Converters;
namespace Artemis.UI.Shared.Converters;
/// <summary>
/// Converts the width in percentage to a real number based on the width of the given parent
/// </summary>
public class WidthNormalizedConverter : IMultiValueConverter
{
#region Implementation of IMultiValueConverter
/// <inheritdoc />
public object? Convert(IList<object?> values, Type targetType, object? parameter, CultureInfo culture)
{
object? first = values.FirstOrDefault();
object? second = values.Skip(1).FirstOrDefault();
if (first is float value && second is double totalWidth)
return (totalWidth / 1.0) * value;
return 0.0;
}
#endregion
}
@@ -1,8 +1,8 @@
using System.Diagnostics.CodeAnalysis;
using Artemis.Core.Modules;
using Stylet;
using ReactiveUI;
namespace Artemis.UI.Shared
namespace Artemis.UI.Shared.DataModelVisualization
{
/// <summary>
/// Represents a <see cref="DataModel" /> display view model
@@ -10,8 +10,7 @@ namespace Artemis.UI.Shared
/// <typeparam name="T">The type of the data model</typeparam>
public abstract class DataModelDisplayViewModel<T> : DataModelDisplayViewModel
{
[AllowNull]
private T _displayValue = default!;
[AllowNull] private T _displayValue = default!;
/// <summary>
/// Gets or sets value that the view model must display
@@ -22,7 +21,8 @@ namespace Artemis.UI.Shared
get => _displayValue;
set
{
if (!SetAndNotify(ref _displayValue, value)) return;
if (Equals(value, _displayValue)) return;
RaiseAndSetIfChanged(ref _displayValue, value);
OnDisplayValueUpdated();
}
}
@@ -46,7 +46,7 @@ namespace Artemis.UI.Shared
/// <summary>
/// For internal use only, implement <see cref="DataModelDisplayViewModel{T}" /> instead.
/// </summary>
public abstract class DataModelDisplayViewModel : PropertyChangedBase
public abstract class DataModelDisplayViewModel : ViewModelBase
{
private DataModelPropertyAttribute? _propertyDescription;
@@ -56,7 +56,7 @@ namespace Artemis.UI.Shared
public DataModelPropertyAttribute? PropertyDescription
{
get => _propertyDescription;
internal set => SetAndNotify(ref _propertyDescription, value);
internal set => RaiseAndSetIfChanged(ref _propertyDescription, value);
}
/// <summary>
@@ -1,14 +1,10 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Data;
using System.Windows.Input;
using Artemis.Core.Modules;
using Stylet;
using ReactiveUI;
namespace Artemis.UI.Shared
namespace Artemis.UI.Shared.DataModelVisualization
{
/// <summary>
/// Represents a <see cref="DataModel" /> input view model
@@ -37,7 +33,7 @@ namespace Artemis.UI.Shared
public T InputValue
{
get => _inputValue;
set => SetAndNotify(ref _inputValue, value);
set => this.RaiseAndSetIfChanged(ref _inputValue, value);
}
/// <summary>
@@ -54,9 +50,6 @@ namespace Artemis.UI.Shared
return;
_closed = true;
foreach (BindingExpressionBase sourceUpdatingBinding in BindingOperations.GetSourceUpdatingBindings(View))
sourceUpdatingBinding.UpdateSource();
OnSubmit();
UpdateCallback(InputValue, true);
}
@@ -76,7 +69,7 @@ namespace Artemis.UI.Shared
/// <summary>
/// For internal use only, implement <see cref="DataModelInputViewModel{T}" /> instead.
/// </summary>
public abstract class DataModelInputViewModel : PropertyChangedBase, IViewAware
public abstract class DataModelInputViewModel : ReactiveObject
{
/// <summary>
/// Prevents this type being implemented directly, implement <see cref="DataModelInputViewModel{T}" /> instead.
@@ -117,24 +110,5 @@ namespace Artemis.UI.Shared
protected virtual void OnCancel()
{
}
/// <inheritdoc />
public void AttachView(UIElement view)
{
if (View != null)
throw new InvalidOperationException(string.Format("Tried to attach View {0} to ViewModel {1}, but it already has a view attached", view.GetType().Name, GetType().Name));
View = view;
// After the animation finishes attempt to focus the input field
Task.Run(async () =>
{
await Task.Delay(50);
await Execute.OnUIThreadAsync(() => View.MoveFocus(new TraversalRequest(FocusNavigationDirection.First)));
});
}
/// <inheritdoc />
public UIElement? View { get; set; }
}
}
@@ -2,8 +2,9 @@
using System.Collections.Generic;
using Artemis.Core;
using Artemis.UI.Shared.Services;
using Artemis.UI.Shared.Services.Interfaces;
namespace Artemis.UI.Shared
namespace Artemis.UI.Shared.DataModelVisualization
{
/// <summary>
/// Represents a layer brush registered through
@@ -3,8 +3,10 @@ using System.Linq;
using Artemis.Core;
using Artemis.Core.Modules;
using Artemis.UI.Shared.Services;
using Artemis.UI.Shared.Services.Interfaces;
using ReactiveUI;
namespace Artemis.UI.Shared
namespace Artemis.UI.Shared.DataModelVisualization.Shared
{
/// <summary>
/// Represents a view model that visualizes an event data model property
@@ -23,7 +25,7 @@ namespace Artemis.UI.Shared
public Type? DisplayValueType
{
get => _displayValueType;
set => SetAndNotify(ref _displayValueType, value);
set => this.RaiseAndSetIfChanged(ref _displayValueType, value);
}
/// <inheritdoc />
@@ -1,36 +1,48 @@
using System;
using Artemis.Core;
using Artemis.UI.Shared.Services;
using Artemis.UI.Shared.Services.Interfaces;
using ReactiveUI;
namespace Artemis.UI.Shared
namespace Artemis.UI.Shared.DataModelVisualization.Shared
{
/// <summary>
/// Represents a view model that visualizes a single data model property contained in a
/// <see cref="DataModelListViewModel" />
/// </summary>
public class DataModelListPropertyViewModel : DataModelPropertyViewModel
public class DataModelListItemViewModel : DataModelVisualizationViewModel
{
private int _index;
private Type? _listType;
private object? _displayValue;
private DataModelDisplayViewModel? _displayViewModel;
internal DataModelListPropertyViewModel(Type listType, DataModelDisplayViewModel displayViewModel, string? name) : base(null, null, null)
internal DataModelListItemViewModel(Type listType, DataModelDisplayViewModel displayViewModel, string? name) : base(null, null, null)
{
ListType = listType;
DisplayViewModel = displayViewModel;
}
internal DataModelListPropertyViewModel(Type listType, string? name) : base(null, null, null)
internal DataModelListItemViewModel(Type listType, string? name) : base(null, null, null)
{
ListType = listType;
}
/// <summary>
/// Gets the view model used to display the display value
/// </summary>
public DataModelDisplayViewModel? DisplayViewModel
{
get => _displayViewModel;
internal set => this.RaiseAndSetIfChanged(ref _displayViewModel, value);
}
/// <summary>
/// Gets the index of the element within the list
/// </summary>
public int Index
{
get => _index;
internal set => SetAndNotify(ref _index, value);
internal set => this.RaiseAndSetIfChanged(ref _index, value);
}
/// <summary>
@@ -39,7 +51,16 @@ namespace Artemis.UI.Shared
public Type? ListType
{
get => _listType;
private set => SetAndNotify(ref _listType, value);
private set => this.RaiseAndSetIfChanged(ref _listType, value);
}
/// <summary>
/// Gets the value of the property that is being visualized
/// </summary>
public object? DisplayValue
{
get => _displayValue;
internal set => this.RaiseAndSetIfChanged(ref _displayValue, value);
}
/// <inheritdoc />
@@ -65,7 +86,7 @@ namespace Artemis.UI.Shared
/// <inheritdoc />
public override string ToString()
{
return $"[List item {Index}] {DisplayPath ?? Path} - {DisplayValue}";
return $"[List item {Index}]";
}
}
}
@@ -1,82 +0,0 @@
using System;
using System.Linq;
using Artemis.Core;
using Artemis.UI.Shared.Services;
namespace Artemis.UI.Shared
{
/// <summary>
/// Represents a view model that wraps a regular <see cref="DataModelPropertiesViewModel" /> but contained in
/// a <see cref="DataModelListViewModel" />
/// </summary>
public class DataModelListPropertiesViewModel : DataModelPropertiesViewModel
{
private object? _displayValue;
private int _index;
private Type? _listType;
internal DataModelListPropertiesViewModel(Type listType, string? name) : base(null, null, null)
{
ListType = listType;
}
/// <summary>
/// Gets the index of the element within the list
/// </summary>
public int Index
{
get => _index;
set => SetAndNotify(ref _index, value);
}
/// <summary>
/// Gets the type of elements contained in the list
/// </summary>
public Type? ListType
{
get => _listType;
set => SetAndNotify(ref _listType, value);
}
/// <summary>
/// Gets the value of the property that is being visualized
/// </summary>
public new object? DisplayValue
{
get => _displayValue;
set => SetAndNotify(ref _displayValue, value);
}
/// <summary>
/// Gets the view model that handles displaying the property
/// </summary>
public DataModelVisualizationViewModel? DisplayViewModel => Children.FirstOrDefault();
/// <inheritdoc />
public override string? DisplayPath => null;
/// <inheritdoc />
public override void Update(IDataModelUIService dataModelUIService, DataModelUpdateConfiguration? configuration)
{
PopulateProperties(dataModelUIService, configuration);
if (DisplayViewModel == null)
return;
if (IsVisualizationExpanded && !DisplayViewModel.IsVisualizationExpanded)
DisplayViewModel.IsVisualizationExpanded = IsVisualizationExpanded;
DisplayViewModel.Update(dataModelUIService, null);
}
/// <inheritdoc />
public override object? GetCurrentValue()
{
return DisplayValue;
}
/// <inheritdoc />
public override string ToString()
{
return $"[List item {Index}] {DisplayPath ?? Path}";
}
}
}
@@ -1,12 +1,13 @@
using System;
using System.Collections;
using System.Windows.Documents;
using System.Collections.ObjectModel;
using Artemis.Core;
using Artemis.Core.Modules;
using Artemis.UI.Shared.Services;
using Stylet;
using Artemis.UI.Shared.Services.Interfaces;
using ReactiveUI;
namespace Artemis.UI.Shared
namespace Artemis.UI.Shared.DataModelVisualization.Shared
{
/// <summary>
/// Represents a view model that visualizes a list data model property
@@ -16,14 +17,14 @@ namespace Artemis.UI.Shared
private string _countDisplay;
private Type? _displayValueType;
private IEnumerable? _list;
private BindableCollection<DataModelVisualizationViewModel> _listChildren;
private ObservableCollection<DataModelVisualizationViewModel> _listChildren;
private int _listCount;
internal DataModelListViewModel(DataModel dataModel, DataModelVisualizationViewModel parent, DataModelPath dataModelPath)
: base(dataModel, parent, dataModelPath)
{
_countDisplay = "0 items";
_listChildren = new BindableCollection<DataModelVisualizationViewModel>();
_listChildren = new ObservableCollection<DataModelVisualizationViewModel>();
}
/// <summary>
@@ -32,7 +33,7 @@ namespace Artemis.UI.Shared
public IEnumerable? List
{
get => _list;
private set => SetAndNotify(ref _list, value);
private set => this.RaiseAndSetIfChanged(ref _list, value);
}
/// <summary>
@@ -41,7 +42,7 @@ namespace Artemis.UI.Shared
public int ListCount
{
get => _listCount;
private set => SetAndNotify(ref _listCount, value);
private set => this.RaiseAndSetIfChanged(ref _listCount, value);
}
/// <summary>
@@ -50,7 +51,7 @@ namespace Artemis.UI.Shared
public Type? DisplayValueType
{
get => _displayValueType;
set => SetAndNotify(ref _displayValueType, value);
set => this.RaiseAndSetIfChanged(ref _displayValueType, value);
}
/// <summary>
@@ -59,16 +60,16 @@ namespace Artemis.UI.Shared
public string CountDisplay
{
get => _countDisplay;
set => SetAndNotify(ref _countDisplay, value);
set => this.RaiseAndSetIfChanged(ref _countDisplay, value);
}
/// <summary>
/// Gets a list of child view models that visualize the elements in the list
/// </summary>
public BindableCollection<DataModelVisualizationViewModel> ListChildren
public ObservableCollection<DataModelVisualizationViewModel> ListChildren
{
get => _listChildren;
private set => SetAndNotify(ref _listChildren, value);
private set => this.RaiseAndSetIfChanged(ref _listChildren, value);
}
/// <inheritdoc />
@@ -97,22 +98,15 @@ namespace Artemis.UI.Shared
ListChildren.Add(child);
}
else
{
child = ListChildren[index];
}
if (child is DataModelListPropertiesViewModel dataModelListClassViewModel)
{
dataModelListClassViewModel.DisplayValue = item;
dataModelListClassViewModel.Index = index;
}
else if (child is DataModelListPropertyViewModel dataModelListPropertyViewModel)
if (child is DataModelListItemViewModel dataModelListPropertyViewModel)
{
dataModelListPropertyViewModel.DisplayValue = item;
dataModelListPropertyViewModel.Index = index;
dataModelListPropertyViewModel.Update(dataModelUIService, configuration);
}
child.Update(dataModelUIService, configuration);
index++;
}
@@ -134,16 +128,10 @@ namespace Artemis.UI.Shared
{
// If a display VM was found, prefer to use that in any case
DataModelDisplayViewModel? typeViewModel = dataModelUIService.GetDataModelDisplayViewModel(listType, PropertyDescription);
if (typeViewModel != null)
return new DataModelListPropertyViewModel(listType, typeViewModel, name);
// For primitives, create a property view model, it may be null that is fine
if (listType.IsPrimitive || listType.IsEnum || listType == typeof(string))
return new DataModelListPropertyViewModel(listType, name);
// For other value types create a child view model
if (listType.IsClass || listType.IsStruct())
return new DataModelListPropertiesViewModel(listType, name);
return null;
return typeViewModel != null
? new DataModelListItemViewModel(listType, typeViewModel, name)
: new DataModelListItemViewModel(listType, name);
}
}
}
@@ -2,8 +2,10 @@
using Artemis.Core;
using Artemis.Core.Modules;
using Artemis.UI.Shared.Services;
using Artemis.UI.Shared.Services.Interfaces;
using ReactiveUI;
namespace Artemis.UI.Shared
namespace Artemis.UI.Shared.DataModelVisualization.Shared
{
/// <summary>
/// Represents a view model that visualizes a class (POCO) data model property containing child properties
@@ -24,7 +26,7 @@ namespace Artemis.UI.Shared
public Type? DisplayValueType
{
get => _displayValueType;
private set => SetAndNotify(ref _displayValueType, value);
private set => this.RaiseAndSetIfChanged(ref _displayValueType, value);
}
/// <summary>
@@ -33,7 +35,7 @@ namespace Artemis.UI.Shared
public object? DisplayValue
{
get => _displayValue;
private set => SetAndNotify(ref _displayValue, value);
private set => this.RaiseAndSetIfChanged(ref _displayValue, value);
}
/// <inheritdoc />
@@ -2,8 +2,10 @@
using Artemis.Core;
using Artemis.Core.Modules;
using Artemis.UI.Shared.Services;
using Artemis.UI.Shared.Services.Interfaces;
using ReactiveUI;
namespace Artemis.UI.Shared
namespace Artemis.UI.Shared.DataModelVisualization.Shared
{
/// <summary>
/// Represents a view model that visualizes a single data model property contained in a
@@ -26,7 +28,7 @@ namespace Artemis.UI.Shared
public object? DisplayValue
{
get => _displayValue;
internal set => SetAndNotify(ref _displayValue, value);
internal set => this.RaiseAndSetIfChanged(ref _displayValue, value);
}
/// <summary>
@@ -35,7 +37,7 @@ namespace Artemis.UI.Shared
public Type? DisplayValueType
{
get => _displayValueType;
protected set => SetAndNotify(ref _displayValueType, value);
protected set => this.RaiseAndSetIfChanged(ref _displayValueType, value);
}
/// <summary>
@@ -44,7 +46,7 @@ namespace Artemis.UI.Shared
public DataModelDisplayViewModel? DisplayViewModel
{
get => _displayViewModel;
internal set => SetAndNotify(ref _displayViewModel, value);
internal set => this.RaiseAndSetIfChanged(ref _displayViewModel, value);
}
/// <inheritdoc />
@@ -1,4 +1,4 @@
namespace Artemis.UI.Shared
namespace Artemis.UI.Shared.DataModelVisualization.Shared
{
/// <summary>
/// Represents a configuration to use while updating a <see cref="DataModelVisualizationViewModel" />
@@ -6,368 +6,396 @@ using System.Reflection;
using System.Text;
using Artemis.Core;
using Artemis.Core.Modules;
using Artemis.UI.Shared.Services;
using Stylet;
using Artemis.UI.Shared.Services.Interfaces;
using ReactiveUI;
namespace Artemis.UI.Shared
namespace Artemis.UI.Shared.DataModelVisualization.Shared;
/// <summary>
/// Represents a base class for a view model that visualizes a part of the data model
/// </summary>
public abstract class DataModelVisualizationViewModel : ReactiveObject, IDisposable
{
/// <summary>
/// Represents a base class for a view model that visualizes a part of the data model
/// </summary>
public abstract class DataModelVisualizationViewModel : PropertyChangedBase, IDisposable
private const int MaxDepth = 4;
private ObservableCollection<DataModelVisualizationViewModel> _children;
private DataModel? _dataModel;
private bool _isMatchingFilteredTypes;
private bool _isVisualizationExpanded;
private DataModelVisualizationViewModel? _parent;
private bool _populatedStaticChildren;
private DataModelPropertyAttribute? _propertyDescription;
internal DataModelVisualizationViewModel(DataModel? dataModel, DataModelVisualizationViewModel? parent, DataModelPath? dataModelPath)
{
private const int MaxDepth = 4;
private BindableCollection<DataModelVisualizationViewModel> _children;
private DataModel? _dataModel;
private bool _isMatchingFilteredTypes;
private bool _isVisualizationExpanded;
private DataModelVisualizationViewModel? _parent;
private DataModelPropertyAttribute? _propertyDescription;
private bool _populatedStaticChildren;
_dataModel = dataModel;
_children = new ObservableCollection<DataModelVisualizationViewModel>();
_parent = parent;
DataModelPath = dataModelPath;
IsMatchingFilteredTypes = true;
internal DataModelVisualizationViewModel(DataModel? dataModel, DataModelVisualizationViewModel? parent, DataModelPath? dataModelPath)
if (parent == null)
IsRootViewModel = true;
else
PropertyDescription = DataModelPath?.GetPropertyDescription() ?? DataModel?.DataModelDescription;
}
/// <summary>
/// Gets a boolean indicating whether this view model is at the root of the data model
/// </summary>
public bool IsRootViewModel { get; protected set; }
/// <summary>
/// Gets the data model path to the property this view model is visualizing
/// </summary>
public DataModelPath? DataModelPath { get; }
/// <summary>
/// Gets a string representation of the path backing this model
/// </summary>
public string? Path => DataModelPath?.Path;
/// <summary>
/// Gets the property depth of the view model
/// </summary>
public int Depth { get; private set; }
/// <summary>
/// Gets the data model backing this view model
/// </summary>
public DataModel? DataModel
{
get => _dataModel;
protected set => this.RaiseAndSetIfChanged(ref _dataModel, value);
}
/// <summary>
/// Gets the property description of the property this view model is visualizing
/// </summary>
public DataModelPropertyAttribute? PropertyDescription
{
get => _propertyDescription;
protected set => this.RaiseAndSetIfChanged(ref _propertyDescription, value);
}
/// <summary>
/// Gets the parent of this view model
/// </summary>
public DataModelVisualizationViewModel? Parent
{
get => _parent;
protected set => this.RaiseAndSetIfChanged(ref _parent, value);
}
/// <summary>
/// Gets or sets an observable collection containing the children of this view model
/// </summary>
public ObservableCollection<DataModelVisualizationViewModel> Children
{
get => _children;
set => this.RaiseAndSetIfChanged(ref _children, value);
}
/// <summary>
/// Gets a boolean indicating whether the property being visualized matches the types last provided to
/// <see cref="ApplyTypeFilter" />
/// </summary>
public bool IsMatchingFilteredTypes
{
get => _isMatchingFilteredTypes;
private set => this.RaiseAndSetIfChanged(ref _isMatchingFilteredTypes, value);
}
/// <summary>
/// Gets or sets a boolean indicating whether the visualization is expanded, exposing the <see cref="Children" />
/// </summary>
public bool IsVisualizationExpanded
{
get => _isVisualizationExpanded;
set
{
_dataModel = dataModel;
_children = new BindableCollection<DataModelVisualizationViewModel>();
_parent = parent;
DataModelPath = dataModelPath;
IsMatchingFilteredTypes = true;
if (parent == null)
IsRootViewModel = true;
else
PropertyDescription = DataModelPath?.GetPropertyDescription() ?? DataModel?.DataModelDescription;
if (!this.RaiseAndSetIfChanged(ref _isVisualizationExpanded, value)) return;
RequestUpdate();
}
}
/// <summary>
/// Gets a boolean indicating whether this view model is at the root of the data model
/// </summary>
public bool IsRootViewModel { get; protected set; }
/// <summary>
/// Gets a user-friendly representation of the <see cref="DataModelPath" />
/// </summary>
public virtual string? DisplayPath => DataModelPath != null
? string.Join(" › ", DataModelPath.Segments.Select(s => s.GetPropertyDescription()?.Name ?? s.Identifier))
: null;
/// <summary>
/// Gets the data model path to the property this view model is visualizing
/// </summary>
public DataModelPath? DataModelPath { get; }
/// <summary>
/// Gets a string representation of the path backing this model
/// </summary>
public string? Path => DataModelPath?.Path;
/// <summary>
/// Gets the property depth of the view model
/// </summary>
public int Depth { get; private set; }
/// <summary>
/// Gets the data model backing this view model
/// </summary>
public DataModel? DataModel
{
get => _dataModel;
protected set => SetAndNotify(ref _dataModel, value);
}
/// <summary>
/// Gets the property description of the property this view model is visualizing
/// </summary>
public DataModelPropertyAttribute? PropertyDescription
{
get => _propertyDescription;
protected set => SetAndNotify(ref _propertyDescription, value);
}
/// <summary>
/// Gets the parent of this view model
/// </summary>
public DataModelVisualizationViewModel? Parent
{
get => _parent;
protected set => SetAndNotify(ref _parent, value);
}
/// <summary>
/// Gets or sets a bindable collection containing the children of this view model
/// </summary>
public BindableCollection<DataModelVisualizationViewModel> Children
{
get => _children;
set => SetAndNotify(ref _children, value);
}
/// <summary>
/// Gets a boolean indicating whether the property being visualized matches the types last provided to
/// <see cref="ApplyTypeFilter" />
/// </summary>
public bool IsMatchingFilteredTypes
{
get => _isMatchingFilteredTypes;
private set => SetAndNotify(ref _isMatchingFilteredTypes, value);
}
/// <summary>
/// Gets or sets a boolean indicating whether the visualization is expanded, exposing the <see cref="Children" />
/// </summary>
public bool IsVisualizationExpanded
{
get => _isVisualizationExpanded;
set
{
if (!SetAndNotify(ref _isVisualizationExpanded, value)) return;
RequestUpdate();
}
}
/// <summary>
/// Gets a user-friendly representation of the <see cref="DataModelPath" />
/// </summary>
public virtual string? DisplayPath => DataModelPath != null
? string.Join(" › ", DataModelPath.Segments.Select(s => s.GetPropertyDescription()?.Name ?? s.Identifier))
: null;
/// <summary>
/// Updates the datamodel and if in an parent, any children
/// </summary>
/// <param name="dataModelUIService">The data model UI service used during update</param>
/// <param name="configuration">The configuration to apply while updating</param>
public abstract void Update(IDataModelUIService dataModelUIService, DataModelUpdateConfiguration? configuration);
/// <summary>
/// Gets the current value of the property being visualized
/// </summary>
/// <returns>The current value of the property being visualized</returns>
public virtual object? GetCurrentValue()
{
if (IsRootViewModel)
return null;
return DataModelPath?.GetValue();
}
/// <summary>
/// Determines whether the provided types match the type of the property being visualized and sets the result in
/// <see cref="IsMatchingFilteredTypes" />
/// </summary>
/// <param name="looseMatch">Whether the type may be a loose match, meaning it can be cast or converted</param>
/// <param name="filteredTypes">The types to filter</param>
public void ApplyTypeFilter(bool looseMatch, params Type[]? filteredTypes)
{
if (filteredTypes != null)
{
if (filteredTypes.All(t => t == null))
filteredTypes = null;
else
filteredTypes = filteredTypes.Where(t => t != null).ToArray();
}
// If the VM has children, its own type is not relevant
if (Children.Any())
{
foreach (DataModelVisualizationViewModel child in Children)
child?.ApplyTypeFilter(looseMatch, filteredTypes);
IsMatchingFilteredTypes = true;
return;
}
// If null is passed, clear the type filter
if (filteredTypes == null || filteredTypes.Length == 0)
{
IsMatchingFilteredTypes = true;
return;
}
// If the type couldn't be retrieved either way, assume false
Type? type = DataModelPath?.GetPropertyType();
if (type == null)
{
IsMatchingFilteredTypes = false;
return;
}
if (looseMatch)
IsMatchingFilteredTypes = filteredTypes.Any(t => t.IsCastableFrom(type) ||
t == typeof(Enum) && type.IsEnum ||
t == typeof(IEnumerable<>) && type.IsGenericEnumerable() ||
type.IsGenericType && t == type.GetGenericTypeDefinition());
else
IsMatchingFilteredTypes = filteredTypes.Any(t => t == type || t == typeof(Enum) && type.IsEnum);
}
internal virtual int GetChildDepth()
{
return 0;
}
internal void PopulateProperties(IDataModelUIService dataModelUIService, DataModelUpdateConfiguration? dataModelUpdateConfiguration)
{
if (IsRootViewModel && DataModel == null)
return;
Type? modelType = IsRootViewModel ? DataModel?.GetType() : DataModelPath?.GetPropertyType();
if (modelType == null)
throw new ArtemisSharedUIException("Failed to populate data model visualization properties, couldn't get a property type");
// Add missing static children only once, they're static after all
if (!_populatedStaticChildren)
{
foreach (PropertyInfo propertyInfo in modelType.GetProperties(BindingFlags.Public | BindingFlags.Instance).OrderBy(t => t.MetadataToken))
{
string childPath = AppendToPath(propertyInfo.Name);
if (Children.Any(c => c.Path != null && c.Path.Equals(childPath)))
continue;
if (propertyInfo.GetCustomAttribute<DataModelIgnoreAttribute>() != null)
continue;
MethodInfo? getMethod = propertyInfo.GetGetMethod();
if (getMethod == null || getMethod.GetParameters().Any())
continue;
DataModelVisualizationViewModel? child = CreateChild(dataModelUIService, childPath, GetChildDepth());
if (child != null)
Children.Add(child);
}
_populatedStaticChildren = true;
}
// Remove static children that should be hidden
if (DataModel != null)
{
ReadOnlyCollection<PropertyInfo> hiddenProperties = DataModel.GetHiddenProperties();
foreach (PropertyInfo hiddenProperty in hiddenProperties)
{
string childPath = AppendToPath(hiddenProperty.Name);
DataModelVisualizationViewModel? toRemove = Children.FirstOrDefault(c => c.Path != null && c.Path == childPath);
if (toRemove != null)
Children.Remove(toRemove);
}
}
// Add missing dynamic children
object? value = Parent == null || Parent.IsRootViewModel ? DataModel : DataModelPath?.GetValue();
if (value is DataModel dataModel)
{
foreach (string key in dataModel.DynamicChildren.Keys.ToList())
{
string childPath = AppendToPath(key);
if (Children.Any(c => c.Path != null && c.Path.Equals(childPath)))
continue;
DataModelVisualizationViewModel? child = CreateChild(dataModelUIService, childPath, GetChildDepth());
if (child != null)
Children.Add(child);
}
}
// Remove dynamic children that have been removed from the data model
List<DataModelVisualizationViewModel> toRemoveDynamic = Children.Where(c => c.DataModelPath != null && !c.DataModelPath.IsValid).ToList();
if (toRemoveDynamic.Any())
Children.RemoveRange(toRemoveDynamic);
}
private DataModelVisualizationViewModel? CreateChild(IDataModelUIService dataModelUIService, string path, int depth)
{
if (DataModel == null)
throw new ArtemisSharedUIException("Cannot create a data model visualization child VM for a parent without a data model");
if (depth > MaxDepth)
return null;
DataModelPath dataModelPath = new(DataModel, path);
if (!dataModelPath.IsValid)
return null;
PropertyInfo? propertyInfo = dataModelPath.GetPropertyInfo();
Type? propertyType = dataModelPath.GetPropertyType();
// Skip properties decorated with DataModelIgnore
if (propertyInfo != null && Attribute.IsDefined(propertyInfo, typeof(DataModelIgnoreAttribute)))
return null;
// Skip properties that are in the ignored properties list of the respective profile module/data model expansion
if (DataModel.GetHiddenProperties().Any(p => p.Equals(propertyInfo)))
return null;
if (propertyType == null)
return null;
// If a display VM was found, prefer to use that in any case
DataModelDisplayViewModel? typeViewModel = dataModelUIService.GetDataModelDisplayViewModel(propertyType, PropertyDescription);
if (typeViewModel != null)
return new DataModelPropertyViewModel(DataModel, this, dataModelPath) {DisplayViewModel = typeViewModel, Depth = depth};
// For primitives, create a property view model, it may be null that is fine
if (propertyType.IsPrimitive || propertyType.IsEnum || propertyType == typeof(string) || propertyType.IsGenericType && propertyType.GetGenericTypeDefinition() == typeof(Nullable<>))
return new DataModelPropertyViewModel(DataModel, this, dataModelPath) {Depth = depth};
if (propertyType.IsGenericEnumerable())
return new DataModelListViewModel(DataModel, this, dataModelPath) {Depth = depth};
if (propertyType == typeof(DataModelEvent) || propertyType.IsGenericType && propertyType.GetGenericTypeDefinition() == typeof(DataModelEvent<>))
return new DataModelEventViewModel(DataModel, this, dataModelPath) {Depth = depth};
// For other value types create a child view model
if (propertyType.IsClass || propertyType.IsStruct())
return new DataModelPropertiesViewModel(DataModel, this, dataModelPath) {Depth = depth};
/// <summary>
/// Updates the datamodel and if in an parent, any children
/// </summary>
/// <param name="dataModelUIService">The data model UI service used during update</param>
/// <param name="configuration">The configuration to apply while updating</param>
public abstract void Update(IDataModelUIService dataModelUIService, DataModelUpdateConfiguration? configuration);
/// <summary>
/// Gets the current value of the property being visualized
/// </summary>
/// <returns>The current value of the property being visualized</returns>
public virtual object? GetCurrentValue()
{
if (IsRootViewModel)
return null;
return DataModelPath?.GetValue();
}
/// <summary>
/// Determines whether the provided types match the type of the property being visualized and sets the result in
/// <see cref="IsMatchingFilteredTypes" />
/// </summary>
/// <param name="looseMatch">Whether the type may be a loose match, meaning it can be cast or converted</param>
/// <param name="filteredTypes">The types to filter</param>
public void ApplyTypeFilter(bool looseMatch, params Type[]? filteredTypes)
{
if (filteredTypes != null)
{
if (filteredTypes.All(t => t == null))
filteredTypes = null;
else
filteredTypes = filteredTypes.Where(t => t != null).ToArray();
}
private string AppendToPath(string toAppend)
// If the VM has children, its own type is not relevant
if (Children.Any())
{
if (string.IsNullOrEmpty(Path))
return toAppend;
foreach (DataModelVisualizationViewModel child in Children)
child?.ApplyTypeFilter(looseMatch, filteredTypes);
StringBuilder builder = new();
builder.Append(Path);
builder.Append(".");
builder.Append(toAppend);
return builder.ToString();
IsMatchingFilteredTypes = true;
return;
}
private void RequestUpdate()
// If null is passed, clear the type filter
if (filteredTypes == null || filteredTypes.Length == 0)
{
Parent?.RequestUpdate();
OnUpdateRequested();
IsMatchingFilteredTypes = true;
return;
}
#region Events
/// <summary>
/// Occurs when an update to the property this view model visualizes is requested
/// </summary>
public event EventHandler? UpdateRequested;
/// <summary>
/// Invokes the <see cref="UpdateRequested" /> event
/// </summary>
protected virtual void OnUpdateRequested()
// If the type couldn't be retrieved either way, assume false
Type? type = DataModelPath?.GetPropertyType();
if (type == null)
{
UpdateRequested?.Invoke(this, EventArgs.Empty);
IsMatchingFilteredTypes = false;
return;
}
#endregion
if (looseMatch)
IsMatchingFilteredTypes = filteredTypes.Any(t => t.IsCastableFrom(type) ||
t == typeof(Enum) && type.IsEnum ||
t == typeof(IEnumerable<>) && type.IsGenericEnumerable() ||
type.IsGenericType && t == type.GetGenericTypeDefinition());
else
IsMatchingFilteredTypes = filteredTypes.Any(t => t == type || t == typeof(Enum) && type.IsEnum);
}
#region IDisposable
/// <summary>
/// Occurs when an update to the property this view model visualizes is requested
/// </summary>
public event EventHandler? UpdateRequested;
/// <summary>
/// Releases the unmanaged resources used by the object and optionally releases the managed resources.
/// </summary>
/// <param name="disposing">
/// <see langword="true" /> to release both managed and unmanaged resources;
/// <see langword="false" /> to release only unmanaged resources.
/// </param>
protected virtual void Dispose(bool disposing)
/// <summary>
/// Expands this view model and any children to expose the provided <paramref name="dataModelPath" />.
/// </summary>
/// <param name="dataModelPath">The data model path to expose.</param>
public void ExpandToPath(DataModelPath dataModelPath)
{
if (dataModelPath.Target != DataModel)
throw new ArtemisSharedUIException("Can't expand to a path that doesn't belong to this data model.");
IsVisualizationExpanded = true;
DataModelPathSegment current = dataModelPath.Segments.Skip(1).First();
Children.FirstOrDefault(c => c.Path == current.Path)?.ExpandToPath(current.Next);
}
/// <summary>
/// Finds the view model that hosts the given path.
/// </summary>
/// <param name="dataModelPath">The path to find</param>
/// <returns>The matching view model, may be null if the path doesn't exist or isn't expanded</returns>
public DataModelVisualizationViewModel? GetViewModelForPath(DataModelPath dataModelPath)
{
if (dataModelPath.Target != DataModel)
throw new ArtemisSharedUIException("Can't expand to a path that doesn't belong to this data model.");
if (DataModelPath?.Path == dataModelPath.Path)
return this;
return Children.Select(c => c.GetViewModelForPath(dataModelPath)).FirstOrDefault(match => match != null);
}
/// <summary>
/// Invokes the <see cref="UpdateRequested" /> event
/// </summary>
protected virtual void OnUpdateRequested()
{
UpdateRequested?.Invoke(this, EventArgs.Empty);
}
/// <summary>
/// Releases the unmanaged resources used by the object and optionally releases the managed resources.
/// </summary>
/// <param name="disposing">
/// <see langword="true" /> to release both managed and unmanaged resources;
/// <see langword="false" /> to release only unmanaged resources.
/// </param>
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
if (disposing)
DataModelPath?.Dispose();
foreach (DataModelVisualizationViewModel dataModelVisualizationViewModel in Children)
dataModelVisualizationViewModel.Dispose(true);
}
}
internal virtual int GetChildDepth()
{
return 0;
}
internal void PopulateProperties(IDataModelUIService dataModelUIService, DataModelUpdateConfiguration? dataModelUpdateConfiguration)
{
if (IsRootViewModel && DataModel == null)
return;
Type? modelType = IsRootViewModel ? DataModel?.GetType() : DataModelPath?.GetPropertyType();
if (modelType == null)
throw new ArtemisSharedUIException("Failed to populate data model visualization properties, couldn't get a property type");
// Add missing static children only once, they're static after all
if (!_populatedStaticChildren)
{
foreach (PropertyInfo propertyInfo in modelType.GetProperties(BindingFlags.Public | BindingFlags.Instance).OrderBy(t => t.MetadataToken))
{
DataModelPath?.Dispose();
foreach (DataModelVisualizationViewModel dataModelVisualizationViewModel in Children)
dataModelVisualizationViewModel.Dispose(true);
string childPath = AppendToPath(propertyInfo.Name);
if (Children.Any(c => c?.Path != null && c.Path.Equals(childPath)))
continue;
if (propertyInfo.GetCustomAttribute<DataModelIgnoreAttribute>() != null)
continue;
MethodInfo? getMethod = propertyInfo.GetGetMethod();
if (getMethod == null || getMethod.GetParameters().Any())
continue;
DataModelVisualizationViewModel? child = CreateChild(dataModelUIService, childPath, GetChildDepth());
if (child != null)
Children.Add(child);
}
_populatedStaticChildren = true;
}
// Remove static children that should be hidden
if (DataModel != null)
{
ReadOnlyCollection<PropertyInfo> hiddenProperties = DataModel.GetHiddenProperties();
foreach (PropertyInfo hiddenProperty in hiddenProperties)
{
string childPath = AppendToPath(hiddenProperty.Name);
DataModelVisualizationViewModel? toRemove = Children.FirstOrDefault(c => c.Path != null && c.Path == childPath);
if (toRemove != null)
Children.Remove(toRemove);
}
}
/// <inheritdoc />
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
// Add missing dynamic children
object? value = Parent == null || Parent.IsRootViewModel ? DataModel : DataModelPath?.GetValue();
if (value is DataModel dataModel)
foreach (string key in dataModel.DynamicChildren.Keys.ToList())
{
string childPath = AppendToPath(key);
if (Children.Any(c => c.Path != null && c.Path.Equals(childPath)))
continue;
#endregion
DataModelVisualizationViewModel? child = CreateChild(dataModelUIService, childPath, GetChildDepth());
if (child != null)
Children.Add(child);
}
// Remove dynamic children that have been removed from the data model
List<DataModelVisualizationViewModel> toRemoveDynamic = Children.Where(c => c.DataModelPath != null && !c.DataModelPath.IsValid).ToList();
foreach (DataModelVisualizationViewModel dataModelVisualizationViewModel in toRemoveDynamic)
Children.Remove(dataModelVisualizationViewModel);
}
private DataModelVisualizationViewModel? CreateChild(IDataModelUIService dataModelUIService, string path, int depth)
{
if (DataModel == null)
throw new ArtemisSharedUIException("Cannot create a data model visualization child VM for a parent without a data model");
if (depth > MaxDepth)
return null;
DataModelPath dataModelPath = new(DataModel, path);
if (!dataModelPath.IsValid)
return null;
PropertyInfo? propertyInfo = dataModelPath.GetPropertyInfo();
Type? propertyType = dataModelPath.GetPropertyType();
// Skip properties decorated with DataModelIgnore
if (propertyInfo != null && Attribute.IsDefined(propertyInfo, typeof(DataModelIgnoreAttribute)))
return null;
// Skip properties that are in the ignored properties list of the respective profile module/data model expansion
if (DataModel.GetHiddenProperties().Any(p => p.Equals(propertyInfo)))
return null;
if (propertyType == null)
return null;
// If a display VM was found, prefer to use that in any case
DataModelDisplayViewModel? typeViewModel = dataModelUIService.GetDataModelDisplayViewModel(propertyType, PropertyDescription);
if (typeViewModel != null)
return new DataModelPropertyViewModel(DataModel, this, dataModelPath) {DisplayViewModel = typeViewModel, Depth = depth};
// For primitives, create a property view model, it may be null that is fine
if (propertyType.IsPrimitive || propertyType.IsEnum || propertyType == typeof(string) || propertyType.IsGenericType && propertyType.GetGenericTypeDefinition() == typeof(Nullable<>))
return new DataModelPropertyViewModel(DataModel, this, dataModelPath) {Depth = depth};
if (propertyType.IsGenericEnumerable())
return new DataModelListViewModel(DataModel, this, dataModelPath) {Depth = depth};
if (propertyType == typeof(DataModelEvent) || propertyType.IsGenericType && propertyType.GetGenericTypeDefinition() == typeof(DataModelEvent<>))
return new DataModelEventViewModel(DataModel, this, dataModelPath) {Depth = depth};
// For other value types create a child view model
if (propertyType.IsClass || propertyType.IsStruct())
return new DataModelPropertiesViewModel(DataModel, this, dataModelPath) {Depth = depth};
return null;
}
private string AppendToPath(string toAppend)
{
if (string.IsNullOrEmpty(Path))
return toAppend;
StringBuilder builder = new();
builder.Append(Path);
builder.Append(".");
builder.Append(toAppend);
return builder.ToString();
}
private void RequestUpdate()
{
Parent?.RequestUpdate();
OnUpdateRequested();
}
private void ExpandToPath(DataModelPathSegment? segment)
{
if (segment == null)
return;
IsVisualizationExpanded = true;
Children.FirstOrDefault(c => c.Path == segment.Path)?.ExpandToPath(segment.Next);
}
/// <inheritdoc />
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
}
@@ -0,0 +1,24 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Shared.DefaultTypes.DataModel.Display.DefaultDataModelDisplayView">
<Grid ColumnDefinitions="*,Auto,Auto">
<!-- Prefix -->
<TextBlock Grid.Column="0"
Text="{Binding PropertyDescription.Prefix}"
IsVisible="{Binding PropertyDescription.Prefix, Converter={x:Static ObjectConverters.IsNotNull}}"
TextAlignment="Right"
Margin="0 0 5 0" />
<!-- Value -->
<TextBlock Grid.Column="1" Text="{Binding Display, Mode=OneWay}" HorizontalAlignment="Right"/>
<!-- Affix -->
<TextBlock Grid.Column="2"
Text="{Binding PropertyDescription.Affix}"
IsVisible="{Binding PropertyDescription.Affix, Converter={x:Static ObjectConverters.IsNotNull}}"
Margin="5 0 0 0" />
</Grid>
</UserControl>
@@ -0,0 +1,24 @@
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace Artemis.UI.Shared.DefaultTypes.DataModel.Display
{
/// <summary>
/// Represents a default data model display view.
/// </summary>
public class DefaultDataModelDisplayView : UserControl
{
/// <summary>
/// Creates a new instance of the <see cref="DefaultDataModelDisplayView" /> class.
/// </summary>
public DefaultDataModelDisplayView()
{
InitializeComponent();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}
}
@@ -1,4 +1,8 @@
using System;
using Artemis.Core;
using Artemis.UI.Shared.DataModelVisualization;
using Newtonsoft.Json;
using ReactiveUI;
namespace Artemis.UI.Shared.DefaultTypes.DataModel.Display
{
@@ -8,33 +12,33 @@ namespace Artemis.UI.Shared.DefaultTypes.DataModel.Display
/// </summary>
internal class DefaultDataModelDisplayViewModel : DataModelDisplayViewModel<object>
{
private bool _showNull;
private bool _showToString;
private readonly JsonSerializerSettings _serializerSettings;
private string _display;
internal DefaultDataModelDisplayViewModel()
public DefaultDataModelDisplayViewModel()
{
ShowNull = true;
_serializerSettings = new JsonSerializerSettings()
{
ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
PreserveReferencesHandling = PreserveReferencesHandling.None
};
_display = "null";
}
public bool ShowToString
public string Display
{
get => _showToString;
private set => SetAndNotify(ref _showToString, value);
get => _display;
set => RaiseAndSetIfChanged(ref _display, value);
}
public bool ShowNull
{
get => _showNull;
set => SetAndNotify(ref _showNull, value);
}
protected override void OnDisplayValueUpdated()
{
if (DisplayValue is Enum enumDisplayValue)
DisplayValue = EnumUtilities.HumanizeValue(enumDisplayValue);
ShowToString = DisplayValue != null;
ShowNull = DisplayValue == null;
Display = EnumUtilities.HumanizeValue(enumDisplayValue);
else if (DisplayValue is not string)
Display = JsonConvert.SerializeObject(DisplayValue, _serializerSettings);
else
Display = DisplayValue?.ToString() ?? "null";
}
}
}
@@ -1,109 +0,0 @@
using System.Windows;
namespace Artemis.UI.Shared
{
/// <summary>
/// Provides a dependency property that can observe the size of an element and apply it to bindings
/// </summary>
public static class SizeObserver
{
/// <summary>
/// Gets or sets whether the element should be observed
/// </summary>
public static readonly DependencyProperty ObserveProperty = DependencyProperty.RegisterAttached(
"Observe",
typeof(bool),
typeof(SizeObserver),
new FrameworkPropertyMetadata(OnObserveChanged));
/// <summary>
/// Gets or sets the observed width of the element
/// </summary>
public static readonly DependencyProperty ObservedWidthProperty = DependencyProperty.RegisterAttached(
"ObservedWidth",
typeof(double),
typeof(SizeObserver));
/// <summary>
/// Gets or sets the observed height of the element
/// </summary>
public static readonly DependencyProperty ObservedHeightProperty = DependencyProperty.RegisterAttached(
"ObservedHeight",
typeof(double),
typeof(SizeObserver));
/// <summary>
/// Gets whether the provided <paramref name="frameworkElement" /> is being observed
/// </summary>
public static bool GetObserve(FrameworkElement frameworkElement)
{
return (bool) frameworkElement.GetValue(ObserveProperty);
}
/// <summary>
/// Sets whether the provided <paramref name="frameworkElement" /> is being observed
/// </summary>
public static void SetObserve(FrameworkElement frameworkElement, bool observe)
{
frameworkElement.SetValue(ObserveProperty, observe);
}
/// <summary>
/// Gets the observed width of the the provided <paramref name="frameworkElement" />
/// </summary>
public static double GetObservedWidth(FrameworkElement frameworkElement)
{
return (double) frameworkElement.GetValue(ObservedWidthProperty);
}
/// <summary>
/// Sets the observed width of the the provided <paramref name="frameworkElement" />
/// </summary>
public static void SetObservedWidth(FrameworkElement frameworkElement, double observedWidth)
{
frameworkElement.SetValue(ObservedWidthProperty, observedWidth);
}
/// <summary>
/// Gets the observed height of the the provided <paramref name="frameworkElement" />
/// </summary>
public static double GetObservedHeight(FrameworkElement frameworkElement)
{
return (double) frameworkElement.GetValue(ObservedHeightProperty);
}
/// <summary>
/// Sets the observed height of the the provided <paramref name="frameworkElement" />
/// </summary>
public static void SetObservedHeight(FrameworkElement frameworkElement, double observedHeight)
{
frameworkElement.SetValue(ObservedHeightProperty, observedHeight);
}
private static void OnObserveChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e)
{
FrameworkElement frameworkElement = (FrameworkElement) dependencyObject;
if ((bool) e.NewValue)
{
frameworkElement.SizeChanged += OnFrameworkElementSizeChanged;
UpdateObservedSizesForFrameworkElement(frameworkElement);
}
else
{
frameworkElement.SizeChanged -= OnFrameworkElementSizeChanged;
}
}
private static void OnFrameworkElementSizeChanged(object sender, SizeChangedEventArgs e)
{
UpdateObservedSizesForFrameworkElement((FrameworkElement) sender);
}
private static void UpdateObservedSizesForFrameworkElement(FrameworkElement frameworkElement)
{
frameworkElement.SetCurrentValue(ObservedWidthProperty, frameworkElement.ActualWidth);
frameworkElement.SetCurrentValue(ObservedHeightProperty, frameworkElement.ActualHeight);
}
}
}
@@ -2,7 +2,7 @@
using Artemis.Core;
using Artemis.UI.Shared.Controls;
namespace Artemis.UI.Shared
namespace Artemis.UI.Shared.Events
{
/// <summary>
/// Provides data about selection events raised by <see cref="DataModelPicker" />
@@ -0,0 +1,14 @@
using System;
namespace Artemis.UI.Shared.Events
{
internal class DialogClosedEventArgs<TResult> : EventArgs
{
public TResult Result { get; }
public DialogClosedEventArgs(TResult result)
{
Result = result;
}
}
}
@@ -1,7 +1,7 @@
using System;
using Artemis.Core;
namespace Artemis.UI.Shared
namespace Artemis.UI.Shared.Events
{
/// <summary>
/// Provides data on LED click events raised by the device visualizer
@@ -1,7 +1,7 @@
using System;
using Artemis.Core;
namespace Artemis.UI.Shared
namespace Artemis.UI.Shared.Events
{
/// <summary>
/// Provides data on profile related events raised by the profile editor
@@ -1,7 +1,7 @@
using System;
using Artemis.Core;
namespace Artemis.UI.Shared
namespace Artemis.UI.Shared.Events
{
/// <summary>
/// Provides data on profile element related events raised by the profile editor
@@ -0,0 +1,38 @@
using System;
using Artemis.UI.Shared.Controls;
using Avalonia;
using Avalonia.Input;
namespace Artemis.UI.Shared.Events
{
/// <summary>
/// Provides data on selection events raised by the <see cref="SelectionRectangle" />.
/// </summary>
public class SelectionRectangleEventArgs : EventArgs
{
/// <summary>
/// Creates a new instance of the <see cref="SelectionRectangleEventArgs" /> class.
/// </summary>
public SelectionRectangleEventArgs(Rect rectangle, Rect absoluteRectangle, KeyModifiers keyModifiers)
{
KeyModifiers = keyModifiers;
Rectangle = rectangle;
AbsoluteRectangle = absoluteRectangle;
}
/// <summary>
/// Gets the rectangle relative to the parent that was selected when the event occurred.
/// </summary>
public Rect Rectangle { get; }
/// <summary>
/// Gets the rectangle relative to the window that was selected when the event occurred.
/// </summary>
public Rect AbsoluteRectangle { get; }
/// <summary>
/// Gets the key modifiers that where pressed when the event occurred.
/// </summary>
public KeyModifiers KeyModifiers { get; }
}
}
@@ -0,0 +1,25 @@
using Avalonia.Controls;
using Avalonia.LogicalTree;
namespace Artemis.UI.Shared.Extensions
{
/// <summary>
/// Provides extension methods for Avalonia's <see cref="Control" /> type
/// </summary>
public static class ControlExtensions
{
/// <summary>
/// Clears all data validation errors on the given control and any of it's logical siblings
/// </summary>
/// <param name="target">The target control</param>
public static void ClearAllDataValidationErrors(this Control target)
{
DataValidationErrors.ClearErrors(target);
foreach (ILogical logicalChild in target.GetLogicalChildren())
{
if (logicalChild is Control childControl)
childControl.ClearAllDataValidationErrors();
}
}
}
}
@@ -0,0 +1,104 @@
using System.Linq;
using Artemis.Core;
using SkiaSharp;
namespace Artemis.UI.Shared.Extensions;
/// <summary>
/// Provides utilities when working with layers in UI elements.
/// </summary>
public static class LayerExtensions
{
/// <summary>
/// Returns the layer's bounds in real coordinates.
/// </summary>
public static SKRect GetLayerBounds(this Layer layer)
{
if (!layer.Leds.Any())
return SKRect.Empty;
return new SKRect(
layer.Leds.Min(l => l.RgbLed.AbsoluteBoundary.Location.X),
layer.Leds.Min(l => l.RgbLed.AbsoluteBoundary.Location.Y),
layer.Leds.Max(l => l.RgbLed.AbsoluteBoundary.Location.X + l.RgbLed.AbsoluteBoundary.Size.Width),
layer.Leds.Max(l => l.RgbLed.AbsoluteBoundary.Location.Y + l.RgbLed.AbsoluteBoundary.Size.Height)
);
}
/// <summary>
/// Returns the layer's anchor in real coordinates.
/// </summary>
public static SKPoint GetLayerAnchorPosition(this Layer layer, SKPoint? positionOverride = null)
{
SKRect layerBounds = GetLayerBounds(layer);
SKPoint positionProperty = layer.Transform.Position.CurrentValue;
if (positionOverride != null)
positionProperty = positionOverride.Value;
// Start at the top left of the shape
SKPoint position = new(layerBounds.Left, layerBounds.Top);
// Apply translation
position.X += positionProperty.X * layerBounds.Width;
position.Y += positionProperty.Y * layerBounds.Height;
return position;
}
/// <summary>
/// Returns an absolute and scaled rectangular path for the given layer in real coordinates.
/// </summary>
public static SKPath GetLayerPath(this Layer layer, bool includeTranslation, bool includeScale, bool includeRotation)
{
SKRect layerBounds = GetLayerBounds(layer);
SKMatrix transform = layer.GetTransformMatrix(false, includeTranslation, includeScale, includeRotation, layerBounds);
SKPath path = new();
path.AddRect(layerBounds);
path.Transform(transform);
return path;
}
/// <summary>
/// Returns a new point normalized to 0.0-1.0
/// </summary>
public static SKPoint GetNormalizedPoint(this Layer layer, SKPoint point, bool absolute)
{
SKRect bounds = GetLayerBounds(layer);
if (absolute)
return new SKPoint(
100 / bounds.Width * (point.X - bounds.Left) / 100,
100 / bounds.Height * (point.Y - bounds.Top) / 100
);
return new SKPoint(
100 / bounds.Width * point.X / 100,
100 / bounds.Height * point.Y / 100
);
}
/// <summary>
/// Returns the offset from the given point to the closest sides of the layer's shape bounds
/// </summary>
public static SKPoint GetDragOffset(this Layer layer, SKPoint dragStart)
{
SKRect bounds = layer.GetLayerPath(true, true, false).Bounds;
SKPoint anchor = layer.GetLayerAnchorPosition();
float xOffset = 0f, yOffset = 0f;
// X offset
if (dragStart.X < anchor.X)
xOffset = bounds.Left - dragStart.X;
else if (dragStart.X > anchor.X)
xOffset = bounds.Right - dragStart.X;
// Y offset
if (dragStart.Y < anchor.Y)
yOffset = bounds.Top - dragStart.Y;
else if (dragStart.Y > anchor.Y)
yOffset = bounds.Bottom - dragStart.Y;
return new SKPoint(xOffset, yOffset);
}
}
@@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Avalonia;
namespace Artemis.UI.Shared.Extensions
{
public static class PointExtensions
{
public static Point Empty = new(0, 0);
}
}
@@ -0,0 +1,20 @@
using Avalonia.Media;
using SkiaSharp;
namespace Artemis.UI.Shared.Extensions;
/// <summary>
/// Provides extension methods for the <see cref="SKColor"/> type.
/// </summary>
public static class SKColorExtensions
{
/// <summary>
/// Converts a SkiaSharp <see cref="SKColor"/> to an Avalonia <see cref="Color"/>.
/// </summary>
/// <param name="color">The color to convert.</param>
/// <returns>The resulting color.</returns>
public static Color ToColor(this SKColor color)
{
return new Color(color.Alpha, color.Red, color.Green, color.Blue);
}
}
@@ -0,0 +1,19 @@
using Avalonia;
using SkiaSharp;
namespace Artemis.UI.Shared.Extensions;
public static class SKMatrixExtensions
{
public static Matrix ToMatrix(this SKMatrix matrix)
{
return new Matrix(
matrix.ScaleX,
matrix.SkewY,
matrix.SkewX,
matrix.ScaleY,
matrix.TransX,
matrix.TransY
);
}
}
@@ -0,0 +1,17 @@
using Avalonia;
using SkiaSharp;
namespace Artemis.UI.Shared.Extensions;
public static class SKRectExtensions
{
public static Rect ToRect(this SKRect rect)
{
return new Rect(rect.Left, rect.Top, rect.Width, rect.Height);
}
public static Rect ToRect(this SKRectI rect)
{
return new Rect(rect.Left, rect.Top, rect.Width, rect.Height);
}
}
@@ -0,0 +1,37 @@
using Artemis.Core;
using Material.Icons;
using SkiaSharp;
using System;
namespace Artemis.UI.Shared.Extensions
{
/// <summary>
/// Provides utilities when working with types in UI elements.
/// </summary>
public static class TypeExtensions
{
/// <summary>
/// Finds an appropriate Material Design icon for the given <paramref name="type"/>.
/// </summary>
/// <param name="type">The type to retrieve an icon for.</param>
/// <returns>An appropriate Material Design icon for the given <paramref name="type"/>.</returns>
public static MaterialIconKind GetTypeIcon(this Type? type)
{
if (type == null)
return MaterialIconKind.QuestionMarkCircle;
if (type.TypeIsNumber())
return MaterialIconKind.CalculatorVariantOutline;
if (type.IsEnum)
return MaterialIconKind.FormatListBulletedSquare;
if (type == typeof(bool))
return MaterialIconKind.CircleHalfFull;
if (type == typeof(string))
return MaterialIconKind.Text;
if (type == typeof(SKColor))
return MaterialIconKind.Palette;
if (type.IsAssignableTo(typeof(IDataModelEvent)))
return MaterialIconKind.LightningBolt;
return MaterialIconKind.Matrix;
}
}
}
@@ -0,0 +1,62 @@
using System.Collections.Generic;
using System.Linq;
using Avalonia;
using Avalonia.VisualTree;
namespace Artemis.UI.Shared.Extensions;
/// <summary>
/// Provides extension methods for Avalonia's <see cref="IVisual" /> type
/// </summary>
public static class VisualExtensions
{
/// <summary>
/// Returns a recursive list of all visual children of type <typeparamref name="T" />.
/// </summary>
/// <typeparam name="T">The type the children should have.</typeparam>
/// <param name="root">The root visual at which to start searching.</param>
/// <returns>A recursive list of all visual children of type <typeparamref name="T" />.</returns>
public static List<T> GetVisualChildrenOfType<T>(this IVisual root)
{
List<T> result = new();
List<IVisual>? visualChildren = root.GetVisualChildren()?.ToList();
if (visualChildren == null || !visualChildren.Any())
return result;
foreach (IVisual visualChild in visualChildren)
{
if (visualChild is T toFind)
result.Add(toFind);
result.AddRange(GetVisualChildrenOfType<T>(visualChild));
}
return result;
}
/// <summary>
/// Returns a recursive list of all visual children with a data context of type <typeparamref name="T" />.
/// </summary>
/// <typeparam name="T">The type of data context the children should have.</typeparam>
/// <param name="root">The root visual at which to start searching.</param>
/// <returns>A recursive list of all visual children with a data context of type <typeparamref name="T" />.</returns>
public static List<T> GetVisualChildrenOfDataContextType<T>(this IVisual root)
{
List<T> result = new();
List<IVisual>? visualChildren = root.GetVisualChildren()?.ToList();
if (visualChildren == null || !visualChildren.Any())
return result;
foreach (IVisual visualChild in visualChildren)
{
if (visualChild is IDataContextProvider dataContextProvider && dataContextProvider.DataContext is T toFind)
result.Add(toFind);
result.AddRange(GetVisualChildrenOfType<T>(visualChild));
}
return result;
}
}
@@ -1,9 +0,0 @@
namespace Artemis.UI.Shared
{
/// <summary>
/// Represents a factory for view models provided by the Artemis Shared UI library
/// </summary>
public interface ISharedVmFactory
{
}
}
@@ -1,10 +1,9 @@
using System;
using Artemis.UI.Shared.Services;
using MaterialDesignThemes.Wpf;
using Artemis.UI.Shared.Services.Interfaces;
using Ninject.Extensions.Conventions;
using Ninject.Modules;
namespace Artemis.UI.Shared
namespace Artemis.UI.Shared.Ninject
{
/// <summary>
/// The main <see cref="NinjectModule" /> of the Artemis Shared UI toolkit that binds all services
@@ -27,17 +26,6 @@ namespace Artemis.UI.Shared
.BindAllInterfaces()
.Configure(c => c.InSingletonScope());
});
Kernel.Bind<ISnackbarMessageQueue>().ToConstant(new SnackbarMessageQueue(TimeSpan.FromSeconds(5))).InSingletonScope();
// Bind UI factories
Kernel.Bind(x =>
{
x.FromThisAssembly()
.SelectAllInterfaces()
.InheritedFrom<ISharedVmFactory>()
.BindToFactory();
});
}
}
}
@@ -1,12 +1,13 @@
using Artemis.Core.LayerBrushes;
using Stylet;
using System;
using System.Threading.Tasks;
using Artemis.Core.LayerBrushes;
namespace Artemis.UI.Shared.LayerBrushes
{
/// <summary>
/// Represents a view model for a brush configuration window
/// </summary>
public abstract class BrushConfigurationViewModel : Screen
public abstract class BrushConfigurationViewModel : ActivatableViewModelBase
{
/// <summary>
/// Creates a new instance of the <see cref="BrushConfigurationViewModel" /> class
@@ -17,19 +18,40 @@ namespace Artemis.UI.Shared.LayerBrushes
LayerBrush = layerBrush;
}
/// <summary>
/// Creates a new instance of the <see cref="BrushConfigurationViewModel" /> class with a validator
/// </summary>
/// <param name="layerBrush"></param>
/// <param name="validator"></param>
protected BrushConfigurationViewModel(BaseLayerBrush layerBrush, IModelValidator validator) : base(validator)
{
LayerBrush = layerBrush;
}
/// <summary>
/// Gets the layer brush this view model is associated with
/// </summary>
public BaseLayerBrush LayerBrush { get; }
/// <summary>
/// Closes the dialog
/// </summary>
public void RequestClose()
{
CloseRequested?.Invoke(this, EventArgs.Empty);
}
/// <summary>
/// Called when the window wants to close, returning <see langword="false" /> will cause the window to stay open.
/// </summary>
/// <returns><see langword="true" /> if the window may close; otherwise <see langword="false" />.</returns>
public virtual bool CanClose()
{
return true;
}
/// <summary>
/// Called when the window wants to close, returning <see langword="false" /> will cause the window to stay open.
/// </summary>
/// <returns>A task <see langword="true" /> if the window may close; otherwise <see langword="false" />.</returns>
public virtual Task<bool> CanCloseAsync()
{
return Task.FromResult(true);
}
/// <summary>
/// Occurs when a close was requested
/// </summary>
public event EventHandler? CloseRequested;
}
}
@@ -38,7 +38,7 @@ namespace Artemis.UI.Shared.LayerBrushes
public int DialogHeight { get; set; } = 800;
/// <summary>
/// The type of view model the tab contains
/// The type of view model the dialog contains
/// </summary>
public abstract Type Type { get; }
}
@@ -1,35 +1,57 @@
using Artemis.Core.LayerEffects;
using Stylet;
using System;
using System.Threading.Tasks;
using Artemis.Core.LayerEffects;
using Avalonia.Threading;
namespace Artemis.UI.Shared.LayerEffects
namespace Artemis.UI.Shared.LayerEffects;
/// <summary>
/// Represents a view model for an effect configuration window
/// </summary>
public abstract class EffectConfigurationViewModel : ActivatableViewModelBase
{
/// <summary>
/// Represents a view model for an effect configuration window
/// Creates a new instance of the <see cref="EffectConfigurationViewModel" /> class
/// </summary>
public abstract class EffectConfigurationViewModel : Screen
/// <param name="layerEffect"></param>
protected EffectConfigurationViewModel(BaseLayerEffect layerEffect)
{
/// <summary>
/// Creates a new instance of the <see cref="EffectConfigurationViewModel" /> class
/// </summary>
/// <param name="layerEffect"></param>
protected EffectConfigurationViewModel(BaseLayerEffect layerEffect)
{
LayerEffect = layerEffect;
}
/// <summary>
/// Creates a new instance of the <see cref="EffectConfigurationViewModel" /> class with a validator
/// </summary>
/// <param name="layerEffect"></param>
/// <param name="validator"></param>
protected EffectConfigurationViewModel(BaseLayerEffect layerEffect, IModelValidator validator) : base(validator)
{
LayerEffect = layerEffect;
}
/// <summary>
/// Gets the layer effect this view model is associated with
/// </summary>
public BaseLayerEffect LayerEffect { get; }
LayerEffect = layerEffect;
}
/// <summary>
/// Gets the layer effect this view model is associated with
/// </summary>
public BaseLayerEffect LayerEffect { get; }
/// <summary>
/// Closes the dialog
/// </summary>
public void RequestClose()
{
CloseRequested?.Invoke(this, EventArgs.Empty);
}
/// <summary>
/// Called when the window wants to close, returning <see langword="false" /> will cause the window to stay open.
/// </summary>
/// <returns><see langword="true" /> if the window may close; otherwise <see langword="false" />.</returns>
public virtual bool CanClose()
{
return true;
}
/// <summary>
/// Called when the window wants to close, returning <see langword="false" /> will cause the window to stay open.
/// </summary>
/// <returns>A task <see langword="true" /> if the window may close; otherwise <see langword="false" />.</returns>
public virtual Task<bool> CanCloseAsync()
{
return Task.FromResult(true);
}
/// <summary>
/// Occurs when a close was requested
/// </summary>
public event EventHandler? CloseRequested;
}
@@ -39,7 +39,7 @@ namespace Artemis.UI.Shared.LayerEffects
public int DialogHeight { get; set; } = 800;
/// <summary>
/// The type of view model the tab contains
/// The type of view model the dialog contains
/// </summary>
public abstract Type Type { get; }
}
@@ -1,27 +0,0 @@
using Artemis.Core.Modules;
using Stylet;
namespace Artemis.UI.Shared.Modules
{
/// <summary>
/// The base class for any view model that belongs to a module
/// </summary>
public abstract class ModuleViewModel : Screen, IModuleViewModel
{
/// <summary>
/// The base class for any view model that belongs to a module
/// </summary>
/// <param name="module">The module this view model belongs to</param>
/// <param name="displayName">The name of the tab that's shown on the modules UI page</param>
protected ModuleViewModel(Module module, string displayName)
{
Module = module;
DisplayName = displayName.ToUpper();
}
/// <summary>
/// Gets the module this view model belongs to
/// </summary>
public Module Module { get; }
}
}
@@ -15,9 +15,7 @@ namespace Artemis.UI.Shared
/// </summary>
public abstract class PluginConfigurationDialog : IPluginConfigurationDialog
{
/// <summary>
/// The type of view model the tab contains
/// </summary>
/// <inheritdoc />
public abstract Type Type { get; }
}
}
@@ -1,12 +1,12 @@
using Artemis.Core;
using Stylet;
using System;
using Artemis.Core;
namespace Artemis.UI.Shared
{
/// <summary>
/// Represents a view model for a plugin configuration window
/// </summary>
public abstract class PluginConfigurationViewModel : Screen, IPluginConfigurationViewModel
public abstract class PluginConfigurationViewModel : ViewModelValidationBase, IPluginConfigurationViewModel
{
/// <summary>
/// Creates a new instance of the <see cref="PluginConfigurationViewModel" /> class
@@ -17,19 +17,30 @@ namespace Artemis.UI.Shared
Plugin = plugin;
}
/// <summary>
/// Creates a new instance of the <see cref="PluginConfigurationViewModel" /> class with a validator
/// </summary>
/// <param name="plugin"></param>
/// <param name="validator"></param>
protected PluginConfigurationViewModel(Plugin plugin, IModelValidator validator) : base(validator)
{
Plugin = plugin;
}
/// <summary>
/// Gets the plugin this configuration view model is associated with
/// </summary>
public Plugin Plugin { get; }
/// <summary>
/// Closes the window hosting the view model
/// </summary>
public void Close()
{
CloseRequested?.Invoke(this, EventArgs.Empty);
OnCloseRequested();
}
/// <summary>
/// Called when the the window hosting the view model should close
/// </summary>
public virtual void OnCloseRequested()
{
}
/// <summary>
/// Occurs when the the window hosting the view model should close
/// </summary>
public event EventHandler? CloseRequested;
}
}
@@ -1,12 +1,12 @@
using Artemis.Core.ScriptingProviders;
using Stylet;
using ReactiveUI;
namespace Artemis.UI.Shared.ScriptingProviders
{
/// <summary>
/// Represents a Stylet view model containing a script editor
/// </summary>
public class ScriptEditorViewModel : Screen, IScriptEditorViewModel
public class ScriptEditorViewModel : ActivatableViewModelBase, IScriptEditorViewModel
{
private Script? _script;
@@ -42,7 +42,7 @@ namespace Artemis.UI.Shared.ScriptingProviders
public Script? Script
{
get => _script;
internal set => SetAndNotify(ref _script, value);
internal set => RaiseAndSetIfChanged(ref _script, value);
}
/// <inheritdoc />
File diff suppressed because it is too large. Load diff
Loaded 100 of 1110 files, more files were not shown because too many files have changed in this diff. Show more