1
0
mirror of https://github.com/Artemis-RGB/Artemis synced 2025-12-13 05:48:35 +00:00

Meta - Replace WPF projects with Avalonia projects

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

View File

@ -4,6 +4,7 @@
<TargetFramework>net6.0</TargetFramework>
<Nullable>enable</Nullable>
<PlatformTarget>x64</PlatformTarget>
<Platforms>x64</Platforms>
</PropertyGroup>
<ItemGroup>
<AvaloniaResource Include="Assets\**" />
@ -18,7 +19,7 @@
<PackageReference Include="ReactiveUI" Version="17.1.50" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\Artemis.Core\Artemis.Core.csproj" />
<ProjectReference Include="..\Artemis.Core\Artemis.Core.csproj" />
<ProjectReference Include="..\Artemis.UI\Artemis.UI.csproj" />
</ItemGroup>
</Project>

View File

Before

Width:  |  Height:  |  Size: 172 KiB

After

Width:  |  Height:  |  Size: 172 KiB

File diff suppressed because it is too large Load Diff

View File

@ -4,6 +4,7 @@
<TargetFramework>net6.0</TargetFramework>
<Nullable>enable</Nullable>
<PlatformTarget>x64</PlatformTarget>
<Platforms>x64</Platforms>
</PropertyGroup>
<ItemGroup>
<AvaloniaResource Include="Assets\**" />
@ -18,7 +19,7 @@
<PackageReference Include="ReactiveUI" Version="17.1.50" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\Artemis.Core\Artemis.Core.csproj" />
<ProjectReference Include="..\Artemis.Core\Artemis.Core.csproj" />
<ProjectReference Include="..\Artemis.UI\Artemis.UI.csproj" />
</ItemGroup>
</Project>

View File

Before

Width:  |  Height:  |  Size: 172 KiB

After

Width:  |  Height:  |  Size: 172 KiB

File diff suppressed because it is too large Load Diff

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>

View File

@ -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>

View File

@ -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);
}
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}
}

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;
}
}
}

View File

@ -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>

View File

@ -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;
}
}
}
}

View File

@ -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>

View File

@ -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;
}
}

View File

@ -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>

View File

@ -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;
}
}

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
}
}

View File

@ -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)
{

View File

@ -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>

View File

@ -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
}
}

View File

@ -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>

View File

@ -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
}
}

View File

@ -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();
}
}
}

View File

@ -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>

View File

@ -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);
}
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}
}

View File

@ -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();
}
}
}

View File

@ -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);
}
}
}
}

View File

@ -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
}
}
}

View File

@ -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
}
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}
}

View File

@ -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();
}

View File

@ -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>

View File

@ -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; }
}
}

View File

@ -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

View File

@ -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 />

View File

@ -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}";
}
}
}

View File

@ -1,71 +0,0 @@
using System;
using Artemis.Core;
using Artemis.UI.Shared.Services;
namespace Artemis.UI.Shared
{
/// <summary>
/// Represents a view model that visualizes a single data model property contained in a
/// <see cref="DataModelListViewModel" />
/// </summary>
public class DataModelListPropertyViewModel : DataModelPropertyViewModel
{
private int _index;
private Type? _listType;
internal DataModelListPropertyViewModel(Type listType, DataModelDisplayViewModel displayViewModel, string? name) : base(null, null, null)
{
ListType = listType;
DisplayViewModel = displayViewModel;
}
internal DataModelListPropertyViewModel(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;
internal set => SetAndNotify(ref _index, value);
}
/// <summary>
/// Gets the type of elements contained in the list
/// </summary>
public Type? ListType
{
get => _listType;
private set => SetAndNotify(ref _listType, value);
}
/// <inheritdoc />
public override object? GetCurrentValue()
{
return DisplayValue;
}
/// <inheritdoc />
public override void Update(IDataModelUIService dataModelUIService, DataModelUpdateConfiguration? configuration)
{
// Display value gets updated by parent, don't do anything if it is null
if (DisplayValue == null)
return;
if (DisplayViewModel == null)
DisplayViewModel = dataModelUIService.GetDataModelDisplayViewModel(DisplayValue.GetType(), PropertyDescription, true);
ListType = DisplayValue.GetType();
DisplayViewModel?.UpdateValue(DisplayValue);
}
/// <inheritdoc />
public override string ToString()
{
return $"[List item {Index}] {DisplayPath ?? Path} - {DisplayValue}";
}
}
}

View File

@ -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);
}
}
}

View File

@ -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 />

View File

@ -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 />

View File

@ -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" />

View File

@ -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);
}
}

View File

@ -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";
}
}
}

Some files were not shown because too many files have changed in this diff Show More