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

UI - Implemented most of the surface editor

This commit is contained in:
Robert 2021-10-31 22:51:33 +01:00
parent ca1e3ce365
commit f98e398bc5
25 changed files with 907 additions and 231 deletions

View File

@ -52,27 +52,31 @@ namespace Artemis.UI.Avalonia.Shared.Controls
// Determine the scale required to fit the desired size of the control
double scale = Math.Min(Bounds.Width / _deviceBounds.Width, Bounds.Height / _deviceBounds.Height);
// Scale the visualization in the desired bounding box
DrawingContext.PushedState? boundsPush = null;
if (Bounds.Width > 0 && Bounds.Height > 0)
boundsPush = drawingContext.PushPostTransform(Matrix.CreateScale(scale, scale));
try
{
// Scale the visualization in the desired bounding box
if (Bounds.Width > 0 && Bounds.Height > 0)
boundsPush = drawingContext.PushPostTransform(Matrix.CreateScale(scale, scale));
// Apply device rotation
using DrawingContext.PushedState translationPush = drawingContext.PushPostTransform(Matrix.CreateTranslation(0 - _deviceBounds.Left, 0 - _deviceBounds.Top));
using DrawingContext.PushedState rotationPush = drawingContext.PushPostTransform(Matrix.CreateRotation(Device.Rotation));
// Apply device rotation
using DrawingContext.PushedState translationPush = drawingContext.PushPostTransform(Matrix.CreateTranslation(0 - _deviceBounds.Left, 0 - _deviceBounds.Top));
using DrawingContext.PushedState rotationPush = drawingContext.PushPostTransform(Matrix.CreateRotation(Device.Rotation));
// Apply device scale
using DrawingContext.PushedState scalePush = drawingContext.PushPostTransform(Matrix.CreateScale(Device.Scale, Device.Scale));
// Apply device scale
using DrawingContext.PushedState scalePush = drawingContext.PushPostTransform(Matrix.CreateScale(Device.Scale, Device.Scale));
// Render device and LED images
if (_deviceImage != null)
drawingContext.DrawImage(_deviceImage, new Rect(0, 0, Device.RgbDevice.ActualSize.Width, Device.RgbDevice.ActualSize.Height));
// Render device and LED images
if (_deviceImage != null)
drawingContext.DrawImage(_deviceImage, new Rect(0, 0, Device.RgbDevice.ActualSize.Width, Device.RgbDevice.ActualSize.Height));
foreach (DeviceVisualizerLed deviceVisualizerLed in _deviceVisualizerLeds)
deviceVisualizerLed.RenderGeometry(drawingContext, false);
boundsPush?.Dispose();
foreach (DeviceVisualizerLed deviceVisualizerLed in _deviceVisualizerLeds)
deviceVisualizerLed.RenderGeometry(drawingContext, false);
}
finally
{
boundsPush?.Dispose();
}
}
/// <summary>
@ -275,6 +279,16 @@ namespace Artemis.UI.Avalonia.Shared.Controls
InvalidateMeasure();
}
#region Overrides of Layoutable
/// <inheritdoc />
protected override Size MeasureOverride(Size availableSize)
{
return new Size(Math.Min(availableSize.Width, _deviceBounds.Width), Math.Min(availableSize.Height, _deviceBounds.Height));
}
#endregion
#endregion
}
}

View File

