From 531b7bc74d5c6460a92244a5a02b60e3b3464882 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartosz=20Korczy=C5=84ski?= Date: Fri, 8 Oct 2021 20:18:01 +0100 Subject: [PATCH] [Database Editor] Greatly improvement database editor UX Among many: arrow movements, copy paste, selected row view --- .../Controls/CompletionComboBox.axaml | 7 +- .../Controls/CompletionComboBox.axaml.cs | 32 +- .../Styles/Windows10/ColorsDark.xaml | 3 + .../Styles/Windows10/ColorsLight.xaml | 6 + .../Styles/Windows10/GroupingListBox.axaml | 6 +- .../Controls/DynamicColumnsPanel.cs | 297 ++++++++++++++++++ .../Controls}/FlagComboBox.cs | 6 +- .../Converters/BoolToIntegerConverter.cs | 26 ++ .../BoolToRowDefinitionsConverter.cs | 43 +++ WDE.Common.Avalonia/Utils/GridUtils.cs | 60 ++++ WDE.Common.Avalonia/Utils/ICustomCopyPaste.cs | 11 + WDE.Common.Avalonia/Utils/MenuBind.cs | 3 + .../Controls/ButtonFastCellView.cs | 27 ++ .../Controls/FastBoolCellView.axaml | 18 +- .../Controls/FastBoolCellView.axaml.cs | 50 +++ .../Controls/FastCellView.axaml | 10 + .../Controls/FastCellView.axaml.cs | 238 ++++---------- .../Controls/FastCellViewBase.cs | 100 +++++- .../Controls/FastFlagCellView.cs | 133 ++++++++ .../Controls/FastItemCellView.cs | 136 ++++++++ .../Controls/OpenableFastCellViewBase.cs | 174 ++++++++++ .../Helpers/FieldValueTemplateSelector.cs | 7 + .../Views/ColorsDark.axaml | 3 + .../Views/ColorsLight.axaml | 2 + .../MultiRowDbTableEditorToolBar.axaml | 7 + .../MultiRow/MultiRowDbTableEditorView.axaml | 188 +++++++++-- .../Views/MultiRow/SelectablePanel.cs | 98 ++++++ .../Template/TemplateDbTableEditorView.axaml | 55 +++- .../Services/DatabaseEditorsSettings.cs | 34 ++ .../Services/IDatabaseEditorsSettings.cs | 17 + .../ViewModels/BaseDatabaseCellViewModel.cs | 95 ++++++ .../MultiRow/DatabaseCellViewModel.cs | 35 ++- .../DatabaseEntitiesGroupViewModel.cs | 7 +- .../MultiRowDbTableEditorViewModel.cs | 55 +++- .../Template/DatabaseCellViewModel.cs | 28 +- .../TemplateDbTableEditorViewModel.cs | 6 +- .../Themes/Generic.axaml | 2 +- WoWDatabaseEditor/Icons/icon_split_horiz.png | Bin 0 -> 641 bytes .../Icons/icon_split_horiz@2x.png | Bin 0 -> 742 bytes .../Icons/icon_split_horiz_dark.png | Bin 0 -> 627 bytes .../Icons/icon_split_horiz_dark@2x.png | Bin 0 -> 720 bytes WoWDatabaseEditor/Icons/icon_split_vert.png | Bin 0 -> 641 bytes .../Icons/icon_split_vert@2x.png | Bin 0 -> 741 bytes .../Icons/icon_split_vert_dark.png | Bin 0 -> 627 bytes .../Icons/icon_split_vert_dark@2x.png | Bin 0 -> 710 bytes 45 files changed, 1741 insertions(+), 284 deletions(-) create mode 100644 WDE.Common.Avalonia/Controls/DynamicColumnsPanel.cs rename {WDE.SmartScriptEditor.Avalonia/Editor/UserControls => WDE.Common.Avalonia/Controls}/FlagComboBox.cs (99%) create mode 100644 WDE.Common.Avalonia/Converters/BoolToIntegerConverter.cs create mode 100644 WDE.Common.Avalonia/Converters/BoolToRowDefinitionsConverter.cs create mode 100644 WDE.Common.Avalonia/Utils/GridUtils.cs create mode 100644 WDE.Common.Avalonia/Utils/ICustomCopyPaste.cs create mode 100644 WDE.DatabaseEditors.Avalonia/Controls/ButtonFastCellView.cs create mode 100644 WDE.DatabaseEditors.Avalonia/Controls/FastFlagCellView.cs create mode 100644 WDE.DatabaseEditors.Avalonia/Controls/FastItemCellView.cs create mode 100644 WDE.DatabaseEditors.Avalonia/Controls/OpenableFastCellViewBase.cs create mode 100644 WDE.DatabaseEditors.Avalonia/Views/MultiRow/SelectablePanel.cs create mode 100644 WDE.DatabaseEditors/Services/DatabaseEditorsSettings.cs create mode 100644 WDE.DatabaseEditors/Services/IDatabaseEditorsSettings.cs create mode 100644 WDE.DatabaseEditors/ViewModels/BaseDatabaseCellViewModel.cs create mode 100644 WoWDatabaseEditor/Icons/icon_split_horiz.png create mode 100644 WoWDatabaseEditor/Icons/icon_split_horiz@2x.png create mode 100644 WoWDatabaseEditor/Icons/icon_split_horiz_dark.png create mode 100644 WoWDatabaseEditor/Icons/icon_split_horiz_dark@2x.png create mode 100644 WoWDatabaseEditor/Icons/icon_split_vert.png create mode 100644 WoWDatabaseEditor/Icons/icon_split_vert@2x.png create mode 100644 WoWDatabaseEditor/Icons/icon_split_vert_dark.png create mode 100644 WoWDatabaseEditor/Icons/icon_split_vert_dark@2x.png diff --git a/AvaloniaStyles/Controls/CompletionComboBox.axaml b/AvaloniaStyles/Controls/CompletionComboBox.axaml index 6b3d1732f..761add3bc 100644 --- a/AvaloniaStyles/Controls/CompletionComboBox.axaml +++ b/AvaloniaStyles/Controls/CompletionComboBox.axaml @@ -32,10 +32,10 @@ IsOpen="{TemplateBinding IsDropDownOpen, Mode=TwoWay}" WindowManagerAddShadowHint="True" MinWidth="{Binding Bounds.Width, RelativeSource={RelativeSource TemplatedParent}}" - IsLightDismissEnabled="True" + IsLightDismissEnabled="{TemplateBinding IsLightDismissEnabled}" MaxWidth="700" MaxHeight="600" - PlacementConstraintAdjustment="SlideY" + PlacementConstraintAdjustment="SlideY,SlideX" PlacementMode="Bottom" PlacementGravity="Bottom" PlacementTarget="PART_Button"> @@ -59,4 +59,7 @@ + \ No newline at end of file diff --git a/AvaloniaStyles/Controls/CompletionComboBox.axaml.cs b/AvaloniaStyles/Controls/CompletionComboBox.axaml.cs index 6e628fe93..d6fb10f84 100644 --- a/AvaloniaStyles/Controls/CompletionComboBox.axaml.cs +++ b/AvaloniaStyles/Controls/CompletionComboBox.axaml.cs @@ -17,6 +17,7 @@ using Avalonia.Threading; using Avalonia.VisualTree; using AvaloniaStyles.Utils; +using FuzzySharp; using WDE.MVVM.Utils; namespace AvaloniaStyles.Controls @@ -87,6 +88,18 @@ public IDataTemplate? ButtonItemTemplate set => SetValue(ButtonItemTemplateProperty, value); } + public bool HideButton + { + get => GetValue(HideButtonProperty); + set => SetValue(HideButtonProperty, value); + } + + public bool IsLightDismissEnabled + { + get => GetValue(IsLightDismissEnabledProperty); + set => SetValue(IsLightDismissEnabledProperty, value); + } + public static readonly StyledProperty ButtonItemTemplateProperty = AvaloniaProperty.Register(nameof(ButtonItemTemplate)); @@ -123,6 +136,14 @@ public IDataTemplate? ButtonItemTemplate (o, v) => o.SelectedItem = v, defaultBindingMode: BindingMode.TwoWay); + public static readonly StyledProperty IsLightDismissEnabledProperty = + AvaloniaProperty.Register(nameof (IsLightDismissEnabled), true); + + public static readonly StyledProperty HideButtonProperty = + AvaloniaProperty.Register(nameof (HideButton)); + + public event Action? Closed; + public CompletionComboBox() { // Default async populator searches in Items (toString) using Fuzzy match or normal match depending on the collection size @@ -138,7 +159,7 @@ public CompletionComboBox() { if (o.Count < 250) { - return FuzzySharp.Process.ExtractSorted(s, items.Select(item => item.ToString()), cutoff: 51) + return Process.ExtractSorted(s, items.Select(item => item.ToString()), cutoff: 51) .Select(item => o[item.Index]!); } @@ -165,7 +186,7 @@ protected override void OnTextInput(TextInputEventArgs e) if (!e.Handled && e.Text != "\n" && e.Text != "\r") { IsDropDownOpen = true; - SearchTextBox.RaiseEvent(new TextInputEventArgs() + SearchTextBox.RaiseEvent(new TextInputEventArgs { Device = e.Device, Handled = false, @@ -180,6 +201,10 @@ protected override void OnTextInput(TextInputEventArgs e) static CompletionComboBox() { + HideButtonProperty.Changed.AddClassHandler((box, args) => + { + box.PseudoClasses.Set(":hideButton", args.NewValue is true); + }); ItemTemplateProperty.Changed.AddClassHandler((box, args) => { if (box.ButtonItemTemplate == null) @@ -308,7 +333,7 @@ private TextBox SearchTextBox } else if (args.Key == Key.Tab) { - SelectionAdapter.HandleKeyDown(new KeyEventArgs() + SelectionAdapter.HandleKeyDown(new KeyEventArgs { Device = args.Device, Key = (args.KeyModifiers & KeyModifiers.Shift) != 0 ? Key.Up : Key.Down, @@ -375,6 +400,7 @@ private void Close() { IsDropDownOpen = false; FocusManager.Instance.Focus(ToggleButton, NavigationMethod.Tab); + Closed?.Invoke(); } /// Filter logic diff --git a/AvaloniaStyles/Styles/Windows10/ColorsDark.xaml b/AvaloniaStyles/Styles/Windows10/ColorsDark.xaml index d6e9e1ef3..6d2766c0f 100644 --- a/AvaloniaStyles/Styles/Windows10/ColorsDark.xaml +++ b/AvaloniaStyles/Styles/Windows10/ColorsDark.xaml @@ -76,6 +76,9 @@ + + + diff --git a/AvaloniaStyles/Styles/Windows10/GroupingListBox.axaml b/AvaloniaStyles/Styles/Windows10/GroupingListBox.axaml index 52ff0f730..a4485c7ad 100644 --- a/AvaloniaStyles/Styles/Windows10/GroupingListBox.axaml +++ b/AvaloniaStyles/Styles/Windows10/GroupingListBox.axaml @@ -6,7 +6,7 @@ - + - + diff --git a/WDE.Common.Avalonia/Controls/DynamicColumnsPanel.cs b/WDE.Common.Avalonia/Controls/DynamicColumnsPanel.cs new file mode 100644 index 000000000..1567e3fb9 --- /dev/null +++ b/WDE.Common.Avalonia/Controls/DynamicColumnsPanel.cs @@ -0,0 +1,297 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Layout; + +namespace WDE.Common.Avalonia.Controls +{ + public class DynamicColumnsPanel: Panel, INavigableContainer + { + public static readonly StyledProperty SpacingProperty = + AvaloniaProperty.Register(nameof(Spacing)); + + public static readonly StyledProperty HorizontalSpacingProperty = + AvaloniaProperty.Register(nameof(HorizontalSpacing)); + + public static readonly StyledProperty ColumnWidthProperty = + AvaloniaProperty.Register(nameof(ColumnWidth), defaultValue: 200); + + public static readonly StyledProperty MaximumColumnsCountProperty = + AvaloniaProperty.Register(nameof(MaximumColumnsCount), defaultValue: int.MaxValue); + + public double ColumnWidth + { + get => GetValue(ColumnWidthProperty); + set => SetValue(ColumnWidthProperty, value); + } + + public double Spacing + { + get => GetValue(SpacingProperty); + set => SetValue(SpacingProperty, value); + } + + public double HorizontalSpacing + { + get => GetValue(HorizontalSpacingProperty); + set => SetValue(HorizontalSpacingProperty, value); + } + + public int MaximumColumnsCount + { + get => GetValue(MaximumColumnsCountProperty); + set => SetValue(MaximumColumnsCountProperty, value); + } + + static DynamicColumnsPanel() + { + AffectsMeasure(SpacingProperty); + AffectsMeasure(HorizontalSpacingProperty); + AffectsMeasure(ColumnWidthProperty); + AffectsMeasure(MaximumColumnsCountProperty); + + AffectsArrange(MaximumColumnsCountProperty); + + AffectsParentMeasure(MaximumColumnsCountProperty); + AffectsParentArrange(MaximumColumnsCountProperty); + } + + /// + /// Gets the next control in the specified direction. + /// + /// The movement direction. + /// The control from which movement begins. + /// Whether to wrap around when the first or last item is reached. + /// The control. + IInputElement INavigableContainer.GetControl(NavigationDirection direction, IInputElement from, bool wrap) + { + var result = GetControlInDirection(direction, from as IControl); + + if (result == null && wrap) + { + switch (direction) + { + case NavigationDirection.Up: + case NavigationDirection.Previous: + case NavigationDirection.PageUp: + result = GetControlInDirection(NavigationDirection.Last, null); + break; + case NavigationDirection.Down: + case NavigationDirection.Next: + case NavigationDirection.PageDown: + result = GetControlInDirection(NavigationDirection.First, null); + break; + } + } + + return result!; + } + + /// + /// Gets the next control in the specified direction. + /// + /// The movement direction. + /// The control from which movement begins. + /// The control. + protected virtual IInputElement? GetControlInDirection(NavigationDirection direction, IControl? from) + { + int index = from != null ? Children.IndexOf(from) : -1; + + switch (direction) + { + case NavigationDirection.First: + index = 0; + break; + case NavigationDirection.Last: + index = Children.Count - 1; + break; + case NavigationDirection.Next: + if (index != -1) ++index; + break; + case NavigationDirection.Previous: + if (index != -1) --index; + break; + case NavigationDirection.Left: + if (index != -1) index = -1; + break; + case NavigationDirection.Right: + if (index != -1) index = -1; + break; + case NavigationDirection.Up: + if (index != -1) index = index - 1; + break; + case NavigationDirection.Down: + if (index != -1) index = index + 1; + break; + default: + index = -1; + break; + } + + if (index >= 0 && index < Children.Count) + { + return Children[index]; + } + else + { + return null; + } + } + + private int GetColumnsCount(Size size) + { + if (double.IsInfinity(size.Width)) + return Math.Min(MaximumColumnsCount, Children.Count); + return Math.Min((int)Math.Max(Math.Floor(size.Width / ColumnWidth), 1), MaximumColumnsCount); + } + + private double GetActualColumnWidth(Size size) + { + if (double.IsInfinity(size.Width)) + return ColumnWidth; + var count = GetColumnsCount(size); + return (size.Width - Math.Max(count - 1, 0) * HorizontalSpacing) / count; + } + + public IEnumerable VisibleChildren() + { + for (int i = 0, count = Children.Count; i < count; ++i) + { + // Get next child. + var child = Children[i]; + + if (child == null) + { + continue; + } + + bool isVisible = child.IsVisible; + + if (isVisible) + yield return child; + } + } + + /// + /// General StackPanel layout behavior is to grow unbounded in the "stacking" direction (Size To Content). + /// Children in this dimension are encouraged to be as large as they like. In the other dimension, + /// StackPanel will assume the maximum size of its children. + /// + /// Constraint + /// Desired size + protected override Size MeasureOverride(Size availableSize) + { + Size stackDesiredSize = new Size(); + var children = Children; + Size layoutSlotSize = availableSize; + double spacing = Spacing; + bool hasVisibleChild = false; + + // + // Initialize child sizing and iterator data + // Allow children as much size as they want along the stack. + // + layoutSlotSize = layoutSlotSize.WithHeight(Double.PositiveInfinity); + + // + // Iterate through children. + // While we still supported virtualization, this was hidden in a child iterator (see source history). + // + foreach (var child in VisibleChildren()) + { + hasVisibleChild = true; + + // Measure the child. + child.Measure(layoutSlotSize); + Size childDesiredSize = child.DesiredSize; + + // Accumulate child size. + stackDesiredSize = stackDesiredSize.WithWidth(Math.Max(stackDesiredSize.Width, childDesiredSize.Width)); + stackDesiredSize = stackDesiredSize.WithHeight(stackDesiredSize.Height + spacing + childDesiredSize.Height); + } + + stackDesiredSize = stackDesiredSize.WithHeight(stackDesiredSize.Height - (hasVisibleChild ? spacing : 0)); + + // columns + int columns = GetColumnsCount(availableSize); + double columnWidth = GetActualColumnWidth(availableSize); + double averageHeight = stackDesiredSize.Height / columns; + + double y = 0; + double maxHeight = 0; + // second pass, actually measure max height + foreach (var child in VisibleChildren()) + { + y += child.DesiredSize.Height + spacing; + if (y > averageHeight) + { + y -= spacing; + maxHeight = Math.Max(maxHeight, y); + y = 0; + } else + maxHeight = Math.Max(maxHeight, y); + } + return new Size(stackDesiredSize.Width, maxHeight); + } + + /// + /// Content arrangement. + /// + /// Arrange size + protected override Size ArrangeOverride(Size finalSize) + { + var children = Children; + Rect rcChild = new Rect(finalSize); + double previousChildSize = 0.0; + var spacing = Spacing; + + int columns = GetColumnsCount(finalSize); + double columnWidth = GetActualColumnWidth(finalSize.WithWidth(finalSize.Width)); + + double x = 0; + double y = 0; + int columnIndex = 1; + // + // Arrange and Position Children. + // + for (int i = 0, count = children.Count; i < count; ++i) + { + var child = children[i]; + + if (child == null || !child.IsVisible) + { continue; } + + rcChild = rcChild.WithX(x); + rcChild = rcChild.WithY(y); + previousChildSize = child.DesiredSize.Height; + rcChild = rcChild.WithHeight(previousChildSize); + rcChild = rcChild.WithWidth(columnWidth); + previousChildSize += spacing; + + y += previousChildSize; + + if (y >= DesiredSize.Height) + { + x += columnWidth + (columnIndex < columns ? HorizontalSpacing : 0); + y = 0; + columnIndex++; + } + + ArrangeChild(child, rcChild, finalSize); + } + + return finalSize; + } + + internal virtual void ArrangeChild( + IControl child, + Rect rect, + Size panelSize) + { + child.Arrange(rect); + } + } +} \ No newline at end of file diff --git a/WDE.SmartScriptEditor.Avalonia/Editor/UserControls/FlagComboBox.cs b/WDE.Common.Avalonia/Controls/FlagComboBox.cs similarity index 99% rename from WDE.SmartScriptEditor.Avalonia/Editor/UserControls/FlagComboBox.cs rename to WDE.Common.Avalonia/Controls/FlagComboBox.cs index 4bced19d7..ee249aa48 100644 --- a/WDE.SmartScriptEditor.Avalonia/Editor/UserControls/FlagComboBox.cs +++ b/WDE.Common.Avalonia/Controls/FlagComboBox.cs @@ -6,18 +6,18 @@ using Avalonia; using Avalonia.Collections; using Avalonia.Controls; +using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using Avalonia.Data; using Avalonia.Layout; +using Avalonia.Media; using Avalonia.Styling; using AvaloniaStyles.Controls; using FuzzySharp; using WDE.Common.Parameters; using WDE.MVVM.Observable; -using Avalonia.Controls.Primitives; -using Avalonia.Media; -namespace WDE.SmartScriptEditor.Avalonia.Editor.UserControls +namespace WDE.Common.Avalonia.Controls { public class FlagComboBox : CompletionComboBox, IStyleable { diff --git a/WDE.Common.Avalonia/Converters/BoolToIntegerConverter.cs b/WDE.Common.Avalonia/Converters/BoolToIntegerConverter.cs new file mode 100644 index 000000000..0661e799a --- /dev/null +++ b/WDE.Common.Avalonia/Converters/BoolToIntegerConverter.cs @@ -0,0 +1,26 @@ +using System; +using System.Globalization; +using Avalonia.Data.Converters; + +namespace WDE.Common.Avalonia.Converters +{ + public class BoolToIntegerConverter : IValueConverter + { + public int TrueValue { get; set; } = 1; + public int FalseValue { get; set; } = 0; + + public object? Convert(object? value, Type targetType, object parameter, CultureInfo culture) + { + if (value is bool b) + return b ? TrueValue : FalseValue; + return false; + } + + public object? ConvertBack(object? value, Type targetType, object parameter, CultureInfo culture) + { + if (value is int i) + return i == TrueValue ? true : (i == FalseValue ? false : null); + return null; + } + } +} \ No newline at end of file diff --git a/WDE.Common.Avalonia/Converters/BoolToRowDefinitionsConverter.cs b/WDE.Common.Avalonia/Converters/BoolToRowDefinitionsConverter.cs new file mode 100644 index 000000000..efaef893d --- /dev/null +++ b/WDE.Common.Avalonia/Converters/BoolToRowDefinitionsConverter.cs @@ -0,0 +1,43 @@ +using System; +using System.Globalization; +using Avalonia.Controls; +using Avalonia.Data.Converters; + +namespace WDE.Common.Avalonia.Converters +{ + public class BoolToRowDefinitionsConverter : IValueConverter + { + public RowDefinitions TrueValue { get; set; } = new(); + public RowDefinitions FalseValue { get; set; } = new(); + + public object? Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value is bool b) + return b ? TrueValue : FalseValue; + return null; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } + } + + public class BoolToColumnDefinitionsConverter : IValueConverter + { + public ColumnDefinitions TrueValue { get; set; } = new(); + public ColumnDefinitions FalseValue { get; set; } = new(); + + public object? Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value is bool b) + return b ? TrueValue : FalseValue; + return null; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } + } +} \ No newline at end of file diff --git a/WDE.Common.Avalonia/Utils/GridUtils.cs b/WDE.Common.Avalonia/Utils/GridUtils.cs new file mode 100644 index 000000000..a138d5ecd --- /dev/null +++ b/WDE.Common.Avalonia/Utils/GridUtils.cs @@ -0,0 +1,60 @@ +using Avalonia; +using Avalonia.Controls; + +namespace WDE.Common.Avalonia.Utils +{ + public class GridUtils + { + public static readonly AttachedProperty DynamicRowsProperty = AvaloniaProperty.RegisterAttached("DynamicRows", typeof(GridUtils)); + + public static RowDefinitions GetDynamicRows(Grid obj) + { + return obj.GetValue(DynamicRowsProperty); + } + + public static void SetDynamicRows(Grid obj, RowDefinitions value) + { + obj.SetValue(DynamicRowsProperty, value); + } + + static GridUtils() + { + DynamicColumnsProperty.Changed.AddClassHandler(UpdateColumns); + DynamicRowsProperty.Changed.AddClassHandler(UpdateRows); + } + + private static void UpdateRows(Grid obj, AvaloniaPropertyChangedEventArgs arg2) + { + obj.RowDefinitions.Clear(); + if (arg2.NewValue is null) + return; + foreach (var rowDef in ((RowDefinitions)arg2.NewValue!)) + { + obj.RowDefinitions.Add(rowDef); + } + } + + private static void UpdateColumns(Grid obj, AvaloniaPropertyChangedEventArgs arg2) + { + obj.ColumnDefinitions.Clear(); + if (arg2.NewValue is null) + return; + foreach (var colDef in ((ColumnDefinitions)arg2.NewValue!)) + { + obj.ColumnDefinitions.Add(colDef); + } + } + + public static readonly AttachedProperty DynamicColumnsProperty = AvaloniaProperty.RegisterAttached("DynamicColumns", typeof(GridUtils)); + + public static ColumnDefinitions GetDynamicColumns(Grid obj) + { + return obj.GetValue(DynamicColumnsProperty); + } + + public static void SetDynamicColumns(Grid obj, ColumnDefinitions value) + { + obj.SetValue(DynamicColumnsProperty, value); + } + } +} \ No newline at end of file diff --git a/WDE.Common.Avalonia/Utils/ICustomCopyPaste.cs b/WDE.Common.Avalonia/Utils/ICustomCopyPaste.cs new file mode 100644 index 000000000..4f822844d --- /dev/null +++ b/WDE.Common.Avalonia/Utils/ICustomCopyPaste.cs @@ -0,0 +1,11 @@ +using System.Threading.Tasks; +using Avalonia.Input.Platform; + +namespace WDE.Common.Avalonia.Utils +{ + public interface ICustomCopyPaste + { + Task DoPaste(); + void DoCopy(IClipboard clipboard); + } +} \ No newline at end of file diff --git a/WDE.Common.Avalonia/Utils/MenuBind.cs b/WDE.Common.Avalonia/Utils/MenuBind.cs index 43487afc2..aa2475fbb 100644 --- a/WDE.Common.Avalonia/Utils/MenuBind.cs +++ b/WDE.Common.Avalonia/Utils/MenuBind.cs @@ -12,6 +12,7 @@ using Prism.Commands; using WDE.Common.Avalonia.Controls; using WDE.Common.Menu; +using WDE.Common.Utils; using WDE.MVVM; using WDE.MVVM.Observable; @@ -85,6 +86,8 @@ private static ICommand WrapCommand(ICommand command, IMenuCommandItem cmd) // However application wise shortcuts take higher priority // and effectively TextBox doesn't handle copy/paste/cut/undo/redo -.- var original = command; + command = OverrideCommand(command, Key.C, key, cmd, tb => tb.DoCopy((IClipboard)AvaloniaLocator.Current.GetService(typeof(IClipboard)))); + command = OverrideCommand(command, Key.V, key, cmd, tb => tb.DoPaste().ListenErrors()); command = OverrideCommand(command, Key.C, key, cmd, tb => tb.Copy()); command = OverrideCommand(command, Key.X, key, cmd, tb => tb.Cut()); command = OverrideCommand(command, Key.V, key, cmd, tb => tb.Paste()); diff --git a/WDE.DatabaseEditors.Avalonia/Controls/ButtonFastCellView.cs b/WDE.DatabaseEditors.Avalonia/Controls/ButtonFastCellView.cs new file mode 100644 index 000000000..056c4d3df --- /dev/null +++ b/WDE.DatabaseEditors.Avalonia/Controls/ButtonFastCellView.cs @@ -0,0 +1,27 @@ +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Input; + +namespace WDE.DatabaseEditors.Avalonia.Controls +{ + public class ButtonFastCellView : FastCellViewBase + { + private Button? partButton; + + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) + { + base.OnApplyTemplate(e); + partButton = e.NameScope.Find - + + + + - + + + + + + + + + + + + + + + diff --git a/WDE.DatabaseEditors.Avalonia/Views/MultiRow/SelectablePanel.cs b/WDE.DatabaseEditors.Avalonia/Views/MultiRow/SelectablePanel.cs new file mode 100644 index 000000000..706be73df --- /dev/null +++ b/WDE.DatabaseEditors.Avalonia/Views/MultiRow/SelectablePanel.cs @@ -0,0 +1,98 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Generators; +using Avalonia.Controls.Presenters; +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.VisualTree; + +namespace WDE.DatabaseEditors.Avalonia.Views.MultiRow +{ + public class SelectablePanel : Panel + { + public static readonly StyledProperty IsSelectedProperty = AvaloniaProperty.Register(nameof(IsSelected)); + public static readonly AttachedProperty SelectedItemProperty = AvaloniaProperty.RegisterAttached("SelectedItem", typeof(SelectablePanel)); + public static readonly AttachedProperty ObserveItemsProperty = AvaloniaProperty.RegisterAttached("ObserveItems", typeof(SelectablePanel)); + + static SelectablePanel() + { + PointerPressedEvent.AddClassHandler(HandlePointerPressed, RoutingStrategies.Tunnel); + IsSelectedProperty.Changed.AddClassHandler(IsSelectedChanged); + SelectedItemProperty.Changed.AddClassHandler(SelectedItemChanged); + ObserveItemsProperty.Changed.AddClassHandler(ObserveItemsChanged); + } + + private static void IsSelectedChanged(SelectablePanel arg1, AvaloniaPropertyChangedEventArgs arg2) + { + arg1.PseudoClasses.Set(":selected", arg2.NewValue is true); + } + + private static void ObserveItemsChanged(ItemsControl arg1, AvaloniaPropertyChangedEventArgs arg2) + { + arg1.ItemContainerGenerator.Materialized += ItemContainerGeneratorOnMaterialized; + } + + private static void ItemContainerGeneratorOnMaterialized(object? sender, ItemContainerEventArgs e) + { + var icg = sender as ItemContainerGenerator; + if (icg == null) + return; + var ic = icg.Owner as ItemsControl; + if (ic == null) + return; + UpdateSelected(ic, GetSelectedItem(ic)); + } + + private static void UpdateSelected(ItemsControl arg1, object? newValue) + { + for (int i = 0; i < arg1.ItemCount; ++i) + { + var panel = arg1.ItemContainerGenerator.ContainerFromIndex(i) as ContentPresenter; + if (panel != null && panel.Child is SelectablePanel sp) + sp.IsSelected = newValue == panel.DataContext; + } + } + + private static void SelectedItemChanged(ItemsControl arg1, AvaloniaPropertyChangedEventArgs arg2) + { + UpdateSelected(arg1, arg2.NewValue); + } + + public bool IsSelected + { + get => GetValue(IsSelectedProperty); + set => SetValue(IsSelectedProperty, value); + } + + public void Select() + { + var parent = this.FindAncestorOfType(); + SetSelectedItem(parent, DataContext); + } + + private static void HandlePointerPressed(SelectablePanel panel, PointerPressedEventArgs args) + { + panel.Select(); + } + + public static object? GetSelectedItem(IAvaloniaObject obj) + { + return (object?)obj.GetValue(SelectedItemProperty); + } + + public static void SetSelectedItem(IAvaloniaObject obj, object? value) + { + obj.SetValue(SelectedItemProperty, value); + } + + public static bool GetObserveItems(IAvaloniaObject obj) + { + return (bool)obj.GetValue(ObserveItemsProperty); + } + + public static void SetObserveItems(IAvaloniaObject obj, bool value) + { + obj.SetValue(ObserveItemsProperty, value); + } + } +} \ No newline at end of file diff --git a/WDE.DatabaseEditors.Avalonia/Views/Template/TemplateDbTableEditorView.axaml b/WDE.DatabaseEditors.Avalonia/Views/Template/TemplateDbTableEditorView.axaml index d30b4a478..93166e408 100644 --- a/WDE.DatabaseEditors.Avalonia/Views/Template/TemplateDbTableEditorView.axaml +++ b/WDE.DatabaseEditors.Avalonia/Views/Template/TemplateDbTableEditorView.axaml @@ -23,32 +23,38 @@ + + + + + + + + + + + + + @@ -72,6 +78,21 @@ + + + diff --git a/WDE.DatabaseEditors/Services/DatabaseEditorsSettings.cs b/WDE.DatabaseEditors/Services/DatabaseEditorsSettings.cs new file mode 100644 index 000000000..60062edc3 --- /dev/null +++ b/WDE.DatabaseEditors/Services/DatabaseEditorsSettings.cs @@ -0,0 +1,34 @@ +using WDE.Common.Services; +using WDE.Module.Attributes; + +namespace WDE.DatabaseEditors.Services +{ + [SingleInstance] + [AutoRegister] + public class DatabaseEditorsSettings : IDatabaseEditorsSettings + { + private readonly IUserSettings userSettings; + private Data data; + + public DatabaseEditorsSettings(IUserSettings userSettings) + { + this.userSettings = userSettings; + data = userSettings.Get(); + } + + private struct Data : ISettings + { + public MultiRowSplitMode MultiRowSplitMode; + } + + public MultiRowSplitMode MultiRowSplitMode + { + get => data.MultiRowSplitMode; + set + { + data.MultiRowSplitMode = value; + userSettings.Update(data); + } + } + } +} \ No newline at end of file diff --git a/WDE.DatabaseEditors/Services/IDatabaseEditorsSettings.cs b/WDE.DatabaseEditors/Services/IDatabaseEditorsSettings.cs new file mode 100644 index 000000000..05b726795 --- /dev/null +++ b/WDE.DatabaseEditors/Services/IDatabaseEditorsSettings.cs @@ -0,0 +1,17 @@ +using WDE.Module.Attributes; + +namespace WDE.DatabaseEditors.Services +{ + public enum MultiRowSplitMode + { + None, + Horizontal, + Vertical + } + + [UniqueProvider] + public interface IDatabaseEditorsSettings + { + MultiRowSplitMode MultiRowSplitMode { get; set; } + } +} \ No newline at end of file diff --git a/WDE.DatabaseEditors/ViewModels/BaseDatabaseCellViewModel.cs b/WDE.DatabaseEditors/ViewModels/BaseDatabaseCellViewModel.cs new file mode 100644 index 000000000..34182f005 --- /dev/null +++ b/WDE.DatabaseEditors/ViewModels/BaseDatabaseCellViewModel.cs @@ -0,0 +1,95 @@ +using System.Collections.Generic; +using System.Linq; +using WDE.Common.Parameters; +using WDE.DatabaseEditors.Models; +using WDE.MVVM; + +namespace WDE.DatabaseEditors.ViewModels +{ + public class BaseDatabaseCellViewModel : ObservableBase + { + public IParameterValue? ParameterValue { get; init; } + + public IList? Items + { + get + { + if (ParameterValue is not ParameterValue p) + return null; + return p.Parameter.HasItems + ? p.Parameter.Items!.Select(pair => (object)new ParameterOption(pair.Key, pair.Value.Name)).ToList() + : null; + } + } + + public Dictionary? Flags => ParameterValue is ParameterValue p ? p.Parameter.Items : null; + + public ParameterOption? OptionValue + { + get + { + if (ParameterValue is ParameterValue p) + { + if (p.Parameter.Items != null && p.Parameter.Items.TryGetValue(p.Value, out var option)) + return new ParameterOption(p.Value, option.Name); + return new ParameterOption(p.Value, "Unknown"); + } + return null; + } + set + { + if (value != null) + { + if (ParameterValue is ParameterValue p) + { + p.Value = value.Value; + } + } + } + } + + public class ParameterOption + { + public ParameterOption(long value, string name) + { + Value = value; + Name = name; + } + + public long Value { get; } + public string Name { get; } + + public override string ToString() + { + return $"{Name} ({Value})"; + } + } + + public long AsLongValue + { + get => ((ParameterValue as ParameterValue)?.Value ?? 0); + set + { + if (ParameterValue is ParameterValue longParam) + { + longParam.Value = value; + } + } + } + + public bool AsBoolValue + { + get => ((ParameterValue as ParameterValue)?.Value ?? 0) != 0; + set + { + if (ParameterValue is ParameterValue longParam) + { + longParam.Value = value ? 1 : 0; + } + } + } + + public bool UseItemPicker => (ParameterValue is ParameterValue vm) && vm.Parameter.HasItems && vm.Parameter.Items!.Count < 300; + public bool UseFlagsPicker => (ParameterValue is ParameterValue vm) && vm.Parameter.HasItems && vm.Parameter is FlagParameter; + } +} \ No newline at end of file diff --git a/WDE.DatabaseEditors/ViewModels/MultiRow/DatabaseCellViewModel.cs b/WDE.DatabaseEditors/ViewModels/MultiRow/DatabaseCellViewModel.cs index 11155b4e8..3916e01c8 100644 --- a/WDE.DatabaseEditors/ViewModels/MultiRow/DatabaseCellViewModel.cs +++ b/WDE.DatabaseEditors/ViewModels/MultiRow/DatabaseCellViewModel.cs @@ -1,17 +1,22 @@ -using System.Windows.Input; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Windows.Input; +using WDE.Common.Parameters; using WDE.DatabaseEditors.Data.Structs; using WDE.DatabaseEditors.Models; using WDE.MVVM; using WDE.MVVM.Observable; +using WDE.Parameters.Models; namespace WDE.DatabaseEditors.ViewModels.MultiRow { - public class DatabaseCellViewModel : ObservableBase + public class DatabaseCellViewModel : BaseDatabaseCellViewModel { public DatabaseEntityViewModel Parent { get; } public DatabaseEntity ParentEntity { get; } public IDatabaseField? TableField { get; } - public IParameterValue? ParameterValue { get; } public bool IsVisible { get; private set; } = true; public string? OriginalValueTooltip { get; private set; } public bool CanBeNull { get; } @@ -29,12 +34,22 @@ public DatabaseCellViewModel(int columnIndex, DatabaseColumnJson columnDefinitio ColumnIndex = columnIndex * 2; CanBeNull = columnDefinition.CanBeNull; IsReadOnly = columnDefinition.IsReadOnly; - ColumnName = columnDefinition.DbColumnName; + ColumnName = columnDefinition.Name; ParentEntity = parentEntity; Parent = parent; TableField = tableField; ParameterValue = parameterValue; + if (UseItemPicker) + { + AutoDispose(ParameterValue.ToObservable().Subscribe(_ => RaisePropertyChanged(nameof(OptionValue)))); + } + + if (UseFlagsPicker) + { + AutoDispose(ParameterValue.ToObservable().Subscribe(_ => RaisePropertyChanged(nameof(AsLongValue)))); + } + AutoDispose(parameterValue.ToObservable().SubscribeAction(_ => { OriginalValueTooltip = @@ -60,17 +75,5 @@ public DatabaseCellViewModel(int columnIndex, string columnName, ICommand action RaisePropertyChanged(nameof(ActionLabel)); })); } - - public bool AsBoolValue - { - get => ((ParameterValue as ParameterValue)?.Value ?? 0) != 0; - set - { - if (ParameterValue is ParameterValue longParam) - { - longParam.Value = value ? 1 : 0; - } - } - } } } \ No newline at end of file diff --git a/WDE.DatabaseEditors/ViewModels/MultiRow/DatabaseEntitiesGroupViewModel.cs b/WDE.DatabaseEditors/ViewModels/MultiRow/DatabaseEntitiesGroupViewModel.cs index e545c7c28..cd2afbed4 100644 --- a/WDE.DatabaseEditors/ViewModels/MultiRow/DatabaseEntitiesGroupViewModel.cs +++ b/WDE.DatabaseEditors/ViewModels/MultiRow/DatabaseEntitiesGroupViewModel.cs @@ -14,16 +14,19 @@ public DatabaseEntitiesGroupViewModel(uint key, string name) Name = name; } - public void Remove(DatabaseEntity entity) + public DatabaseEntityViewModel? GetAndRemove(DatabaseEntity entity) { for (int i = 0; i < Count; ++i) { if (this[i].Entity == entity) { + var vm = this[i]; RemoveAt(i); - break; + return vm; } } + + return null; } } } \ No newline at end of file diff --git a/WDE.DatabaseEditors/ViewModels/MultiRow/MultiRowDbTableEditorViewModel.cs b/WDE.DatabaseEditors/ViewModels/MultiRow/MultiRowDbTableEditorViewModel.cs index 22ea26add..26cd7af0a 100644 --- a/WDE.DatabaseEditors/ViewModels/MultiRow/MultiRowDbTableEditorViewModel.cs +++ b/WDE.DatabaseEditors/ViewModels/MultiRow/MultiRowDbTableEditorViewModel.cs @@ -24,6 +24,7 @@ using WDE.DatabaseEditors.Loaders; using WDE.DatabaseEditors.Models; using WDE.DatabaseEditors.QueryGenerators; +using WDE.DatabaseEditors.Services; using WDE.DatabaseEditors.Solution; using WDE.MVVM; using WDE.MVVM.Observable; @@ -39,11 +40,53 @@ public class MultiRowDbTableEditorViewModel : ViewModelBase private readonly IQueryGenerator queryGenerator; private readonly IDatabaseTableModelGenerator modelGenerator; private readonly IConditionEditService conditionEditService; + private readonly IDatabaseEditorsSettings editorSettings; private readonly IDatabaseTableDataProvider tableDataProvider; private Dictionary byEntryGroups = new(); public ObservableCollection Rows { get; } = new(); + private DatabaseEntityViewModel? selectedRow; + public DatabaseEntityViewModel? SelectedRow + { + get => selectedRow; + set => SetProperty(ref selectedRow, value); + } + + private MultiRowSplitMode splitMode; + public bool SplitView => splitMode != MultiRowSplitMode.None; + + public bool SplitHorizontal + { + get => splitMode == MultiRowSplitMode.Horizontal; + set + { + if (value && splitMode == MultiRowSplitMode.Horizontal) + return; + + splitMode = value ? MultiRowSplitMode.Horizontal : (splitMode == MultiRowSplitMode.Horizontal ? MultiRowSplitMode.None : splitMode); + editorSettings.MultiRowSplitMode = splitMode; + RaisePropertyChanged(nameof(SplitHorizontal)); + RaisePropertyChanged(nameof(SplitVertical)); + RaisePropertyChanged(nameof(SplitView)); + } + } + public bool SplitVertical + { + get => splitMode == MultiRowSplitMode.Vertical; + set + { + if (value && splitMode == MultiRowSplitMode.Vertical) + return; + + splitMode = value ? MultiRowSplitMode.Vertical : (splitMode == MultiRowSplitMode.Vertical ? MultiRowSplitMode.None : splitMode); + editorSettings.MultiRowSplitMode = splitMode; + RaisePropertyChanged(nameof(SplitHorizontal)); + RaisePropertyChanged(nameof(SplitVertical)); + RaisePropertyChanged(nameof(SplitView)); + } + } + private IList columns = new List(); public ObservableCollection Columns { get; } = new(); private DatabaseColumnJson? autoIncrementColumn; @@ -72,7 +115,7 @@ public MultiRowDbTableEditorViewModel(DatabaseTableSolutionItem solutionItem, IQueryGenerator queryGenerator, IDatabaseTableModelGenerator modelGenerator, ITableDefinitionProvider tableDefinitionProvider, IConditionEditService conditionEditService, ISolutionItemIconRegistry iconRegistry, - ISessionService sessionService) + ISessionService sessionService, IDatabaseEditorsSettings editorSettings) : base(history, solutionItem, solutionItemName, solutionManager, solutionTasksService, eventAggregator, queryGenerator, tableDataProvider, messageBoxService, taskRunner, parameterFactory, @@ -87,6 +130,9 @@ public MultiRowDbTableEditorViewModel(DatabaseTableSolutionItem solutionItem, this.queryGenerator = queryGenerator; this.modelGenerator = modelGenerator; this.conditionEditService = conditionEditService; + this.editorSettings = editorSettings; + + splitMode = editorSettings.MultiRowSplitMode; OpenParameterWindow = new AsyncAutoCommand(EditParameter); RemoveTemplateCommand = new AsyncAutoCommand(RemoveTemplate, vm => vm != null); @@ -311,7 +357,9 @@ public override bool ForceRemoveEntity(DatabaseEntity entity) return false; Entities.RemoveAt(indexOfEntity); - byEntryGroups[entity.Key].Remove(entity); + var vm = byEntryGroups[entity.Key].GetAndRemove(entity); + if (SelectedRow == vm) + SelectedRow = null; return true; } @@ -334,7 +382,7 @@ public override bool ForceInsertEntity(DatabaseEntity entity, int index) if (column.IsConditionColumn) { var label = entity.ToObservable(e => e.Conditions).Select(c => "Edit (" + (c?.Count ?? 0) + ")"); - cellViewModel = AutoDispose(new DatabaseCellViewModel(columnIndex, "conditions", EditConditionsCommand, row, entity, label)); + cellViewModel = AutoDispose(new DatabaseCellViewModel(columnIndex, "Conditions", EditConditionsCommand, row, entity, label)); } else { @@ -371,6 +419,7 @@ public override bool ForceInsertEntity(DatabaseEntity entity, int index) EnsureKey(entity.Key); byEntryGroups[entity.Key].Add(row); + SelectedRow = row; return true; } diff --git a/WDE.DatabaseEditors/ViewModels/Template/DatabaseCellViewModel.cs b/WDE.DatabaseEditors/ViewModels/Template/DatabaseCellViewModel.cs index 53ee235ea..7dbf5010c 100644 --- a/WDE.DatabaseEditors/ViewModels/Template/DatabaseCellViewModel.cs +++ b/WDE.DatabaseEditors/ViewModels/Template/DatabaseCellViewModel.cs @@ -5,12 +5,11 @@ namespace WDE.DatabaseEditors.ViewModels.Template { - public class DatabaseCellViewModel : ObservableBase + public class DatabaseCellViewModel : BaseDatabaseCellViewModel { public DatabaseRowViewModel Parent { get; } public DatabaseEntity ParentEntity { get; } public IDatabaseField? TableField { get; } - public IParameterValue ParameterValue { get; } public bool IsVisible { get; private set; } = true; public bool IsModified { get; private set; } public string? OriginalValueTooltip { get; private set; } @@ -47,6 +46,16 @@ public DatabaseCellViewModel(DatabaseRowViewModel parent, RaisePropertyChanged(nameof(OriginalValueTooltip)); RaisePropertyChanged(nameof(AsBoolValue)); })); + + if (UseItemPicker) + { + AutoDispose(ParameterValue.ToObservable().Subscribe(_ => RaisePropertyChanged(nameof(OptionValue)))); + } + + if (UseFlagsPicker) + { + AutoDispose(ParameterValue.ToObservable().Subscribe(_ => RaisePropertyChanged(nameof(AsLongValue)))); + } inConstructor = false; } @@ -65,20 +74,5 @@ public DatabaseCellViewModel(DatabaseRowViewModel parent, })); inConstructor = false; } - - public bool? AsBoolValue - { - get => ParameterValue.IsNull ? null : ((ParameterValue as ParameterValue)?.Value ?? 0) != 0; - set - { - if (ParameterValue is ParameterValue longParam) - { - if (value == null) - longParam.SetNull(); - else - longParam.Value = value.Value ? 1 : 0; - } - } - } } } \ No newline at end of file diff --git a/WDE.DatabaseEditors/ViewModels/Template/TemplateDbTableEditorViewModel.cs b/WDE.DatabaseEditors/ViewModels/Template/TemplateDbTableEditorViewModel.cs index 12ca603d4..34aede98a 100644 --- a/WDE.DatabaseEditors/ViewModels/Template/TemplateDbTableEditorViewModel.cs +++ b/WDE.DatabaseEditors/ViewModels/Template/TemplateDbTableEditorViewModel.cs @@ -161,7 +161,7 @@ await messageBoxService.ShowDialog(new MessageBoxFactory().SetTitle("Entit private void SetToNull(DatabaseCellViewModel? view) { if (view != null && view.CanBeNull && !view.Parent.IsReadOnly) - view.ParameterValue.SetNull(); + view.ParameterValue!.SetNull(); } private async Task Revert(DatabaseCellViewModel? view) @@ -169,7 +169,7 @@ private async Task Revert(DatabaseCellViewModel? view) if (view == null || view.Parent.IsReadOnly || view.TableField == null) return; - view.ParameterValue.Revert(); + view.ParameterValue!.Revert(); if (!view.ParentEntity.ExistInDatabase) return; @@ -233,7 +233,7 @@ private bool ContainsEntity(DatabaseEntity entity) return false; } - private Task EditParameter(DatabaseCellViewModel cell) => EditParameter(cell.ParameterValue); + private Task EditParameter(DatabaseCellViewModel cell) => EditParameter(cell.ParameterValue!); protected override List? GetOriginalFields(DatabaseEntity entity) { diff --git a/WDE.SmartScriptEditor.Avalonia/Themes/Generic.axaml b/WDE.SmartScriptEditor.Avalonia/Themes/Generic.axaml index a44e1843b..acd77dae8 100644 --- a/WDE.SmartScriptEditor.Avalonia/Themes/Generic.axaml +++ b/WDE.SmartScriptEditor.Avalonia/Themes/Generic.axaml @@ -242,7 +242,7 @@ - + diff --git a/WoWDatabaseEditor/Icons/icon_split_horiz.png b/WoWDatabaseEditor/Icons/icon_split_horiz.png new file mode 100644 index 0000000000000000000000000000000000000000..8e94ef08c2fc3b1a2e68ea4cfc08dbe1408dc2dd GIT binary patch literal 641 zcmV-{0)G98P)4m zd|d8TLfQsxg@UL}uSNX@wGCPaK~V(JE@)lUrs8&P21eu=&YW+4Gv_dK2CUJHlGW?_ zA)6~`k)Yo_J2&sPKjOenGkv&Ximr}@!y#(g+YJ^oUiFx@YM=Yp8kZ8f0+t5hQAO1X z!V|)wm4a&Wrf{2*jx7lv3wt%WpzyV+$1D6~a;m}~CTr110IV&-?$j@i|6(PrWrgMC zc(R$I@^c^aCC)@{W?EvWcn3NWg7|SW$v6Rg3@{?Ij~)i;l@&Ge`@i$2$V;vwvq}k# zWm2Rm;1#tf)e{nvq9^*3tD0vQTx)Q0Ue)4TaJmldcWO+FeM>fRYcdJXN6<6}t(V|fsPyb-pTk>- zuF6+k!1fMw%X!!07eYv9)r@Q2+5i9m32;bRa{vG?BLDy{BLR4&KXw2B0H#SqK~zYI zV_+Bs1OOw9!RFlmUqxDgXpMKCeqi{uw~&#=vG>1}APb7zv-f`({_HPiWME)mfXk>z z3ou+=m_W1z+VKk*t}!q$NC~nq%ubO;kxQNPl;IJS%|x_`WC9ewy?*_g>K4$~2cn{) zRJUL>4hVwLI3Nf{4m zd|d8TLfQsxg@UL}uSNX@wGCPaK~V(JE@)lUrs8&P21eu=&YW+4Gv_dK2CUJHlGW?_ zA)6~`k)Yo_J2&sPKjOenGkv&Ximr}@!y#(g+YJ^oUiFx@YM=Yp8kZ8f0+t5hQAO1X z!V|)wm4a&Wrf{2*jx7lv3wt%WpzyV+$1D6~a;m}~CTr110IV&-?$j@i|6(PrWrgMC zc(R$I@^c^aCC)@{W?EvWcn3NWg7|SW$v6Rg3@{?Ij~)i;l@&Ge`@i$2$V;vwvq}k# zWm2Rm;1#tf)e{nvq9^*3tD0vQTx)Q0Ue)4TaJmldcWO+FeM>fRYcdJXN6<6}t(V|fsPyb-pTk>- zuF6+k!1fMw%X!!07eYv9)r@Q2+5i9m32;bRa{vHbMgRa@MgbI*LqPxl0Sie)K~!jg z?btmE!axuO;PIzOpClGRhG~Ph4@e28b{%M<&b6fF>D5w2mpXnO{#R~*RSln8`Vnf_gHt=RDI)^E%IEu?9sIRAR>`u zPc^gMppIi`*M%adB)v#=WdP5$X*un^am-?a$%a9O?d~1h~hZ@<~d6H`wk!gx57JMS^EF-ulV~8AOJVa0d5Gu4Re4S0&v3|;D!L) zFbB9H05{A5Zpse$K}5sFp^^>#Ad+gK$Z59LcRUoivaSpul1zF6uTqj;luzdX006Jt Y039nrg582WcK`qY07*qoM6N<$f~Uzyw*UYD literal 0 HcmV?d00001 diff --git a/WoWDatabaseEditor/Icons/icon_split_horiz_dark.png b/WoWDatabaseEditor/Icons/icon_split_horiz_dark.png new file mode 100644 index 0000000000000000000000000000000000000000..2ba517fc556c48510dbecc94f01339138d6d5cd7 GIT binary patch literal 627 zcmV-(0*w8MP)4m zd|d8TLfQsxg@UL}uSNX@wGCPaK~V(JE@)lUrs8&P21eu=&YW+4Gv_dK2CUJHlGW?_ zA)6~`k)Yo_J2&sPKjOenGkv&Ximr}@!y#(g+YJ^oUiFx@YM=Yp8kZ8f0+t5hQAO1X z!V|)wm4a&Wrf{2*jx7lv3wt%WpzyV+$1D6~a;m}~CTr110IV&-?$j@i|6(PrWrgMC zc(R$I@^c^aCC)@{W?EvWcn3NWg7|SW$v6Rg3@{?Ij~)i;l@&Ge`@i$2$V;vwvq}k# zWm2Rm;1#tf)e{nvq9^*3tD0vQTx)Q0Ue)4TaJmldcWO+FeM>fRYcdJXN6<6}t(V|fsPyb-pTk>- zuF6+k!1fMw%X!!07eYv9)r@Q2+5i9m32;bRa{vG?BLDy{BLR4&KXw2B0GLTcK~zYI z?Ub<%#2^esol|N{Y`_S>C``dfOu;HJ0vlNJ0=X(ECJh{T3UDX`YwZxInej8PmALg@7LEt(!My@SR7c?0}DDMiDIm*oHe N002ovPDHLkV1hkI4~hT) literal 0 HcmV?d00001 diff --git a/WoWDatabaseEditor/Icons/icon_split_horiz_dark@2x.png b/WoWDatabaseEditor/Icons/icon_split_horiz_dark@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..fe75960a03dd8172119b95e0e42cdaf5f8b9c5eb GIT binary patch literal 720 zcmV;>0x$iEP)4m zd|d8TLfQsxg@UL}uSNX@wGCPaK~V(JE@)lUrs8&P21eu=&YW+4Gv_dK2CUJHlGW?_ zA)6~`k)Yo_J2&sPKjOenGkv&Ximr}@!y#(g+YJ^oUiFx@YM=Yp8kZ8f0+t5hQAO1X z!V|)wm4a&Wrf{2*jx7lv3wt%WpzyV+$1D6~a;m}~CTr110IV&-?$j@i|6(PrWrgMC zc(R$I@^c^aCC)@{W?EvWcn3NWg7|SW$v6Rg3@{?Ij~)i;l@&Ge`@i$2$V;vwvq}k# zWm2Rm;1#tf)e{nvq9^*3tD0vQTx)Q0Ue)4TaJmldcWO+FeM>fRYcdJXN6<6}t(V|fsPyb-pTk>- zuF6+k!1fMw%X!!07eYv9)r@Q2+5i9m32;bRa{vHbMgRa@MgbI*LqPxl0QE^kK~!jg z?btDH#2^p_(4Qz35-rsfmOD2-1XtjkzJd?o6S$zFwd6MGlY#6Oc-E``N~ZZ=7?2DQ z5n1~9y|8WDw?&Zu%=7#wD*$|Hi|2Wwl+riRgF*;!&Q<4Xfl>nr2Ux&b^$hI$eqoHdaU7fT+wTAiSmQfjjRman9k8bEKnMXb z#;dcaLI};dTEIDXn`@M;z2|?S<2aCW?wjaAA_DJyb*>i3Ib&ItZlRu$Qo=M%)%oq; z0T!^v0@l=9NJNlQx;l$WM4EH8fcL(6brBH}y^0?Z4J&h&$+`ak00004m zd|d8TLfQsxg@UL}uSNX@wGCPaK~V(JE@)lUrs8&P21eu=&YW+4Gv_dK2CUJHlGW?_ zA)6~`k)Yo_J2&sPKjOenGkv&Ximr}@!y#(g+YJ^oUiFx@YM=Yp8kZ8f0+t5hQAO1X z!V|)wm4a&Wrf{2*jx7lv3wt%WpzyV+$1D6~a;m}~CTr110IV&-?$j@i|6(PrWrgMC zc(R$I@^c^aCC)@{W?EvWcn3NWg7|SW$v6Rg3@{?Ij~)i;l@&Ge`@i$2$V;vwvq}k# zWm2Rm;1#tf)e{nvq9^*3tD0vQTx)Q0Ue)4TaJmldcWO+FeM>fRYcdJXN6<6}t(V|fsPyb-pTk>- zuF6+k!1fMw%X!!07eYv9)r@Q2+5i9m32;bRa{vG?BLDy{BLR4&KXw2B0H#SqK~zYI zV_+Bs1OOw9!RFlmUqxDgVgv6y{lM^RZy_UwgjU@A|I`3lar6Hpo6AJ8lL-J8WX|i? zuc>YUeSIJ*DoS+=M&p1Wpm`j?Ex7aa1A}(_0*Vd1^YjD7W{?EnWd@65?|&&l7JO=n z0?*$6VfeGZ7-24qAtlJdFgrz>Vhd8|JY{%<#HV>GqF*w7_Wln;>YS$(8~E(~ABxQw b1=IxqR3UCy`z=%a00000NkvXXu0mjf!c7;% literal 0 HcmV?d00001 diff --git a/WoWDatabaseEditor/Icons/icon_split_vert@2x.png b/WoWDatabaseEditor/Icons/icon_split_vert@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..4d05ad4de98f5041a59d6a56ee694382172fe598 GIT binary patch literal 741 zcmV4m zd|d8TLfQsxg@UL}uSNX@wGCPaK~V(JE@)lUrs8&P21eu=&YW+4Gv_dK2CUJHlGW?_ zA)6~`k)Yo_J2&sPKjOenGkv&Ximr}@!y#(g+YJ^oUiFx@YM=Yp8kZ8f0+t5hQAO1X z!V|)wm4a&Wrf{2*jx7lv3wt%WpzyV+$1D6~a;m}~CTr110IV&-?$j@i|6(PrWrgMC zc(R$I@^c^aCC)@{W?EvWcn3NWg7|SW$v6Rg3@{?Ij~)i;l@&Ge`@i$2$V;vwvq}k# zWm2Rm;1#tf)e{nvq9^*3tD0vQTx)Q0Ue)4TaJmldcWO+FeM>fRYcdJXN6<6}t(V|fsPyb-pTk>- zuF6+k!1fMw%X!!07eYv9)r@Q2+5i9m32;bRa{vHbMgRa@MgbI*LqPxl0SZY(K~!jg z?btmE!ax)R;PI!h(9SgG1cDc^@dkp8H?b5%u=FNc2wp(6$OQrs`%RN{Dx?uDEqoh) zAbE+*SMFFtEYvr~vn|%UlXYIrF-s;`K%4jt06-K)dFJ?8>34tyv{}Ca$8qw^@w3wJ01Ig2 zJ)n&RwDBI$ChmaehZxVcd1n!MewaEJ3z+zl`ivub`p^GD?N$w4w~=k24+SPce7;%d zVu7yPz;slMdtkANa5!GA^U~h~ETD}Aw27xsU=l1g#dwhgCP|%(1>*B9_2?oZA}Wdp XFf=@WF>7@s00000NkvXXu0mjfYCl8$ literal 0 HcmV?d00001 diff --git a/WoWDatabaseEditor/Icons/icon_split_vert_dark.png b/WoWDatabaseEditor/Icons/icon_split_vert_dark.png new file mode 100644 index 0000000000000000000000000000000000000000..c802f285225928bcaf9db3f513823a44d48dffeb GIT binary patch literal 627 zcmV-(0*w8MP)4m zd|d8TLfQsxg@UL}uSNX@wGCPaK~V(JE@)lUrs8&P21eu=&YW+4Gv_dK2CUJHlGW?_ zA)6~`k)Yo_J2&sPKjOenGkv&Ximr}@!y#(g+YJ^oUiFx@YM=Yp8kZ8f0+t5hQAO1X z!V|)wm4a&Wrf{2*jx7lv3wt%WpzyV+$1D6~a;m}~CTr110IV&-?$j@i|6(PrWrgMC zc(R$I@^c^aCC)@{W?EvWcn3NWg7|SW$v6Rg3@{?Ij~)i;l@&Ge`@i$2$V;vwvq}k# zWm2Rm;1#tf)e{nvq9^*3tD0vQTx)Q0Ue)4TaJmldcWO+FeM>fRYcdJXN6<6}t(V|fsPyb-pTk>- zuF6+k!1fMw%X!!07eYv9)r@Q2+5i9m32;bRa{vG?BLDy{BLR4&KXw2B0GLTcK~zYI z?Uc<8!ypWWA63ql*nkm$QJ8{}n1WTp2y9@<3#5mNC^=zOZPf$c$@qsS>q!O{6U+!& zYln!a2Z;#Yd$aHR!OXXswH6@+0|47diU?}0YJrpz0DzfMO1af?&S(5k|M(``_2HcR zxWI2W=)J24V~nc4<-wHY!Ib5}l&^bm3J{Tc?a>dsZvXW>kKE#acmgDMCxOd?b-Vxo N002ovPDHLkV1jCN7n1-0 literal 0 HcmV?d00001 diff --git a/WoWDatabaseEditor/Icons/icon_split_vert_dark@2x.png b/WoWDatabaseEditor/Icons/icon_split_vert_dark@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..fb987e15d7ea716363d25bbe310a8b009644a01b GIT binary patch literal 710 zcmV;%0y+JOP)4m zd|d8TLfQsxg@UL}uSNX@wGCPaK~V(JE@)lUrs8&P21eu=&YW+4Gv_dK2CUJHlGW?_ zA)6~`k)Yo_J2&sPKjOenGkv&Ximr}@!y#(g+YJ^oUiFx@YM=Yp8kZ8f0+t5hQAO1X z!V|)wm4a&Wrf{2*jx7lv3wt%WpzyV+$1D6~a;m}~CTr110IV&-?$j@i|6(PrWrgMC zc(R$I@^c^aCC)@{W?EvWcn3NWg7|SW$v6Rg3@{?Ij~)i;l@&Ge`@i$2$V;vwvq}k# zWm2Rm;1#tf)e{nvq9^*3tD0vQTx)Q0Ue)4TaJmldcWO+FeM>fRYcdJXN6<6}t(V|fsPyb-pTk>- zuF6+k!1fMw%X!!07eYv9)r@Q2+5i9m32;bRa{vHbMgRa@MgbI*LqPxl0P9IaK~!jg z?btmEgfI{V;Bnb1kT%s6VrSzayh6_D6>(-eEwo5#(b(nr%YQA%n5?lPQnWQUvsw25zkh*tOXU)}x$5J21J8yJS+ zlm)A*I&{9|{t^Z{)MpbdRM8v