From 6ed7d26dfb8868de9a7fc61d7a754ce997cd5bb5 Mon Sep 17 00:00:00 2001 From: Alex <48759429+Aurumaker72@users.noreply.github.com> Date: Sun, 12 Nov 2023 10:38:26 +0100 Subject: [PATCH] Initial multiselect implementation --- .../DialogEditorViewModel.cs | 249 ++++++++---------- .../Helpers/TransformationHelper.cs | 41 +++ RsrcArchitect.Views.WPF/MainWindow.xaml | 2 - .../Rendering/StyledObjectRenderer.cs | 69 ++--- 4 files changed, 188 insertions(+), 173 deletions(-) create mode 100644 RsrcArchitect.ViewModels/Helpers/TransformationHelper.cs diff --git a/RsrcArchitect.ViewModels/DialogEditorViewModel.cs b/RsrcArchitect.ViewModels/DialogEditorViewModel.cs index ea695b3..e36144a 100644 --- a/RsrcArchitect.ViewModels/DialogEditorViewModel.cs +++ b/RsrcArchitect.ViewModels/DialogEditorViewModel.cs @@ -1,5 +1,6 @@ -using System.Numerics; -using System.Text; +using System.Collections.ObjectModel; +using System.Diagnostics; +using System.Numerics; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Messaging; @@ -7,7 +8,6 @@ using RsrcArchitect.ViewModels.Factories; using RsrcArchitect.ViewModels.Helpers; using RsrcArchitect.ViewModels.Messages; -using RsrcArchitect.ViewModels.Positioners; using RsrcArchitect.ViewModels.Types; using RsrcCore; using RsrcCore.Controls; @@ -25,14 +25,13 @@ public partial class DialogEditorViewModel : ObservableObject private Transformation _transformation = Transformation.None; private Sizing _sizing = Sizing.Empty; - - private Rectangle _transformationStartRectangle; + + private List _transformationStartRectangles = new(); private Vector2 _transformationStartPointerPosition; private Vector2 _panStartPointerPosition; private Vector2 _panStartTranslation; private float _scale = 1f; private bool _isPanning; - private TreeNode? _selectedNode; public DialogEditorViewModel(Dialog dialog, string friendlyName, DialogEditorSettingsViewModel dialogEditorSettingsViewModel, IFilePickerService filePickerService) @@ -50,34 +49,31 @@ public DialogEditorViewModel(Dialog dialog, string friendlyName, new(this, "combobox", () => new ComboBox()), new(this, "label", () => new Label()) }; - WeakReferenceMessenger.Default.RegisterAll(this); - } - - public List ToolboxItemViewModels { get; } - public DialogViewModel DialogViewModel { get; } - public string FriendlyName { get; } - - private TreeNode? SelectedNode - { - get => _selectedNode; - set + SelectedNodes.CollectionChanged += (sender, args) => { - SetProperty(ref _selectedNode, value); - OnPropertyChanged(nameof(IsNodeSelected)); + OnPropertyChanged(nameof(HasSelection)); DeleteSelectedNodeCommand.NotifyCanExecuteChanged(); BringSelectedNodeToFrontCommand.NotifyCanExecuteChanged(); - SelectedControlViewModel = value != null - ? ControlViewModelFactory.Create(value.Data, + SelectedControlViewModels.Clear(); + foreach (var node in SelectedNodes) + { + SelectedControlViewModels.Add(ControlViewModelFactory.Create(node.Data, s => { return DialogViewModel.Dialog.Root.Flatten().Any(x => x.Identifier.Equals(s, StringComparison.InvariantCultureIgnoreCase)); - }) - : null; - OnPropertyChanged(nameof(SelectedControlViewModel)); - } + })); + } + }; + WeakReferenceMessenger.Default.RegisterAll(this); } + public List ToolboxItemViewModels { get; } + public DialogViewModel DialogViewModel { get; } + public string FriendlyName { get; } + + public ObservableCollection> SelectedNodes { get; } = new(); + public float Scale { get => _scale; @@ -88,45 +84,13 @@ public float Scale } } - public bool IsNodeSelected => SelectedNode != null; - public ControlViewModel? SelectedControlViewModel { get; private set; } + public bool HasSelection => SelectedControlViewModels.Count > 0; + public ObservableCollection SelectedControlViewModels { get; } = new(); public Vector2 Translation { get; private set; } = Vector2.Zero; #region Private Methods - private (Transformation transformation, Sizing sizing) GetTransformationOperationCandidate(Control control, - Vector2 position) - { - var relative = position - control.Rectangle.ToVector2(); - - var transformation = Transformation.Size; - var sizing = Sizing.Empty; - - sizing = sizing with { Left = Math.Abs(relative.X - 0) < _dialogEditorSettingsViewModel.GripSize }; - sizing = sizing with { Top = Math.Abs(relative.Y - 0) < _dialogEditorSettingsViewModel.GripSize }; - sizing = sizing with - { - Right = Math.Abs(relative.X - control.Rectangle.Width) < _dialogEditorSettingsViewModel.GripSize - }; - sizing = sizing with - { - Bottom = Math.Abs(relative.Y - control.Rectangle.Height) < _dialogEditorSettingsViewModel.GripSize - }; - - if (sizing.IsEmpty) - { - transformation = Transformation.Move; - } - - if (!control.Rectangle.Inflate(_dialogEditorSettingsViewModel.GripSize).Contains(new(position))) - { - transformation = Transformation.None; - } - - return (transformation, sizing); - } - private TreeNode? GetControlNodeAtPosition(Vector2 position) { // TODO: make special case for groupboxes; the center is clickthrough @@ -157,24 +121,43 @@ private void PointerPress(Vector2 position) // do grip-test first, then store if it hits var isGripHit = false; - if (SelectedNode != null) - isGripHit = GetTransformationOperationCandidate(SelectedNode.Data, dialogPosition).transformation != - Transformation.None; + (Transformation transformation, Sizing sizing) transformationOperation = (Transformation.None, Sizing.Empty); + if (SelectedNodes.Count > 0) + { + // if any candidate hits, we grabbed a control's grip + transformationOperation = SelectedNodes + .Select(x => + TransformationHelper.GetCandidate(x.Data, dialogPosition, _dialogEditorSettingsViewModel.GripSize)) + .FirstOrDefault(x => x.Item1 != Transformation.None, (Transformation.None, Sizing.Empty)); + + isGripHit = transformationOperation.transformation != Transformation.None; + } // if no grip hits, we know we aren't starting to resize or move a control, // thus we can select a new one - if (!isGripHit) SelectedNode = GetControlNodeAtPosition(dialogPosition); + if (!isGripHit) + { + Debug.Print("No grip hit"); + var newSelectedNode = GetControlNodeAtPosition(dialogPosition); + SelectedNodes.Clear(); + if (newSelectedNode != null) + { + SelectedNodes.Add(GetControlNodeAtPosition(dialogPosition)); + } + } - if (SelectedNode != null) + if (SelectedNodes.Count > 0) { - (_transformation, _sizing) = GetTransformationOperationCandidate(SelectedNode.Data, dialogPosition); - _transformationStartRectangle = SelectedNode.Data.Rectangle; + Debug.Print("Start control transformation"); + (_transformation, _sizing) = (transformationOperation.transformation, transformationOperation.sizing); + _transformationStartRectangles = SelectedNodes.Select(x => x.Data.Rectangle).ToList(); _transformationStartPointerPosition = dialogPosition; } else { // no node caught but stil clicked, // so start translating the camera + Debug.Print("Start pan"); _isPanning = true; _panStartPointerPosition = position; _panStartTranslation = Translation; @@ -200,74 +183,81 @@ private void PointerMove(Vector2 position) WeakReferenceMessenger.Default.Send(new CanvasInvalidationMessage(0)); return; } - - - if (SelectedControlViewModel == null) return; + + if (SelectedNodes.Count == 0) return; position = RelativePositionToDialog(position); - switch (_transformation) + int i = 0; + foreach (var controlViewModel in SelectedControlViewModels) { - case Transformation.Size: - if (_sizing.Left) - { - position.X = Math.Min(position.X, _transformationStartRectangle.Right); - SelectedControlViewModel.X = (int)position.X; - SelectedControlViewModel.Width = - _transformationStartRectangle.Right - SelectedControlViewModel.X; - } - - if (_sizing.Top) - { - position.Y = Math.Min(position.Y, _transformationStartRectangle.Bottom); - SelectedControlViewModel.Y = (int)position.Y; - SelectedControlViewModel.Height = - _transformationStartRectangle.Bottom - SelectedControlViewModel.Y; - } - - if (_sizing.Right) - { - position.X = Math.Max(position.X, _transformationStartRectangle.X); - SelectedControlViewModel.Width = (int)(_transformationStartRectangle.Width + - (position.X - _transformationStartRectangle.Right)); - } - - if (_sizing.Bottom) - { - position.Y = Math.Max(position.Y, _transformationStartRectangle.Y); - SelectedControlViewModel.Height = (int)(_transformationStartRectangle.Height + - (position.Y - _transformationStartRectangle.Bottom)); - } - - break; - case Transformation.Move: - SelectedControlViewModel.X = - (int)(_transformationStartRectangle.X + (position.X - _transformationStartPointerPosition.X)); - SelectedControlViewModel.Y = - (int)(_transformationStartRectangle.Y + (position.Y - _transformationStartPointerPosition.Y)); - break; - } + var transformationStartRectangle = _transformationStartRectangles[i]; + switch (_transformation) + { + case Transformation.Size: + if (_sizing.Left) + { + position.X = Math.Min(position.X, transformationStartRectangle.Right); + controlViewModel.X = (int)position.X; + controlViewModel.Width = + transformationStartRectangle.Right - controlViewModel.X; + } + + if (_sizing.Top) + { + position.Y = Math.Min(position.Y, transformationStartRectangle.Bottom); + controlViewModel.Y = (int)position.Y; + controlViewModel.Height = + transformationStartRectangle.Bottom - controlViewModel.Y; + } - var processedRectangle = _dialogEditorSettingsViewModel.Positioner.Transform(DialogViewModel.Dialog.Root, SelectedControlViewModel.Control); - SelectedControlViewModel.X = processedRectangle.X; - SelectedControlViewModel.Y = processedRectangle.Y; - SelectedControlViewModel.Width = processedRectangle.Width; - SelectedControlViewModel.Height = processedRectangle.Height; + if (_sizing.Right) + { + position.X = Math.Max(position.X, transformationStartRectangle.X); + controlViewModel.Width = (int)(transformationStartRectangle.Width + + (position.X - transformationStartRectangle.Right)); + } + + if (_sizing.Bottom) + { + position.Y = Math.Max(position.Y, transformationStartRectangle.Y); + controlViewModel.Height = (int)(transformationStartRectangle.Height + + (position.Y - transformationStartRectangle.Bottom)); + } + + break; + case Transformation.Move: + controlViewModel.X = + (int)(transformationStartRectangle.X + (position.X - _transformationStartPointerPosition.X)); + controlViewModel.Y = + (int)(transformationStartRectangle.Y + (position.Y - _transformationStartPointerPosition.Y)); + break; + } + + var processedRectangle = + _dialogEditorSettingsViewModel.Positioner.Transform(DialogViewModel.Dialog.Root, + controlViewModel.Control); + controlViewModel.X = processedRectangle.X; + controlViewModel.Y = processedRectangle.Y; + controlViewModel.Width = processedRectangle.Width; + controlViewModel.Height = processedRectangle.Height; + i++; + } } - [RelayCommand(CanExecute = nameof(IsNodeSelected))] + [RelayCommand(CanExecute = nameof(HasSelection))] private void DeleteSelectedNode() { - DialogViewModel.Dialog.Root.Children.Remove(SelectedNode!); - SelectedNode = null; + // DialogViewModel.Dialog.Root.Children.Remove(SelectedNodes!); + // SelectedNode = null; WeakReferenceMessenger.Default.Send(new CanvasInvalidationMessage(0)); } - [RelayCommand(CanExecute = nameof(IsNodeSelected))] + [RelayCommand(CanExecute = nameof(HasSelection))] private void BringSelectedNodeToFront() { - DialogViewModel.Dialog.Root.Children.Remove(SelectedNode!); - DialogViewModel.Dialog.Root.Children.Add(SelectedNode!); + // DialogViewModel.Dialog.Root.Children.Remove(SelectedNode!); + // DialogViewModel.Dialog.Root.Children.Add(SelectedNode!); WeakReferenceMessenger.Default.Send(new CanvasInvalidationMessage(0)); } @@ -291,24 +281,7 @@ private async Task SaveRc() await File.WriteAllTextAsync(resourceFile, rc); await File.WriteAllTextAsync(headerFile, header); } - - [RelayCommand] - private async Task SaveUgui() - { - var lua = new UguiDialogSerializer().Serialize(new DefaultLayoutEngine().DoLayout(DialogViewModel.Dialog), - DialogViewModel.Dialog); - - var luaFile = - await _filePickerService.TryPickSaveFileAsync("ugui.lua", ("Lua Script File", new[] { "lua" })); - - if (luaFile == null) - { - return; - } - - await File.WriteAllTextAsync(luaFile, lua); - } - + #endregion diff --git a/RsrcArchitect.ViewModels/Helpers/TransformationHelper.cs b/RsrcArchitect.ViewModels/Helpers/TransformationHelper.cs new file mode 100644 index 0000000..bd14074 --- /dev/null +++ b/RsrcArchitect.ViewModels/Helpers/TransformationHelper.cs @@ -0,0 +1,41 @@ +using System.Numerics; +using RsrcArchitect.ViewModels.Types; +using RsrcCore.Controls; +using RsrcCore.Geometry; + +namespace RsrcArchitect.ViewModels.Helpers; + +internal static class TransformationHelper +{ + public static (Transformation transformation, Sizing sizing) GetCandidate(Control control, + Vector2 position, int gripSize) + { + var relative = position - control.Rectangle.ToVector2(); + + var transformation = Transformation.Size; + var sizing = Sizing.Empty; + + sizing = sizing with { Left = Math.Abs(relative.X - 0) < gripSize }; + sizing = sizing with { Top = Math.Abs(relative.Y - 0) < gripSize }; + sizing = sizing with + { + Right = Math.Abs(relative.X - control.Rectangle.Width) < gripSize + }; + sizing = sizing with + { + Bottom = Math.Abs(relative.Y - control.Rectangle.Height) < gripSize + }; + + if (sizing.IsEmpty) + { + transformation = Transformation.Move; + } + + if (!control.Rectangle.Inflate(gripSize).Contains(new Vector2Int(position))) + { + transformation = Transformation.None; + } + + return (transformation, sizing); + } +} \ No newline at end of file diff --git a/RsrcArchitect.Views.WPF/MainWindow.xaml b/RsrcArchitect.Views.WPF/MainWindow.xaml index ec2c413..c9e811c 100644 --- a/RsrcArchitect.Views.WPF/MainWindow.xaml +++ b/RsrcArchitect.Views.WPF/MainWindow.xaml @@ -29,8 +29,6 @@ - diff --git a/RsrcArchitect.Views.WPF/Rendering/StyledObjectRenderer.cs b/RsrcArchitect.Views.WPF/Rendering/StyledObjectRenderer.cs index 0ac7192..34d05f2 100644 --- a/RsrcArchitect.Views.WPF/Rendering/StyledObjectRenderer.cs +++ b/RsrcArchitect.Views.WPF/Rendering/StyledObjectRenderer.cs @@ -292,44 +292,47 @@ public void RenderDecorations(SKCanvas canvas, DialogEditorViewModel dialogEdito dialogEditorViewModel.DialogViewModel.Height); _previousGridSize = dialogEditorSettingsViewModel.GridSize; - if (dialogEditorViewModel.SelectedControlViewModel == null) return; + if (dialogEditorViewModel.SelectedControlViewModels.Count == 0) return; + foreach (var selectedControlViewModel in dialogEditorViewModel.SelectedControlViewModels) + { + // drawing the selection-related graphics + var selectionRectangle = SKRect.Create(0, 0, + selectedControlViewModel.Rectangle.Width, + selectedControlViewModel.Rectangle.Height); - // drawing the selection-related graphics - var selectionRectangle = SKRect.Create(0, 0, - dialogEditorViewModel.SelectedControlViewModel.Rectangle.Width, - dialogEditorViewModel.SelectedControlViewModel.Rectangle.Height); + canvas.Translate( + selectedControlViewModel.Rectangle.X, + selectedControlViewModel.Rectangle.Y); - canvas.Translate( - dialogEditorViewModel.SelectedControlViewModel.Rectangle.X, - dialogEditorViewModel.SelectedControlViewModel.Rectangle.Y); + // draw the selection rectangle + DrawImageNinePatch(canvas, _visualStyle.Image, _visualStyle.Selection.Source, _visualStyle.Selection.Center, + selectionRectangle); - // draw the selection rectangle - DrawImageNinePatch(canvas, _visualStyle.Image, _visualStyle.Selection.Source, _visualStyle.Selection.Center, - selectionRectangle); + // and the corner points + var cornerPoints = new SKPoint[] + { + new(0, 0), + new(selectionRectangle.Width, selectionRectangle.Height), + new(0, selectionRectangle.Height), + new(selectionRectangle.Width, 0), + new(selectionRectangle.Width / 2, 0), + new(0, selectionRectangle.Height / 2), + new(selectionRectangle.Width / 2, selectionRectangle.Height), + new(selectionRectangle.Width, selectionRectangle.Height / 2), + }; + canvas.Translate(-dialogEditorSettingsViewModel.GripSize / 2f, -dialogEditorSettingsViewModel.GripSize / 2f); + foreach (var point in cornerPoints) + { + canvas.Translate(point.X, point.Y); + DrawImageNinePatch(canvas, _visualStyle.Image, _visualStyle.SelectionCorner.Source, + _visualStyle.SelectionCorner.Center, + SKRect.Create(0, 0, dialogEditorSettingsViewModel.GripSize, dialogEditorSettingsViewModel.GripSize)); + canvas.Translate(-point.X, -point.Y); + } - // and the corner points - var cornerPoints = new SKPoint[] - { - new(0, 0), - new(selectionRectangle.Width, selectionRectangle.Height), - new(0, selectionRectangle.Height), - new(selectionRectangle.Width, 0), - new(selectionRectangle.Width / 2, 0), - new(0, selectionRectangle.Height / 2), - new(selectionRectangle.Width / 2, selectionRectangle.Height), - new(selectionRectangle.Width, selectionRectangle.Height / 2), - }; - canvas.Translate(-dialogEditorSettingsViewModel.GripSize / 2f, -dialogEditorSettingsViewModel.GripSize / 2f); - foreach (var point in cornerPoints) - { - canvas.Translate(point.X, point.Y); - DrawImageNinePatch(canvas, _visualStyle.Image, _visualStyle.SelectionCorner.Source, - _visualStyle.SelectionCorner.Center, - SKRect.Create(0, 0, dialogEditorSettingsViewModel.GripSize, dialogEditorSettingsViewModel.GripSize)); - canvas.Translate(-point.X, -point.Y); + canvas.Translate(dialogEditorSettingsViewModel.GripSize / 2f, dialogEditorSettingsViewModel.GripSize / 2f); } - - canvas.Translate(dialogEditorSettingsViewModel.GripSize / 2f, dialogEditorSettingsViewModel.GripSize / 2f); + } } \ No newline at end of file