mirror of
https://github.com/Artemis-RGB/Artemis
synced 2026-11-12 11:56:30 +00:00
Noise brush - Greatly improved performance on large layouts
Profiles core - Pass real delta time brushes/effects Profiles core - Allow main segment to be disabled on layers General module - Removed test data model General module - Removed expensive open window scan, was fun to try :P Profile editor - Refactored segments Profile editor - Updated segment visuals to scale well when really narrow
This commit is contained in:
1 parent
64a1010100
commit
beff2a0788
13 files changed
+584
-396
No files matched your search
+159
@@ -0,0 +1,159 @@
|
||||
<UserControl x:Class="Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.Timeline.TimelineSegmentView"
|
||||
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.Module.ProfileEditor.LayerProperties.Timeline"
|
||||
xmlns:s="https://github.com/canton7/Stylet"
|
||||
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
|
||||
xmlns:utilities="clr-namespace:Artemis.UI.Utilities"
|
||||
mc:Ignorable="d"
|
||||
d:DesignHeight="450" d:DesignWidth="800"
|
||||
d:DataContext="{d:DesignInstance {x:Type local:TimelineSegmentViewModel}}">
|
||||
<UserControl.Resources>
|
||||
<utilities:BindingProxy x:Key="DataContextProxy" Data="{Binding}" />
|
||||
</UserControl.Resources>
|
||||
<StackPanel Orientation="Horizontal" Visibility="{Binding SegmentEnabled, Converter={x:Static s:BoolToVisibilityConverter.Instance}}">
|
||||
<StackPanel.ContextMenu>
|
||||
<ContextMenu>
|
||||
<MenuItem Header="Repeat main segment"
|
||||
IsCheckable="True"
|
||||
IsChecked="{Binding Data.RepeatSegment, Source={StaticResource DataContextProxy}}"
|
||||
Visibility="{Binding Data.IsMainSegment, Converter={x:Static s:BoolToVisibilityConverter.Instance}, Source={StaticResource DataContextProxy}}"/>
|
||||
<MenuItem Header="Disable segment" Command="{s:Action DisableSegment}"/>
|
||||
</ContextMenu>
|
||||
</StackPanel.ContextMenu>
|
||||
|
||||
<!-- Segment content -->
|
||||
<Grid VerticalAlignment="Top" Background="{StaticResource MaterialDesignPaper}" Width="{Binding SegmentWidth}">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock TextAlignment="Center"
|
||||
Padding="0 3"
|
||||
Margin="5 0 0 0"
|
||||
FontSize="12"
|
||||
ToolTip="{Binding ToolTip}"
|
||||
Text="{Binding Segment}"
|
||||
ClipToBounds="False">
|
||||
<TextBlock.Style>
|
||||
<Style TargetType="TextBlock">
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding ShowSegmentName}" Value="False">
|
||||
<DataTrigger.EnterActions>
|
||||
<BeginStoryboard>
|
||||
<Storyboard>
|
||||
<DoubleAnimation Storyboard.TargetProperty="Opacity" From="1.0" To="0.0" Duration="0:0:0.25">
|
||||
<DoubleAnimation.EasingFunction>
|
||||
<QuadraticEase EasingMode="EaseInOut" />
|
||||
</DoubleAnimation.EasingFunction>
|
||||
</DoubleAnimation>
|
||||
</Storyboard>
|
||||
</BeginStoryboard>
|
||||
</DataTrigger.EnterActions>
|
||||
<DataTrigger.ExitActions>
|
||||
<BeginStoryboard>
|
||||
<Storyboard>
|
||||
<DoubleAnimation Storyboard.TargetProperty="Opacity" From="0.0" To="1.0" Duration="0:0:0.25" />
|
||||
</Storyboard>
|
||||
</BeginStoryboard>
|
||||
</DataTrigger.ExitActions>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</TextBlock.Style>
|
||||
</TextBlock>
|
||||
|
||||
<ToggleButton Grid.Column="1"
|
||||
ToolTip="Toggle main segment repeat"
|
||||
Width="16"
|
||||
Height="16"
|
||||
IsChecked="{Binding RepeatSegment}"
|
||||
VerticalAlignment="Center"
|
||||
Visibility="{Binding IsMainSegment, Converter={x:Static s:BoolToVisibilityConverter.Instance}}">
|
||||
<ToggleButton.Style>
|
||||
<Style TargetType="ToggleButton" BasedOn="{StaticResource MaterialDesignFlatToggleButton}">
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding ShowRepeatButton}" Value="False">
|
||||
<DataTrigger.EnterActions>
|
||||
<BeginStoryboard>
|
||||
<Storyboard>
|
||||
<DoubleAnimation Storyboard.TargetProperty="Opacity" From="1.0" To="0.0" Duration="0:0:0.25">
|
||||
<DoubleAnimation.EasingFunction>
|
||||
<QuadraticEase EasingMode="EaseInOut" />
|
||||
</DoubleAnimation.EasingFunction>
|
||||
</DoubleAnimation>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Visibility)" >
|
||||
<DiscreteObjectKeyFrame KeyTime="00:00:0.25" Value="{x:Static Visibility.Collapsed}"/>
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
</Storyboard>
|
||||
</BeginStoryboard>
|
||||
</DataTrigger.EnterActions>
|
||||
<DataTrigger.ExitActions>
|
||||
<BeginStoryboard>
|
||||
<Storyboard>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Visibility)">
|
||||
<DiscreteObjectKeyFrame KeyTime="00:00:00" Value="{x:Static Visibility.Visible}"/>
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
<DoubleAnimation Storyboard.TargetProperty="Opacity" From="0.0" To="1.0" Duration="0:0:0.25" />
|
||||
</Storyboard>
|
||||
</BeginStoryboard>
|
||||
</DataTrigger.ExitActions>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</ToggleButton.Style>
|
||||
<materialDesign:PackIcon Kind="Repeat" Height="12" Width="12" />
|
||||
</ToggleButton>
|
||||
|
||||
<Button Grid.Column="2" ToolTip="Disable segment" Width="20" Height="20" Margin="0 0 5 0" Command="{s:Action DisableSegment}">
|
||||
<Button.Style>
|
||||
<Style TargetType="Button" BasedOn="{StaticResource MaterialDesignIconButton}">
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding ShowDisableButton}" Value="False">
|
||||
<DataTrigger.EnterActions>
|
||||
<BeginStoryboard>
|
||||
<Storyboard>
|
||||
<DoubleAnimation Storyboard.TargetProperty="Opacity" From="1.0" To="0.0" Duration="0:0:0.25">
|
||||
<DoubleAnimation.EasingFunction>
|
||||
<QuadraticEase EasingMode="EaseInOut" />
|
||||
</DoubleAnimation.EasingFunction>
|
||||
</DoubleAnimation>
|
||||
</Storyboard>
|
||||
</BeginStoryboard>
|
||||
</DataTrigger.EnterActions>
|
||||
<DataTrigger.ExitActions>
|
||||
<BeginStoryboard>
|
||||
<Storyboard>
|
||||
<DoubleAnimation Storyboard.TargetProperty="Opacity" From="0.0" To="1.0" Duration="0:0:0.25" />
|
||||
</Storyboard>
|
||||
</BeginStoryboard>
|
||||
</DataTrigger.ExitActions>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Button.Style>
|
||||
<materialDesign:PackIcon Kind="Close" Height="18" Width="18" />
|
||||
</Button>
|
||||
</Grid>
|
||||
|
||||
<!-- Segment movement display -->
|
||||
<Rectangle RadiusX="2" RadiusY="2" Fill="{DynamicResource PrimaryHueDarkBrush}" Margin="-2 -2 0 0" Width="5" Height="24" VerticalAlignment="Top" HorizontalAlignment="Right" />
|
||||
|
||||
<!-- Segment movement handles -->
|
||||
<Rectangle RadiusX="2"
|
||||
RadiusY="2"
|
||||
Fill="Transparent"
|
||||
Margin="-9 -2 0 0"
|
||||
Width="12"
|
||||
Height="25"
|
||||
VerticalAlignment="Top"
|
||||
HorizontalAlignment="Right"
|
||||
Cursor="SizeWE"
|
||||
MouseDown="{s:Action SegmentMouseDown}"
|
||||
MouseUp="{s:Action SegmentMouseUp}"
|
||||
MouseMove="{s:Action SegmentMouseMove}" />
|
||||
</StackPanel>
|
||||
</UserControl>
|
||||
+275
@@ -0,0 +1,275 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using Artemis.Core.Models.Profile;
|
||||
using Artemis.UI.Shared.Services.Interfaces;
|
||||
using Artemis.UI.Shared.Utilities;
|
||||
using Stylet;
|
||||
|
||||
namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.Timeline
|
||||
{
|
||||
public class TimelineSegmentViewModel : PropertyChangedBase, IDisposable
|
||||
{
|
||||
private bool _draggingSegment;
|
||||
private bool _showSegmentName;
|
||||
private bool _showRepeatButton;
|
||||
private bool _showDisableButton;
|
||||
|
||||
public TimelineSegmentViewModel(IProfileEditorService profileEditorService, SegmentViewModelType segment)
|
||||
{
|
||||
ProfileEditorService = profileEditorService;
|
||||
Segment = segment;
|
||||
SelectedProfileElement = ProfileEditorService.SelectedProfileElement;
|
||||
|
||||
switch (Segment)
|
||||
{
|
||||
case SegmentViewModelType.Start:
|
||||
ToolTip = "This segment is played when a layer starts displaying because it's conditions are met";
|
||||
break;
|
||||
case SegmentViewModelType.Main:
|
||||
ToolTip = "This segment is played while a condition is met, either once or on a repeating loop";
|
||||
break;
|
||||
case SegmentViewModelType.End:
|
||||
ToolTip = "This segment is played once a condition is no longer met";
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(segment));
|
||||
}
|
||||
|
||||
UpdateDisplay();
|
||||
ProfileEditorService.PixelsPerSecondChanged += ProfileEditorServiceOnPixelsPerSecondChanged;
|
||||
SelectedProfileElement.PropertyChanged += SelectedProfileElementOnPropertyChanged;
|
||||
}
|
||||
|
||||
public RenderProfileElement SelectedProfileElement { get; }
|
||||
|
||||
public SegmentViewModelType Segment { get; }
|
||||
public IProfileEditorService ProfileEditorService { get; }
|
||||
public string ToolTip { get; }
|
||||
|
||||
public TimeSpan SegmentLength
|
||||
{
|
||||
get
|
||||
{
|
||||
return Segment switch
|
||||
{
|
||||
SegmentViewModelType.Start => SelectedProfileElement?.StartSegmentLength ?? TimeSpan.Zero,
|
||||
SegmentViewModelType.Main => SelectedProfileElement?.MainSegmentLength ?? TimeSpan.Zero,
|
||||
SegmentViewModelType.End => SelectedProfileElement?.EndSegmentLength ?? TimeSpan.Zero,
|
||||
_ => throw new ArgumentOutOfRangeException()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public double SegmentWidth => ProfileEditorService.PixelsPerSecond * SegmentLength.TotalSeconds;
|
||||
|
||||
public bool SegmentEnabled => SegmentLength != TimeSpan.Zero;
|
||||
public bool IsMainSegment => Segment == SegmentViewModelType.Main;
|
||||
|
||||
// Only the main segment supports this, for any other segment the getter always returns false and the setter does nothing
|
||||
public bool RepeatSegment
|
||||
{
|
||||
get
|
||||
{
|
||||
if (Segment != SegmentViewModelType.Main)
|
||||
return false;
|
||||
|
||||
return SelectedProfileElement?.RepeatMainSegment ?? false;
|
||||
}
|
||||
set
|
||||
{
|
||||
if (Segment != SegmentViewModelType.Main)
|
||||
return;
|
||||
|
||||
SelectedProfileElement.RepeatMainSegment = value;
|
||||
ProfileEditorService.UpdateSelectedProfileElement();
|
||||
NotifyOfPropertyChange(nameof(RepeatSegment));
|
||||
}
|
||||
}
|
||||
|
||||
public double SegmentStartPosition
|
||||
{
|
||||
get
|
||||
{
|
||||
return Segment switch
|
||||
{
|
||||
SegmentViewModelType.Start => 0,
|
||||
SegmentViewModelType.Main => ProfileEditorService.PixelsPerSecond * SelectedProfileElement.StartSegmentLength.TotalSeconds,
|
||||
SegmentViewModelType.End => ProfileEditorService.PixelsPerSecond * (SelectedProfileElement.StartSegmentLength.TotalSeconds + SelectedProfileElement.MainSegmentLength.TotalSeconds),
|
||||
_ => throw new ArgumentOutOfRangeException()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public bool ShowSegmentName
|
||||
{
|
||||
get => _showSegmentName;
|
||||
set => SetAndNotify(ref _showSegmentName, value);
|
||||
}
|
||||
|
||||
public bool ShowRepeatButton
|
||||
{
|
||||
get => _showRepeatButton;
|
||||
set => SetAndNotify(ref _showRepeatButton, value);
|
||||
}
|
||||
|
||||
public bool ShowDisableButton
|
||||
{
|
||||
get => _showDisableButton;
|
||||
set => SetAndNotify(ref _showDisableButton, value);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
ProfileEditorService.PixelsPerSecondChanged -= ProfileEditorServiceOnPixelsPerSecondChanged;
|
||||
SelectedProfileElement.PropertyChanged -= SelectedProfileElementOnPropertyChanged;
|
||||
}
|
||||
|
||||
public void DisableSegment()
|
||||
{
|
||||
switch (Segment)
|
||||
{
|
||||
case SegmentViewModelType.Start:
|
||||
SelectedProfileElement.StartSegmentLength = TimeSpan.Zero;
|
||||
break;
|
||||
case SegmentViewModelType.Main:
|
||||
SelectedProfileElement.MainSegmentLength = TimeSpan.Zero;
|
||||
break;
|
||||
case SegmentViewModelType.End:
|
||||
SelectedProfileElement.EndSegmentLength = TimeSpan.Zero;
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
|
||||
NotifyOfPropertyChange(nameof(SegmentEnabled));
|
||||
ProfileEditorService.UpdateSelectedProfileElement();
|
||||
}
|
||||
|
||||
public void EnableSegment()
|
||||
{
|
||||
switch (Segment)
|
||||
{
|
||||
case SegmentViewModelType.Start:
|
||||
SelectedProfileElement.StartSegmentLength = TimeSpan.FromSeconds(1);
|
||||
break;
|
||||
case SegmentViewModelType.Main:
|
||||
SelectedProfileElement.MainSegmentLength = TimeSpan.FromSeconds(1);
|
||||
break;
|
||||
case SegmentViewModelType.End:
|
||||
SelectedProfileElement.EndSegmentLength = TimeSpan.FromSeconds(1);
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
|
||||
NotifyOfPropertyChange(nameof(SegmentEnabled));
|
||||
ProfileEditorService.UpdateSelectedProfileElement();
|
||||
}
|
||||
|
||||
public void SegmentMouseDown(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
((IInputElement) sender).CaptureMouse();
|
||||
_draggingSegment = true;
|
||||
}
|
||||
|
||||
public void SegmentMouseUp(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
((IInputElement) sender).ReleaseMouseCapture();
|
||||
_draggingSegment = false;
|
||||
}
|
||||
|
||||
public void SegmentMouseMove(object sender, MouseEventArgs e)
|
||||
{
|
||||
if (e.LeftButton != MouseButtonState.Pressed || !_draggingSegment)
|
||||
return;
|
||||
|
||||
// Get the parent scroll viewer, need that for our position
|
||||
var parent = VisualTreeUtilities.FindParent<ScrollViewer>((DependencyObject) sender, "TimelineHeaderScrollViewer");
|
||||
|
||||
var x = Math.Max(0, e.GetPosition(parent).X);
|
||||
var newTime = TimeSpan.FromSeconds(x / ProfileEditorService.PixelsPerSecond);
|
||||
|
||||
// Round the time to something that fits the current zoom level
|
||||
if (ProfileEditorService.PixelsPerSecond < 200)
|
||||
newTime = TimeSpan.FromMilliseconds(Math.Round(newTime.TotalMilliseconds / 5.0) * 5.0);
|
||||
else if (ProfileEditorService.PixelsPerSecond < 500)
|
||||
newTime = TimeSpan.FromMilliseconds(Math.Round(newTime.TotalMilliseconds / 2.0) * 2.0);
|
||||
else
|
||||
newTime = TimeSpan.FromMilliseconds(Math.Round(newTime.TotalMilliseconds));
|
||||
|
||||
// If holding down shift, snap to the closest element on the timeline
|
||||
if (Keyboard.IsKeyDown(Key.LeftShift) || Keyboard.IsKeyDown(Key.RightShift))
|
||||
newTime = ProfileEditorService.SnapToTimeline(newTime, TimeSpan.FromMilliseconds(1000f / ProfileEditorService.PixelsPerSecond * 5), false, true, true);
|
||||
// If holding down control, round to the closest 50ms
|
||||
else if (Keyboard.IsKeyDown(Key.LeftCtrl) || Keyboard.IsKeyDown(Key.RightCtrl))
|
||||
newTime = TimeSpan.FromMilliseconds(Math.Round(newTime.TotalMilliseconds / 50.0) * 50.0);
|
||||
|
||||
|
||||
if (Segment == SegmentViewModelType.Start)
|
||||
{
|
||||
if (newTime < TimeSpan.FromMilliseconds(100))
|
||||
newTime = TimeSpan.FromMilliseconds(100);
|
||||
SelectedProfileElement.StartSegmentLength = newTime;
|
||||
}
|
||||
else if (Segment == SegmentViewModelType.Main)
|
||||
{
|
||||
if (newTime < SelectedProfileElement.StartSegmentLength + TimeSpan.FromMilliseconds(100))
|
||||
newTime = SelectedProfileElement.StartSegmentLength + TimeSpan.FromMilliseconds(100);
|
||||
SelectedProfileElement.MainSegmentLength = newTime - SelectedProfileElement.StartSegmentLength;
|
||||
}
|
||||
else if (Segment == SegmentViewModelType.End)
|
||||
{
|
||||
if (newTime < SelectedProfileElement.StartSegmentLength + SelectedProfileElement.MainSegmentLength + TimeSpan.FromMilliseconds(100))
|
||||
newTime = SelectedProfileElement.StartSegmentLength + SelectedProfileElement.MainSegmentLength + TimeSpan.FromMilliseconds(100);
|
||||
SelectedProfileElement.EndSegmentLength = newTime - SelectedProfileElement.StartSegmentLength - SelectedProfileElement.MainSegmentLength;
|
||||
}
|
||||
|
||||
NotifyOfPropertyChange(nameof(SegmentLength));
|
||||
NotifyOfPropertyChange(nameof(SegmentWidth));
|
||||
|
||||
UpdateDisplay();
|
||||
}
|
||||
|
||||
private void SelectedProfileElementOnPropertyChanged(object sender, PropertyChangedEventArgs e)
|
||||
{
|
||||
if (e.PropertyName == nameof(RenderProfileElement.StartSegmentLength) ||
|
||||
e.PropertyName == nameof(RenderProfileElement.MainSegmentLength) ||
|
||||
e.PropertyName == nameof(RenderProfileElement.EndSegmentLength))
|
||||
{
|
||||
NotifyOfPropertyChange(nameof(SegmentStartPosition));
|
||||
NotifyOfPropertyChange(nameof(SegmentWidth));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void ProfileEditorServiceOnPixelsPerSecondChanged(object? sender, EventArgs e)
|
||||
{
|
||||
NotifyOfPropertyChange(nameof(SegmentWidth));
|
||||
NotifyOfPropertyChange(nameof(SegmentStartPosition));
|
||||
|
||||
UpdateDisplay();
|
||||
}
|
||||
|
||||
private void UpdateDisplay()
|
||||
{
|
||||
if (!IsMainSegment)
|
||||
ShowSegmentName = SegmentWidth > 60;
|
||||
else
|
||||
ShowSegmentName = SegmentWidth > 80;
|
||||
|
||||
ShowRepeatButton = SegmentWidth > 45 && IsMainSegment;
|
||||
ShowDisableButton = SegmentWidth > 25;
|
||||
}
|
||||
}
|
||||
|
||||
public enum SegmentViewModelType
|
||||
{
|
||||
Start,
|
||||
Main,
|
||||
End
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user