// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. using System; using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets.Edit; using osuTK; using osuTK.Input; namespace osu.Game.Screens.Edit.Compose.Components { /// /// A component which outlines items and handles movement of selections. /// public abstract class SelectionHandler : CompositeDrawable, IKeyBindingHandler, IHasContextMenu { /// /// The currently selected blueprints. /// Should be used when operations are dealing directly with the visible blueprints. /// For more general selection operations, use instead. /// public IReadOnlyList> SelectedBlueprints => selectedBlueprints; /// /// The currently selected items. /// public readonly BindableList SelectedItems = new BindableList(); private readonly List> selectedBlueprints; private Drawable content; private OsuSpriteText selectionDetailsText; protected SelectionBox SelectionBox { get; private set; } [Resolved(CanBeNull = true)] protected IEditorChangeHandler ChangeHandler { get; private set; } protected SelectionHandler() { selectedBlueprints = new List>(); RelativeSizeAxes = Axes.Both; AlwaysPresent = true; } [BackgroundDependencyLoader] private void load(OsuColour colours) { InternalChild = content = new Container { Children = new Drawable[] { // todo: should maybe be inside the SelectionBox? new Container { Name = "info text", AutoSizeAxes = Axes.Both, Children = new Drawable[] { new Box { Colour = colours.YellowDark, RelativeSizeAxes = Axes.Both, }, selectionDetailsText = new OsuSpriteText { Padding = new MarginPadding(2), Colour = colours.Gray0, Font = OsuFont.Default.With(size: 11) } } }, SelectionBox = CreateSelectionBox(), } }; SelectedItems.CollectionChanged += (sender, args) => { Scheduler.AddOnce(updateVisibility); }; } public SelectionBox CreateSelectionBox() => new SelectionBox { OperationStarted = OnOperationBegan, OperationEnded = OnOperationEnded, OnRotation = HandleRotation, OnScale = HandleScale, OnFlip = HandleFlip, OnReverse = HandleReverse, }; /// /// Fired when a drag operation ends from the selection box. /// protected virtual void OnOperationBegan() { ChangeHandler?.BeginChange(); } /// /// Fired when a drag operation begins from the selection box. /// protected virtual void OnOperationEnded() { ChangeHandler?.EndChange(); } #region User Input Handling /// /// Handles the selected items being moved. /// /// /// Just returning true is enough to allow default movement to take place. /// Custom implementation is only required if other attributes are to be considered, like changing columns. /// /// The move event. /// /// Whether any items could be moved. /// public virtual bool HandleMovement(MoveSelectionEvent moveEvent) => false; /// /// Handles the selected items being rotated. /// /// The delta angle to apply to the selection. /// Whether any items could be rotated. public virtual bool HandleRotation(float angle) => false; /// /// Handles the selected items being scaled. /// /// The delta scale to apply, in local coordinates. /// The point of reference where the scale is originating from. /// Whether any items could be scaled. public virtual bool HandleScale(Vector2 scale, Anchor anchor) => false; /// /// Handles the selected items being flipped. /// /// The direction to flip /// Whether any items could be flipped. public virtual bool HandleFlip(Direction direction) => false; /// /// Handles the selected items being reversed pattern-wise. /// /// Whether any items could be reversed. public virtual bool HandleReverse() => false; public bool OnPressed(PlatformAction action) { switch (action.ActionMethod) { case PlatformActionMethod.Delete: DeleteSelected(); return true; } return false; } public void OnReleased(PlatformAction action) { } #endregion #region Selection Handling /// /// Bind an action to deselect all selected blueprints. /// internal Action DeselectAll { private get; set; } /// /// Handle a blueprint becoming selected. /// /// The blueprint. internal virtual void HandleSelected(SelectionBlueprint blueprint) { // there are potentially multiple SelectionHandlers active, but we only want to add items to the selected list once. if (!SelectedItems.Contains(blueprint.Item)) SelectedItems.Add(blueprint.Item); selectedBlueprints.Add(blueprint); } /// /// Handle a blueprint becoming deselected. /// /// The blueprint. internal virtual void HandleDeselected(SelectionBlueprint blueprint) { SelectedItems.Remove(blueprint.Item); selectedBlueprints.Remove(blueprint); } /// /// Handle a blueprint requesting selection. /// /// The blueprint. /// The mouse event responsible for selection. /// Whether a selection was performed. internal bool MouseDownSelectionRequested(SelectionBlueprint blueprint, MouseButtonEvent e) { if (e.ShiftPressed && e.Button == MouseButton.Right) { handleQuickDeletion(blueprint); return true; } // while holding control, we only want to add to selection, not replace an existing selection. if (e.ControlPressed && e.Button == MouseButton.Left && !blueprint.IsSelected) { blueprint.ToggleSelection(); return true; } return ensureSelected(blueprint); } /// /// Handle a blueprint requesting selection. /// /// The blueprint. /// The mouse event responsible for deselection. /// Whether a deselection was performed. internal bool MouseUpSelectionRequested(SelectionBlueprint blueprint, MouseButtonEvent e) { if (blueprint.IsSelected) { blueprint.ToggleSelection(); return true; } return false; } private void handleQuickDeletion(SelectionBlueprint blueprint) { if (blueprint.HandleQuickDeletion()) return; if (!blueprint.IsSelected) DeleteItems(new[] { blueprint.Item }); else DeleteSelected(); } /// /// Called whenever the deletion of items has been requested. /// /// The items to be deleted. protected abstract void DeleteItems(IEnumerable items); /// /// Ensure the blueprint is in a selected state. /// /// The blueprint to select. /// Whether selection state was changed. private bool ensureSelected(SelectionBlueprint blueprint) { if (blueprint.IsSelected) return false; DeselectAll?.Invoke(); blueprint.Select(); return true; } protected void DeleteSelected() { DeleteItems(selectedBlueprints.Select(b => b.Item)); } #endregion #region Outline Display /// /// Updates whether this is visible. /// private void updateVisibility() { int count = SelectedItems.Count; selectionDetailsText.Text = count > 0 ? count.ToString() : string.Empty; content.FadeTo(count > 0 ? 1 : 0); OnSelectionChanged(); } /// /// Triggered whenever the set of selected items changes. /// Should update the selection box's state to match supported operations. /// protected virtual void OnSelectionChanged() { } protected override void Update() { base.Update(); if (selectedBlueprints.Count == 0) return; // Move the rectangle to cover the items RectangleF selectionRect = ToLocalSpace(selectedBlueprints[0].SelectionQuad).AABBFloat; for (int i = 1; i < selectedBlueprints.Count; i++) selectionRect = RectangleF.Union(selectionRect, ToLocalSpace(selectedBlueprints[i].SelectionQuad).AABBFloat); selectionRect = selectionRect.Inflate(5f); content.Position = selectionRect.Location; content.Size = selectionRect.Size; } #endregion #region Context Menu public MenuItem[] ContextMenuItems { get { if (!SelectedBlueprints.Any(b => b.IsHovered)) return Array.Empty(); var items = new List(); items.AddRange(GetContextMenuItemsForSelection(SelectedBlueprints)); if (SelectedBlueprints.Count == 1) items.AddRange(SelectedBlueprints[0].ContextMenuItems); items.Add(new OsuMenuItem("Delete", MenuItemType.Destructive, DeleteSelected)); return items.ToArray(); } } /// /// Provide context menu items relevant to current selection. Calling base is not required. /// /// The current selection. /// The relevant menu items. protected virtual IEnumerable GetContextMenuItemsForSelection(IEnumerable> selection) => Enumerable.Empty(); #endregion } }