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

Sidebar - Added profile reordering and moving to different categories

This commit is contained in:
Robert 2022-05-27 10:06:47 +02:00
parent cd97ed420c
commit bcd03becc7
8 changed files with 333 additions and 16 deletions

View File

@ -55,7 +55,7 @@
<entry key="Artemis.UI/Screens/Settings/Tabs/GeneralTabView.axaml" value="Artemis.UI.Linux/Artemis.UI.Linux.csproj" />
<entry key="Artemis.UI/Screens/Sidebar/ContentDialogs/SidebarCategoryEditView.axaml" value="Artemis.UI.Linux/Artemis.UI.Linux.csproj" />
<entry key="Artemis.UI/Screens/Sidebar/Dialogs/ProfileConfigurationEditView.axaml" value="Artemis.UI.Linux/Artemis.UI.Linux.csproj" />
<entry key="Artemis.UI/Screens/Sidebar/SidebarCategoryView.axaml" value="Artemis.UI.Linux/Artemis.UI.Linux.csproj" />
<entry key="Artemis.UI/Screens/Sidebar/SidebarCategoryView.axaml" value="Artemis.UI.Windows/Artemis.UI.Windows.csproj" />
<entry key="Artemis.UI/Screens/Sidebar/SidebarProfileConfigurationView.axaml" value="Artemis.UI.Linux/Artemis.UI.Linux.csproj" />
<entry key="Artemis.UI/Screens/Sidebar/SidebarView.axaml" value="Artemis.UI.Linux/Artemis.UI.Linux.csproj" />
<entry key="Artemis.UI/Screens/SurfaceEditor/ListDeviceView.axaml" value="Artemis.UI.Linux/Artemis.UI.Linux.csproj" />

View File

