1
0
mirror of https://github.com/Artemis-RGB/Artemis synced 2025-12-31 01:42:02 +00:00

Added surface editor panning/zooming

Replaced surface editor's PNG background with XAML background
This commit is contained in:
Robert 2019-10-18 17:32:03 +02:00
parent 7f49d2ae6d
commit e4dcc3044e
2 changed files with 246 additions and 74 deletions

View File

@ -3,6 +3,7 @@ using System.Collections.ObjectModel;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Windows; using System.Windows;
using System.Windows.Controls;
using System.Windows.Input; using System.Windows.Input;
using System.Windows.Media; using System.Windows.Media;
using Artemis.Core.Events; using Artemis.Core.Events;
@ -11,6 +12,7 @@ using Artemis.Core.Services.Interfaces;
using Artemis.Core.Services.Storage; using Artemis.Core.Services.Storage;
using Artemis.UI.ViewModels.Controls.SurfaceEditor; using Artemis.UI.ViewModels.Controls.SurfaceEditor;
using Artemis.UI.ViewModels.Interfaces; using Artemis.UI.ViewModels.Interfaces;
using MahApps.Metro.Controls;
using RGB.NET.Core; using RGB.NET.Core;
using Stylet; using Stylet;
using Point = System.Windows.Point; using Point = System.Windows.Point;
@ -40,6 +42,8 @@ namespace Artemis.UI.ViewModels.Screens
} }
} }
public string Title => "Surface Editor";
public RectangleGeometry SelectionRectangle { get; set; } public RectangleGeometry SelectionRectangle { get; set; }
public ObservableCollection<SurfaceDeviceViewModel> Devices { get; set; } public ObservableCollection<SurfaceDeviceViewModel> Devices { get; set; }
@ -47,7 +51,16 @@ namespace Artemis.UI.ViewModels.Screens
public ObservableCollection<SurfaceConfiguration> SurfaceConfigurations { get; set; } public ObservableCollection<SurfaceConfiguration> SurfaceConfigurations { get; set; }
public string NewConfigurationName { get; set; } public string NewConfigurationName { get; set; }
public string Title => "Surface Editor"; public double Zoom { get; set; } = 1;
public double ZoomPercentage
{
get => Zoom * 100;
set => Zoom = value / 100;
}
public double PanX { get; set; } = 0;
public double PanY { get; set; } = 0;
private void RgbServiceOnDeviceLoaded(object sender, DeviceEventArgs e) private void RgbServiceOnDeviceLoaded(object sender, DeviceEventArgs e)
{ {
@ -150,23 +163,57 @@ namespace Artemis.UI.ViewModels.Screens
#endregion #endregion
#region Mouse actions #region Device selection
private MouseDragStatus _mouseDragStatus; private MouseDragStatus _mouseDragStatus;
private Point _mouseDragStartPoint; private Point _mouseDragStartPoint;
// ReSharper disable once UnusedMember.Global - Called from view
public void EditorGridMouseClick(object sender, MouseEventArgs e)
{
if (IsPanKeyDown())
return;
var position = e.GetPosition((IInputElement) sender);
if (e.LeftButton == MouseButtonState.Pressed)
StartMouseDrag(position);
else
StopMouseDrag(position);
}
// ReSharper disable once UnusedMember.Global - Called from view
public void EditorGridMouseMove(object sender, MouseEventArgs e)
{
// If holding down Ctrl, pan instead of move/select
if (IsPanKeyDown())
{
Pan(sender, e);
return;
}
var position = e.GetPosition((IInputElement) sender);
if (_mouseDragStatus == MouseDragStatus.Dragging)
MoveSelected(position);
else if (_mouseDragStatus == MouseDragStatus.Selecting)
UpdateSelection(position);
}
private void StartMouseDrag(Point position) private void StartMouseDrag(Point position)
{ {
// If drag started on top of a device, initialise dragging // If drag started on top of a device, initialise dragging
var device = Devices.LastOrDefault(d => d.DeviceRectangle.Contains(position)); var device = Devices.LastOrDefault(d => TransformDeviceRect(d.DeviceRectangle).Contains(position));
if (device != null) if (device != null)
{ {
_mouseDragStatus = MouseDragStatus.Dragging; _mouseDragStatus = MouseDragStatus.Dragging;
// If the device is not selected, deselect others and select only this one // If the device is not selected, deselect others and select only this one (if shift not held)
if (device.SelectionStatus != SelectionStatus.Selected) if (device.SelectionStatus != SelectionStatus.Selected)
{ {
foreach (var others in Devices) if (!Keyboard.IsKeyDown(Key.LeftShift) && !Keyboard.IsKeyDown(Key.RightShift))
others.SelectionStatus = SelectionStatus.None; {
foreach (var others in Devices)
others.SelectionStatus = SelectionStatus.None;
}
device.SelectionStatus = SelectionStatus.Selected; device.SelectionStatus = SelectionStatus.Selected;
} }
@ -196,9 +243,9 @@ namespace Artemis.UI.ViewModels.Screens
var selectedRect = new Rect(_mouseDragStartPoint, position); var selectedRect = new Rect(_mouseDragStartPoint, position);
foreach (var device in Devices) foreach (var device in Devices)
{ {
if (device.DeviceRectangle.IntersectsWith(selectedRect)) if (TransformDeviceRect(device.DeviceRectangle).IntersectsWith(selectedRect))
device.SelectionStatus = SelectionStatus.Selected; device.SelectionStatus = SelectionStatus.Selected;
else else if (!Keyboard.IsKeyDown(Key.LeftShift) && !Keyboard.IsKeyDown(Key.RightShift))
device.SelectionStatus = SelectionStatus.None; device.SelectionStatus = SelectionStatus.None;
} }
} }
@ -209,6 +256,9 @@ namespace Artemis.UI.ViewModels.Screens
private void UpdateSelection(Point position) private void UpdateSelection(Point position)
{ {
if (IsPanKeyDown())
return;
lock (Devices) lock (Devices)
{ {
var selectedRect = new Rect(_mouseDragStartPoint, position); var selectedRect = new Rect(_mouseDragStartPoint, position);
@ -216,9 +266,9 @@ namespace Artemis.UI.ViewModels.Screens
foreach (var device in Devices) foreach (var device in Devices)
{ {
if (device.DeviceRectangle.IntersectsWith(selectedRect)) if (TransformDeviceRect(device.DeviceRectangle).IntersectsWith(selectedRect))
device.SelectionStatus = SelectionStatus.Selected; device.SelectionStatus = SelectionStatus.Selected;
else else if (!Keyboard.IsKeyDown(Key.LeftShift) && !Keyboard.IsKeyDown(Key.RightShift))
device.SelectionStatus = SelectionStatus.None; device.SelectionStatus = SelectionStatus.None;
} }
} }
@ -230,24 +280,92 @@ namespace Artemis.UI.ViewModels.Screens
device.UpdateMouseDrag(position); device.UpdateMouseDrag(position);
} }
// ReSharper disable once UnusedMember.Global - Called from view private Rect TransformDeviceRect(Rect deviceRect)
public void EditorGridMouseClick(object sender, MouseEventArgs e)
{ {
var position = e.GetPosition((IInputElement) sender); // Create the same transform group the view is using
if (e.LeftButton == MouseButtonState.Pressed) var transformGroup = new TransformGroup();
StartMouseDrag(position); transformGroup.Children.Add(new ScaleTransform(Zoom, Zoom));
else transformGroup.Children.Add(new TranslateTransform(PanX, PanY));
StopMouseDrag(position);
// Apply it to the device rect
return transformGroup.TransformBounds(deviceRect);
} }
// ReSharper disable once UnusedMember.Global - Called from view #endregion
public void EditorGridMouseMove(object sender, MouseEventArgs e)
#region Panning and zooming
private Point? _lastPanPosition;
public void EditorGridMouseWheel(object sender, MouseWheelEventArgs e)
{ {
// Get the mouse position relative to the panned / zoomed grid, not very MVVM but I can't find a better way
var relative = e.GetPosition(((Grid)sender).Children[0]);
var absoluteX = relative.X * Zoom + PanX;
var absoluteY = relative.Y * Zoom + PanY;
if (e.Delta > 0)
Zoom *= 1.1;
else
Zoom *= 0.9;
// Limit to a min of 0.1 and a max of 4 (10% - 400% in the view)
Zoom = Math.Max(0.1, Math.Min(4, Zoom));
// Update the PanX/Y to enable zooming relative to cursor
PanX = absoluteX - relative.X * Zoom;
PanY = absoluteY - relative.Y * Zoom;
}
public void EditorGridKeyDown(object sender, KeyEventArgs e)
{
if (e.Key == Key.LeftCtrl || e.Key == Key.RightCtrl)
{
Mouse.OverrideCursor = Cursors.ScrollAll;
}
}
public void EditorGridKeyUp(object sender, KeyEventArgs e)
{
if (e.Key == Key.LeftCtrl || e.Key == Key.RightCtrl)
{
Mouse.OverrideCursor = null;
}
}
public void Pan(object sender, MouseEventArgs e)
{
if (e.LeftButton == MouseButtonState.Released)
{
Mouse.OverrideCursor = Cursors.Arrow;
_lastPanPosition = null;
return;
}
if (_lastPanPosition == null)
_lastPanPosition = e.GetPosition((IInputElement) sender);
// Empty the selection rect since it's shown while mouse is down
SelectionRectangle.Rect = Rect.Empty;
var position = e.GetPosition((IInputElement) sender); var position = e.GetPosition((IInputElement) sender);
if (_mouseDragStatus == MouseDragStatus.Dragging) var delta = _lastPanPosition - position;
MoveSelected(position); PanX -= delta.Value.X;
else if (_mouseDragStatus == MouseDragStatus.Selecting) PanY -= delta.Value.Y;
UpdateSelection(position);
_lastPanPosition = position;
}
public void ResetZoomAndPan()
{
Zoom = 1;
PanX = 0;
PanY = 0;
}
private bool IsPanKeyDown()
{
return Keyboard.IsKeyDown(Key.LeftCtrl) || Keyboard.IsKeyDown(Key.RightCtrl);
} }
#endregion #endregion

View File

@ -13,8 +13,10 @@
<UserControl.Resources> <UserControl.Resources>
<ResourceDictionary> <ResourceDictionary>
<ResourceDictionary.MergedDictionaries> <ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesignTheme.TextBlock.xaml" /> <ResourceDictionary
<ResourceDictionary Source="pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesignTheme.PopupBox.xaml" /> Source="pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesignTheme.TextBlock.xaml" />
<ResourceDictionary
Source="pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesignTheme.PopupBox.xaml" />
</ResourceDictionary.MergedDictionaries> </ResourceDictionary.MergedDictionaries>
</ResourceDictionary> </ResourceDictionary>
</UserControl.Resources> </UserControl.Resources>
@ -32,12 +34,36 @@
<TextBlock Grid.Row="0" Grid.Column="0" Style="{StaticResource MaterialDesignDisplay1TextBlock}">Surface layout</TextBlock> <TextBlock Grid.Row="0" Grid.Column="0" Style="{StaticResource MaterialDesignDisplay1TextBlock}">Surface layout</TextBlock>
<TextBlock Grid.Row="1" Grid.Column="0" Style="{StaticResource MaterialDesignCaptionTextBlock}">The surface is a digital representation of your LED setup. Set this up accurately and effects will seamlessly move from one device to the other.</TextBlock> <TextBlock Grid.Row="1" Grid.Column="0" Style="{StaticResource MaterialDesignCaptionTextBlock}">The surface is a digital representation of your LED setup. Set this up accurately and effects will seamlessly move from one device to the other.</TextBlock>
<materialDesign:Card materialDesign:ShadowAssist.ShadowDepth="Depth1" Grid.Row="2" Grid.Column="0" VerticalAlignment="Stretch" Margin="0,0,5,0"> <materialDesign:Card materialDesign:ShadowAssist.ShadowDepth="Depth1" Grid.Row="2" Grid.Column="0"
<Grid Name="EditorDisplayGrid" VerticalAlignment="Stretch" Margin="0,0,5,0">
Margin="5" <Grid ClipToBounds="True"
KeyUp="{s:Action EditorGridKeyUp}"
KeyDown="{s:Action EditorGridKeyDown}"
MouseWheel="{s:Action EditorGridMouseWheel}"
MouseUp="{s:Action EditorGridMouseClick}" MouseUp="{s:Action EditorGridMouseClick}"
MouseDown="{s:Action EditorGridMouseClick}" MouseDown="{s:Action EditorGridMouseClick}"
MouseMove="{s:Action EditorGridMouseMove}"> MouseMove="{s:Action EditorGridMouseMove}">
<Grid.Background>
<VisualBrush TileMode="Tile" Stretch="Uniform" Viewport="20,20,20,20" ViewportUnits="Absolute">
<VisualBrush.Visual>
<Grid Width="20" Height="20">
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Rectangle Grid.Row="0" Grid.Column="0" Fill="LightGray" />
<Rectangle Grid.Row="0" Grid.Column="1" />
<Rectangle Grid.Row="1" Grid.Column="0" />
<Rectangle Grid.Row="1" Grid.Column="1" Fill="LightGray" />
</Grid>
</VisualBrush.Visual>
</VisualBrush>
</Grid.Background>
<Grid.Triggers> <Grid.Triggers>
<EventTrigger RoutedEvent="Grid.MouseLeftButtonDown"> <EventTrigger RoutedEvent="Grid.MouseLeftButtonDown">
<BeginStoryboard> <BeginStoryboard>
@ -54,73 +80,97 @@
</BeginStoryboard> </BeginStoryboard>
</EventTrigger> </EventTrigger>
</Grid.Triggers> </Grid.Triggers>
<Grid.Background>
<ImageBrush TileMode="Tile" ViewportUnits="Absolute" Viewport="0,0,15,15" ImageSource="/Artemis.UI;component/Resources/tile.png" /> <Grid Name="EditorDisplayGrid">
</Grid.Background> <Grid.RenderTransform>
<ItemsControl ItemsSource="{Binding Devices}" ClipToBounds="True"> <TransformGroup>
<ItemsControl.ItemsPanel> <ScaleTransform ScaleX="{Binding Zoom}" ScaleY="{Binding Zoom}" />
<ItemsPanelTemplate> <TranslateTransform X="{Binding PanX}" Y="{Binding PanY}" />
<Canvas /> </TransformGroup>
</ItemsPanelTemplate> </Grid.RenderTransform>
</ItemsControl.ItemsPanel> <ItemsControl ItemsSource="{Binding Devices}">
<ItemsControl.ItemContainerStyle> <ItemsControl.ItemsPanel>
<Style TargetType="ContentPresenter"> <ItemsPanelTemplate>
<Setter Property="Canvas.Left" Value="{Binding Device.Location.X}" /> <Canvas />
<Setter Property="Canvas.Top" Value="{Binding Device.Location.Y}" /> </ItemsPanelTemplate>
</Style> </ItemsControl.ItemsPanel>
</ItemsControl.ItemContainerStyle> <ItemsControl.ItemContainerStyle>
<ItemsControl.ItemTemplate> <Style TargetType="ContentPresenter">
<DataTemplate> <Setter Property="Canvas.Left" Value="{Binding Device.Location.X}" />
<ContentControl Width="{Binding Device.Size.Width}" Height="{Binding Device.Size.Height}" s:View.Model="{Binding }" > <Setter Property="Canvas.Top" Value="{Binding Device.Location.Y}" />
<ContentControl.ContextMenu> </Style>
<ContextMenu> </ItemsControl.ItemContainerStyle>
<MenuItem Header="Bring to Front"> <ItemsControl.ItemTemplate>
<MenuItem.Icon> <DataTemplate>
<materialDesign:PackIcon Kind="ArrangeBringToFront" /> <ContentControl Width="{Binding Device.Size.Width}"
</MenuItem.Icon> Height="{Binding Device.Size.Height}" s:View.Model="{Binding }">
<MenuItem Header="Bring to Front" Command="{s:Action BringToFront}" CommandParameter="{Binding}"> <ContentControl.ContextMenu>
<ContextMenu>
<MenuItem Header="Bring to Front" Command="{s:Action BringToFront}"
CommandParameter="{Binding}">
<MenuItem.Icon> <MenuItem.Icon>
<materialDesign:PackIcon Kind="ArrangeBringToFront" /> <materialDesign:PackIcon Kind="ArrangeBringToFront" />
</MenuItem.Icon> </MenuItem.Icon>
</MenuItem> </MenuItem>
<MenuItem Header="Bring Forward" Command="{s:Action BringForward}" CommandParameter="{Binding}"> <MenuItem Header="Bring Forward" Command="{s:Action BringForward}"
CommandParameter="{Binding}">
<MenuItem.Icon> <MenuItem.Icon>
<materialDesign:PackIcon Kind="ArrangeBringForward" /> <materialDesign:PackIcon Kind="ArrangeBringForward" />
</MenuItem.Icon> </MenuItem.Icon>
</MenuItem> </MenuItem>
</MenuItem> <MenuItem Header="Send to Back" Command="{s:Action SendToBack}"
<MenuItem Header="Send to Back"> CommandParameter="{Binding}">
<MenuItem.Icon>
<materialDesign:PackIcon Kind="ArrangeSendToBack" />
</MenuItem.Icon>
<MenuItem Header="Send to Back" Command="{s:Action SendToBack}" CommandParameter="{Binding}">
<MenuItem.Icon> <MenuItem.Icon>
<materialDesign:PackIcon Kind="ArrangeSendToBack" /> <materialDesign:PackIcon Kind="ArrangeSendToBack" />
</MenuItem.Icon> </MenuItem.Icon>
</MenuItem> </MenuItem>
<MenuItem Header="Send Backward" Command="{s:Action SendBackward}" CommandParameter="{Binding}"> <MenuItem Header="Send Backward" Command="{s:Action SendBackward}"
CommandParameter="{Binding}">
<MenuItem.Icon> <MenuItem.Icon>
<materialDesign:PackIcon Kind="ArrangeSendBackward" /> <materialDesign:PackIcon Kind="ArrangeSendBackward" />
</MenuItem.Icon> </MenuItem.Icon>
</MenuItem> </MenuItem>
</MenuItem> </ContextMenu>
</ContextMenu> </ContentControl.ContextMenu>
</ContentControl.ContextMenu> </ContentControl>
</ContentControl> </DataTemplate>
</DataTemplate> </ItemsControl.ItemTemplate>
</ItemsControl.ItemTemplate> </ItemsControl>
</ItemsControl> </Grid>
<!-- Multi-selection rectangle --> <!-- Multi-selection rectangle -->
<Path Data="{Binding SelectionRectangle}" Opacity="0" Stroke="{DynamicResource PrimaryHueLightBrush}" StrokeThickness="1" Name="MultiSelectionPath" IsHitTestVisible="False"> <Path Data="{Binding SelectionRectangle}" Opacity="0"
Stroke="{DynamicResource PrimaryHueLightBrush}"
StrokeThickness="1"
Name="MultiSelectionPath"
IsHitTestVisible="False">
<Path.Fill> <Path.Fill>
<SolidColorBrush Color="{DynamicResource Primary400}" Opacity="0.25" /> <SolidColorBrush Color="{DynamicResource Primary400}" Opacity="0.25" />
</Path.Fill> </Path.Fill>
</Path> </Path>
<StackPanel Orientation="Vertical" VerticalAlignment="Bottom" HorizontalAlignment="Right"
Margin="0, 0, 15, 15">
<Slider Margin="0,0,14,0"
Orientation="Vertical"
Minimum="10"
Maximum="400"
Height="100"
FocusVisualStyle="{x:Null}"
Value="{Binding ZoomPercentage}"
Style="{StaticResource MaterialDesignDiscreteSlider}" />
<Button Command="{s:Action ResetZoomAndPan}"
Style="{StaticResource MaterialDesignFloatingActionMiniButton}"
HorizontalAlignment="Right"
ToolTip="Reset zoom &amp; position">
<materialDesign:PackIcon Kind="ImageFilterCenterFocus" Height="24" Width="24" />
</Button>
</StackPanel>
</Grid> </Grid>
</materialDesign:Card> </materialDesign:Card>
<materialDesign:Card materialDesign:ShadowAssist.ShadowDepth="Depth1" Grid.Row="2" Grid.Column="1" VerticalAlignment="Stretch" Margin="5,0,0,0"> <materialDesign:Card materialDesign:ShadowAssist.ShadowDepth="Depth1" Grid.Row="2" Grid.Column="1"
VerticalAlignment="Stretch" Margin="5,0,0,0">
<materialDesign:DialogHost DialogClosing="{s:Action ConfirmationDialogClosing}"> <materialDesign:DialogHost DialogClosing="{s:Action ConfirmationDialogClosing}">
<materialDesign:DialogHost.DialogContent> <materialDesign:DialogHost.DialogContent>
<StackPanel Margin="16"> <StackPanel Margin="16">
@ -129,7 +179,8 @@
</TextBlock> </TextBlock>
<TextBox Margin="0 8 0 0" HorizontalAlignment="Stretch" Text="{Binding NewConfigurationName}" /> <TextBox Margin="0 8 0 0" HorizontalAlignment="Stretch" Text="{Binding NewConfigurationName}" />
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right"> <StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
<Button Style="{StaticResource MaterialDesignFlatButton}" IsDefault="True" Margin="0 8 8 0" Command="materialDesign:DialogHost.CloseDialogCommand"> <Button Style="{StaticResource MaterialDesignFlatButton}" IsDefault="True" Margin="0 8 8 0"
Command="materialDesign:DialogHost.CloseDialogCommand">
<Button.CommandParameter> <Button.CommandParameter>
<system:Boolean xmlns:system="clr-namespace:System;assembly=mscorlib"> <system:Boolean xmlns:system="clr-namespace:System;assembly=mscorlib">
True True
@ -137,7 +188,8 @@
</Button.CommandParameter> </Button.CommandParameter>
ACCEPT ACCEPT
</Button> </Button>
<Button Style="{StaticResource MaterialDesignFlatButton}" IsCancel="True" Margin="0 8 8 0" Command="materialDesign:DialogHost.CloseDialogCommand"> <Button Style="{StaticResource MaterialDesignFlatButton}" IsCancel="True" Margin="0 8 8 0"
Command="materialDesign:DialogHost.CloseDialogCommand">
<Button.CommandParameter> <Button.CommandParameter>
<system:Boolean xmlns:system="clr-namespace:System;assembly=mscorlib"> <system:Boolean xmlns:system="clr-namespace:System;assembly=mscorlib">
False False
@ -172,10 +224,12 @@
VerticalAlignment="Bottom"> VerticalAlignment="Bottom">
<StackPanel <StackPanel
Orientation="Horizontal"> Orientation="Horizontal">
<Button ToolTip="Add a new surface layout" Command="{x:Static materialDesign:DialogHost.OpenDialogCommand}"> <Button ToolTip="Add a new surface layout"
Command="{x:Static materialDesign:DialogHost.OpenDialogCommand}">
<materialDesign:PackIcon Kind="Add" /> <materialDesign:PackIcon Kind="Add" />
</Button> </Button>
<Button ToolTip="Remove selected surface layout" Command="{x:Static materialDesign:DialogHost.OpenDialogCommand}"> <Button ToolTip="Remove selected surface layout"
Command="{x:Static materialDesign:DialogHost.OpenDialogCommand}">
<materialDesign:PackIcon Kind="Delete" /> <materialDesign:PackIcon Kind="Delete" />
</Button> </Button>
</StackPanel> </StackPanel>