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

View File

@ -54,6 +54,7 @@ namespace Artemis.UI.Avalonia.Shared.Controls
public SelectionRectangle() public SelectionRectangle()
{ {
AffectsRender<TextBlock>(BackgroundProperty, BorderBrushProperty, BorderThicknessProperty); AffectsRender<TextBlock>(BackgroundProperty, BorderBrushProperty, BorderThicknessProperty);
IsHitTestVisible = false;
} }
/// <summary> /// <summary>
@ -108,6 +109,9 @@ namespace Artemis.UI.Avalonia.Shared.Controls
private void ParentOnPointerPressed(object? sender, PointerPressedEventArgs e) private void ParentOnPointerPressed(object? sender, PointerPressedEventArgs e)
{ {
if (!e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
return;
e.Pointer.Capture(this); e.Pointer.Capture(this);
_startPosition = e.GetPosition(Parent); _startPosition = e.GetPosition(Parent);
@ -131,6 +135,9 @@ namespace Artemis.UI.Avalonia.Shared.Controls
private void ParentOnPointerReleased(object? sender, PointerReleasedEventArgs e) private void ParentOnPointerReleased(object? sender, PointerReleasedEventArgs e)
{ {
if (!ReferenceEquals(e.Pointer.Captured, this))
return;
e.Pointer.Capture(null); e.Pointer.Capture(null);
if (_displayRect != 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 public interface IWindowService : IArtemisSharedUIService
{ {
@ -11,10 +13,11 @@
void ShowWindow(object viewModel); void ShowWindow(object viewModel);
/// <summary> /// <summary>
/// Given a ViewModel, show its corresponding View as a Dialog /// Given a ViewModel, show its corresponding View as a Dialog
/// </summary> /// </summary>
/// <typeparam name="T">The return type</typeparam>
/// <param name="viewModel">ViewModel to show the View for</param> /// <param name="viewModel">ViewModel to show the View for</param>
/// <returns>DialogResult of the View</returns> /// <returns>A task containing the return value of type <typeparamref name="T"/></returns>
bool? ShowDialog(object viewModel); Task<T> ShowDialog<T>(object viewModel);
} }
} }

View File

@ -1,7 +1,11 @@
using System; using System;
using System.Linq;
using System.Threading.Tasks;
using Artemis.UI.Avalonia.Shared.Exceptions; using Artemis.UI.Avalonia.Shared.Exceptions;
using Artemis.UI.Avalonia.Shared.Services.Interfaces; using Artemis.UI.Avalonia.Shared.Services.Interfaces;
using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Ninject; using Ninject;
namespace Artemis.UI.Avalonia.Shared.Services namespace Artemis.UI.Avalonia.Shared.Services
@ -42,9 +46,23 @@ namespace Artemis.UI.Avalonia.Shared.Services
} }
/// <inheritdoc /> /// <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 #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>
<ItemGroup> <ItemGroup>
<None Remove="Assets\Images\home-banner.png" /> <None Remove="Assets\Images\home-banner.png" />
<None Remove="Screens\SurfaceEditor\Views\ListDeviceView.xaml" />
<None Remove="Screens\SurfaceEditor\Views\SurfaceDeviceView.xaml" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Avalonia" Version="0.10.10" /> <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-white.ico" />
<Content Include="Assets\Images\Logo\bow.ico" /> <Content Include="Assets\Images\Logo\bow.ico" />
</ItemGroup> </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> <ItemGroup>
<Reference Include="RGB.NET.Core"> <Reference Include="RGB.NET.Core">
<HintPath>..\..\..\RGB.NET\bin\net5.0\RGB.NET.Core.dll</HintPath> <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.Core;
using Artemis.UI.Avalonia.Screens.Device.ViewModels;
using Artemis.UI.Avalonia.Screens.Root.ViewModels; using Artemis.UI.Avalonia.Screens.Root.ViewModels;
using Artemis.UI.Avalonia.Screens.SurfaceEditor.ViewModels;
using ReactiveUI; using ReactiveUI;
namespace Artemis.UI.Avalonia.Ninject.Factories 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 public interface ISidebarVmFactory : IVmFactory
{ {
SidebarViewModel SidebarViewModel(IScreen hostScreen); SidebarViewModel SidebarViewModel(IScreen hostScreen);
SidebarCategoryViewModel SidebarCategoryViewModel(ProfileCategory profileCategory); SidebarCategoryViewModel SidebarCategoryViewModel(ProfileCategory profileCategory);
SidebarProfileConfigurationViewModel SidebarProfileConfigurationViewModel(ProfileConfiguration profileConfiguration); 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" Margin="30"
Text=" Welcome to Artemis, the unified RGB platform." /> Text=" Welcome to Artemis, the unified RGB platform." />
<Grid Grid.Row="1" MaxWidth="840" Margin="30" VerticalAlignment="Bottom"> <Grid Grid.Row="1" MaxWidth="840" Margin="30" VerticalAlignment="Bottom" ColumnDefinitions="*,*" RowDefinitions="*,*">
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<Border Classes="card" Margin="8" Grid.ColumnSpan="2"> <Border Classes="card" Margin="8" Grid.ColumnSpan="2">
<Grid VerticalAlignment="Stretch" RowDefinitions="Auto,Auto" ColumnDefinitions="150,*"> <Grid VerticalAlignment="Stretch" RowDefinitions="Auto,Auto" ColumnDefinitions="150,*">
<avalonia:MaterialIcon Kind="Plug" Width="100" Height="100" HorizontalAlignment="Center" VerticalAlignment="Center" /> <avalonia:MaterialIcon Kind="Plug" Width="100" Height="100" HorizontalAlignment="Center" VerticalAlignment="Center"/>
<StackPanel Grid.Row="0" Grid.Column="1"> <StackPanel Grid.Row="0" Grid.Column="1">
<TextBlock FontSize="24" Margin="16 16 16 8">Plugins</TextBlock> <TextBlock Classes="h3">Plugins</TextBlock>
<TextBlock TextWrapping="Wrap" Margin="16 0 16 8" VerticalAlignment="Top"> <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. Artemis is built up using plugins. This means devices, brushes, effects and modules (for supporting games) can all be added via plugins.
</TextBlock> </TextBlock>
<TextBlock TextWrapping="Wrap" Margin="16 0 16 8" VerticalAlignment="Top"> <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. 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> </TextBlock>
<TextBlock TextWrapping="Wrap" Margin="16 0 16 8" VerticalAlignment="Top"> </StackPanel>
We're also keeping track of a list of third-party plugins on our wiki. <controls:HyperlinkButton Grid.Row="1" Grid.ColumnSpan="2" Grid.Column="0" NavigateUri="https://wiki.artemis-rgb.com/en/guides/user/plugins" HorizontalAlignment="Right">
</TextBlock> <controls:HyperlinkButton.ContextMenu>
</StackPanel> <ContextMenu>
<Border Grid.Row="1" Grid.ColumnSpan="2" Grid.Column="0" BorderThickness="0 1 0 0" BorderBrush="{DynamicResource MaterialDesignDivider}" Padding="8"> <MenuItem Header="Test"></MenuItem>
<controls:HyperlinkButton NavigateUri="https://wiki.artemis-rgb.com/en/guides/user/plugins" HorizontalAlignment="Right"> </ContextMenu>
<StackPanel Orientation="Horizontal"> </controls:HyperlinkButton.ContextMenu>
<avalonia:MaterialIcon Kind="OpenInBrowser" /> <StackPanel Orientation="Horizontal">
<TextBlock Margin="8 0 0 0" VerticalAlignment="Center">Get more plugins</TextBlock> <avalonia:MaterialIcon Kind="OpenInBrowser" />
</StackPanel> <TextBlock Margin="8 0 0 0" VerticalAlignment="Center">Get more plugins</TextBlock>
</controls:HyperlinkButton> </StackPanel>
</Border> </controls:HyperlinkButton>
</Grid> </Grid>
</Border> </Border>
<Border Classes="card" Margin="8" Grid.Row="1" Grid.Column="0"> <Border Classes="card" Margin="8" Grid.Row="1" Grid.Column="0">
<Grid VerticalAlignment="Stretch" RowDefinitions="175,95" ColumnDefinitions="150,*"> <Grid VerticalAlignment="Stretch" RowDefinitions="150,95" ColumnDefinitions="150,*">
<avalonia:MaterialIcon Kind="Discord" Width="100" Height="100" <avalonia:MaterialIcon Kind="Discord" Width="100" Height="100" HorizontalAlignment="Center" VerticalAlignment="Center" />
HorizontalAlignment="Center" VerticalAlignment="Center" /> <StackPanel Grid.Row="0" Grid.Column="1">
<StackPanel Grid.Row="0" Grid.Column="1"> <TextBlock Classes="h3">Have a chat</TextBlock>
<TextBlock FontSize="24" Margin="16 16 16 8">Have a chat</TextBlock> <TextBlock TextWrapping="Wrap" VerticalAlignment="Top">
<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.
If you need help, have some feedback or have any other questions feel free to contact us through any of the following channels. </TextBlock>
</TextBlock> </StackPanel>
</StackPanel>
<Border Grid.Row="1" Grid.ColumnSpan="2" Grid.Column="0" BorderThickness="0 1 0 0" <DockPanel Grid.Row="1" Grid.ColumnSpan="2" Grid.Column="0">
BorderBrush="{DynamicResource MaterialDesignDivider}" Padding="8"> <Grid Margin="8" RowDefinitions="*,*">
<DockPanel> <controls:HyperlinkButton Grid.Row="0" NavigateUri="https://github.com/Artemis-RGB/Artemis">
<Grid Margin="8" RowDefinitions="*,*"> <StackPanel Orientation="Horizontal">
<controls:HyperlinkButton Grid.Row="0" NavigateUri="https://github.com/Artemis-RGB/Artemis"> <avalonia:MaterialIcon Kind="Gift" />
<StackPanel Orientation="Horizontal"> <TextBlock Margin="8 0 0 0" VerticalAlignment="Center">GitHub</TextBlock>
<avalonia:MaterialIcon Kind="Gift" /> </StackPanel>
<TextBlock Margin="8 0 0 0" VerticalAlignment="Center">GitHub</TextBlock> </controls:HyperlinkButton>
</StackPanel> <controls:HyperlinkButton Grid.Row="0" HorizontalAlignment="Right" NavigateUri="https://artemis-rgb.com">
</controls:HyperlinkButton> <StackPanel Orientation="Horizontal">
<controls:HyperlinkButton Grid.Row="0" NavigateUri="https://artemis-rgb.com" HorizontalAlignment="Right"> <avalonia:MaterialIcon Kind="Web" />
<StackPanel Orientation="Horizontal"> <TextBlock Margin="8 0 0 0" VerticalAlignment="Center">Website</TextBlock>
<avalonia:MaterialIcon Kind="Web" /> </StackPanel>
<TextBlock Margin="8 0 0 0" VerticalAlignment="Center">Website</TextBlock> </controls:HyperlinkButton>
</StackPanel> <controls:HyperlinkButton Grid.Row="1" NavigateUri="https://discordapp.com/invite/S3MVaC9">
</controls:HyperlinkButton> <StackPanel Orientation="Horizontal">
<controls:HyperlinkButton Grid.Row="1" NavigateUri="https://discordapp.com/invite/S3MVaC9"> <avalonia:MaterialIcon Kind="Discord" />
<StackPanel Orientation="Horizontal"> <TextBlock Margin="8 0 0 0" VerticalAlignment="Center">Discord</TextBlock>
<avalonia:MaterialIcon Kind="Discord" /> </StackPanel>
<TextBlock Margin="8 0 0 0" VerticalAlignment="Center">Discord</TextBlock> </controls:HyperlinkButton>
</StackPanel> <controls:HyperlinkButton Grid.Row="1" HorizontalAlignment="Right" NavigateUri="mailto:spoinky.nl@gmail.com">
</controls:HyperlinkButton> <StackPanel Orientation="Horizontal">
<controls:HyperlinkButton Grid.Row="1" NavigateUri="mailto:spoinky.nl@gmail.com" HorizontalAlignment="Right"> <avalonia:MaterialIcon Kind="Email" />
<StackPanel Orientation="Horizontal"> <TextBlock Margin="8 0 0 0" VerticalAlignment="Center">E-mail</TextBlock>
<avalonia:MaterialIcon Kind="Email" /> </StackPanel>
<TextBlock Margin="8 0 0 0" VerticalAlignment="Center">E-mail</TextBlock> </controls:HyperlinkButton>
</StackPanel> </Grid>
</controls:HyperlinkButton> </DockPanel>
</Grid> </Grid>
</DockPanel> </Border>
</Border> <Border Classes="card" Margin="8" Grid.Row="1" Grid.Column="1">
</Grid> <Grid VerticalAlignment="Stretch" RowDefinitions="150,95" ColumnDefinitions="150,*">
</Border> <avalonia:MaterialIcon Kind="Github" Width="100" Height="100" HorizontalAlignment="Center" VerticalAlignment="Center" />
<Border Classes="card" Margin="8" Grid.Row="1" Grid.Column="1"> <StackPanel Grid.Row="0" Grid.Column="1">
<Grid VerticalAlignment="Stretch" RowDefinitions="175,95" ColumnDefinitions="150,*"> <TextBlock Classes="h3">Open Source</TextBlock>
<avalonia:MaterialIcon Kind="Github" Width="100" Height="100" HorizontalAlignment="Center" VerticalAlignment="Center" /> <TextBlock TextWrapping="Wrap" VerticalAlignment="Top">
<StackPanel Grid.Row="0" Grid.Column="1"> 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 FontSize="16" Margin="16 16 16 8">Open Source</TextBlock> </TextBlock>
<TextBlock TextWrapping="Wrap" Margin="16 0 16 8" VerticalAlignment="Top"> </StackPanel>
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> <controls:HyperlinkButton Grid.Row="1"
</StackPanel> Grid.Column="0"
<Border Grid.Row="1" Grid.ColumnSpan="2" Grid.Column="0" BorderThickness="0 1 0 0" HorizontalAlignment="Center"
BorderBrush="{DynamicResource MaterialDesignDivider}" Padding="8"> NavigateUri="https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&amp;hosted_button_id=VQBAEJYUFLU4J">
<DockPanel> <StackPanel Orientation="Horizontal">
<controls:HyperlinkButton NavigateUri="https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&amp;hosted_button_id=VQBAEJYUFLU4J" <avalonia:MaterialIcon Kind="Gift" />
HorizontalAlignment="Right"> <TextBlock Margin="8 0 0 0" VerticalAlignment="Center">Donate</TextBlock>
<StackPanel Orientation="Horizontal"> </StackPanel>
<avalonia:MaterialIcon Kind="Gift" /> </controls:HyperlinkButton>
<TextBlock Margin="8 0 0 0" VerticalAlignment="Center">Donate</TextBlock> <TextBlock Grid.Row="1"
</StackPanel> Grid.Column="1"
</controls:HyperlinkButton> Foreground="{DynamicResource MaterialDesignBodyLight}"
<TextBlock Foreground="{DynamicResource MaterialDesignBodyLight}" TextWrapping="Wrap"
TextWrapping="Wrap" VerticalAlignment="Center">
Margin="16" Feel like making a donation? It would be gratefully received. Click the button to donate via PayPal.
VerticalAlignment="Center"> </TextBlock>
Feel like you want to make a donation? It would be gratefully received. Click the button to donate via PayPal. </Grid>
</TextBlock> </Border>
</DockPanel>
</Border> </Grid>
</Grid>
</Border>
</Grid>
</Grid> </Grid>
</UserControl> </UserControl>

View File

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

View File

@ -12,7 +12,9 @@
</TabControl.ItemTemplate> </TabControl.ItemTemplate>
<TabControl.ContentTemplate> <TabControl.ContentTemplate>
<DataTemplate> <DataTemplate>
<ContentControl Content="{Binding}" /> <ScrollViewer HorizontalScrollBarVisibility="Hidden" VerticalScrollBarVisibility="Auto">
<ContentControl Content="{Binding}" />
</ScrollViewer>
</DataTemplate> </DataTemplate>
</TabControl.ContentTemplate> </TabControl.ContentTemplate>
</TabControl> </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;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Linq;
using System.Reactive; using System.Reactive;
using System.Reactive.Linq;
using System.Threading.Tasks;
using Artemis.Core; using Artemis.Core;
using Artemis.Core.Services; using Artemis.Core.Services;
using Artemis.UI.Avalonia.Extensions;
using Artemis.UI.Avalonia.Ninject.Factories;
using Avalonia; using Avalonia;
using Avalonia.Skia;
using ReactiveUI; using ReactiveUI;
using SkiaSharp;
namespace Artemis.UI.Avalonia.Screens.SurfaceEditor.ViewModels namespace Artemis.UI.Avalonia.Screens.SurfaceEditor.ViewModels
{ {
public class SurfaceEditorViewModel : MainScreenViewModel 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"; 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); 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> 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) 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:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:controls="clr-namespace:Artemis.UI.Avalonia.Shared.Controls;assembly=Artemis.UI.Avalonia.Shared" 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" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Avalonia.Screens.SurfaceEditor.Views.SurfaceEditorView"> x:Class="Artemis.UI.Avalonia.Screens.SurfaceEditor.Views.SurfaceEditorView">
@ -14,7 +15,10 @@
Focusable="True" Focusable="True"
VerticalAlignment="Stretch" VerticalAlignment="Stretch"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
ZoomChanged="ZoomBorder_OnZoomChanged"> ZoomChanged="ZoomBorder_OnZoomChanged"
PointerPressed="ZoomBorder_OnPointerPressed"
PointerMoved="ZoomBorder_OnPointerMoved"
PointerReleased="ZoomBorder_OnPointerReleased">
<paz:ZoomBorder.Background> <paz:ZoomBorder.Background>
<VisualBrush TileMode="Tile" Stretch="Uniform" SourceRect="0,0,25,25"> <VisualBrush TileMode="Tile" Stretch="Uniform" SourceRect="0,0,25,25">
<VisualBrush.Visual> <VisualBrush.Visual>
@ -27,12 +31,13 @@
</VisualBrush.Visual> </VisualBrush.Visual>
</VisualBrush> </VisualBrush>
</paz:ZoomBorder.Background> </paz:ZoomBorder.Background>
<Grid Background="Transparent"> <Grid Name="ContainerGrid"
<ItemsControl Items="{Binding Devices}"> Background="Transparent">
<ItemsControl Items="{Binding SurfaceDeviceViewModels}" ClipToBounds="False">
<ItemsControl.Styles> <ItemsControl.Styles>
<Style Selector="ContentPresenter"> <Style Selector="ContentPresenter">
<Setter Property="Canvas.Left" Value="{Binding X}" /> <Setter Property="Canvas.Left" Value="{Binding Device.X}" />
<Setter Property="Canvas.Top" Value="{Binding Y}" /> <Setter Property="Canvas.Top" Value="{Binding Device.Y}" />
</Style> </Style>
</ItemsControl.Styles> </ItemsControl.Styles>
<ItemsControl.ItemsPanel> <ItemsControl.ItemsPanel>
@ -42,17 +47,68 @@
</ItemsControl.ItemsPanel> </ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate> <ItemsControl.ItemTemplate>
<DataTemplate> <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> </DataTemplate>
</ItemsControl.ItemTemplate> </ItemsControl.ItemTemplate>
</ItemsControl> </ItemsControl>
<controls:SelectionRectangle Name="SelectionRectangle" <controls:SelectionRectangle Name="SelectionRectangle"
InputElement="{Binding #ZoomBorder}" InputElement="{Binding #ZoomBorder}"
SelectionUpdated="{Binding UpdateSelection}" SelectionUpdated="{Binding UpdateSelection}" />
SelectionFinished="{Binding ApplySelection}"/>
<Border Name="SurfaceBounds"
VerticalAlignment="Top"
HorizontalAlignment="Left"
Width="{Binding MaxTextureSize}"
Height="{Binding MaxTextureSize}"
BorderThickness="2"
BorderBrush="{DynamicResource SystemAccentColorDark3}"
CornerRadius="8" />
</Grid> </Grid>
</paz:ZoomBorder> </paz:ZoomBorder>
</UserControl> </UserControl>

View File

@ -3,23 +3,30 @@ using Artemis.UI.Avalonia.Shared.Controls;
using Avalonia; using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Controls.PanAndZoom; using Avalonia.Controls.PanAndZoom;
using Avalonia.Input;
using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.ReactiveUI; using Avalonia.ReactiveUI;
using Avalonia.VisualTree;
namespace Artemis.UI.Avalonia.Screens.SurfaceEditor.Views namespace Artemis.UI.Avalonia.Screens.SurfaceEditor.Views
{ {
public class SurfaceEditorView : ReactiveUserControl<SurfaceEditorViewModel> public class SurfaceEditorView : ReactiveUserControl<SurfaceEditorViewModel>
{ {
private readonly SelectionRectangle _selectionRectangle; private readonly SelectionRectangle _selectionRectangle;
private readonly Grid _containerGrid;
private readonly ZoomBorder _zoomBorder; private readonly ZoomBorder _zoomBorder;
private readonly Border _surfaceBounds;
public SurfaceEditorView() public SurfaceEditorView()
{ {
InitializeComponent(); InitializeComponent();
_zoomBorder = this.Find<ZoomBorder>("ZoomBorder"); _zoomBorder = this.Find<ZoomBorder>("ZoomBorder");
_containerGrid = this.Find<Grid>("ContainerGrid");
_selectionRectangle = this.Find<SelectionRectangle>("SelectionRectangle"); _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); ((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); ((VisualBrush) _zoomBorder.Background).DestinationRect = new RelativeRect(_zoomBorder.OffsetX * -1, _zoomBorder.OffsetY * -1, 20, 20, RelativeUnit.Absolute);
_selectionRectangle.BorderThickness = 1 / _zoomBorder.ZoomX; _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" /> <Setter Property="FontSize" Value="24" />
</Style> </Style>
<Style Selector="TextBlock.h5"> <Style Selector="TextBlock.h5">
<Setter Property="FontSize" Value="16" /> <Setter Property="FontSize" Value="18" />
</Style> </Style>
<Style Selector="TextBlock.h6"> <Style Selector="TextBlock.h6">
<Setter Property="FontSize" Value="14" /> <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 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(); public ViewModelActivator Activator { get; } = new();
} }
} }