@ -54,6 +54,7 @@ namespace Artemis.UI.Avalonia.Shared.Controls
public SelectionRectangle()
{
AffectsRender<TextBlock>(BackgroundProperty, BorderBrushProperty, BorderThicknessProperty);
IsHitTestVisible = false;
}
/// <summary>
@ -108,6 +109,9 @@ namespace Artemis.UI.Avalonia.Shared.Controls
private void ParentOnPointerPressed(object? sender, PointerPressedEventArgs e)
{
if (!e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
return;
e.Pointer.Capture(this);
_startPosition = e.GetPosition(Parent);
@ -131,6 +135,9 @@ namespace Artemis.UI.Avalonia.Shared.Controls
private void ParentOnPointerReleased(object? sender, PointerReleasedEventArgs e)
{
if (!ReferenceEquals(e.Pointer.Captured, this))
return;
e.Pointer.Capture(null);
if (_displayRect != null)

View File

@ -1,4 +1,6 @@
namespace Artemis.UI.Avalonia.Shared.Services.Interfaces
using System.Threading.Tasks;
namespace Artemis.UI.Avalonia.Shared.Services.Interfaces
{
public interface IWindowService : IArtemisSharedUIService
{
@ -11,10 +13,11 @@
void ShowWindow(object viewModel);
/// <summary>
/// Given a ViewModel, show its corresponding View as a Dialog
/// Given a ViewModel, show its corresponding View as a Dialog
/// </summary>
/// <typeparam name="T">The return type</typeparam>
/// <param name="viewModel">ViewModel to show the View for</param>
/// <returns>DialogResult of the View</returns>
bool? ShowDialog(object viewModel);
/// <returns>A task containing the return value of type <typeparamref name="T"/></returns>
Task<T> ShowDialog<T>(object viewModel);
}
}

View File

@ -1,7 +1,11 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Artemis.UI.Avalonia.Shared.Exceptions;
using Artemis.UI.Avalonia.Shared.Services.Interfaces;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Ninject;
namespace Artemis.UI.Avalonia.Shared.Services
@ -42,9 +46,23 @@ namespace Artemis.UI.Avalonia.Shared.Services
}
/// <inheritdoc />
public bool? ShowDialog(object viewModel)
public async Task<T> ShowDialog<T>(object viewModel)
{
throw new NotImplementedException();
if (Application.Current.ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime classic)
throw new ArtemisSharedUIException($"Can't show a dialog when application lifetime is not IClassicDesktopStyleApplicationLifetime.");
string name = viewModel.GetType().FullName!.Split('`')[0].Replace("ViewModel", "View");
Type? type = viewModel.GetType().Assembly.GetType(name);
if (type == null)
throw new ArtemisSharedUIException($"Failed to find a window named {name}.");
if (!type.IsAssignableTo(typeof(Window)))
throw new ArtemisSharedUIException($"Type {name} is not a window.");
Window window = (Window) Activator.CreateInstance(type)!;
window.DataContext = viewModel;
Window parent = classic.Windows.FirstOrDefault(w => w.IsActive) ?? classic.MainWindow;
return await window.ShowDialog<T>(parent);
}
#endregion

View File

@ -1,76 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net5.0</TargetFramework>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<Folder Include="Controls\" />
<Folder Include="Models\" />
<AvaloniaResource Include="Assets\**" />
</ItemGroup>
<ItemGroup>
<None Remove="Assets\Images\home-banner.png" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Avalonia" Version="0.10.7" />
<PackageReference Include="Avalonia.Desktop" Version="0.10.7" />
<PackageReference Include="Avalonia.Diagnostics" Version="0.10.7" />
<PackageReference Include="Avalonia.ReactiveUI" Version="0.10.7" />
<PackageReference Include="Avalonia.Svg.Skia" Version="0.10.7.2" />
<PackageReference Include="FluentAvaloniaUI" Version="1.1.3" />
<PackageReference Include="Flurl.Http" Version="3.2.0" />
<PackageReference Include="Live.Avalonia" Version="1.3.1" />
<PackageReference Include="Material.Icons.Avalonia" Version="1.0.2" />
<PackageReference Include="Splat.Ninject" Version="13.1.22" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Artemis.Core\Artemis.Core.csproj" />
<ProjectReference Include="..\Artemis.UI.Avalonia.Shared\Artemis.UI.Avalonia.Shared.csproj" />
</ItemGroup>
<ItemGroup>
<Compile Update="MainWindow.axaml.cs">
<DependentUpon>%(Filename)</DependentUpon>
</Compile>
<Compile Update="Screens\Root\Views\SidebarCategoryView.axaml.cs">
<DependentUpon>%(Filename)</DependentUpon>
</Compile>
<Compile Update="Screens\Root\Views\SidebarProfileConfigurationView.axaml.cs">
<DependentUpon>%(Filename)</DependentUpon>
</Compile>
<Compile Update="Screens\Root\Views\SidebarScreenView.axaml.cs">
<DependentUpon>%(Filename)</DependentUpon>
</Compile>
<Compile Update="Screens\Root\Views\SidebarView.axaml.cs">
<DependentUpon>%(Filename)</DependentUpon>
</Compile>
<Compile Update="Screens\Sidebar\Views\SidebarView.axaml.cs">
<DependentUpon>SidebarView.axaml</DependentUpon>
<SubType>Code</SubType>
</Compile>
<Compile Update="Screens\Root\Views\RootView.axaml.cs">
<DependentUpon>RootView.axaml</DependentUpon>
<SubType>Code</SubType>
</Compile>
</ItemGroup>
<ItemGroup>
<UpToDateCheckInput Remove="Views\MainWindow.axaml" />
</ItemGroup>
<ItemGroup>
<Content Include="Assets\Images\Logo\bow-black.ico" />
<Content Include="Assets\Images\Logo\bow-white.ico" />
<Content Include="Assets\Images\Logo\bow.ico" />
</ItemGroup>
<ItemGroup>
<Reference Include="RGB.NET.Core">
<HintPath>..\..\..\RGB.NET\bin\net5.0\RGB.NET.Core.dll</HintPath>
</Reference>
</ItemGroup>
<ItemGroup>
<Resource Include="Assets\Images\Logo\bow-black.ico" />
<Resource Include="Assets\Images\Logo\bow-white.ico" />
<Resource Include="Assets\Images\Logo\bow-white.svg" />
<Resource Include="Assets\Images\Logo\bow.ico" />
<Resource Include="Assets\Images\Logo\bow.svg" />
</ItemGroup>
</Project>

View File

@ -11,6 +11,8 @@
</ItemGroup>
<ItemGroup>
<None Remove="Assets\Images\home-banner.png" />
<None Remove="Screens\SurfaceEditor\Views\ListDeviceView.xaml" />
<None Remove="Screens\SurfaceEditor\Views\SurfaceDeviceView.xaml" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Avalonia" Version="0.10.10" />
@ -62,6 +64,16 @@
<Content Include="Assets\Images\Logo\bow-white.ico" />
<Content Include="Assets\Images\Logo\bow.ico" />
</ItemGroup>
<ItemGroup>
<Page Include="Screens\SurfaceEditor\Views\ListDeviceView.xaml">
<XamlRuntime>$(DefaultXamlRuntime)</XamlRuntime>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="Screens\SurfaceEditor\Views\SurfaceDeviceView.xaml">
<XamlRuntime>$(DefaultXamlRuntime)</XamlRuntime>
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Reference Include="RGB.NET.Core">
<HintPath>..\..\..\RGB.NET\bin\net5.0\RGB.NET.Core.dll</HintPath>

View File

@ -0,0 +1,21 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
namespace Artemis.UI.Avalonia.Extensions
{
public static class ObservableCollectionExtensions
{
public static void Sort<T>(this ObservableCollection<T> collection, Func<T, object> order)
{
List<T> ordered = collection.OrderBy(order).ToList();
for (int index = 0; index < ordered.Count; index++)
{
T dataBindingConditionViewModel = ordered[index];
if (collection.IndexOf(dataBindingConditionViewModel) != index)
collection.Move(collection.IndexOf(dataBindingConditionViewModel), index);
}
}
}
}

View File

@ -1,5 +1,7 @@
using Artemis.Core;
using Artemis.UI.Avalonia.Screens.Device.ViewModels;
using Artemis.UI.Avalonia.Screens.Root.ViewModels;
using Artemis.UI.Avalonia.Screens.SurfaceEditor.ViewModels;
using ReactiveUI;
namespace Artemis.UI.Avalonia.Ninject.Factories
@ -8,10 +10,21 @@ namespace Artemis.UI.Avalonia.Ninject.Factories
{
}
public interface IDeviceVmFactory : IVmFactory
{
DevicePropertiesViewModel DevicePropertiesViewModel(ArtemisDevice device);
}
public interface ISidebarVmFactory : IVmFactory
{
SidebarViewModel SidebarViewModel(IScreen hostScreen);
SidebarCategoryViewModel SidebarCategoryViewModel(ProfileCategory profileCategory);
SidebarProfileConfigurationViewModel SidebarProfileConfigurationViewModel(ProfileConfiguration profileConfiguration);
}
public interface SurfaceVmFactory : IVmFactory
{
SurfaceDeviceViewModel SurfaceDeviceViewModel(ArtemisDevice device);
ListDeviceViewModel ListDeviceViewModel(ArtemisDevice device);
}
}

View File

@ -0,0 +1,14 @@
using Artemis.Core;
namespace Artemis.UI.Avalonia.Screens.Device.ViewModels
{
public class DevicePropertiesViewModel : ActivatableViewModelBase
{
public DevicePropertiesViewModel(ArtemisDevice device)
{
Device = device;
}
public ArtemisDevice Device { get; }
}
}

View File

@ -0,0 +1,62 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:controls="clr-namespace:Artemis.UI.Avalonia.Shared.Controls;assembly=Artemis.UI.Avalonia.Shared"
mc:Ignorable="d" d:DesignWidth="1200" d:DesignHeight="800"
x:Class="Artemis.UI.Avalonia.Screens.Device.Views.DevicePropertiesView"
Title="Artemis | Device Properties"
Width="1200"
Height="800"
ExtendClientAreaToDecorationsHint="True">
<Grid ColumnDefinitions="*,0,*">
<Grid Grid.Column="0" Name="DeviceDisplayGrid">
<Grid.Background>
<VisualBrush TileMode="Tile" Stretch="Uniform" DestinationRect="0,0,25,25">
<VisualBrush.Visual>
<Grid Width="25" Height="25" RowDefinitions="*,*" ColumnDefinitions="*,*">
<Rectangle Grid.Row="0" Grid.Column="0" Fill="Black" Opacity="0.15" />
<Rectangle Grid.Row="0" Grid.Column="1" />
<Rectangle Grid.Row="1" Grid.Column="0" />
<Rectangle Grid.Row="1" Grid.Column="1" Fill="Black" Opacity="0.15" />
</Grid>
</VisualBrush.Visual>
</VisualBrush>
</Grid.Background>
<!-- No need to provide LEDs to highlight as LEDs are already physically highlighted -->
<controls:DeviceVisualizer Device="{Binding Device}"
HorizontalAlignment="Center"
VerticalAlignment="Center"
ShowColors="True"
Margin="20" />
<StackPanel Orientation="Horizontal" VerticalAlignment="Bottom" HorizontalAlignment="Left" Margin="15" IsVisible="{Binding Device.Layout.RgbLayout.Author}">
<TextBlock Classes="h5" Text="Device layout by " />
<TextBlock Classes="h5" FontWeight="Bold" Text="{Binding Device.Layout.RgbLayout.Author}" />
</StackPanel>
</Grid>
<GridSplitter Grid.Column="1" Width="15" Margin="-15 0 0 0" Background="Transparent" HorizontalAlignment="Stretch"/>
<Border Grid.Column="2" Classes="card" CornerRadius="10 0 0 0">
<TabControl Margin="12" Items="{Binding Tabs}">
<TabControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding DisplayName}" />
</DataTemplate>
</TabControl.ItemTemplate>
<TabControl.ContentTemplate>
<DataTemplate>
<ScrollViewer HorizontalScrollBarVisibility="Hidden" VerticalScrollBarVisibility="Auto">
<ContentControl Content="{Binding}" />
</ScrollViewer>
</DataTemplate>
</TabControl.ContentTemplate>
</TabControl>
</Border>
</Grid>
</Window>

