Restructured the VM/Vs folder layout while it's still feasible

This commit is contained in:
Robert committed 2019-11-15 15:53:55 +01:00
1 parent 7443c0eda7
commit 368b73ce9c
48 files changed
+397 -469

No files matched your search

@@ -0,0 +1,24 @@
<UserControl x:Class="Artemis.UI.Screens.Dialogs.ConfirmDialogView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:s="https://github.com/canton7/Stylet"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
xmlns:dialogs="clr-namespace:Artemis.UI.Screens.Dialogs"
mc:Ignorable="d"
d:DesignHeight="163.274" d:DesignWidth="254.425"
d:DataContext="{d:DesignInstance dialogs:ConfirmDialogViewModel}">
<StackPanel Margin="16">
<TextBlock Style="{StaticResource MaterialDesignTitleTextBlock}" Text="{Binding Header}" TextWrapping="Wrap" />
<TextBlock Style="{StaticResource MaterialDesignBody1TextBlock}" Margin="0 20 0 20" Text="{Binding Text}"
TextWrapping="Wrap" />
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
<Button Style="{StaticResource MaterialDesignFlatButton}" IsCancel="True" Margin="0 8 8 0"
Command="{s:Action Cancel}" Content="{Binding CancelText}" />
<Button Style="{StaticResource MaterialDesignFlatButton}" IsDefault="True" Margin="0 8 0 0"
Command="{s:Action Confirm}" Content="{Binding ConfirmText}" />
</StackPanel>
</StackPanel>
</UserControl>
@@ -0,0 +1,30 @@
using Artemis.UI.ViewModels.Dialogs;
namespace Artemis.UI.Screens.Dialogs
{
public class ConfirmDialogViewModel : DialogViewModelBase
{
public ConfirmDialogViewModel(string header, string text, string confirmText, string cancelText)
{
Header = header;
Text = text;
ConfirmText = confirmText;
CancelText = cancelText;
}
public string Header { get; }
public string Text { get; }
public string ConfirmText { get; }
public string CancelText { get; }
public void Confirm()
{
Session.Close(true);
}
public void Cancel()
{
Session.Close(false);
}
}
}
+217
View File
@@ -0,0 +1,217 @@
<UserControl x:Class="Artemis.UI.Screens.Home.HomeView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:s="https://github.com/canton7/Stylet"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
xmlns:home="clr-namespace:Artemis.UI.Screens.Home"
mc:Ignorable="d"
d:DesignHeight="574.026"
d:DesignWidth="1029.87"
d:DataContext="{d:DesignInstance home:HomeViewModel, IsDesignTimeCreatable=True}">
<UserControl.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary
Source="pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesignTheme.Button.xaml" />
<ResourceDictionary
Source="pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesignTheme.PopupBox.xaml" />
<ResourceDictionary
Source="pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesignTheme.TextBlock.xaml" />
<!-- throw in some extra colour for our floating action button -->
<ResourceDictionary
Source="pack://application:,,,/MaterialDesignColors;component/Themes/MaterialDesignColor.Green.Named.Primary.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</UserControl.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid HorizontalAlignment="Center" VerticalAlignment="Top" Margin="16">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Image Source="/Resources/logo-512.png" Stretch="Uniform" Width="100" Height="100" />
<StackPanel Grid.Column="1" Margin="24 0 0 0" VerticalAlignment="Center">
<TextBlock Style="{StaticResource MaterialDesignDisplay1TextBlock}" TextWrapping="Wrap">Welcome to Artemis, RGB on steroids.</TextBlock>
<Button Style="{StaticResource MaterialDesignFlatButton}"
Command="{x:Static materialDesign:DrawerHost.OpenDrawerCommand}">
<StackPanel Orientation="Horizontal">
<materialDesign:PackIcon Kind="Binoculars" />
<TextBlock Margin="8 0 0 0">EXPLORE</TextBlock>
</StackPanel>
</Button>
</StackPanel>
</Grid>
<ScrollViewer Grid.Row="1" HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Disabled"
HorizontalAlignment="Center"
VerticalAlignment="Bottom"
Margin="0 0 0 32">
<Grid>
<StackPanel Orientation="Horizontal">
<materialDesign:Card Width="420" Margin="8 2 4 16" Height="Auto">
<Grid VerticalAlignment="Stretch">
<Grid.RowDefinitions>
<RowDefinition Height="155" />
<RowDefinition Height="95" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="200" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<materialDesign:PackIcon Kind="Discord" Width="140" Height="140"
HorizontalAlignment="Center" VerticalAlignment="Center" />
<StackPanel Grid.Row="0" Grid.Column="1">
<TextBlock Style="{StaticResource MaterialDesignHeadlineTextBlock}" Margin="16 16 16 8">Have a chat</TextBlock>
<TextBlock TextWrapping="Wrap" Margin="16 0 16 8"
Foreground="{DynamicResource MaterialDesignBodyLight}"
VerticalAlignment="Top">
<Run
Text="If you need help, have some feedback or have any other questions feel free to contact us through any of the following channels" />
<Run Text=". " />
</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">
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<Button Style="{DynamicResource MaterialDesignFlatButton}"
HorizontalAlignment="Left"
x:Name="GitHubButton" Command="{s:Action OpenUrl}"
CommandParameter="https://github.com/SpoinkyNL/Artemis">
<StackPanel Orientation="Horizontal">
<materialDesign:PackIcon Kind="GithubCircle" />
<TextBlock Margin="8 0 0 0" VerticalAlignment="Center">GitHub</TextBlock>
</StackPanel>
</Button>
<Button Grid.Row="0" Style="{DynamicResource MaterialDesignFlatButton}"
HorizontalAlignment="Right"
x:Name="TwitterButton" Command="{s:Action OpenUrl}"
CommandParameter="https://twitter.com/spoinkynl">
<StackPanel Orientation="Horizontal">
<materialDesign:PackIcon Kind="Twitter" />
<TextBlock Margin="8 0 0 0" VerticalAlignment="Center">Twitter</TextBlock>
</StackPanel>
</Button>
<Button Grid.Row="1" Style="{DynamicResource MaterialDesignFlatButton}"
HorizontalAlignment="Left"
x:Name="DiscordButton" Command="{s:Action OpenUrl}"
CommandParameter="https://discordapp.com/invite/S3MVaC9">
<StackPanel Orientation="Horizontal">
<materialDesign:PackIcon Kind="Discord" />
<TextBlock Margin="8 0 0 0" VerticalAlignment="Center">Discord</TextBlock>
</StackPanel>
</Button>
<Button Grid.Row="1" Style="{DynamicResource MaterialDesignFlatButton}"
HorizontalAlignment="Right"
x:Name="MailButton" Command="{s:Action OpenUrl}"
CommandParameter="mailto:spoinky.nl@gmail.com">
<StackPanel Orientation="Horizontal">
<materialDesign:PackIcon Kind="Email" />
<TextBlock Margin="8 0 0 0" VerticalAlignment="Center">E-mail</TextBlock>
</StackPanel>
</Button>
</Grid>
</DockPanel>
</Border>
</Grid>
</materialDesign:Card>
<materialDesign:Card Width="420" Margin="8 2 4 16" Height="Auto">
<Grid VerticalAlignment="Stretch">
<Grid.RowDefinitions>
<RowDefinition Height="155" />
<RowDefinition Height="95" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="200" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<materialDesign:PackIcon Kind="GithubCircle" Width="160" Height="160"
HorizontalAlignment="Center" VerticalAlignment="Center" />
<StackPanel Grid.Row="0" Grid.Column="1">
<TextBlock Style="{StaticResource MaterialDesignHeadlineTextBlock}" Margin="16 16 16 8">Open Source</TextBlock>
<TextBlock TextWrapping="Wrap" Margin="16 0 16 8"
Foreground="{DynamicResource MaterialDesignBodyLight}"
VerticalAlignment="Top">
<Run
Text="This project is completely open source. If you like it and want to say thanks you could hit the GitHub Star button, tweet or post about it" />
<Run Text=". You can also make your own modules!" />
</TextBlock>
</StackPanel>
<Border Grid.Row="1" Grid.ColumnSpan="2" Grid.Column="0" BorderThickness="0 1 0 0"
BorderBrush="{DynamicResource MaterialDesignDivider}" Padding="8">
<DockPanel>
<Button Style="{DynamicResource MaterialDesignFlatButton}"
DockPanel.Dock="Right"
x:Name="DonateButton" Command="{s:Action OpenUrl}"
CommandParameter="https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&amp;hosted_button_id=VQBAEJYUFLU4J">
<StackPanel Orientation="Horizontal">
<materialDesign:PackIcon Kind="Gift" />
<TextBlock Margin="8 0 0 0" VerticalAlignment="Center">Donate</TextBlock>
</StackPanel>
</Button>
<TextBlock Foreground="{DynamicResource MaterialDesignBodyLight}"
TextWrapping="Wrap" Margin="16" VerticalAlignment="Center">
<Run
Text="Feel like you want to make a donation? It would be gratefully received. Click the button to donate via " />
<Run Text="Paypal" /><Run Text="." />
</TextBlock>
</DockPanel>
</Border>
</Grid>
</materialDesign:Card>
</StackPanel>
</Grid>
</ScrollViewer>
<!-- PopupBox could be nice in the future when we actually have some stuff to send ppl to -->
<!--<materialDesign:PopupBox Style="{StaticResource MaterialDesignMultiFloatingActionPopupBox}"
HorizontalAlignment="Right" VerticalAlignment="Bottom"
Margin="32" Grid.Row="1">
--><!-- add the visibility binding https://github.com/ButchersBoy/MaterialDesignInXamlToolkit/issues/723 --><!--
<StackPanel Visibility="{Binding Path=IsPopupOpen, ElementName=MyPopupBox, Converter={StaticResource BoolToVisibilityConverter}}">
<Button ToolTip="GitHub" Command="{s:Action OpenUrl}" CommandParameter="https://github.com/SpoinkyNL/Artemis">
<materialDesign:PackIcon Kind="GithubCircle" Height="20" Width="20" />
</Button>
<Button ToolTip="Twitter" Command="{s:Action OpenUrl}" CommandParameter="https://github.com/SpoinkyNL/Artemis"
Background="{DynamicResource PrimaryHueMidBrush}"
Foreground="{DynamicResource PrimaryHueMidForegroundBrush}">
<materialDesign:PackIcon Kind="Twitter" />
</Button>
<Button ToolTip="Chat" Command="{s:Action OpenUrl}" CommandParameter="https://github.com/SpoinkyNL/Artemis">
--><!-- mix up the colours by brinking in a named palette (see merged dictionaries at top) --><!--
<Button.Background>
<SolidColorBrush Color="{StaticResource GreenPrimary500}" />
</Button.Background>
<Button.Foreground>
<SolidColorBrush Color="{StaticResource GreenPrimary500Foreground}" />
</Button.Foreground>
<materialDesign:PackIcon Kind="Message" />
</Button>
<Button ToolTip="Email" Command="{s:Action OpenUrl}" CommandParameter="https://github.com/SpoinkyNL/Artemis"
Background="{DynamicResource SecondaryAccentBrush}"
Foreground="{DynamicResource SecondaryAccentForegroundBrush}"
>
<materialDesign:PackIcon Kind="Email" />
</Button>
<Button ToolTip="Feel like you want to make a donation? It would be gratefully received. Click the button to donate via Pledgie."
Command="{s:Action OpenUrl}" CommandParameter="https://github.com/SpoinkyNL/Artemis">
<Button.Background>
<SolidColorBrush Color="{StaticResource GreenPrimary200}" />
</Button.Background>
<Button.Foreground>
<SolidColorBrush Color="{StaticResource GreenPrimary200Foreground}" />
</Button.Foreground>
<materialDesign:PackIcon Kind="Gift" />
</Button>
</StackPanel>
</materialDesign:PopupBox>-->
</Grid>
</UserControl>
@@ -0,0 +1,18 @@
using System;
using System.Diagnostics;
using Stylet;
namespace Artemis.UI.Screens.Home
{
public class HomeViewModel : Screen, IScreenViewModel
{
public string Title => "Home";
public void OpenUrl(string url)
{
// Don't open anything but valid URIs
if (Uri.IsWellFormedUriString(url, UriKind.RelativeOrAbsolute))
Process.Start(url);
}
}
}
@@ -0,0 +1,9 @@
using Stylet;
namespace Artemis.UI.Screens
{
public interface IScreenViewModel : IScreen
{
string Title { get; }
}
}
@@ -0,0 +1,24 @@
<UserControl x:Class="Artemis.UI.Screens.Module.ModuleRootView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:dragablz="http://dragablz.net/winfx/xaml/dragablz"
xmlns:s="https://github.com/canton7/Stylet"
xmlns:module="clr-namespace:Artemis.UI.Screens.Module"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800"
d:DataContext="{d:DesignInstance module:ModuleRootViewModel}">
<dragablz:TabablzControl Margin="0 -1 0 0" ItemsSource="{Binding Items}" SelectedItem="{Binding ActiveItem}" FixedHeaderCount="{Binding FixedHeaderCount}">
<dragablz:TabablzControl.HeaderItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}" />
</DataTemplate>
</dragablz:TabablzControl.HeaderItemTemplate>
<dragablz:TabablzControl.ContentTemplate>
<DataTemplate>
<ContentControl s:View.Model="{Binding}" IsTabStop="False" />
</DataTemplate>
</dragablz:TabablzControl.ContentTemplate>
</dragablz:TabablzControl>
</UserControl>
@@ -0,0 +1,26 @@
using Artemis.Core.Plugins.Abstract;
using Artemis.UI.Ninject.Factories;
using Stylet;
namespace Artemis.UI.Screens.Module
{
public class ModuleRootViewModel : Conductor<ModuleViewModel>.Collection.OneActive
{
public ModuleRootViewModel(Core.Plugins.Abstract.Module module, IProfileEditorViewModelFactory profileEditorViewModelFactory)
{
Module = module;
// Add the profile editor and module VMs
var profileEditor = profileEditorViewModelFactory.CreateModuleViewModel(Module);
Items.Add(profileEditor);
Items.AddRange(Module.GetViewModels());
// Activate the profile editor
ActiveItem = profileEditor;
}
public string Title => Module?.DisplayName;
public Core.Plugins.Abstract.Module Module { get; }
public int FixedHeaderCount => Items.Count;
}
}
@@ -0,0 +1,200 @@
<UserControl x:Class="Artemis.UI.Screens.Module.ProfileEditor.ProfileEditorView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
xmlns:s="https://github.com/canton7/Stylet"
xmlns:profileEditor="clr-namespace:Artemis.UI.Screens.Module.ProfileEditor"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800"
d:DataContext="{d:DesignInstance {x:Type profileEditor:ProfileEditorViewModel}}">
<UserControl.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary
Source="pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesignTheme.TextBlock.xaml" />
<ResourceDictionary
Source="pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesignTheme.PopupBox.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</UserControl.Resources>
<Grid Margin="16">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="300" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
<RowDefinition Height="200" />
</Grid.RowDefinitions>
<StackPanel Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="3">
<TextBlock>
<materialDesign:PackIcon Kind="AboutOutline" Margin="0 0 0 -3" /> The profile defines what colors the LEDs will have. Multiple groups of LEDs are defined into layers. On these layers you can apply effects.
</TextBlock>
<Border BorderBrush="{DynamicResource MaterialDesignDivider}" BorderThickness="0 0 0 1" Margin="0 10" />
</StackPanel>
<!-- Design area -->
<materialDesign:Card materialDesign:ShadowAssist.ShadowDepth="Depth1" Grid.Row="1" Grid.Column="0" VerticalAlignment="Stretch">
<Grid ClipToBounds="True"
KeyUp="{s:Action EditorGridKeyUp}"
KeyDown="{s:Action EditorGridKeyDown}"
MouseWheel="{s:Action EditorGridMouseWheel}"
MouseUp="{s:Action EditorGridMouseClick}"
MouseDown="{s:Action EditorGridMouseClick}"
MouseMove="{s:Action EditorGridMouseMove}">
<Grid.Background>
<VisualBrush TileMode="Tile" Stretch="Uniform"
Viewport="{Binding PanZoomViewModel.BackgroundViewport}" 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>
<EventTrigger RoutedEvent="Grid.MouseLeftButtonDown">
<BeginStoryboard>
<Storyboard TargetName="MultiSelectionPath" TargetProperty="Opacity">
<DoubleAnimation From="0" To="1" Duration="0:0:0.1" />
</Storyboard>
</BeginStoryboard>
</EventTrigger>
<EventTrigger RoutedEvent="Grid.MouseLeftButtonUp">
<BeginStoryboard>
<Storyboard TargetName="MultiSelectionPath" TargetProperty="Opacity">
<DoubleAnimation From="1" To="0" Duration="0:0:0.2" />
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Grid.Triggers>
<Grid Name="EditorDisplayGrid">
<Grid.RenderTransform>
<TransformGroup>
<ScaleTransform ScaleX="{Binding PanZoomViewModel.Zoom}" ScaleY="{Binding PanZoomViewModel.Zoom}" />
<TranslateTransform X="{Binding PanZoomViewModel.PanX}" Y="{Binding PanZoomViewModel.PanY}" />
</TransformGroup>
</Grid.RenderTransform>
<ItemsControl ItemsSource="{Binding Devices}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemContainerStyle>
<Style TargetType="ContentPresenter">
<Setter Property="Canvas.Left" Value="{Binding X}" />
<Setter Property="Canvas.Top" Value="{Binding Y}" />
</Style>
</ItemsControl.ItemContainerStyle>
<ItemsControl.ItemTemplate>
<DataTemplate>
<ContentControl Width="{Binding Device.RgbDevice.Size.Width}"
Height="{Binding Device.RgbDevice.Size.Height}"
s:View.Model="{Binding}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
<!-- Multi-selection rectangle -->
<Path Data="{Binding SelectionRectangle}" Opacity="0"
Stroke="{DynamicResource PrimaryHueLightBrush}"
StrokeThickness="1"
Name="MultiSelectionPath"
IsHitTestVisible="False">
<Path.Fill>
<SolidColorBrush Color="{DynamicResource Primary400}" Opacity="0.25" />
</Path.Fill>
</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 PanZoomViewModel.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 Background="{StaticResource MaterialDesignPaper}" Visibility="{Binding IsInitializing, Converter={StaticResource BoolToVisibilityConverter}, Mode=OneWay}">
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
<TextBlock FontSize="16" HorizontalAlignment="Center" VerticalAlignment="Center">
Initializing LED visualization...
</TextBlock>
<ProgressBar Style="{StaticResource MaterialDesignCircularProgressBar}" Value="0" IsIndeterminate="True" />
</StackPanel>
</Grid>
</Grid>
</materialDesign:Card>
<GridSplitter Grid.Row="1" Grid.Column="1" Width="5" HorizontalAlignment="Stretch" ShowsPreview="True" Cursor="SizeWE" Margin="5 0" />
<!-- Right panels -->
<Grid Grid.Row="1" Grid.Column="2">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<materialDesign:Card Grid.Row="0" materialDesign:ShadowAssist.ShadowDepth="Depth1" VerticalAlignment="Stretch">
<TextBlock>Right top</TextBlock>
</materialDesign:Card>
<GridSplitter Grid.Row="1" Height="5" HorizontalAlignment="Stretch" ShowsPreview="True" Cursor="SizeNS" Margin="0 5" />
<materialDesign:Card Grid.Row="2" materialDesign:ShadowAssist.ShadowDepth="Depth1" VerticalAlignment="Stretch">
<TextBlock>Right bottom</TextBlock>
</materialDesign:Card>
</Grid>
<GridSplitter Grid.Row="2" Grid.Column="0" Grid.ColumnSpan="3" Height="5" HorizontalAlignment="Stretch" ShowsPreview="True" Cursor="SizeNS" Margin="0 5" />
<!-- Bottom panels -->
<Grid Grid.Row="3" Grid.Column="0" Grid.ColumnSpan="3">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<materialDesign:Card Grid.Column="0" materialDesign:ShadowAssist.ShadowDepth="Depth1" VerticalAlignment="Stretch">
<TextBlock>Bottom left</TextBlock>
</materialDesign:Card>
<GridSplitter Grid.Column="1" Width="5" Margin="5 0" HorizontalAlignment="Stretch" ShowsPreview="True" Cursor="SizeWE" />
<materialDesign:Card Grid.Column="2" materialDesign:ShadowAssist.ShadowDepth="Depth1" VerticalAlignment="Stretch">
<TextBlock>Bottom right</TextBlock>
</materialDesign:Card>
</Grid>
</Grid>
</UserControl>
@@ -0,0 +1,214 @@
using System;
using System.Collections.ObjectModel;
using System.Linq;
using System.Windows;
using System.Windows.Input;
using System.Windows.Media;
using Artemis.Core.Events;
using Artemis.Core.Models.Surface;
using Artemis.Core.Plugins.Abstract;
using Artemis.Core.Services;
using Artemis.Core.Services.Storage;
using Artemis.UI.Screens.Module.ProfileEditor.Visualization;
using Artemis.UI.Screens.Shared;
using Artemis.UI.Screens.SurfaceEditor;
using RGB.NET.Core;
using Stylet;
using Point = System.Windows.Point;
namespace Artemis.UI.Screens.Module.ProfileEditor
{
public class ProfileEditorViewModel : ModuleViewModel
{
private readonly TimerUpdateTrigger _updateTrigger;
public ProfileEditorViewModel(Core.Plugins.Abstract.Module module, ISurfaceService surfaceService, ISettingsService settingsService) : base(module, "Profile Editor")
{
surfaceService.ActiveSurfaceConfigurationChanged += OnActiveSurfaceConfigurationChanged;
Devices = new ObservableCollection<ProfileDeviceViewModel>();
Execute.OnUIThread(() =>
{
SelectionRectangle = new RectangleGeometry();
PanZoomViewModel = new PanZoomViewModel();
});
ApplySurfaceConfiguration(surfaceService.ActiveSurface);
// Borrow RGB.NET's update trigger but limit the FPS
var targetFpsSetting = settingsService.GetSetting("TargetFrameRate", 25);
var editorTargetFpsSetting = settingsService.GetSetting("EditorTargetFrameRate", 15);
var targetFps = Math.Min(targetFpsSetting.Value, editorTargetFpsSetting.Value);
_updateTrigger = new TimerUpdateTrigger {UpdateFrequency = 1.0 / targetFps};
_updateTrigger.Update += UpdateLeds;
}
public ObservableCollection<ProfileDeviceViewModel> Devices { get; set; }
public bool IsInitializing { get; private set; }
public RectangleGeometry SelectionRectangle { get; set; }
public PanZoomViewModel PanZoomViewModel { get; set; }
private void OnActiveSurfaceConfigurationChanged(object sender, SurfaceConfigurationEventArgs e)
{
ApplySurfaceConfiguration(e.Surface);
}
private void UpdateLeds(object sender, CustomUpdateData customUpdateData)
{
if (IsInitializing)
IsInitializing = Devices.Any(d => !d.AddedLeds);
foreach (var profileDeviceViewModel in Devices)
profileDeviceViewModel.Update();
}
private void ApplySurfaceConfiguration(Surface surface)
{
// Make sure all devices have an up-to-date VM
foreach (var surfaceDeviceConfiguration in surface.Devices)
{
// Create VMs for missing devices
var viewModel = Devices.FirstOrDefault(vm => vm.Device.RgbDevice == surfaceDeviceConfiguration.RgbDevice);
if (viewModel == null)
{
// Create outside the UI thread to avoid slowdowns as much as possible
var profileDeviceViewModel = new ProfileDeviceViewModel(surfaceDeviceConfiguration);
Execute.OnUIThread(() =>
{
// Gotta call IsInitializing on the UI thread or its never gets picked up
IsInitializing = true;
Devices.Add(profileDeviceViewModel);
});
}
// Update existing devices
else
viewModel.Device = surfaceDeviceConfiguration;
}
// Sort the devices by ZIndex
Execute.OnUIThread(() =>
{
foreach (var device in Devices.OrderBy(d => d.ZIndex).ToList())
Devices.Move(Devices.IndexOf(device), device.ZIndex - 1);
});
}
protected override void OnActivate()
{
_updateTrigger.Start();
base.OnActivate();
}
protected override void OnDeactivate()
{
_updateTrigger.Stop();
base.OnDeactivate();
}
#region Selection
private MouseDragStatus _mouseDragStatus;
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);
var relative = PanZoomViewModel.GetRelativeMousePosition(sender, e);
if (e.LeftButton == MouseButtonState.Pressed)
StartMouseDrag(position, relative);
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.Selecting)
UpdateSelection(position);
}
private void StartMouseDrag(Point position, Point relative)
{
_mouseDragStatus = MouseDragStatus.Selecting;
_mouseDragStartPoint = position;
// Any time dragging starts, start with a new rect
SelectionRectangle.Rect = new Rect();
}
private void StopMouseDrag(Point position)
{
var selectedRect = new Rect(_mouseDragStartPoint, position);
// TODO: Select LEDs
Mouse.OverrideCursor = null;
_mouseDragStatus = MouseDragStatus.None;
}
private void UpdateSelection(Point position)
{
if (IsPanKeyDown())
return;
lock (Devices)
{
var selectedRect = new Rect(_mouseDragStartPoint, position);
SelectionRectangle.Rect = selectedRect;
// TODO: Highlight LEDs
}
}
#endregion
#region Panning and zooming
public void EditorGridMouseWheel(object sender, MouseWheelEventArgs e)
{
PanZoomViewModel.ProcessMouseScroll(sender, e);
}
public void EditorGridKeyDown(object sender, KeyEventArgs e)
{
if ((e.Key == Key.LeftCtrl || e.Key == Key.RightCtrl) && e.IsDown)
Mouse.OverrideCursor = Cursors.ScrollAll;
}
public void EditorGridKeyUp(object sender, KeyEventArgs e)
{
if ((e.Key == Key.LeftCtrl || e.Key == Key.RightCtrl) && e.IsUp)
Mouse.OverrideCursor = null;
}
public void Pan(object sender, MouseEventArgs e)
{
PanZoomViewModel.ProcessMouseMove(sender, e);
// Empty the selection rect since it's shown while mouse is down
SelectionRectangle.Rect = Rect.Empty;
}
public void ResetZoomAndPan()
{
PanZoomViewModel.Reset();
}
private bool IsPanKeyDown()
{
return Keyboard.IsKeyDown(Key.LeftCtrl) || Keyboard.IsKeyDown(Key.RightCtrl);
}
#endregion
}
}
@@ -0,0 +1,51 @@
<UserControl x:Class="Artemis.UI.Screens.Module.ProfileEditor.Visualization.ProfileDeviceView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:s="https://github.com/canton7/Stylet"
xmlns:converters="clr-namespace:Artemis.UI.Converters"
xmlns:visualization="clr-namespace:Artemis.UI.Screens.Module.ProfileEditor.Visualization"
mc:Ignorable="d"
d:DataContext="{d:DesignInstance {x:Type visualization:ProfileDeviceViewModel}}"
d:DesignHeight="450" d:DesignWidth="800">
<UserControl.Resources>
<converters:NullToImageConverter x:Key="NullToImageConverter" />
<converters:NullToVisibilityConverter x:Key="NullToVisibilityConverter" />
</UserControl.Resources>
<Grid>
<!-- Device image with fallback -->
<Image Source="{Binding Device.RgbDevice.DeviceInfo.Image, Converter={StaticResource NullToImageConverter}, Mode=OneWay}" />
<Rectangle Fill="{DynamicResource ControlBackgroundBrush}"
Stroke="{DynamicResource ControlBorderBrush}"
StrokeThickness="1"
Visibility="{Binding Device.RgbDevice.DeviceInfo.Image, ConverterParameter=Inverted, Converter={StaticResource NullToVisibilityConverter}, Mode=OneWay}" />
<TextBlock Text="{Binding Device.RgbDevice.DeviceInfo.DeviceName}"
Visibility="{Binding Device.RgbDevice.DeviceInfo.Image, ConverterParameter=Inverted, Converter={StaticResource NullToVisibilityConverter}, Mode=OneWay}"
HorizontalAlignment="Center"
VerticalAlignment="Center"
TextWrapping="Wrap"
TextAlignment="Center" />
<!-- LEDs -->
<ItemsControl ItemsSource="{Binding Leds}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemContainerStyle>
<Style TargetType="{x:Type ContentPresenter}">
<Setter Property="Canvas.Left" Value="{Binding X, Mode=OneWay}" />
<Setter Property="Canvas.Top" Value="{Binding Y, Mode=OneWay}" />
</Style>
</ItemsControl.ItemContainerStyle>
<ItemsControl.ItemTemplate>
<DataTemplate>
<ContentControl Width="{Binding Width, Mode=OneWay}" Height="{Binding Height, Mode=OneWay}" s:View.Model="{Binding}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
</UserControl>
@@ -0,0 +1,79 @@
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading.Tasks;
using System.Windows;
using Artemis.Core.Models.Surface;
using Stylet;
namespace Artemis.UI.Screens.Module.ProfileEditor.Visualization
{
public class ProfileDeviceViewModel : PropertyChangedBase
{
public ProfileDeviceViewModel(Device device)
{
Device = device;
Leds = new ObservableCollection<ProfileLedViewModel>();
if (Device.RgbDevice != null)
Task.Run(AddLedsAsync);
}
public ObservableCollection<ProfileLedViewModel> Leds { get; set; }
public Device Device { get; set; }
public bool AddedLeds { get; private set; }
public double X
{
get => Device.X;
set => Device.X = value;
}
public double Y
{
get => Device.Y;
set => Device.Y = value;
}
public int ZIndex
{
get => Device.ZIndex;
set => Device.ZIndex = value;
}
public Rect DeviceRectangle => Device.RgbDevice == null
? new Rect()
: new Rect(X, Y, Device.RgbDevice.Size.Width, Device.RgbDevice.Size.Height);
/// <summary>
/// Adds LEDs in batches of 5 to avoid UI freezes
/// </summary>
/// <returns></returns>
private async Task AddLedsAsync()
{
var index = 0;
foreach (var led in Device.RgbDevice.ToList())
{
Execute.OnUIThreadSync(() => Leds.Add(new ProfileLedViewModel(led)));
if (index % 5 == 0)
await Task.Delay(1);
index++;
}
AddedLeds = true;
}
/// <summary>
/// Update the color of all LEDs if finished adding
/// </summary>
public void Update()
{
if (!AddedLeds)
return;
foreach (var ledViewModel in Leds)
ledViewModel.Update();
}
}
}
@@ -0,0 +1,32 @@
<UserControl
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:converters="clr-namespace:Artemis.UI.Converters"
xmlns:visualization="clr-namespace:Artemis.UI.Screens.Module.ProfileEditor.Visualization"
x:Class="Artemis.UI.Screens.Module.ProfileEditor.Visualization.ProfileLedView"
mc:Ignorable="d"
d:DataContext="{d:DesignInstance {x:Type visualization:ProfileLedViewModel}}"
d:DesignHeight="25" d:DesignWidth="25">
<UserControl.Resources>
<converters:NullToImageConverter x:Key="NullToImageConverter" />
</UserControl.Resources>
<Canvas Width="{Binding Width, Mode=OneWay}" Height="{Binding Height, Mode=OneWay}">
<Canvas.Background>
<ImageBrush AlignmentX="Center" AlignmentY="Center" Stretch="Fill"
ImageSource="{Binding Led.Image, Converter={StaticResource NullToImageConverter}, Mode=OneWay}" />
</Canvas.Background>
<Path Data="{Binding DisplayGeometry, Mode=OneWay}" ClipToBounds="False">
<Path.Fill>
<SolidColorBrush Color="{Binding DisplayColor, Mode=OneWay}" Opacity="0.333" />
</Path.Fill>
</Path>
<Path Data="{Binding StrokeGeometry, Mode=OneWay}" ClipToBounds="False">
<Path.Fill>
<SolidColorBrush Color="{Binding DisplayColor, Mode=OneWay}" />
</Path.Fill>
</Path>
</Canvas>
</UserControl>
@@ -0,0 +1,113 @@
using System;
using System.Windows;
using System.Windows.Media;
using Artemis.UI.Extensions;
using RGB.NET.Core;
using Stylet;
using Color = System.Windows.Media.Color;
namespace Artemis.UI.Screens.Module.ProfileEditor.Visualization
{
public class ProfileLedViewModel : PropertyChangedBase
{
public ProfileLedViewModel(Led led)
{
Led = led;
X = Led.LedRectangle.X;
Y = Led.LedRectangle.Y;
Width = Led.LedRectangle.Width;
Height = Led.LedRectangle.Height;
Execute.OnUIThread(CreateLedGeometry);
}
public Led Led { get; }
public double X { get; }
public double Y { get; }
public double Width { get; }
public double Height { get; }
public Geometry DisplayGeometry { get; private set; }
public Geometry StrokeGeometry { get; private set; }
public Color DisplayColor { get; private set; }
private void CreateLedGeometry()
{
switch (Led.Shape)
{
case Shape.Custom:
if (Led.Device.DeviceInfo.DeviceType == RGBDeviceType.Keyboard || Led.Device.DeviceInfo.DeviceType == RGBDeviceType.Keypad)
CreateCustomGeometry(2.0);
else
CreateCustomGeometry(1.0);
break;
case Shape.Rectangle:
if (Led.Device.DeviceInfo.DeviceType == RGBDeviceType.Keyboard || Led.Device.DeviceInfo.DeviceType == RGBDeviceType.Keypad)
CreateKeyCapGeometry();
else
CreateRectangleGeometry();
break;
case Shape.Circle:
CreateCircleGeometry();
break;
default:
throw new ArgumentOutOfRangeException();
}
// Stroke geometry is the display geometry excluding the inner geometry
StrokeGeometry = DisplayGeometry.GetWidenedPathGeometry(new Pen(null, 1.0), 0.1, ToleranceType.Absolute);
DisplayGeometry.Freeze();
StrokeGeometry.Freeze();
}
private void CreateRectangleGeometry()
{
DisplayGeometry = new RectangleGeometry(new Rect(0.5, 0.5, Led.LedRectangle.Width - 1, Led.LedRectangle.Height - 1));
}
private void CreateCircleGeometry()
{
DisplayGeometry = new EllipseGeometry(new Rect(0.5, 0.5, Led.LedRectangle.Width - 1, Led.LedRectangle.Height - 1));
}
private void CreateKeyCapGeometry()
{
DisplayGeometry = new RectangleGeometry(new Rect(1, 1, Led.LedRectangle.Width - 2, Led.LedRectangle.Height - 2), 1.6, 1.6);
}
private void CreateCustomGeometry(double deflateAmount)
{
try
{
DisplayGeometry = Geometry.Combine(
Geometry.Empty,
Geometry.Parse(Led.ShapeData),
GeometryCombineMode.Union,
new TransformGroup
{
Children = new TransformCollection
{
new ScaleTransform(Led.LedRectangle.Width - deflateAmount, Led.LedRectangle.Height - deflateAmount),
new TranslateTransform(deflateAmount / 2, deflateAmount / 2)
}
}
);
}
catch (Exception)
{
CreateRectangleGeometry();
}
}
public void Update()
{
var newColor = Led.Color.ToMediaColor();
Execute.OnUIThread(() =>
{
if (!DisplayColor.Equals(newColor))
DisplayColor = newColor;
});
}
}
}
+12
View File
@@ -0,0 +1,12 @@
<UserControl x:Class="Artemis.UI.Screens.News.NewsView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:Artemis.UI.Screens.News"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<Grid>
<TextBlock>Today's news: There is no news (yet)!</TextBlock>
</Grid>
</UserControl>
@@ -0,0 +1,9 @@
using Stylet;
namespace Artemis.UI.Screens.News
{
public class NewsViewModel : Screen, IScreenViewModel
{
public string Title => "News";
}
}
+146
View File
@@ -0,0 +1,146 @@
<metro:MetroWindow x:Class="Artemis.UI.Screens.RootView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
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:metro="http://metro.mahapps.com/winfx/xaml/controls"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
xmlns:s="https://github.com/canton7/Stylet"
xmlns:abstract="clr-namespace:Artemis.Core.Plugins.Abstract;assembly=Artemis.Core"
mc:Ignorable="d"
GlowBrush="{DynamicResource AccentColorBrush}"
FontFamily="{StaticResource DefaultFont}"
Title="Artemis"
d:DesignHeight="640"
d:DesignWidth="1200"
d:DataContext="{d:DesignInstance screens:RootViewModel}"
SaveWindowPosition="True"
Icon="/Artemis.UI;component/Resources/logo-512.png">
<metro:MetroWindow.LeftWindowCommands>
<metro:WindowCommands>
<!-- Make the window title dissapear by providing an empty overwrite of WindowCommands -->
</metro:WindowCommands>
</metro:MetroWindow.LeftWindowCommands>
<metro:MetroWindow.IconTemplate>
<DataTemplate>
<Grid Width="{TemplateBinding Width}"
Height="{TemplateBinding Height}"
Background="Transparent"
RenderOptions.BitmapScalingMode="HighQuality"
Margin="0,0,-10,0">
<Image Source="{StaticResource BowIcon}" Stretch="Uniform" Margin="6" />
</Grid>
</DataTemplate>
</metro:MetroWindow.IconTemplate>
<materialDesign:DialogHost Identifier="RootDialog">
<materialDesign:DrawerHost IsLeftDrawerOpen="{Binding ElementName=MenuToggleButton, Path=IsChecked}">
<materialDesign:DrawerHost.LeftDrawerContent>
<DockPanel MinWidth="212">
<ToggleButton Style="{StaticResource MaterialDesignHamburgerToggleButton}"
DockPanel.Dock="Top"
HorizontalAlignment="Right"
Margin="16"
IsChecked="{Binding ElementName=MenuToggleButton, Path=IsChecked, Mode=TwoWay}" />
<!-- Built-in pages -->
<ListBox Margin="0 16 0 16"
SelectedIndex="0"
DockPanel.Dock="Top"
SelectedItem="{Binding SelectedPage}">
<ListBoxItem x:Name="Home">
<StackPanel Orientation="Horizontal">
<materialDesign:PackIcon Kind="Home"
Margin="0,0,8,0" />
<TextBlock>Home</TextBlock>
</StackPanel>
</ListBoxItem>
<ListBoxItem x:Name="News">
<StackPanel Orientation="Horizontal">
<materialDesign:PackIcon Kind="Newspaper"
Margin="0,0,8,0" />
<TextBlock>News</TextBlock>
</StackPanel>
</ListBoxItem>
<ListBoxItem x:Name="Workshop">
<StackPanel Orientation="Horizontal">
<materialDesign:PackIcon Kind="TestTube"
Margin="0,0,8,0" />
<TextBlock>Workshop</TextBlock>
</StackPanel>
</ListBoxItem>
<ListBoxItem x:Name="SurfaceEditor">
<StackPanel Orientation="Horizontal">
<materialDesign:PackIcon Kind="Edit"
Margin="0,0,8,0" />
<TextBlock>Surface Editor</TextBlock>
</StackPanel>
</ListBoxItem>
<ListBoxItem x:Name="Settings">
<StackPanel Orientation="Horizontal">
<materialDesign:PackIcon Kind="Settings"
Margin="0,0,8,0" />
<TextBlock>Settings</TextBlock>
</StackPanel>
</ListBoxItem>
</ListBox>
<!-- Modules -->
<TextBlock Margin="8,8,0,8"
DockPanel.Dock="Top">
Modules
</TextBlock>
<Border BorderThickness="0 1 0 0"
BorderBrush="{DynamicResource MaterialDesignDivider}">
<ListBox Margin="0 0 0 16"
SelectedIndex="-1"
ItemsSource="{Binding Modules}"
SelectedItem="{Binding SelectedModule}"
DockPanel.Dock="Top">
<ListBox.ItemTemplate>
<DataTemplate DataType="abstract:Module">
<DockPanel HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Margin="0">
<materialDesign:PackIcon Kind="Home"
Margin="0,0,8,0" />
<TextBlock Text="{Binding DisplayName}" />
</DockPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Border>
</DockPanel>
</materialDesign:DrawerHost.LeftDrawerContent>
<DockPanel>
<materialDesign:ColorZone Padding="10"
materialDesign:ShadowAssist.ShadowDepth="Depth2"
Mode="PrimaryMid"
DockPanel.Dock="Top">
<DockPanel>
<ToggleButton Style="{StaticResource MaterialDesignHamburgerToggleButton}"
IsChecked="{Binding MenuOpen}"
x:Name="MenuToggleButton" />
<materialDesign:PopupBox DockPanel.Dock="Right"
PlacementMode="BottomAndAlignRightEdges"
StaysOpen="False">
<StackPanel>
<Button Content="Hello World" />
<Button Content="Nice Popup" />
<Button Content="Can't Touch This"
IsEnabled="False" />
<Separator />
<Button Content="Goodbye" />
</StackPanel>
</materialDesign:PopupBox>
<TextBlock HorizontalAlignment="Center"
VerticalAlignment="Center"
FontSize="22"
Text="{Binding ActiveItem.Title}" />
</DockPanel>
</materialDesign:ColorZone>
<ContentControl s:View.Model="{Binding ActiveItem}" />
</DockPanel>
</materialDesign:DrawerHost>
</materialDesign:DialogHost>
</metro:MetroWindow>
+122
View File
@@ -0,0 +1,122 @@
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Controls;
using Artemis.Core.Events;
using Artemis.Core.Services.Interfaces;
using Artemis.UI.Ninject.Factories;
using Artemis.UI.Screens.Home;
using Artemis.UI.Screens.News;
using Artemis.UI.Screens.Settings;
using Artemis.UI.Screens.SurfaceEditor;
using Artemis.UI.Screens.Workshop;
using Stylet;
namespace Artemis.UI.Screens
{
public class RootViewModel : Conductor<IScreen>.Collection.OneActive
{
private readonly ICollection<IScreenViewModel> _artemisViewModels;
private readonly IModuleViewModelFactory _moduleViewModelFactory;
private readonly IPluginService _pluginService;
public RootViewModel(ICollection<IScreenViewModel> artemisViewModels, IPluginService pluginService, IModuleViewModelFactory moduleViewModelFactory)
{
_artemisViewModels = artemisViewModels;
_pluginService = pluginService;
_moduleViewModelFactory = moduleViewModelFactory;
// Add the built-in items
Items.AddRange(artemisViewModels);
// Activate the home item
ActiveItem = _artemisViewModels.First(v => v.GetType() == typeof(HomeViewModel));
// Sync up with the plugin service
Modules = new BindableCollection<Core.Plugins.Abstract.Module>();
Modules.AddRange(_pluginService.GetPluginsOfType<Core.Plugins.Abstract.Module>());
_pluginService.PluginEnabled += PluginServiceOnPluginEnabled;
_pluginService.PluginDisabled += PluginServiceOnPluginDisabled;
PropertyChanged += OnSelectedModuleChanged;
PropertyChanged += OnSelectedPageChanged;
}
public IObservableCollection<Core.Plugins.Abstract.Module> Modules { get; set; }
public bool MenuOpen { get; set; }
public ListBoxItem SelectedPage { get; set; }
public Core.Plugins.Abstract.Module SelectedModule { get; set; }
public async Task NavigateToSelectedModule()
{
if (SelectedModule == null)
return;
// Create a view model for the given plugin info (which will be a module)
var viewModel = await Task.Run(() => _moduleViewModelFactory.CreateModuleViewModel(SelectedModule));
ActivateItem(viewModel);
SelectedPage = null;
MenuOpen = false;
}
private void PluginServiceOnPluginEnabled(object sender, PluginEventArgs e)
{
var existing = Modules.FirstOrDefault(m => _pluginService.GetPluginInfo(m)?.Guid == e.PluginInfo.Guid);
if (existing != null)
{
if (SelectedModule == existing && SelectedModule != null)
SelectedModule = null;
Modules.Remove(existing);
}
if (e.PluginInfo.Instance is Core.Plugins.Abstract.Module module)
Modules.Add(module);
}
private void PluginServiceOnPluginDisabled(object sender, PluginEventArgs e)
{
var existing = Modules.FirstOrDefault(m => _pluginService.GetPluginInfo(m)?.Guid == e.PluginInfo.Guid);
if (existing != null)
{
if (SelectedModule == existing && SelectedModule != null)
SelectedModule = null;
Modules.Remove(existing);
}
}
private async void OnSelectedModuleChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(SelectedModule))
await NavigateToSelectedModule();
}
private void OnSelectedPageChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName != "SelectedPage" || SelectedPage == null)
return;
switch (SelectedPage.Name)
{
case "Home":
ActivateItem(_artemisViewModels.First(v => v is HomeViewModel));
break;
case "News":
ActivateItem(_artemisViewModels.First(v => v is NewsViewModel));
break;
case "Workshop":
ActivateItem(_artemisViewModels.First(v => v is WorkshopViewModel));
break;
case "SurfaceEditor":
ActivateItem(_artemisViewModels.First(v => v is SurfaceEditorViewModel));
break;
case "Settings":
ActivateItem(_artemisViewModels.First(v => v is SettingsViewModel));
break;
}
SelectedModule = null;
MenuOpen = false;
}
}
}
@@ -0,0 +1,86 @@
<UserControl x:Class="Artemis.UI.Screens.Settings.SettingsView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:s="https://github.com/canton7/Stylet"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:xaml="https://github.com/canton7/Stylet"
mc:Ignorable="d"
d:DataContext="{d:DesignInstance screens:SettingsViewModel}"
d:DesignHeight="600" d:DesignWidth="600">
<UserControl.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary
Source="pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesignTheme.TextBlock.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</UserControl.Resources>
<ScrollViewer>
<StackPanel Margin="16">
<TextBlock Style="{StaticResource MaterialDesignHeadlineTextBlock}">General</TextBlock>
<StackPanel>
<TextBlock>General settings like start up with Windows etc.</TextBlock>
<Grid>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<TextBlock VerticalAlignment="Bottom" Grid.Row="0" Grid.Column="0">Render scale</TextBlock>
<Slider Minimum="0.10"
Maximum="1"
TickFrequency="0.05"
Grid.Row="1"
Grid.Column="0"
Style="{StaticResource MaterialDesignDiscreteSlider}"
ToolTip="MaterialDesignDiscreteSlider"
Value="{Binding RenderScale}" />
<TextBlock VerticalAlignment="Bottom" Grid.Row="0" Grid.Column="1">Target framerate</TextBlock>
<Slider Minimum="5"
Maximum="60"
Grid.Row="1"
Grid.Column="1"
Style="{StaticResource MaterialDesignDiscreteSlider}"
ToolTip="MaterialDesignDiscreteSlider"
Value="{Binding TargetFrameRate}" />
</Grid>
<Button Command="{s:Action ShowDebugger}" Style="{StaticResource MaterialDesignRaisedButton}"
HorizontalAlignment="Left" Margin="0, 10, 0, 0">
Show debugger
</Button>
</StackPanel>
<TextBlock Style="{StaticResource MaterialDesignHeadlineTextBlock}">Devices</TextBlock>
<TextBlock>A list of devices and options to disable them</TextBlock>
<ScrollViewer VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled" Margin="0 12 0 0">
<ItemsControl ItemsSource="{Binding DeviceSettingsViewModels}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<ContentControl xaml:View.Model="{Binding}" Margin="5" HorizontalAlignment="Left"
VerticalAlignment="Top" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
<TextBlock Style="{StaticResource MaterialDesignHeadlineTextBlock}">Plugins</TextBlock>
<Grid>
<TextBlock>A list of plugins and options to disable them</TextBlock>
</Grid>
</StackPanel>
</ScrollViewer>
</UserControl>
@@ -0,0 +1,68 @@
using Artemis.Core.Services;
using Artemis.Core.Services.Interfaces;
using Artemis.Core.Services.Storage;
using Artemis.UI.Screens.Settings.Debug;
using Artemis.UI.Screens.Settings.Tabs.Devices;
using Ninject;
using Stylet;
namespace Artemis.UI.Screens.Settings
{
public class SettingsViewModel : Screen, IScreenViewModel
{
private readonly ICoreService _coreService;
private readonly IKernel _kernel;
private readonly ISettingsService _settingsService;
private readonly ISurfaceService _surfaceService;
private readonly IWindowManager _windowManager;
public SettingsViewModel(IKernel kernel, ICoreService coreService, ISurfaceService surfaceService, IWindowManager windowManager, ISettingsService settingsService)
{
_kernel = kernel;
_coreService = coreService;
_surfaceService = surfaceService;
_windowManager = windowManager;
_settingsService = settingsService;
DeviceSettingsViewModels = new BindableCollection<DeviceSettingsViewModel>();
}
public BindableCollection<DeviceSettingsViewModel> DeviceSettingsViewModels { get; set; }
public double RenderScale
{
get => _settingsService.GetSetting("RenderScale", 1.0).Value;
set
{
_settingsService.GetSetting("RenderScale", 1.0).Value = value;
_settingsService.GetSetting("RenderScale", 1.0).Save();
}
}
public int TargetFrameRate
{
get => _settingsService.GetSetting("TargetFrameRate", 25).Value;
set
{
_settingsService.GetSetting("TargetFrameRate", 25).Value = value;
_settingsService.GetSetting("TargetFrameRate", 25).Save();
}
}
public string Title => "Settings";
protected override void OnActivate()
{
DeviceSettingsViewModels.Clear();
foreach (var device in _surfaceService.ActiveSurface.Devices)
DeviceSettingsViewModels.Add(new DeviceSettingsViewModel(device, _coreService));
base.OnActivate();
}
public void ShowDebugger()
{
_windowManager.ShowWindow(_kernel.Get<DebugViewModel>());
}
}
}
@@ -0,0 +1,63 @@
<UserControl x:Class="Artemis.UI.Screens.Settings.Tabs.Devices.DeviceSettingsView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
xmlns:s="https://github.com/canton7/Stylet"
d:DataContext="{d:DesignInstance settings:DeviceSettingsViewModel}"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<UserControl.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary
Source="pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesignTheme.Button.xaml" />
<ResourceDictionary
Source="pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesignTheme.PopupBox.xaml" />
<ResourceDictionary
Source="pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesignTheme.TextBlock.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</UserControl.Resources>
<materialDesign:Card Width="200">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="140" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Rectangle Grid.Row="0" Height="140" Width="196" Fill="{DynamicResource MaterialDesignPaper}" />
<Image Grid.Row="0" Source="{Binding Device.RgbDevice.DeviceInfo.Image}" Height="130" Width="190"
RenderOptions.BitmapScalingMode="HighQuality" />
<Button Grid.Row="0"
Style="{StaticResource MaterialDesignFloatingActionMiniButton}"
HorizontalAlignment="Right"
VerticalAlignment="Bottom"
Margin="0 0 16 -20"
ToolTip="Identify"
Command="{s:Action Identify}">
<materialDesign:PackIcon Kind="AlarmLight" />
</Button>
<StackPanel Grid.Row="1" Margin="8 24 8 0">
<TextBlock FontWeight="Bold" Text="{Binding Device.RgbDevice.DeviceInfo.Model}" />
<TextBlock TextWrapping="Wrap" VerticalAlignment="Center">
<Run Text="{Binding Device.RgbDevice.DeviceInfo.Manufacturer, Mode=OneWay}" />
-
<Run Text="{Binding Device.RgbDevice.DeviceInfo.DeviceType, Mode=OneWay}" />
</TextBlock>
</StackPanel>
<StackPanel HorizontalAlignment="Right" Grid.Row="2" Orientation="Horizontal" Margin="8">
<CheckBox Style="{StaticResource MaterialDesignCheckBox}" IsChecked="{Binding IsDeviceEnabled}">
Device enabled
</CheckBox>
<materialDesign:PopupBox Style="{StaticResource MaterialDesignToolPopupBox}" Padding="2 0 2 0">
<StackPanel>
<Button Content="Show device debugger" Command="{s:Action ShowDeviceDebugger}" />
<Button Content="Open plugin directory" Command="{s:Action OpenPluginDirectory}" />
</StackPanel>
</materialDesign:PopupBox>
</StackPanel>
</Grid>
</materialDesign:Card>
</UserControl>
@@ -0,0 +1,76 @@
using System.Diagnostics;
using System.Drawing;
using System.Threading.Tasks;
using Artemis.Core.Events;
using Artemis.Core.Models.Surface;
using Artemis.Core.Services.Interfaces;
using Humanizer;
namespace Artemis.UI.Screens.Settings.Tabs.Devices
{
public class DeviceSettingsViewModel
{
private readonly ICoreService _coreService;
public DeviceSettingsViewModel(Device device, ICoreService coreService)
{
_coreService = coreService;
Device = device;
Type = Device.RgbDevice.DeviceInfo.DeviceType.ToString().Humanize();
Name = Device.RgbDevice.DeviceInfo.Model;
Manufacturer = Device.RgbDevice.DeviceInfo.Manufacturer;
IsDeviceEnabled = true;
}
public Device Device { get; }
public string Type { get; set; }
public string Name { get; set; }
public string Manufacturer { get; set; }
public bool IsDeviceEnabled { get; set; }
public void Identify()
{
BlinkDevice(0);
}
private void BlinkDevice(int blinkCount)
{
// Draw a white overlay over the device
void DrawOverlay(object sender, FrameRenderingEventArgs args)
{
using (var g = Graphics.FromImage(args.Bitmap))
{
g.FillPath(new SolidBrush(Color.White), Device.RenderPath);
}
}
_coreService.FrameRendering += DrawOverlay;
// After 200ms, stop drawing the overlay
Task.Run(async () =>
{
await Task.Delay(200);
_coreService.FrameRendering -= DrawOverlay;
if (blinkCount < 5)
{
// After another 200ms, draw the overlay again, repeat six times
await Task.Delay(200);
BlinkDevice(blinkCount + 1);
}
});
}
public void ShowDeviceDebugger()
{
}
public void OpenPluginDirectory()
{
Process.Start(Device.Plugin.PluginInfo.Directory.FullName);
}
}
}
@@ -0,0 +1,99 @@
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using Stylet;
namespace Artemis.UI.Screens.Shared
{
public class PanZoomViewModel : PropertyChangedBase
{
private Point? _lastPanPosition;
public double Zoom { get; set; } = 1;
public double ZoomPercentage
{
get => Zoom * 100;
set => SetZoomFromPercentage(value);
}
public double PanX { get; set; }
public double PanY { get; set; }
public Rect BackgroundViewport => new Rect(PanX, PanY, 20, 20);
public void ProcessMouseScroll(object sender, MouseWheelEventArgs e)
{
var relative = GetRelativeMousePosition(sender, e);
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 = Math.Min(0, absoluteX - relative.X * Zoom);
PanY = Math.Min(0, absoluteY - relative.Y * Zoom);
}
public void ProcessMouseMove(object sender, MouseEventArgs e)
{
if (e.LeftButton == MouseButtonState.Released)
{
Mouse.OverrideCursor = Cursors.Arrow;
_lastPanPosition = null;
return;
}
if (_lastPanPosition == null)
_lastPanPosition = e.GetPosition((IInputElement) sender);
var position = e.GetPosition((IInputElement) sender);
var delta = _lastPanPosition - position;
PanX = Math.Min(0, PanX - delta.Value.X);
PanY = Math.Min(0, PanY - delta.Value.Y);
_lastPanPosition = position;
}
public void Reset()
{
Zoom = 1;
PanX = 0;
PanY = 0;
}
public Rect TransformContainingRect(Rect rect)
{
// Create the same transform group the view is using
var transformGroup = new TransformGroup();
transformGroup.Children.Add(new ScaleTransform(Zoom, Zoom));
transformGroup.Children.Add(new TranslateTransform(PanX, PanY));
// Apply it to the device rect
return transformGroup.TransformBounds(rect);
}
public Point GetRelativeMousePosition(object container, MouseEventArgs e)
{
// Get the mouse position relative to the panned / zoomed panel, not very MVVM but I can't find a better way
return e.GetPosition(((Panel) container).Children[0]);
}
private void SetZoomFromPercentage(double value)
{
var newZoom = value / 100;
// Focus towards the center of the zoomed area
PanX += newZoom - Zoom;
PanY += newZoom - Zoom;
Zoom = value / 100;
}
}
}
@@ -0,0 +1,34 @@
<mah:MetroWindow x:Class="Artemis.UI.Screens.Splash.SplashView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
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:mah="http://metro.mahapps.com/winfx/xaml/controls"
xmlns:splash="clr-namespace:Artemis.UI.Screens.Splash"
mc:Ignorable="d"
Title="Artemis"
Height="450"
Width="450"
ShowTitleBar="False"
ShowMaxRestoreButton="False"
ShowCloseButton="False"
ShowMinButton="False"
WindowStartupLocation="CenterScreen"
GlowBrush="{DynamicResource AccentColorBrush}"
FontFamily="{StaticResource DefaultFont}"
d:DataContext="{d:DesignInstance splash:SplashViewModel}">
<Grid Background="{DynamicResource PrimaryHueMidBrush}">
<Grid.RowDefinitions>
<RowDefinition Height="250" />
<RowDefinition Height="50" />
<RowDefinition Height="Auto" />
<RowDefinition />
</Grid.RowDefinitions>
<Image Source="{StaticResource BowIcon}" Stretch="Uniform" Margin="6,50,6,6" />
<TextBlock Grid.Row="1" HorizontalAlignment="Center" VerticalAlignment="Bottom" Foreground="White" FontSize="16">
Artemis is initializing...
</TextBlock>
<TextBlock Grid.Row="2" HorizontalAlignment="Center" Foreground="#FFDDDDDD" Text="{Binding Status}" />
<ProgressBar Grid.Row="3" IsIndeterminate="True" Maximum="1" Minimum="1" Margin="16 0" />
</Grid>
</mah:MetroWindow>
@@ -0,0 +1,27 @@
using Artemis.Core.Services.Interfaces;
using Ninject;
using Stylet;
namespace Artemis.UI.Screens.Splash
{
public class SplashViewModel : Screen
{
private readonly IKernel _kernel;
public SplashViewModel(IKernel kernel)
{
_kernel = kernel;
Status = "Initializing Core";
}
public string Status { get; set; }
public void ListenToEvents()
{
var pluginService = _kernel.Get<IPluginService>();
pluginService.CopyingBuildInPlugins += (sender, args) => Status = "Updating built-in plugins";
pluginService.PluginLoading += (sender, args) => Status = "Loading plugin: " + args.PluginInfo.Name;
pluginService.PluginLoaded += (sender, args) => Status = "Initializing UI";
}
}
}
@@ -0,0 +1,39 @@
<UserControl x:Class="Artemis.UI.Screens.SurfaceEditor.Dialogs.SurfaceCreateView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:s="https://github.com/canton7/Stylet"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
mc:Ignorable="d"
d:DesignHeight="213.053" d:DesignWidth="254.425">
<StackPanel Margin="16">
<TextBlock>
Add a new surface layout
</TextBlock>
<TextBox materialDesign:HintAssist.Hint="Layout name" Margin="0 8 0 16"
Style="{StaticResource MaterialDesignFloatingHintTextBox}" Text="{Binding SurfaceName}" />
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
<Button Style="{StaticResource MaterialDesignFlatButton}" IsCancel="True" Margin="0 8 8 0"
Command="{s:Action Cancel}">
<Button.CommandParameter>
<system:Boolean xmlns:system="clr-namespace:System;assembly=mscorlib">
False
</system:Boolean>
</Button.CommandParameter>
CANCEL
</Button>
<Button Style="{StaticResource MaterialDesignFlatButton}" IsDefault="True" Margin="0 8 8 0"
Command="{s:Action Accept}">
<Button.CommandParameter>
<system:Boolean xmlns:system="clr-namespace:System;assembly=mscorlib">
True
</system:Boolean>
</Button.CommandParameter>
ACCEPT
</Button>
</StackPanel>
</StackPanel>
</UserControl>
@@ -0,0 +1,30 @@
using System.Threading.Tasks;
using Artemis.UI.ViewModels.Dialogs;
using Stylet;
namespace Artemis.UI.Screens.SurfaceEditor.Dialogs
{
public class SurfaceCreateViewModel : DialogViewModelBase
{
public SurfaceCreateViewModel(IModelValidator<SurfaceCreateViewModel> validator) : base(validator)
{
}
public string SurfaceName { get; set; }
public async Task Accept()
{
await ValidateAsync();
if (HasErrors)
return;
Session.Close(SurfaceName);
}
public void Cancel()
{
Session.Close();
}
}
}
@@ -0,0 +1,12 @@
using FluentValidation;
namespace Artemis.UI.Screens.SurfaceEditor.Dialogs
{
public class SurfaceCreateViewModelValidator : AbstractValidator<SurfaceCreateViewModel>
{
public SurfaceCreateViewModelValidator()
{
RuleFor(m => m.SurfaceName).NotEmpty().WithMessage("Layout name may not be empty");
}
}
}
@@ -0,0 +1,235 @@
<UserControl x:Class="Artemis.UI.Screens.SurfaceEditor.SurfaceEditorView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:s="https://github.com/canton7/Stylet"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
xmlns:models="clr-namespace:Artemis.Core.Models.Surface;assembly=Artemis.Core"
mc:Ignorable="d"
d:DesignHeight="600" d:DesignWidth="600">
<UserControl.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary
Source="pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesignTheme.TextBlock.xaml" />
<ResourceDictionary
Source="pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesignTheme.PopupBox.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</UserControl.Resources>
<Grid Margin="16">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" MinWidth="100" />
<ColumnDefinition Width="5" />
<ColumnDefinition Width="{Binding SurfaceListWidth.Value, Mode=TwoWay}" MinWidth="100" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<StackPanel Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="3">
<TextBlock>
<materialDesign:PackIcon Kind="AboutOutline" Margin="0 0 0 -3" /> 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>
<Border BorderBrush="{DynamicResource MaterialDesignDivider}" BorderThickness="0 0 0 1" Margin="0 10" />
</StackPanel>
<materialDesign:Card materialDesign:ShadowAssist.ShadowDepth="Depth1" Grid.Row="1" Grid.Column="0"
VerticalAlignment="Stretch" Margin="0,0,5,0">
<Grid ClipToBounds="True"
KeyUp="{s:Action EditorGridKeyUp}"
KeyDown="{s:Action EditorGridKeyDown}"
MouseWheel="{s:Action EditorGridMouseWheel}"
MouseUp="{s:Action EditorGridMouseClick}"
MouseDown="{s:Action EditorGridMouseClick}"
MouseMove="{s:Action EditorGridMouseMove}">
<Grid.Background>
<VisualBrush TileMode="Tile" Stretch="Uniform"
Viewport="{Binding PanZoomViewModel.BackgroundViewport}" 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>
<EventTrigger RoutedEvent="Grid.MouseLeftButtonDown">
<BeginStoryboard>
<Storyboard TargetName="MultiSelectionPath" TargetProperty="Opacity">
<DoubleAnimation From="0" To="1" Duration="0:0:0.1" />
</Storyboard>
</BeginStoryboard>
</EventTrigger>
<EventTrigger RoutedEvent="Grid.MouseLeftButtonUp">
<BeginStoryboard>
<Storyboard TargetName="MultiSelectionPath" TargetProperty="Opacity">
<DoubleAnimation From="1" To="0" Duration="0:0:0.2" />
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Grid.Triggers>
<Grid Name="EditorDisplayGrid">
<Grid.RenderTransform>
<TransformGroup>
<ScaleTransform ScaleX="{Binding PanZoomViewModel.Zoom}"
ScaleY="{Binding PanZoomViewModel.Zoom}" />
<TranslateTransform X="{Binding PanZoomViewModel.PanX}" Y="{Binding PanZoomViewModel.PanY}" />
</TransformGroup>
</Grid.RenderTransform>
<ItemsControl ItemsSource="{Binding Devices}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemContainerStyle>
<Style TargetType="ContentPresenter">
<Setter Property="Canvas.Left" Value="{Binding X}" />
<Setter Property="Canvas.Top" Value="{Binding Y}" />
</Style>
</ItemsControl.ItemContainerStyle>
<ItemsControl.ItemTemplate>
<DataTemplate>
<ContentControl Width="{Binding Device.RgbDevice.Size.Width}"
Height="{Binding Device.RgbDevice.Size.Height}"
s:View.Model="{Binding}">
<ContentControl.ContextMenu>
<ContextMenu>
<MenuItem Header="Bring to Front" Command="{s:Action BringToFront}"
CommandParameter="{Binding}">
<MenuItem.Icon>
<materialDesign:PackIcon Kind="ArrangeBringToFront" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Bring Forward" Command="{s:Action BringForward}"
CommandParameter="{Binding}">
<MenuItem.Icon>
<materialDesign:PackIcon Kind="ArrangeBringForward" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Send to Back" Command="{s:Action SendToBack}"
CommandParameter="{Binding}">
<MenuItem.Icon>
<materialDesign:PackIcon Kind="ArrangeSendToBack" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Send Backward" Command="{s:Action SendBackward}"
CommandParameter="{Binding}">
<MenuItem.Icon>
<materialDesign:PackIcon Kind="ArrangeSendBackward" />
</MenuItem.Icon>
</MenuItem>
</ContextMenu>
</ContentControl.ContextMenu>
</ContentControl>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
<!-- Multi-selection rectangle -->
<Path Data="{Binding SelectionRectangle}" Opacity="0"
Stroke="{DynamicResource PrimaryHueLightBrush}"
StrokeThickness="1"
Name="MultiSelectionPath"
IsHitTestVisible="False">
<Path.Fill>
<SolidColorBrush Color="{DynamicResource Primary400}" Opacity="0.25" />
</Path.Fill>
</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 PanZoomViewModel.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>
</materialDesign:Card>
<GridSplitter Grid.Column="1" Grid.Row="1" Width="5" HorizontalAlignment="Stretch" ShowsPreview="True" Cursor="SizeWE" />
<materialDesign:Card materialDesign:ShadowAssist.ShadowDepth="Depth1" Grid.Row="1" Grid.Column="2"
VerticalAlignment="Stretch" Margin="5,0,0,0">
<materialDesign:DialogHost Identifier="SurfaceListDialogHost" CloseOnClickAway="True"
UseLayoutRounding="True">
<Grid HorizontalAlignment="Stretch">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<ListBox Grid.Row="0" HorizontalContentAlignment="Stretch"
ItemsSource="{Binding SurfaceConfigurations}" SelectedItem="{Binding SelectedSurface}">
<ListBox.Resources>
<DataTemplate DataType="{x:Type models:Surface}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="40" />
</Grid.ColumnDefinitions>
<TextBlock Text="{Binding Name}" ToolTip="{Binding Name}"
HorizontalAlignment="Left" VerticalAlignment="Center" />
<Button Grid.Column="1"
Command="{s:Action DeleteSurfaceConfiguration}"
CommandParameter="{Binding}"
Style="{StaticResource MaterialDesignIconForegroundButton}"
ToolTip="Delete layout configuration"
HorizontalAlignment="Right">
<materialDesign:PackIcon Kind="Trashcan" />
</Button>
</Grid>
</DataTemplate>
</ListBox.Resources>
</ListBox>
<Button Grid.Row="0"
Style="{StaticResource MaterialDesignFloatingActionButton}"
Command="{s:Action AddSurfaceConfiguration}"
Margin="0 0 10 10"
HorizontalAlignment="Right"
VerticalAlignment="Bottom"
ToolTip="Add a new surface layout">
<materialDesign:PackIcon Kind="Add" Height="24" Width="24" />
</Button>
</Grid>
</materialDesign:DialogHost>
</materialDesign:Card>
<TextBlock Grid.Column="0" Grid.Row="2" Grid.ColumnSpan="2" Style="{StaticResource MaterialDesignCaptionTextBlock}" Margin="0,5,0,0">
<materialDesign:PackIcon Kind="Keyboard" Margin="0 0 0 -3" />
<Run Text="Hold" />
<Run FontWeight="Bold" Text="shift" />
<Run Text="or click and drag to select multiple devices at once. To move around the surface hold down" />
<Run FontWeight="Bold" Text="ctrl" />
<Run Text="and drag." />
</TextBlock>
</Grid>
</UserControl>
@@ -0,0 +1,390 @@
using System;
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Input;
using System.Windows.Media;
using Artemis.Core.Models.Surface;
using Artemis.Core.Plugins.Models;
using Artemis.Core.Services;
using Artemis.Core.Services.Storage;
using Artemis.UI.Screens.Shared;
using Artemis.UI.Screens.SurfaceEditor.Dialogs;
using Artemis.UI.Screens.SurfaceEditor.Visualization;
using Artemis.UI.Services.Interfaces;
using Stylet;
namespace Artemis.UI.Screens.SurfaceEditor
{
public class SurfaceEditorViewModel : Screen, IScreenViewModel
{
private readonly IDialogService _dialogService;
private readonly ISettingsService _settingsService;
private readonly ISurfaceService _surfaceService;
public SurfaceEditorViewModel(ISurfaceService surfaceService, IDialogService dialogService, ISettingsService settingsService)
{
Devices = new ObservableCollection<SurfaceDeviceViewModel>();
SurfaceConfigurations = new ObservableCollection<Surface>();
SelectionRectangle = new RectangleGeometry();
PanZoomViewModel = new PanZoomViewModel();
_surfaceService = surfaceService;
_dialogService = dialogService;
_settingsService = settingsService;
}
public ObservableCollection<SurfaceDeviceViewModel> Devices { get; set; }
public ObservableCollection<Surface> SurfaceConfigurations { get; set; }
public RectangleGeometry SelectionRectangle { get; set; }
public PanZoomViewModel PanZoomViewModel { get; set; }
public PluginSetting<GridLength> SurfaceListWidth { get; set; }
public Surface SelectedSurface
{
get => _selectedSurface;
set
{
_selectedSurface = value;
ApplySelectedSurfaceConfiguration();
}
}
public string Title => "Surface Editor";
public Surface CreateSurfaceConfiguration(string name)
{
var config = _surfaceService.CreateSurfaceConfiguration(name);
Execute.OnUIThread(() => SurfaceConfigurations.Add(config));
return config;
}
private void LoadWorkspaceSettings()
{
SurfaceListWidth = _settingsService.GetSetting("SurfaceEditor.SurfaceListWidth", new GridLength(300.0));
}
private void SaveWorkspaceSettings()
{
SurfaceListWidth.Save();
}
private void LoadSurfaceConfigurations()
{
// Get surface configs
var configs = _surfaceService.SurfaceConfigurations;
// Get the active config, if empty, create a default config
var activeConfig = _surfaceService.ActiveSurface;
if (activeConfig == null)
{
activeConfig = CreateSurfaceConfiguration("Default");
_surfaceService.SetActiveSurfaceConfiguration(activeConfig);
}
Execute.OnUIThread(() =>
{
// Populate the UI collection
SurfaceConfigurations.Clear();
foreach (var surfaceConfiguration in configs)
SurfaceConfigurations.Add(surfaceConfiguration);
// Set the active config
SelectedSurface = activeConfig;
});
}
private void ApplySelectedSurfaceConfiguration()
{
if (SelectedSurface == null)
{
Execute.OnUIThread(Devices.Clear);
return;
}
// Make sure all devices have an up-to-date VM
foreach (var surfaceDeviceConfiguration in SelectedSurface.Devices)
{
// Create VMs for missing devices
var viewModel = Devices.FirstOrDefault(vm => vm.Device.RgbDevice == surfaceDeviceConfiguration.RgbDevice);
if (viewModel == null)
Execute.OnUIThread(() => Devices.Add(new SurfaceDeviceViewModel(surfaceDeviceConfiguration)));
// Update existing devices
else
viewModel.Device = surfaceDeviceConfiguration;
}
// Sort the devices by ZIndex
Execute.OnUIThread(() =>
{
foreach (var device in Devices.OrderBy(d => d.ZIndex).ToList())
Devices.Move(Devices.IndexOf(device), device.ZIndex - 1);
});
_surfaceService.SetActiveSurfaceConfiguration(SelectedSurface);
}
#region Overrides of Screen
protected override void OnActivate()
{
LoadSurfaceConfigurations();
LoadWorkspaceSettings();
base.OnActivate();
}
protected override void OnDeactivate()
{
SaveWorkspaceSettings();
base.OnDeactivate();
}
#endregion
#region Configuration management
public async Task DeleteSurfaceConfiguration(Surface surface)
{
var result = await _dialogService.ShowConfirmDialogAt(
"SurfaceListDialogHost",
"Delete surface configuration",
"Are you sure you want to delete\nthis surface configuration?"
);
if (result)
{
SurfaceConfigurations.Remove(surface);
_surfaceService.DeleteSurfaceConfiguration(surface);
}
}
public async Task AddSurfaceConfiguration()
{
var result = await _dialogService.ShowDialogAt<SurfaceCreateViewModel>("SurfaceListDialogHost");
if (result is string name)
CreateSurfaceConfiguration(name);
}
#endregion
#region Context menu actions
public void BringToFront(SurfaceDeviceViewModel surfaceDeviceViewModel)
{
Devices.Move(Devices.IndexOf(surfaceDeviceViewModel), Devices.Count - 1);
for (var i = 0; i < Devices.Count; i++)
{
var deviceViewModel = Devices[i];
deviceViewModel.ZIndex = i + 1;
}
}
public void BringForward(SurfaceDeviceViewModel surfaceDeviceViewModel)
{
var currentIndex = Devices.IndexOf(surfaceDeviceViewModel);
var newIndex = Math.Min(currentIndex + 1, Devices.Count - 1);
Devices.Move(currentIndex, newIndex);
for (var i = 0; i < Devices.Count; i++)
{
var deviceViewModel = Devices[i];
deviceViewModel.ZIndex = i + 1;
}
}
public void SendToBack(SurfaceDeviceViewModel surfaceDeviceViewModel)
{
Devices.Move(Devices.IndexOf(surfaceDeviceViewModel), 0);
for (var i = 0; i < Devices.Count; i++)
{
var deviceViewModel = Devices[i];
deviceViewModel.ZIndex = i + 1;
}
}
public void SendBackward(SurfaceDeviceViewModel surfaceDeviceViewModel)
{
var currentIndex = Devices.IndexOf(surfaceDeviceViewModel);
var newIndex = Math.Max(currentIndex - 1, 0);
Devices.Move(currentIndex, newIndex);
for (var i = 0; i < Devices.Count; i++)
{
var deviceViewModel = Devices[i];
deviceViewModel.ZIndex = i + 1;
}
}
#endregion
#region Selection
private MouseDragStatus _mouseDragStatus;
private Point _mouseDragStartPoint;
private Surface _selectedSurface;
// ReSharper disable once UnusedMember.Global - Called from view
public void EditorGridMouseClick(object sender, MouseButtonEventArgs e)
{
if (IsPanKeyDown() || e.ChangedButton == MouseButton.Right)
return;
var position = e.GetPosition((IInputElement) sender);
var relative = PanZoomViewModel.GetRelativeMousePosition(sender, e);
if (e.LeftButton == MouseButtonState.Pressed)
StartMouseDrag(position, relative);
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);
var relative = PanZoomViewModel.GetRelativeMousePosition(sender, e);
if (_mouseDragStatus == MouseDragStatus.Dragging)
MoveSelected(relative);
else if (_mouseDragStatus == MouseDragStatus.Selecting)
UpdateSelection(position);
}
private void StartMouseDrag(Point position, Point relative)
{
// If drag started on top of a device, initialise dragging
var device = Devices.LastOrDefault(d => PanZoomViewModel.TransformContainingRect(d.DeviceRectangle).Contains(position));
if (device != null)
{
_mouseDragStatus = MouseDragStatus.Dragging;
// If the device is not selected, deselect others and select only this one (if shift not held)
if (device.SelectionStatus != SelectionStatus.Selected)
{
if (!Keyboard.IsKeyDown(Key.LeftShift) && !Keyboard.IsKeyDown(Key.RightShift))
{
foreach (var others in Devices)
others.SelectionStatus = SelectionStatus.None;
}
device.SelectionStatus = SelectionStatus.Selected;
}
foreach (var selectedDevice in Devices.Where(d => d.SelectionStatus == SelectionStatus.Selected))
selectedDevice.StartMouseDrag(relative);
}
// Start multi-selection
else
{
_mouseDragStatus = MouseDragStatus.Selecting;
_mouseDragStartPoint = position;
}
// While dragging always show a hand to avoid cursor flicker
if (_mouseDragStatus == MouseDragStatus.Dragging)
Mouse.OverrideCursor = Cursors.Hand;
else
Mouse.OverrideCursor = Cursors.Arrow;
// Any time dragging starts, start with a new rect
SelectionRectangle.Rect = new Rect();
}
private void StopMouseDrag(Point position)
{
if (_mouseDragStatus != MouseDragStatus.Dragging)
{
var selectedRect = new Rect(_mouseDragStartPoint, position);
foreach (var device in Devices)
{
if (PanZoomViewModel.TransformContainingRect(device.DeviceRectangle).IntersectsWith(selectedRect))
device.SelectionStatus = SelectionStatus.Selected;
else if (!Keyboard.IsKeyDown(Key.LeftShift) && !Keyboard.IsKeyDown(Key.RightShift))
device.SelectionStatus = SelectionStatus.None;
}
}
else
_surfaceService.UpdateSurfaceConfiguration(SelectedSurface, true);
Mouse.OverrideCursor = null;
_mouseDragStatus = MouseDragStatus.None;
}
private void UpdateSelection(Point position)
{
if (IsPanKeyDown())
return;
lock (Devices)
{
var selectedRect = new Rect(_mouseDragStartPoint, position);
SelectionRectangle.Rect = selectedRect;
foreach (var device in Devices)
{
if (PanZoomViewModel.TransformContainingRect(device.DeviceRectangle).IntersectsWith(selectedRect))
device.SelectionStatus = SelectionStatus.Selected;
else if (!Keyboard.IsKeyDown(Key.LeftShift) && !Keyboard.IsKeyDown(Key.RightShift))
device.SelectionStatus = SelectionStatus.None;
}
}
}
private void MoveSelected(Point position)
{
foreach (var device in Devices.Where(d => d.SelectionStatus == SelectionStatus.Selected))
device.UpdateMouseDrag(position);
}
#endregion
#region Panning and zooming
public void EditorGridMouseWheel(object sender, MouseWheelEventArgs e)
{
PanZoomViewModel.ProcessMouseScroll(sender, e);
}
public void EditorGridKeyDown(object sender, KeyEventArgs e)
{
if ((e.Key == Key.LeftCtrl || e.Key == Key.RightCtrl) && e.IsDown)
Mouse.OverrideCursor = Cursors.ScrollAll;
}
public void EditorGridKeyUp(object sender, KeyEventArgs e)
{
if ((e.Key == Key.LeftCtrl || e.Key == Key.RightCtrl) && e.IsUp)
Mouse.OverrideCursor = null;
}
public void Pan(object sender, MouseEventArgs e)
{
PanZoomViewModel.ProcessMouseMove(sender, e);
// Empty the selection rect since it's shown while mouse is down
SelectionRectangle.Rect = Rect.Empty;
}
public void ResetZoomAndPan()
{
PanZoomViewModel.Reset();
}
private bool IsPanKeyDown()
{
return Keyboard.IsKeyDown(Key.LeftCtrl) || Keyboard.IsKeyDown(Key.RightCtrl);
}
#endregion
}
internal enum MouseDragStatus
{
None,
Selecting,
Dragging
}
}
@@ -0,0 +1,121 @@
<UserControl x:Class="Artemis.UI.Screens.SurfaceEditor.Visualization.SurfaceDeviceView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:s="https://github.com/canton7/Stylet"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
xmlns:converters="clr-namespace:Artemis.UI.Converters"
xmlns:surfaceEditor="clr-namespace:Artemis.UI.Screens.SurfaceEditor.Visualization"
mc:Ignorable="d"
d:DataContext="{d:DesignInstance {x:Type surfaceEditor:SurfaceDeviceViewModel}}"
d:DesignHeight="450" d:DesignWidth="800"
Cursor="{Binding Cursor}"
MouseEnter="{s:Action MouseEnter}"
MouseLeave="{s:Action MouseLeave}"
ToolTip="{Binding Device.RgbDevice.DeviceInfo.DeviceName}">
<UserControl.Resources>
<converters:NullToImageConverter x:Key="NullToImageConverter" />
<converters:NullToVisibilityConverter x:Key="NullToVisibilityConverter" />
</UserControl.Resources>
<Grid>
<!-- Device image with fallback -->
<Image Source="{Binding Device.RgbDevice.DeviceInfo.Image, Converter={StaticResource NullToImageConverter}}" />
<Rectangle Fill="{DynamicResource ControlBackgroundBrush}"
Stroke="{DynamicResource ControlBorderBrush}"
StrokeThickness="1"
Visibility="{Binding Device.RgbDevice.DeviceInfo.Image, ConverterParameter=Inverted, Converter={StaticResource NullToVisibilityConverter}}" />
<TextBlock Text="{Binding Device.RgbDevice.DeviceInfo.DeviceName}"
Visibility="{Binding Device.RgbDevice.DeviceInfo.Image, ConverterParameter=Inverted, Converter={StaticResource NullToVisibilityConverter}}"
HorizontalAlignment="Center"
VerticalAlignment="Center"
TextWrapping="Wrap"
TextAlignment="Center" />
<!-- LEDs -->
<ItemsControl ItemsSource="{Binding Leds}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemContainerStyle>
<Style TargetType="{x:Type ContentPresenter}">
<Setter Property="Canvas.Left" Value="{Binding X}" />
<Setter Property="Canvas.Top" Value="{Binding Y}" />
</Style>
</ItemsControl.ItemContainerStyle>
<ItemsControl.ItemTemplate>
<DataTemplate>
<ContentControl Width="{Binding Width}" Height="{Binding Height}" s:View.Model="{Binding}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<!-- Selection rectangle -->
<Rectangle Width="Auto" Height="Auto" StrokeThickness="2">
<Rectangle.Stroke>
<SolidColorBrush Color="{StaticResource IdealForegroundColor}" />
</Rectangle.Stroke>
<Rectangle.Fill>
<SolidColorBrush Color="{StaticResource IdealForegroundColor}" Opacity="0.25" />
</Rectangle.Fill>
<Rectangle.Style>
<Style TargetType="{x:Type Rectangle}">
<Style.Triggers>
<DataTrigger Binding="{Binding SelectionStatus}"
Value="{x:Static surfaceEditor:SelectionStatus.Hover}">
<DataTrigger.EnterActions>
<StopStoryboard BeginStoryboardName="ToSelected" />
<BeginStoryboard x:Name="ToHover">
<Storyboard>
<ColorAnimation
Storyboard.TargetProperty="(Shape.Fill).(SolidColorBrush.Color)"
To="{StaticResource IdealForegroundColor}" Duration="0:0:0.25" />
<ColorAnimation
Storyboard.TargetProperty="(Shape.Stroke).(SolidColorBrush.Color)"
To="{StaticResource IdealForegroundColor}" Duration="0:0:0.25" />
</Storyboard>
</BeginStoryboard>
</DataTrigger.EnterActions>
</DataTrigger>
<DataTrigger Binding="{Binding SelectionStatus}"
Value="{x:Static surfaceEditor:SelectionStatus.Selected}">
<DataTrigger.EnterActions>
<StopStoryboard BeginStoryboardName="ToHover" />
<BeginStoryboard x:Name="ToSelected">
<Storyboard>
<ColorAnimation
Storyboard.TargetProperty="(Shape.Fill).(SolidColorBrush.Color)"
To="{StaticResource Primary400}" Duration="0:0:0.25" />
<ColorAnimation
Storyboard.TargetProperty="(Shape.Stroke).(SolidColorBrush.Color)"
To="{StaticResource Primary400}" Duration="0:0:0.25" />
</Storyboard>
</BeginStoryboard>
</DataTrigger.EnterActions>
</DataTrigger>
<DataTrigger Binding="{Binding SelectionStatus}"
Value="{x:Static surfaceEditor:SelectionStatus.None}">
<DataTrigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="Opacity" To="0" Duration="0:0:0.25" />
</Storyboard>
</BeginStoryboard>
</DataTrigger.EnterActions>
<DataTrigger.ExitActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="Opacity" To="1" Duration="0:0:0.25" />
</Storyboard>
</BeginStoryboard>
</DataTrigger.ExitActions>
</DataTrigger>
</Style.Triggers>
</Style>
</Rectangle.Style>
</Rectangle>
</Grid>
</UserControl>
@@ -0,0 +1,97 @@
using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Input;
using Artemis.Core.Models.Surface;
using Stylet;
namespace Artemis.UI.Screens.SurfaceEditor.Visualization
{
public class SurfaceDeviceViewModel : PropertyChangedBase
{
private readonly List<SurfaceLedViewModel> _leds;
private double _dragOffsetX;
private double _dragOffsetY;
public SurfaceDeviceViewModel(Device device)
{
Device = device;
_leds = new List<SurfaceLedViewModel>();
if (Device.RgbDevice != null)
{
foreach (var led in Device.RgbDevice)
_leds.Add(new SurfaceLedViewModel(led));
}
}
public Device Device { get; set; }
public SelectionStatus SelectionStatus { get; set; }
public Cursor Cursor { get; set; }
public double X
{
get => Device.X;
set => Device.X = value;
}
public double Y
{
get => Device.Y;
set => Device.Y = value;
}
public int ZIndex
{
get => Device.ZIndex;
set => Device.ZIndex = value;
}
public IReadOnlyCollection<SurfaceLedViewModel> Leds => _leds.AsReadOnly();
public Rect DeviceRectangle => Device.RgbDevice == null
? new Rect()
: new Rect(X, Y, Device.RgbDevice.Size.Width, Device.RgbDevice.Size.Height);
public void StartMouseDrag(Point mouseStartPosition)
{
_dragOffsetX = X - mouseStartPosition.X;
_dragOffsetY = Y - mouseStartPosition.Y;
}
public void UpdateMouseDrag(Point mousePosition)
{
var roundedX = Math.Round((mousePosition.X + _dragOffsetX) / 10, 0, MidpointRounding.AwayFromZero) * 10;
var roundedY = Math.Round((mousePosition.Y + _dragOffsetY) / 10, 0, MidpointRounding.AwayFromZero) * 10;
X = Math.Max(0, roundedX);
Y = Math.Max(0, roundedY);
}
// ReSharper disable once UnusedMember.Global - Called from view
public void MouseEnter()
{
if (SelectionStatus == SelectionStatus.None)
{
SelectionStatus = SelectionStatus.Hover;
Cursor = Cursors.Hand;
}
}
// ReSharper disable once UnusedMember.Global - Called from view
public void MouseLeave()
{
if (SelectionStatus == SelectionStatus.Hover)
{
SelectionStatus = SelectionStatus.None;
Cursor = Cursors.Arrow;
}
}
}
public enum SelectionStatus
{
None,
Hover,
Selected
}
}
@@ -0,0 +1,21 @@
<UserControl
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:Converters="clr-namespace:Artemis.UI.Converters"
xmlns:surfaceEditor="clr-namespace:Artemis.UI.Screens.SurfaceEditor.Visualization"
x:Class="Artemis.UI.Screens.SurfaceEditor.Visualization.SurfaceLedView"
mc:Ignorable="d"
d:DataContext="{d:DesignInstance {x:Type surfaceEditor:SurfaceLedViewModel}}"
d:DesignHeight="25" d:DesignWidth="25">
<UserControl.Resources>
<Converters:NullToImageConverter x:Key="NullToImageConverter" />
</UserControl.Resources>
<Grid Width="{Binding Width}" Height="{Binding Height}">
<Grid.Background>
<ImageBrush AlignmentX="Center" AlignmentY="Center" Stretch="Fill"
ImageSource="{Binding Led.Image, Converter={StaticResource NullToImageConverter}}" />
</Grid.Background>
</Grid>
</UserControl>
@@ -0,0 +1,29 @@
using RGB.NET.Core;
using Stylet;
namespace Artemis.UI.Screens.SurfaceEditor.Visualization
{
public class SurfaceLedViewModel : PropertyChangedBase
{
public SurfaceLedViewModel(Led led)
{
Led = led;
ApplyLedToViewModel();
}
public Led Led { get; set; }
public double X { get; set; }
public double Y { get; set; }
public double Width { get; set; }
public double Height { get; set; }
public void ApplyLedToViewModel()
{
X = Led.LedRectangle.X;
Y = Led.LedRectangle.Y;
Width = Led.LedRectangle.Width;
Height = Led.LedRectangle.Height;
}
}
}
@@ -0,0 +1,12 @@
<UserControl x:Class="Artemis.UI.Screens.Workshop.WorkshopView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:Artemis.UI.Screens.Workshop"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<Grid>
<TextBlock>Work work!</TextBlock>
</Grid>
</UserControl>
@@ -0,0 +1,9 @@
using Stylet;
namespace Artemis.UI.Screens.Workshop
{
public class WorkshopViewModel : Screen, IScreenViewModel
{
public string Title => "Workshop";
}
}