@ -98,7 +98,10 @@ namespace Artemis.Core
configuration.Category.RemoveProfileConfiguration(configuration);
if (targetIndex != null)
_profileConfigurations.Insert(Math.Clamp(targetIndex.Value, 0, _profileConfigurations.Count), configuration);
{
targetIndex = Math.Clamp(targetIndex.Value, 0, _profileConfigurations.Count);
_profileConfigurations.Insert(targetIndex.Value, configuration);
}
else
_profileConfigurations.Add(configuration);
configuration.Category = this;
@ -116,7 +119,8 @@ namespace Artemis.Core
internal void RemoveProfileConfiguration(ProfileConfiguration configuration)
{
if (!_profileConfigurations.Remove(configuration)) return;
if (!_profileConfigurations.Remove(configuration))
return;
for (int index = 0; index < _profileConfigurations.Count; index++)
_profileConfigurations[index].Order = index;

View File

@ -90,7 +90,9 @@
</Style>
</UserControl.Styles>
<Grid RowDefinitions="*,Auto">
<TreeView Name="ProfileTreeView" Classes="no-right-margin draggable" Items="{CompiledBinding Children}" SelectedItem="{CompiledBinding SelectedChild}"
<TreeView Name="ProfileTreeView" Classes="no-right-margin draggable"
Items="{CompiledBinding Children}"
SelectedItem="{CompiledBinding SelectedChild}"
SelectionChanged="ProfileTreeView_OnSelectionChanged">
<TreeView.Styles>
<Style Selector="TreeViewItem">

View File

@ -0,0 +1,124 @@
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using Artemis.Core;
using Artemis.UI.Screens.ProfileEditor.ProfileTree;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Generators;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.VisualTree;
using Avalonia.Xaml.Interactions.DragAndDrop;
namespace Artemis.UI.Screens.Sidebar.Behaviors;
public class SidebarCategoryViewDropHandler : DropHandlerBase
{
private bool Validate(ListBox listBox, DragEventArgs e, object? sourceContext, object? targetContext, bool bExecute)
{
if (sourceContext is not SidebarProfileConfigurationViewModel sourceItem || targetContext is not SidebarCategoryViewModel vm ||
listBox.GetVisualAt(e.GetPosition(listBox)) is not IControl targetControl)
return false;
if (e.DragEffects != DragDropEffects.Move)
return false;
SidebarProfileConfigurationViewModel? targetItem = targetControl.DataContext as SidebarProfileConfigurationViewModel;
ListBoxItem? targetVisual = null;
bool before = true;
if (targetItem != null)
{
Point position = e.GetPosition(listBox);
targetVisual = listBox.GetVisualAt(position).FindAncestorOfType<ListBoxItem>();
if (targetVisual != null)
{
Point positionInTarget = e.GetPosition(targetVisual);
if (positionInTarget.Y > (targetVisual.Bounds.Height / 2))
before = false;
}
}
foreach (ItemContainerInfo? item in listBox.ItemContainerGenerator.Containers)
SetDraggingPseudoClasses(item.ContainerControl, false, false);
if (bExecute)
{
if (targetItem != null)
{
int index = vm.ProfileConfigurations.IndexOf(targetItem);
if (!before)
index++;
vm.AddProfileConfiguration(sourceItem.ProfileConfiguration, index);
}
else
{
vm.AddProfileConfiguration(sourceItem.ProfileConfiguration, null);
}
}
else if (targetVisual != null)
{
SetDraggingPseudoClasses(targetVisual, true, before);
}
return true;
}
public override bool Validate(object? sender, DragEventArgs e, object? sourceContext, object? targetContext, object? state)
{
if (sender is ItemsControl itemsControl)
{
foreach (ItemContainerInfo? item in itemsControl.ItemContainerGenerator.Containers)
SetDraggingPseudoClasses(item.ContainerControl, false, false);
}
if (e.Source is IControl && sender is ListBox listBox)
{
return Validate(listBox, e, sourceContext, targetContext, false);
}
return false;
}
public override bool Execute(object? sender, DragEventArgs e, object? sourceContext, object? targetContext, object? state)
{
if (sender is ItemsControl itemsControl)
{
foreach (ItemContainerInfo? item in itemsControl.ItemContainerGenerator.Containers)
SetDraggingPseudoClasses(item.ContainerControl, false, false);
}
if (e.Source is IControl && sender is ListBox listBox)
{
return Validate(listBox, e, sourceContext, targetContext, true);
}
return false;
}
private void SetDraggingPseudoClasses(IControl control, bool dragging, bool before)
{
if (!dragging)
{
((IPseudoClasses) control.Classes).Remove(":dragging");
((IPseudoClasses) control.Classes).Remove(":dragging-before");
((IPseudoClasses) control.Classes).Remove(":dragging-after");
((IPseudoClasses) control.Classes).Remove(":dragging-into");
}
else
{
((IPseudoClasses) control.Classes).Add(":dragging");
if (before)
{
((IPseudoClasses) control.Classes).Add(":dragging-before");
((IPseudoClasses) control.Classes).Remove(":dragging-after");
((IPseudoClasses) control.Classes).Remove(":dragging-into");
}
else
{
((IPseudoClasses) control.Classes).Remove(":dragging-before");
((IPseudoClasses) control.Classes).Add(":dragging-after");
((IPseudoClasses) control.Classes).Remove(":dragging-into");
}
}
}
}

View File

@ -4,9 +4,17 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
xmlns:local="clr-namespace:Artemis.UI.Screens.Sidebar"
xmlns:i="clr-namespace:Avalonia.Xaml.Interactivity;assembly=Avalonia.Xaml.Interactivity"
xmlns:idd="clr-namespace:Avalonia.Xaml.Interactions.DragAndDrop;assembly=Avalonia.Xaml.Interactions"
xmlns:sb="clr-namespace:Artemis.UI.Screens.Sidebar.Behaviors"
xmlns:converters="clr-namespace:Artemis.UI.Converters"
xmlns:b="clr-namespace:Artemis.UI.Behaviors"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.Sidebar.SidebarCategoryView"
x:DataType="local:SidebarCategoryViewModel">
<UserControl.Resources>
<converters:ColorOpacityConverter x:Key="ColorOpacityConverter" />
</UserControl.Resources>
<UserControl.Styles>
<Style Selector=":is(Button).category-button">
<Setter Property="IsVisible" Value="False" />
@ -28,17 +36,66 @@
<Setter Property="Opacity" Value="1" />
</Style>
<Style Selector=".sidebar-listbox > ListBoxItem">
<Setter Property="Padding" Value="6 0"/>
<Setter Property="Padding" Value="6 0" />
</Style>
<!-- Dragging -->
<Style Selector="ListBox.sidebar-listbox">
<Style.Resources>
<sb:SidebarCategoryViewDropHandler x:Key="SidebarCategoryViewDropHandler" />
</Style.Resources>
<Setter Property="(i:Interaction.Behaviors)">
<i:BehaviorCollectionTemplate>
<i:BehaviorCollection>
<idd:ContextDropBehavior Handler="{StaticResource SidebarCategoryViewDropHandler}" />
</i:BehaviorCollection>
</i:BehaviorCollectionTemplate>
</Setter>
</Style>
<Style Selector="ListBox.sidebar-listbox ListBoxItem">
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
<Setter Property="(i:Interaction.Behaviors)">
<i:BehaviorCollectionTemplate>
<i:BehaviorCollection>
<b:SimpleContextDragBehavior />
</i:BehaviorCollection>
</i:BehaviorCollectionTemplate>
</Setter>
</Style>
<Style Selector="ListBox.sidebar-listbox ListBoxItem:dragging-before">
<Setter Property="Background">
<Setter.Value>
<LinearGradientBrush StartPoint="0,0" EndPoint="0,30">
<GradientStop Color="{StaticResource SystemAccentColorLight3}" Offset="0.0" />
<GradientStop Color="{StaticResource SystemAccentColorLight3}" Offset="0.05" />
<GradientStop Color="{Binding Source={StaticResource SystemAccentColorLight3}, Converter={StaticResource ColorOpacityConverter}, ConverterParameter=0.25}" Offset="0.05" />
<GradientStop Color="{Binding Source={StaticResource SystemAccentColorLight3}, Converter={StaticResource ColorOpacityConverter}, ConverterParameter=0}" Offset="0.25" />
</LinearGradientBrush>
</Setter.Value>
</Setter>
</Style>
<Style Selector="ListBox.sidebar-listbox ListBoxItem:dragging-after">
<Setter Property="Background">
<Setter.Value>
<LinearGradientBrush StartPoint="0,0" EndPoint="0,32">
<GradientStop Color="{Binding Source={StaticResource SystemAccentColorLight3}, Converter={StaticResource ColorOpacityConverter}, ConverterParameter=0}" Offset="0.75" />
<GradientStop Color="{Binding Source={StaticResource SystemAccentColorLight3}, Converter={StaticResource ColorOpacityConverter}, ConverterParameter=0.25}" Offset="0.95" />
<GradientStop Color="{StaticResource SystemAccentColorLight3}" Offset="0.95" />
<GradientStop Color="{StaticResource SystemAccentColorLight3}" Offset="1" />
</LinearGradientBrush>
</Setter.Value>
</Setter>
</Style>
</UserControl.Styles>
<Grid x:Name="ContainerGrid" Margin="0 8 0 0" RowDefinitions="Auto,*">
<Grid Grid.Row="0" Background="Transparent" Margin="0 0 6 0" ColumnDefinitions="Auto,*,Auto,Auto,Auto,Auto">
<avalonia:MaterialIcon Classes.chevron-collapsed="{CompiledBinding !IsCollapsed}"
Kind="ChevronUp"
Grid.Column="0"
Margin="5 0"
PointerPressed="Title_OnPointerPressed"
PointerReleased="InputElement_OnPointerReleased"
Background="Transparent">
<avalonia:MaterialIcon.Transitions>
<Transitions>
@ -56,7 +113,7 @@
VerticalAlignment="Center"
Text="{CompiledBinding ProfileCategory.Name, FallbackValue='Profile name'}"
TextTrimming="CharacterEllipsis"
PointerPressed="Title_OnPointerPressed"
PointerReleased="InputElement_OnPointerReleased"
Background="Transparent">
<TextBlock.Transitions>
<Transitions>
@ -107,7 +164,8 @@
</Grid>
<Border Grid.Row="1">
<ListBox Classes="sidebar-listbox"
<ListBox Name="SidebarListBox"
Classes="sidebar-listbox"
Items="{CompiledBinding ProfileConfigurations}"
SelectedItem="{CompiledBinding SelectedProfileConfiguration}"
MinHeight="10"

View File

@ -1,15 +1,34 @@
using System;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Layout;
using Avalonia.LogicalTree;
using Avalonia.Markup.Xaml;
using Avalonia.Media;
using Avalonia.Media.Imaging;
using Avalonia.ReactiveUI;
using Avalonia.VisualTree;
namespace Artemis.UI.Screens.Sidebar
{
public class SidebarCategoryView : ReactiveUserControl<SidebarCategoryViewModel>
{
private static Image? _dragAdorner;
private Point _dragStartPosition;
private Point _elementDragOffset;
private ListBox _listBox;
public SidebarCategoryView()
{
InitializeComponent();
_listBox = this.Get<ListBox>("SidebarListBox");
AddHandler(DragDrop.DragEnterEvent, HandleDragEnterEvent, RoutingStrategies.Direct | RoutingStrategies.Tunnel | RoutingStrategies.Bubble, true);
AddHandler(DragDrop.DragOverEvent, HandleDragOver, RoutingStrategies.Direct | RoutingStrategies.Tunnel | RoutingStrategies.Bubble, true);
AddHandler(PointerEnterEvent, HandlePointerEnter, RoutingStrategies.Direct | RoutingStrategies.Tunnel | RoutingStrategies.Bubble, true);
}
private void InitializeComponent()
@ -17,9 +36,104 @@ namespace Artemis.UI.Screens.Sidebar
AvaloniaXamlLoader.Load(this);
}
private void Title_OnPointerPressed(object? sender, PointerPressedEventArgs e)
private void InputElement_OnPointerReleased(object? sender, PointerReleasedEventArgs e)
{
ViewModel?.ToggleCollapsed.Execute().Subscribe();
if (e.InitialPressMouseButton == MouseButton.Left)
ViewModel?.ToggleCollapsed.Execute().Subscribe();
}
#region Dragging
private void HandlePointerEnter(object? sender, PointerEventArgs e)
{
DisposeDragAdorner();
}
private void HandleDragEnterEvent(object? sender, DragEventArgs e)
{
CreateDragAdorner(e);
}
private void CreateDragAdorner(DragEventArgs e)
{
if (_dragAdorner != null)
return;
if (e.Source is not Control c)
return;
// Get the list box item that raised the event
ListBoxItem? container = c.FindLogicalAncestorOfType<ListBoxItem>();
if (container == null)
return;
// Take a snapshot of said tree view item and add it as an adorner
ITransform? originalTransform = container.RenderTransform;
try
{
_dragStartPosition = e.GetPosition(this.FindAncestorOfType<Window>());
_elementDragOffset = e.GetPosition(container);
RenderTargetBitmap renderTarget = new(new PixelSize((int) container.Bounds.Width, (int) container.Bounds.Height));
container.RenderTransform = new TranslateTransform(container.Bounds.X * -1, container.Bounds.Y * -1);
renderTarget.Render(container);
_dragAdorner = new Image
{
Source = renderTarget,
VerticalAlignment = VerticalAlignment.Top,
HorizontalAlignment = HorizontalAlignment.Left,
Stretch = Stretch.None,
IsHitTestVisible = false
};
AdornerLayer.GetAdornerLayer(this)!.Children.Add(_dragAdorner);
}
finally
{
container.RenderTransform = originalTransform;
}
}
private void HandleDragOver(object? sender, DragEventArgs e)
{
UpdateDragAdorner(e.GetPosition(this.FindAncestorOfType<Window>()));
}
private void HandleLeaveEvent(object? sender, RoutedEventArgs e)
{
// If there is currently an adorner, dispose of it
DisposeDragAdorner();
}
private void HandleDrop(object? sender, DragEventArgs e)
{
// If there is currently an adorner, dispose of it
DisposeDragAdorner();
}
private void DisposeDragAdorner()
{
if (_dragAdorner == null)
return;
AdornerLayer.GetAdornerLayer(this)!.Children.Remove(_dragAdorner);
(_dragAdorner.Source as RenderTargetBitmap)?.Dispose();
_dragAdorner = null;
}
private void UpdateDragAdorner(Point position)
{
if (_dragAdorner == null)
return;
_dragAdorner.RenderTransform = new TranslateTransform(_dragStartPosition.X - _elementDragOffset.X, position.Y - _elementDragOffset.Y);
}
private void InputElement_OnPointerMoved(object? sender, PointerEventArgs e)
{
DisposeDragAdorner();
}
#endregion
}
}