View File

@ -0,0 +1,24 @@
using Artemis.UI.Avalonia.Screens.Device.ViewModels;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
namespace Artemis.UI.Avalonia.Screens.Device.Views
{
public partial class DevicePropertiesView : ReactiveWindow<DevicePropertiesViewModel>
{
public DevicePropertiesView()
{
InitializeComponent();
#if DEBUG
this.AttachDevTools();
#endif
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}
}

View File

@ -28,113 +28,102 @@
Margin="30"
Text=" Welcome to Artemis, the unified RGB platform." />
<Grid Grid.Row="1" MaxWidth="840" Margin="30" VerticalAlignment="Bottom">
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<Grid Grid.Row="1" MaxWidth="840" Margin="30" VerticalAlignment="Bottom" ColumnDefinitions="*,*" RowDefinitions="*,*">
<Border Classes="card" Margin="8" Grid.ColumnSpan="2">
<Grid VerticalAlignment="Stretch" RowDefinitions="Auto,Auto" ColumnDefinitions="150,*">
<avalonia:MaterialIcon Kind="Plug" Width="100" Height="100" HorizontalAlignment="Center" VerticalAlignment="Center" />
<StackPanel Grid.Row="0" Grid.Column="1">
<TextBlock FontSize="24" Margin="16 16 16 8">Plugins</TextBlock>
<TextBlock TextWrapping="Wrap" Margin="16 0 16 8" VerticalAlignment="Top">
Artemis is built up using plugins. This means devices, brushes, effects and modules (for supporting games!) can all be added via plugins.
</TextBlock>
<TextBlock TextWrapping="Wrap" Margin="16 0 16 8" VerticalAlignment="Top">
Under Settings > Plugins you can find your currently installed plugins, these default plugins are created by Artemis developers.
</TextBlock>
<TextBlock TextWrapping="Wrap" Margin="16 0 16 8" VerticalAlignment="Top">
We're also keeping track of a list of third-party plugins on our wiki.
</TextBlock>
</StackPanel>
<Border Grid.Row="1" Grid.ColumnSpan="2" Grid.Column="0" BorderThickness="0 1 0 0" BorderBrush="{DynamicResource MaterialDesignDivider}" Padding="8">
<controls:HyperlinkButton NavigateUri="https://wiki.artemis-rgb.com/en/guides/user/plugins" HorizontalAlignment="Right">
<StackPanel Orientation="Horizontal">
<avalonia:MaterialIcon Kind="OpenInBrowser" />
<TextBlock Margin="8 0 0 0" VerticalAlignment="Center">Get more plugins</TextBlock>
</StackPanel>
</controls:HyperlinkButton>
</Border>
</Grid>
</Border>
<Grid VerticalAlignment="Stretch" RowDefinitions="Auto,Auto" ColumnDefinitions="150,*">
<avalonia:MaterialIcon Kind="Plug" Width="100" Height="100" HorizontalAlignment="Center" VerticalAlignment="Center"/>
<StackPanel Grid.Row="0" Grid.Column="1">
<TextBlock Classes="h3">Plugins</TextBlock>
<TextBlock TextWrapping="Wrap" VerticalAlignment="Top">
Artemis is built up using plugins. This means devices, brushes, effects and modules (for supporting games) can all be added via plugins.
</TextBlock>
<TextBlock TextWrapping="Wrap" VerticalAlignment="Top" Margin="0 15">
Under Settings > Plugins you can find your currently installed plugins, these default plugins are created by Artemis developers. We're also keeping track of a list of third-party plugins on our wiki.
</TextBlock>
</StackPanel>
<controls:HyperlinkButton Grid.Row="1" Grid.ColumnSpan="2" Grid.Column="0" NavigateUri="https://wiki.artemis-rgb.com/en/guides/user/plugins" HorizontalAlignment="Right">
<controls:HyperlinkButton.ContextMenu>
<ContextMenu>
<MenuItem Header="Test"></MenuItem>
</ContextMenu>
</controls:HyperlinkButton.ContextMenu>
<StackPanel Orientation="Horizontal">
<avalonia:MaterialIcon Kind="OpenInBrowser" />
<TextBlock Margin="8 0 0 0" VerticalAlignment="Center">Get more plugins</TextBlock>
</StackPanel>
</controls:HyperlinkButton>
</Grid>
</Border>
<Border Classes="card" Margin="8" Grid.Row="1" Grid.Column="0">
<Grid VerticalAlignment="Stretch" RowDefinitions="175,95" ColumnDefinitions="150,*">
<avalonia:MaterialIcon Kind="Discord" Width="100" Height="100"
HorizontalAlignment="Center" VerticalAlignment="Center" />
<StackPanel Grid.Row="0" Grid.Column="1">
<TextBlock FontSize="24" Margin="16 16 16 8">Have a chat</TextBlock>
<TextBlock TextWrapping="Wrap" Margin="16 0 16 8" VerticalAlignment="Top">
If you need help, have some feedback or have any other questions feel free to contact us through any of the following channels.
</TextBlock>
</StackPanel>
<Border Grid.Row="1" Grid.ColumnSpan="2" Grid.Column="0" BorderThickness="0 1 0 0"
BorderBrush="{DynamicResource MaterialDesignDivider}" Padding="8">
<DockPanel>
<Grid Margin="8" RowDefinitions="*,*">
<controls:HyperlinkButton Grid.Row="0" NavigateUri="https://github.com/Artemis-RGB/Artemis">
<StackPanel Orientation="Horizontal">
<avalonia:MaterialIcon Kind="Gift" />
<TextBlock Margin="8 0 0 0" VerticalAlignment="Center">GitHub</TextBlock>
</StackPanel>
</controls:HyperlinkButton>
<controls:HyperlinkButton Grid.Row="0" NavigateUri="https://artemis-rgb.com" HorizontalAlignment="Right">
<StackPanel Orientation="Horizontal">
<avalonia:MaterialIcon Kind="Web" />
<TextBlock Margin="8 0 0 0" VerticalAlignment="Center">Website</TextBlock>
</StackPanel>
</controls:HyperlinkButton>
<controls:HyperlinkButton Grid.Row="1" NavigateUri="https://discordapp.com/invite/S3MVaC9">
<StackPanel Orientation="Horizontal">
<avalonia:MaterialIcon Kind="Discord" />
<TextBlock Margin="8 0 0 0" VerticalAlignment="Center">Discord</TextBlock>
</StackPanel>
</controls:HyperlinkButton>
<controls:HyperlinkButton Grid.Row="1" NavigateUri="mailto:spoinky.nl@gmail.com" HorizontalAlignment="Right">
<StackPanel Orientation="Horizontal">
<avalonia:MaterialIcon Kind="Email" />
<TextBlock Margin="8 0 0 0" VerticalAlignment="Center">E-mail</TextBlock>
</StackPanel>
</controls:HyperlinkButton>
</Grid>
</DockPanel>
</Border>
</Grid>
</Border>
<Border Classes="card" Margin="8" Grid.Row="1" Grid.Column="1">
<Grid VerticalAlignment="Stretch" RowDefinitions="175,95" ColumnDefinitions="150,*">
<avalonia:MaterialIcon Kind="Github" Width="100" Height="100" HorizontalAlignment="Center" VerticalAlignment="Center" />
<StackPanel Grid.Row="0" Grid.Column="1">
<TextBlock FontSize="16" Margin="16 16 16 8">Open Source</TextBlock>
<TextBlock TextWrapping="Wrap" Margin="16 0 16 8" VerticalAlignment="Top">
This project is completely open source. If you like it and want to say thanks you could hit the GitHub Star button, I like numbers. You could even make plugins, there's a full documentation on the website
</TextBlock>
</StackPanel>
<Border Grid.Row="1" Grid.ColumnSpan="2" Grid.Column="0" BorderThickness="0 1 0 0"
BorderBrush="{DynamicResource MaterialDesignDivider}" Padding="8">
<DockPanel>
<controls:HyperlinkButton NavigateUri="https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&amp;hosted_button_id=VQBAEJYUFLU4J"
HorizontalAlignment="Right">
<StackPanel Orientation="Horizontal">
<avalonia:MaterialIcon Kind="Gift" />
<TextBlock Margin="8 0 0 0" VerticalAlignment="Center">Donate</TextBlock>
</StackPanel>
</controls:HyperlinkButton>
<TextBlock Foreground="{DynamicResource MaterialDesignBodyLight}"
TextWrapping="Wrap"
Margin="16"
VerticalAlignment="Center">
Feel like you want to make a donation? It would be gratefully received. Click the button to donate via PayPal.
</TextBlock>
</DockPanel>
</Border>
</Grid>
</Border>
</Grid>
<Border Classes="card" Margin="8" Grid.Row="1" Grid.Column="0">
<Grid VerticalAlignment="Stretch" RowDefinitions="150,95" ColumnDefinitions="150,*">
<avalonia:MaterialIcon Kind="Discord" Width="100" Height="100" HorizontalAlignment="Center" VerticalAlignment="Center" />
<StackPanel Grid.Row="0" Grid.Column="1">
<TextBlock Classes="h3">Have a chat</TextBlock>
<TextBlock TextWrapping="Wrap" VerticalAlignment="Top">
If you need help, have some feedback or have any other questions feel free to contact us through any of the following channels.
</TextBlock>
</StackPanel>
<DockPanel Grid.Row="1" Grid.ColumnSpan="2" Grid.Column="0">
<Grid Margin="8" RowDefinitions="*,*">
<controls:HyperlinkButton Grid.Row="0" NavigateUri="https://github.com/Artemis-RGB/Artemis">
<StackPanel Orientation="Horizontal">
<avalonia:MaterialIcon Kind="Gift" />
<TextBlock Margin="8 0 0 0" VerticalAlignment="Center">GitHub</TextBlock>
</StackPanel>
</controls:HyperlinkButton>
<controls:HyperlinkButton Grid.Row="0" HorizontalAlignment="Right" NavigateUri="https://artemis-rgb.com">
<StackPanel Orientation="Horizontal">
<avalonia:MaterialIcon Kind="Web" />
<TextBlock Margin="8 0 0 0" VerticalAlignment="Center">Website</TextBlock>
</StackPanel>
</controls:HyperlinkButton>
<controls:HyperlinkButton Grid.Row="1" NavigateUri="https://discordapp.com/invite/S3MVaC9">
<StackPanel Orientation="Horizontal">
<avalonia:MaterialIcon Kind="Discord" />
<TextBlock Margin="8 0 0 0" VerticalAlignment="Center">Discord</TextBlock>
</StackPanel>
</controls:HyperlinkButton>
<controls:HyperlinkButton Grid.Row="1" HorizontalAlignment="Right" NavigateUri="mailto:spoinky.nl@gmail.com">
<StackPanel Orientation="Horizontal">
<avalonia:MaterialIcon Kind="Email" />
<TextBlock Margin="8 0 0 0" VerticalAlignment="Center">E-mail</TextBlock>
</StackPanel>
</controls:HyperlinkButton>
</Grid>
</DockPanel>
</Grid>
</Border>
<Border Classes="card" Margin="8" Grid.Row="1" Grid.Column="1">
<Grid VerticalAlignment="Stretch" RowDefinitions="150,95" ColumnDefinitions="150,*">
<avalonia:MaterialIcon Kind="Github" Width="100" Height="100" HorizontalAlignment="Center" VerticalAlignment="Center" />
<StackPanel Grid.Row="0" Grid.Column="1">
<TextBlock Classes="h3">Open Source</TextBlock>
<TextBlock TextWrapping="Wrap" VerticalAlignment="Top">
This project is open source. If you like it and want to say thanks you could hit the GitHub Star button, I like numbers.
</TextBlock>
</StackPanel>
<controls:HyperlinkButton Grid.Row="1"
Grid.Column="0"
HorizontalAlignment="Center"
NavigateUri="https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&amp;hosted_button_id=VQBAEJYUFLU4J">
<StackPanel Orientation="Horizontal">
<avalonia:MaterialIcon Kind="Gift" />
<TextBlock Margin="8 0 0 0" VerticalAlignment="Center">Donate</TextBlock>
</StackPanel>
</controls:HyperlinkButton>
<TextBlock Grid.Row="1"
Grid.Column="1"
Foreground="{DynamicResource MaterialDesignBodyLight}"
TextWrapping="Wrap"
VerticalAlignment="Center">
Feel like making a donation? It would be gratefully received. Click the button to donate via PayPal.
</TextBlock>
</Grid>
</Border>
</Grid>
</Grid>
</UserControl>

View File

@ -14,13 +14,13 @@
<ContentControl Grid.Column="0" Content="{Binding SidebarViewModel}" />
<Border Classes="router-container" Grid.Column="1" Margin="0 40 0 0">
<ScrollViewer HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto" >
<!-- <ScrollViewer HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto" > -->
<reactiveUi:RoutedViewHost Router="{Binding Router}">
<reactiveUi:RoutedViewHost.PageTransition>
<CrossFade Duration="0.1" />
</reactiveUi:RoutedViewHost.PageTransition>
</reactiveUi:RoutedViewHost>
</ScrollViewer>
<!-- </ScrollViewer> -->
</Border>
</Grid>
</UserControl>

View File

@ -12,7 +12,9 @@
</TabControl.ItemTemplate>
<TabControl.ContentTemplate>
<DataTemplate>
<ContentControl Content="{Binding}" />
<ScrollViewer HorizontalScrollBarVisibility="Hidden" VerticalScrollBarVisibility="Auto">
<ContentControl Content="{Binding}" />
</ScrollViewer>
</DataTemplate>
</TabControl.ContentTemplate>
</TabControl>

View File

@ -0,0 +1,31 @@
using Artemis.Core;
using ReactiveUI;
using SkiaSharp;
namespace Artemis.UI.Avalonia.Screens.SurfaceEditor.ViewModels
{
public class ListDeviceViewModel : ViewModelBase
{
private SKColor _color;
private bool _isSelected;
public ListDeviceViewModel(ArtemisDevice device)
{
Device = device;
}
public ArtemisDevice Device { get; }
public bool IsSelected
{
get => _isSelected;
set => this.RaiseAndSetIfChanged(ref _isSelected, value);
}
public SKColor Color
{
get => _color;
set => this.RaiseAndSetIfChanged(ref _color, value);
}
}
}

View File

@ -0,0 +1,140 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reactive;
using System.Threading.Tasks;
using Artemis.Core;
using Artemis.Core.Services;
using Artemis.UI.Avalonia.Ninject.Factories;
using Artemis.UI.Avalonia.Shared.Services.Interfaces;
using Avalonia.Input;
using ReactiveUI;
using RGB.NET.Core;
using SkiaSharp;
using Point = Avalonia.Point;
namespace Artemis.UI.Avalonia.Screens.SurfaceEditor.ViewModels
{
public class SurfaceDeviceViewModel : ActivatableViewModelBase
{
private readonly IRgbService _rgbService;
private readonly IDeviceService _deviceService;
private readonly ISettingsService _settingsService;
private readonly IDeviceVmFactory _deviceVmFactory;
private readonly IWindowService _windowService;
private Cursor _cursor;
private double _dragOffsetX;
private double _dragOffsetY;
private SelectionStatus _selectionStatus;
public SurfaceDeviceViewModel(ArtemisDevice device, IRgbService rgbService, IDeviceService deviceService, ISettingsService settingsService, IDeviceVmFactory deviceVmFactory, IWindowService windowService)
{
_rgbService = rgbService;
_deviceService = deviceService;
_settingsService = settingsService;
_deviceVmFactory = deviceVmFactory;
_windowService = windowService;
Device = device;
IdentifyDevice = ReactiveCommand.Create<ArtemisDevice>(ExecuteIdentifyDevice);
ViewProperties = ReactiveCommand.CreateFromTask<ArtemisDevice>(ExecuteViewProperties);
}
public ReactiveCommand<ArtemisDevice, Unit> IdentifyDevice { get; }
public ReactiveCommand<ArtemisDevice, Unit> ViewProperties { get; }
public ArtemisDevice Device { get; }
public SelectionStatus SelectionStatus
{
get => _selectionStatus;
set
{
this.RaiseAndSetIfChanged(ref _selectionStatus, value);
this.RaisePropertyChanged(nameof(Highlighted));
}
}
public bool Highlighted => SelectionStatus != SelectionStatus.None;
public bool CanDetectInput => Device.DeviceType == RGBDeviceType.Keyboard || Device.DeviceType == RGBDeviceType.Mouse;
public Cursor Cursor
{
get => _cursor;
set => this.RaiseAndSetIfChanged(ref _cursor, value);
}
public void StartMouseDrag(Point mouseStartPosition)
{
if (SelectionStatus != SelectionStatus.Selected)
return;
_dragOffsetX = Device.X - mouseStartPosition.X;
_dragOffsetY = Device.Y - mouseStartPosition.Y;
}
public void UpdateMouseDrag(Point mousePosition)
{
if (SelectionStatus != SelectionStatus.Selected)
return;
float roundedX = (float) Math.Round((mousePosition.X + _dragOffsetX) / 10d, 0, MidpointRounding.AwayFromZero) * 10f;
float roundedY = (float) Math.Round((mousePosition.Y + _dragOffsetY) / 10d, 0, MidpointRounding.AwayFromZero) * 10f;
if (Fits(roundedX, roundedY))
{
Device.X = roundedX;
Device.Y = roundedY;
}
else if (Fits(roundedX, Device.Y))
{
Device.X = roundedX;
}
else if (Fits(Device.X, roundedY))
{
Device.Y = roundedY;
}
}
private void ExecuteIdentifyDevice(ArtemisDevice device)
{
_deviceService.IdentifyDevice(device);
}
private async Task ExecuteViewProperties(ArtemisDevice device)
{
await _windowService.ShowDialog<bool>(_deviceVmFactory.DevicePropertiesViewModel(device));
}
private bool Fits(float x, float y)
{
if (x < 0 || y < 0)
return false;
double maxTextureSize = 4096 / _settingsService.GetSetting("Core.RenderScale", 0.25).Value;
if (x + Device.Rectangle.Width > maxTextureSize || y + Device.Rectangle.Height > maxTextureSize)
return false;
List<SKRect> own = Device.Leds
.Select(l => SKRect.Create(l.Rectangle.Left + x, l.Rectangle.Top + y, l.Rectangle.Width, l.Rectangle.Height))
.ToList();
List<SKRect> others = _rgbService.EnabledDevices
.Where(d => d != Device && d.IsEnabled)
.SelectMany(d => d.Leds)
.Select(l => SKRect.Create(l.Rectangle.Left + l.Device.X, l.Rectangle.Top + l.Device.Y, l.Rectangle.Width, l.Rectangle.Height))
.ToList();
return !own.Any(o => others.Any(l => l.IntersectsWith(o)));
}
}
public enum SelectionStatus
{
None,
Hover,
Selected
}
}

View File

@ -1,36 +1,188 @@
using System;
using System.Collections.ObjectModel;
using System.Linq;
using System.Reactive;
using System.Reactive.Linq;
using System.Threading.Tasks;
using Artemis.Core;
using Artemis.Core.Services;
using Artemis.UI.Avalonia.Extensions;
using Artemis.UI.Avalonia.Ninject.Factories;
using Avalonia;
using Avalonia.Skia;
using ReactiveUI;
using SkiaSharp;
namespace Artemis.UI.Avalonia.Screens.SurfaceEditor.ViewModels
{
public class SurfaceEditorViewModel : MainScreenViewModel
{
public SurfaceEditorViewModel(IScreen hostScreen, IRgbService rgbService) : base(hostScreen, "surface-editor")
private readonly IInputService _inputService;
private readonly IRgbService _rgbService;
private readonly ISettingsService _settingsService;
private bool _saving;
public SurfaceEditorViewModel(IScreen hostScreen,
IRgbService rgbService,
SurfaceVmFactory surfaceVmFactory,
IInputService inputService,
ISettingsService settingsService) : base(hostScreen, "surface-editor")
{
_rgbService = rgbService;
_inputService = inputService;
_settingsService = settingsService;
DisplayName = "Surface Editor";
Devices = new ObservableCollection<ArtemisDevice>(rgbService.Devices);
SurfaceDeviceViewModels = new ObservableCollection<SurfaceDeviceViewModel>(rgbService.Devices.Select(surfaceVmFactory.SurfaceDeviceViewModel));
ListDeviceViewModels = new ObservableCollection<ListDeviceViewModel>(rgbService.Devices.Select(surfaceVmFactory.ListDeviceViewModel));
BringToFront = ReactiveCommand.Create<ArtemisDevice>(ExecuteBringToFront);
BringForward = ReactiveCommand.Create<ArtemisDevice>(ExecuteBringForward);
SendToBack = ReactiveCommand.Create<ArtemisDevice>(ExecuteSendToBack);
SendBackward = ReactiveCommand.Create<ArtemisDevice>(ExecuteSendBackward);
UpdateSelection = ReactiveCommand.Create<Rect>(ExecuteUpdateSelection);
ApplySelection = ReactiveCommand.Create<Rect>(ExecuteApplySelection);
}
public ObservableCollection<ArtemisDevice> Devices { get; }
public ObservableCollection<SurfaceDeviceViewModel> SurfaceDeviceViewModels { get; }
public ObservableCollection<ListDeviceViewModel> ListDeviceViewModels { get; }
public ReactiveCommand<ArtemisDevice, Unit> BringToFront { get; }
public ReactiveCommand<ArtemisDevice, Unit> BringForward { get; }
public ReactiveCommand<ArtemisDevice, Unit> SendToBack { get; }
public ReactiveCommand<ArtemisDevice, Unit> SendBackward { get; }
public ReactiveCommand<Rect, Unit> UpdateSelection { get; }
public ReactiveCommand<Rect, Unit> ApplySelection { get; }
public double MaxTextureSize => 4096 / _settingsService.GetSetting("Core.RenderScale", 0.25).Value;
public void ClearSelection()
{
foreach (SurfaceDeviceViewModel surfaceDeviceViewModel in SurfaceDeviceViewModels)
surfaceDeviceViewModel.SelectionStatus = SelectionStatus.None;
}
public void StartMouseDrag(Point mousePosition)
{
foreach (SurfaceDeviceViewModel surfaceDeviceViewModel in SurfaceDeviceViewModels)
surfaceDeviceViewModel.StartMouseDrag(mousePosition);
}
public void UpdateMouseDrag(Point mousePosition)
{
foreach (SurfaceDeviceViewModel surfaceDeviceViewModel in SurfaceDeviceViewModels)
surfaceDeviceViewModel.UpdateMouseDrag(mousePosition);
}
public void StopMouseDrag(Point mousePosition)
{
foreach (SurfaceDeviceViewModel surfaceDeviceViewModel in SurfaceDeviceViewModels)
surfaceDeviceViewModel.UpdateMouseDrag(mousePosition);
if (_saving)
return;
// TODO: Figure out why the UI still locks up here
Task.Run(() =>
{
try
{
_saving = true;
_rgbService.SaveDevices();
}
finally
{
_saving = false;
}
});
}
private void ExecuteUpdateSelection(Rect rect)
{
SKRect hitTestRect = rect.ToSKRect();
foreach (SurfaceDeviceViewModel device in SurfaceDeviceViewModels)
{
if (device.Device.Rectangle.IntersectsWith(hitTestRect))
device.SelectionStatus = SelectionStatus.Selected;
else if (!_inputService.IsKeyDown(KeyboardKey.LeftShift) && !_inputService.IsKeyDown(KeyboardKey.RightShift))
device.SelectionStatus = SelectionStatus.None;
}
ApplySurfaceSelection();
}
private void ExecuteApplySelection(Rect rect)
private void ApplySurfaceSelection()
{
foreach (ListDeviceViewModel viewModel in ListDeviceViewModels)
viewModel.IsSelected = SurfaceDeviceViewModels.Any(s => s.Device == viewModel.Device && s.SelectionStatus == SelectionStatus.Selected);
}
#region Context menu commands
private void ExecuteBringToFront(ArtemisDevice device)
{
SurfaceDeviceViewModel surfaceDeviceViewModel = SurfaceDeviceViewModels.First(d => d.Device == device);
SurfaceDeviceViewModels.Move(SurfaceDeviceViewModels.IndexOf(surfaceDeviceViewModel), SurfaceDeviceViewModels.Count - 1);
for (int i = 0; i < SurfaceDeviceViewModels.Count; i++)
{
SurfaceDeviceViewModel deviceViewModel = SurfaceDeviceViewModels[i];
deviceViewModel.Device.ZIndex = i + 1;
}
ListDeviceViewModels.Sort(l => l.Device.ZIndex * -1);
_rgbService.SaveDevices();
}
private void ExecuteBringForward(ArtemisDevice device)
{
SurfaceDeviceViewModel surfaceDeviceViewModel = SurfaceDeviceViewModels.First(d => d.Device == device);
int currentIndex = SurfaceDeviceViewModels.IndexOf(surfaceDeviceViewModel);
int newIndex = Math.Min(currentIndex + 1, SurfaceDeviceViewModels.Count - 1);
SurfaceDeviceViewModels.Move(currentIndex, newIndex);
for (int i = 0; i < SurfaceDeviceViewModels.Count; i++)
{
SurfaceDeviceViewModel deviceViewModel = SurfaceDeviceViewModels[i];
deviceViewModel.Device.ZIndex = i + 1;
}
ListDeviceViewModels.Sort(l => l.Device.ZIndex * -1);
_rgbService.SaveDevices();
}
private void ExecuteSendToBack(ArtemisDevice device)
{
SurfaceDeviceViewModel surfaceDeviceViewModel = SurfaceDeviceViewModels.First(d => d.Device == device);
SurfaceDeviceViewModels.Move(SurfaceDeviceViewModels.IndexOf(surfaceDeviceViewModel), 0);
for (int i = 0; i < SurfaceDeviceViewModels.Count; i++)
{
SurfaceDeviceViewModel deviceViewModel = SurfaceDeviceViewModels[i];
deviceViewModel.Device.ZIndex = i + 1;
}
ListDeviceViewModels.Sort(l => l.Device.ZIndex * -1);
_rgbService.SaveDevices();
}
private void ExecuteSendBackward(ArtemisDevice device)
{
SurfaceDeviceViewModel surfaceDeviceViewModel = SurfaceDeviceViewModels.First(d => d.Device == device);
int currentIndex = SurfaceDeviceViewModels.IndexOf(surfaceDeviceViewModel);
int newIndex = Math.Max(currentIndex - 1, 0);
SurfaceDeviceViewModels.Move(currentIndex, newIndex);
for (int i = 0; i < SurfaceDeviceViewModels.Count; i++)
{
SurfaceDeviceViewModel deviceViewModel = SurfaceDeviceViewModels[i];
deviceViewModel.Device.ZIndex = i + 1;
}
ListDeviceViewModels.Sort(l => l.Device.ZIndex * -1);
_rgbService.SaveDevices();
}
#endregion
}
}

View File

@ -0,0 +1,8 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Avalonia.Screens.SurfaceEditor.Views.ListDeviceView">
Welcome to Avalonia!
</UserControl>

View File

@ -0,0 +1,19 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace Artemis.UI.Avalonia.Screens.SurfaceEditor.Views
{
public partial class ListDeviceView : UserControl
{
public ListDeviceView()
{
InitializeComponent();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}
}

View File

@ -0,0 +1,37 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:controls="clr-namespace:Artemis.UI.Avalonia.Shared.Controls;assembly=Artemis.UI.Avalonia.Shared"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Avalonia.Screens.SurfaceEditor.Views.SurfaceDeviceView">
<Grid>
<Grid.Styles>
<Style Selector="Border.selection-border">
<Setter Property="Opacity" Value="0" />
<Setter Property="Transitions">
<Transitions>
<DoubleTransition Property="Opacity" Duration="0:0:0.2" />
</Transitions>
</Setter>
</Style>
<Style Selector="Border.selection-border-selected">
<Setter Property="Opacity" Value="1" />
</Style>
</Grid.Styles>
<controls:DeviceVisualizer Device="{Binding Device}" ShowColors="True"/>
<Border x:Name="SurfaceDeviceBorder"
Classes="selection-border"
Classes.selection-border-selected="{Binding Highlighted}"
BorderThickness="1">
<Border.BorderBrush>
<SolidColorBrush Color="{DynamicResource SystemAccentColor}"></SolidColorBrush>
</Border.BorderBrush>
<Border.Background>
<SolidColorBrush Color="{DynamicResource SystemAccentColor}" Opacity="0.2"></SolidColorBrush>
</Border.Background>
</Border>
</Grid>
</UserControl>

View File

@ -0,0 +1,57 @@
using Artemis.UI.Avalonia.Screens.SurfaceEditor.ViewModels;
using Avalonia.Input;
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
namespace Artemis.UI.Avalonia.Screens.SurfaceEditor.Views
{
public class SurfaceDeviceView : ReactiveUserControl<SurfaceDeviceViewModel>
{
public SurfaceDeviceView()
{
InitializeComponent();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
#region Overrides of InputElement
/// <inheritdoc />
protected override void OnPointerEnter(PointerEventArgs e)
{
if (ViewModel?.SelectionStatus == SelectionStatus.None)
{
ViewModel.SelectionStatus = SelectionStatus.Hover;
Cursor = new Cursor(StandardCursorType.Hand);
}
base.OnPointerEnter(e);
}
/// <inheritdoc />
protected override void OnPointerLeave(PointerEventArgs e)
{
if (ViewModel?.SelectionStatus == SelectionStatus.Hover)
{
ViewModel.SelectionStatus = SelectionStatus.None;
Cursor = new Cursor(StandardCursorType.Arrow);
}
base.OnPointerLeave(e);
}
/// <inheritdoc />
protected override void OnPointerPressed(PointerPressedEventArgs e)
{
if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed && ViewModel != null)
ViewModel.SelectionStatus = SelectionStatus.Selected;
base.OnPointerPressed(e);
}
#endregion
}
}

View File

@ -3,7 +3,8 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:controls="clr-namespace:Artemis.UI.Avalonia.Shared.Controls;assembly=Artemis.UI.Avalonia.Shared"
xmlns:paz="using:Avalonia.Controls.PanAndZoom"
xmlns:paz="clr-namespace:Avalonia.Controls.PanAndZoom;assembly=Avalonia.Controls.PanAndZoom"
xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Avalonia.Screens.SurfaceEditor.Views.SurfaceEditorView">
@ -14,7 +15,10 @@
Focusable="True"
VerticalAlignment="Stretch"
HorizontalAlignment="Stretch"
ZoomChanged="ZoomBorder_OnZoomChanged">
ZoomChanged="ZoomBorder_OnZoomChanged"
PointerPressed="ZoomBorder_OnPointerPressed"
PointerMoved="ZoomBorder_OnPointerMoved"
PointerReleased="ZoomBorder_OnPointerReleased">
<paz:ZoomBorder.Background>
<VisualBrush TileMode="Tile" Stretch="Uniform" SourceRect="0,0,25,25">
<VisualBrush.Visual>
@ -27,12 +31,13 @@
</VisualBrush.Visual>
</VisualBrush>
</paz:ZoomBorder.Background>
<Grid Background="Transparent">
<ItemsControl Items="{Binding Devices}">
<Grid Name="ContainerGrid"
Background="Transparent">
<ItemsControl Items="{Binding SurfaceDeviceViewModels}" ClipToBounds="False">
<ItemsControl.Styles>
<Style Selector="ContentPresenter">
<Setter Property="Canvas.Left" Value="{Binding X}" />
<Setter Property="Canvas.Top" Value="{Binding Y}" />
<Setter Property="Canvas.Left" Value="{Binding Device.X}" />
<Setter Property="Canvas.Top" Value="{Binding Device.Y}" />
</Style>
</ItemsControl.Styles>
<ItemsControl.ItemsPanel>
@ -42,17 +47,68 @@
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<controls:DeviceVisualizer Device="{Binding}" ShowColors="True" />
<ContentControl Content="{Binding}">
<ContentControl.ContextFlyout>
<MenuFlyout>
<MenuItem Header="Identify" Command="{Binding IdentifyDevice}" CommandParameter="{Binding Device}">
<MenuItem.Icon>
<avalonia:MaterialIcon Kind="AlarmLight" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="-" />
<MenuItem Header="Bring to Front" Command="{Binding $parent[4].DataContext.BringToFront}" CommandParameter="{Binding Device}">
<MenuItem.Icon>
<avalonia:MaterialIcon Kind="ArrangeBringToFront" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Bring Forward" Command="{Binding $parent[4].DataContext.BringForward}" CommandParameter="{Binding Device}">
<MenuItem.Icon>
<avalonia:MaterialIcon Kind="ArrangeBringForward" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Send to Back" Command="{Binding $parent[4].DataContext.SendToBack}" CommandParameter="{Binding Device}">
<MenuItem.Icon>
<avalonia:MaterialIcon Kind="ArrangeSendToBack" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Send Backward" Command="{Binding $parent[4].DataContext.SendBackward}" CommandParameter="{Binding Device}">
<MenuItem.Icon>
<avalonia:MaterialIcon Kind="ArrangeSendBackward" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="-" />
<MenuItem Header="Identify input"
Command="{Binding DetectInput}"
CommandParameter="{Binding Device}"
ToolTip.Tip="Teach Artemis which keypresses and/or button presses belong to this device">
<MenuItem.Icon>
<avalonia:MaterialIcon Kind="GestureDoubleTap" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="View properties" Command="{Binding ViewProperties}" CommandParameter="{Binding Device}">
<MenuItem.Icon>
<avalonia:MaterialIcon Kind="Gear" />
</MenuItem.Icon>
</MenuItem>
</MenuFlyout>
</ContentControl.ContextFlyout>
</ContentControl>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<controls:SelectionRectangle Name="SelectionRectangle"
InputElement="{Binding #ZoomBorder}"
SelectionUpdated="{Binding UpdateSelection}"
SelectionFinished="{Binding ApplySelection}"/>
SelectionUpdated="{Binding UpdateSelection}" />
<Border Name="SurfaceBounds"
VerticalAlignment="Top"
HorizontalAlignment="Left"
Width="{Binding MaxTextureSize}"
Height="{Binding MaxTextureSize}"
BorderThickness="2"
BorderBrush="{DynamicResource SystemAccentColorDark3}"
CornerRadius="8" />
</Grid>
</paz:ZoomBorder>
</UserControl>

View File

@ -3,23 +3,30 @@ using Artemis.UI.Avalonia.Shared.Controls;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.PanAndZoom;
using Avalonia.Input;
using Avalonia.Markup.Xaml;
using Avalonia.Media;
using Avalonia.ReactiveUI;
using Avalonia.VisualTree;
namespace Artemis.UI.Avalonia.Screens.SurfaceEditor.Views
{
public class SurfaceEditorView : ReactiveUserControl<SurfaceEditorViewModel>
{
private readonly SelectionRectangle _selectionRectangle;
private readonly Grid _containerGrid;
private readonly ZoomBorder _zoomBorder;
private readonly Border _surfaceBounds;
public SurfaceEditorView()
{
InitializeComponent();
_zoomBorder = this.Find<ZoomBorder>("ZoomBorder");
_containerGrid = this.Find<Grid>("ContainerGrid");
_selectionRectangle = this.Find<SelectionRectangle>("SelectionRectangle");
_surfaceBounds = this.Find<Border>("SurfaceBounds");
((VisualBrush) _zoomBorder.Background).DestinationRect = new RelativeRect(_zoomBorder.OffsetX * -1, _zoomBorder.OffsetY * -1, 20, 20, RelativeUnit.Absolute);
}
@ -33,6 +40,43 @@ namespace Artemis.UI.Avalonia.Screens.SurfaceEditor.Views
{
((VisualBrush) _zoomBorder.Background).DestinationRect = new RelativeRect(_zoomBorder.OffsetX * -1, _zoomBorder.OffsetY * -1, 20, 20, RelativeUnit.Absolute);
_selectionRectangle.BorderThickness = 1 / _zoomBorder.ZoomX;
_surfaceBounds.BorderThickness = new Thickness(2 / _zoomBorder.ZoomX);
}
private void ZoomBorder_OnPointerPressed(object? sender, PointerPressedEventArgs e)
{
if (!e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
return;
if (e.Source is Border {Name: "SurfaceDeviceBorder"})
{
e.Pointer.Capture(_zoomBorder);
e.Handled = true;
ViewModel?.StartMouseDrag(e.GetPosition(_containerGrid));
}
else
ViewModel?.ClearSelection();
}
private void ZoomBorder_OnPointerMoved(object? sender, PointerEventArgs e)
{
if (!e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
return;
if (ReferenceEquals(e.Pointer.Captured, sender))
ViewModel?.UpdateMouseDrag(e.GetPosition(_containerGrid));
}
private void ZoomBorder_OnPointerReleased(object? sender, PointerReleasedEventArgs e)
{
if (e.InitialPressMouseButton != MouseButton.Left)
return;
if (ReferenceEquals(e.Pointer.Captured, sender))
{
ViewModel?.StopMouseDrag(e.GetPosition(_containerGrid));
e.Pointer.Capture(null);
}
}
}
}

View File

@ -28,7 +28,7 @@
<Setter Property="FontSize" Value="24" />
</Style>
<Style Selector="TextBlock.h5">
<Setter Property="FontSize" Value="16" />
<Setter Property="FontSize" Value="18" />
</Style>
<Style Selector="TextBlock.h6">
<Setter Property="FontSize" Value="14" />

View File

@ -1,4 +1,6 @@
using ReactiveUI;
using System;
using System.Reactive.Disposables;
using ReactiveUI;
namespace Artemis.UI.Avalonia
{
@ -13,8 +15,35 @@ namespace Artemis.UI.Avalonia
}
}
public abstract class ActivatableViewModelBase : ViewModelBase, IActivatableViewModel
public abstract class ActivatableViewModelBase : ViewModelBase, IActivatableViewModel, IDisposable
{
/// <inheritdoc />
protected ActivatableViewModelBase()
{
this.WhenActivated(disposables =>
{
Disposable.Create(Dispose).DisposeWith(disposables);
});
}
#region IDisposable
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
}
}
/// <inheritdoc />
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
#endregion
public ViewModelActivator Activator { get; } = new();
}
}