View File

@ -14,6 +14,7 @@ using Artemis.UI.Shared.Services;
using Artemis.UI.Shared.Services.Builders;
using Artemis.UI.Shared.Services.ProfileEditor;
using DynamicData;
using DynamicData.Binding;
using ReactiveUI;
namespace Artemis.UI.Screens.Sidebar
@ -37,13 +38,13 @@ namespace Artemis.UI.Screens.Sidebar
_vmFactory = vmFactory;
ProfileCategory = profileCategory;
SourceCache<ProfileConfiguration, Guid> profileConfigurations = new(t => t.ProfileId);
SourceList<ProfileConfiguration> profileConfigurations = new();
// Only show items when not collapsed
IObservable<Func<ProfileConfiguration, bool>> profileConfigurationsFilter = this.WhenAnyValue(vm => vm.IsCollapsed).Select(b => new Func<object, bool>(_ => !b));
profileConfigurations.Connect()
.SortBy(c => c.Order)
.Filter(profileConfigurationsFilter)
.Sort(SortExpressionComparer<ProfileConfiguration>.Ascending(c => c.Order))
.Transform(c => _vmFactory.SidebarProfileConfigurationViewModel(_sidebarViewModel, c))
.Bind(out ReadOnlyObservableCollection<SidebarProfileConfigurationViewModel> profileConfigurationViewModels)
.Subscribe();
@ -58,7 +59,7 @@ namespace Artemis.UI.Screens.Sidebar
{
// Update the list of profiles whenever the category fires events
Observable.FromEventPattern<ProfileConfigurationEventArgs>(x => profileCategory.ProfileConfigurationAdded += x, x => profileCategory.ProfileConfigurationAdded -= x)
.Subscribe(e => profileConfigurations.AddOrUpdate(e.EventArgs.ProfileConfiguration))
.Subscribe(e => profileConfigurations.Add(e.EventArgs.ProfileConfiguration))
.DisposeWith(d);
Observable.FromEventPattern<ProfileConfigurationEventArgs>(x => profileCategory.ProfileConfigurationRemoved += x, x => profileCategory.ProfileConfigurationRemoved -= x)
.Subscribe(e => profileConfigurations.Remove(e.EventArgs.ProfileConfiguration))
@ -77,7 +78,7 @@ namespace Artemis.UI.Screens.Sidebar
profileConfigurations.Edit(updater =>
{
foreach (ProfileConfiguration profileConfiguration in profileCategory.ProfileConfigurations)
updater.AddOrUpdate(profileConfiguration);
updater.Add(profileConfiguration);
});
}
@ -135,5 +136,16 @@ namespace Artemis.UI.Screens.Sidebar
ProfileCategory.IsSuspended = !ProfileCategory.IsSuspended;
_profileService.SaveProfileCategory(ProfileCategory);
}
public void AddProfileConfiguration(ProfileConfiguration profileConfiguration, int? index)
{
ProfileCategory oldCategory = profileConfiguration.Category;
ProfileCategory.AddProfileConfiguration(profileConfiguration, index);
_profileService.SaveProfileCategory(ProfileCategory);
// If the profile moved to a new category, also save the old category
if (oldCategory != ProfileCategory)
_profileService.SaveProfileCategory(oldCategory);
}
}
}

View File

@ -7,6 +7,9 @@
xmlns:svg="clr-namespace:Avalonia.Svg.Skia;assembly=Avalonia.Svg.Skia"
mc:Ignorable="d" d:DesignWidth="240" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.Sidebar.SidebarView">
<UserControl.Styles>
<StyleInclude Source="avares://Avalonia.Xaml.Interactions/Draggable/Styles.axaml" />
</UserControl.Styles>
<Grid RowDefinitions="60,Auto,Auto,*,Auto,Auto">
<Grid Grid.Row="0" IsHitTestVisible="False" ColumnDefinitions="Auto,*">
<Image Grid.Column="0">
@ -33,7 +36,7 @@
<!-- Categories -->
<ScrollViewer Grid.Row="3" VerticalScrollBarVisibility="Auto">
<StackPanel>
<ItemsControl Margin="10 2" Items="{Binding SidebarCategories}" />
<ItemsControl Margin="10 2" Items="{Binding SidebarCategories}" Classes="profile-categories" />
<Button Content="Add new category"
Margin="10"
HorizontalAlignment="Stretch"