// 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.UserInterface; using osu.Framework.Input; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Game.Graphics.Cursor; using osu.Game.Graphics.UserInterface; using osu.Game.Input.Bindings; using osu.Game.Resources.Localisation.Web; using osu.Game.Rulesets.Edit; using osu.Game.Screens.Edit.Compose.Components.Timeline; using osuTK; using osuTK.Input; namespace osu.Game.Screens.Edit.Compose.Components { /// /// A component which outlines items and handles movement of selections. /// public abstract partial class SelectionHandler : CompositeDrawable, IKeyBindingHandler, IKeyBindingHandler, IHasContextMenu { /// /// How much padding around the selection area is added. /// public const float INFLATE_SIZE = 5; /// /// 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; protected SelectionBox SelectionBox { get; private set; } = null!; [Resolved(CanBeNull = true)] protected IEditorChangeHandler? ChangeHandler { get; private set; } public SelectionRotationHandler RotationHandler { get; private set; } = null!; public SelectionScaleHandler ScaleHandler { get; private set; } = null!; [Resolved(CanBeNull = true)] protected OsuContextMenuContainer? ContextMenuContainer { get; private set; } protected SelectionHandler() { selectedBlueprints = new List>(); RelativeSizeAxes = Axes.Both; AlwaysPresent = true; } protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) { var dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); dependencies.CacheAs(RotationHandler = CreateRotationHandler()); dependencies.CacheAs(ScaleHandler = CreateScaleHandler()); return dependencies; } [BackgroundDependencyLoader] private void load() { AddRangeInternal(new Drawable[] { RotationHandler, ScaleHandler, SelectionBox = CreateSelectionBox(), }); SelectedItems.BindCollectionChanged((_, _) => Scheduler.AddOnce(updateVisibility), true); } public SelectionBox CreateSelectionBox() => new SelectionBox { OperationStarted = OnOperationBegan, OperationEnded = OnOperationEnded, 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 /// /// Positional input must be received outside the container's bounds, /// in order to handle blueprints which are partially offscreen. /// /// /// public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; /// /// 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; /// /// Creates the handler to use for rotation operations. /// public virtual SelectionRotationHandler CreateRotationHandler() => new SelectionRotationHandler(); /// /// 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; /// /// Creates the handler to use for scale operations. /// public virtual SelectionScaleHandler CreateScaleHandler() => new SelectionScaleHandler(); /// /// Handles the selected items being flipped. /// /// The direction to flip. /// Whether the flip operation should be global to the playfield's origin or local to the selected pattern. /// Whether any items could be flipped. public virtual bool HandleFlip(Direction direction, bool flipOverOrigin) => false; /// /// Handles the selected items being reversed pattern-wise. /// /// Whether any items could be reversed. public virtual bool HandleReverse() => false; public virtual bool OnPressed(KeyBindingPressEvent e) { if (e.Repeat) return false; bool handled; switch (e.Action) { case GlobalAction.EditorFlipHorizontally: ChangeHandler?.BeginChange(); handled = HandleFlip(Direction.Horizontal, true); ChangeHandler?.EndChange(); return handled; case GlobalAction.EditorFlipVertically: ChangeHandler?.BeginChange(); handled = HandleFlip(Direction.Vertical, true); ChangeHandler?.EndChange(); return handled; } return false; } public void OnReleased(KeyBindingReleaseEvent e) { } public bool OnPressed(KeyBindingPressEvent e) { switch (e.Action) { case PlatformAction.Delete: DeleteSelected(); return true; } return false; } public void OnReleased(KeyBindingReleaseEvent e) { } #endregion #region Selection Handling /// /// Deselect all selected items. /// protected void DeselectAll() { SelectedItems.Clear(); ContextMenuContainer?.CloseMenu(); } /// /// 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); ContextMenuContainer?.CloseMenu(); } /// /// 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 virtual bool MouseDownSelectionRequested(SelectionBlueprint blueprint, MouseButtonEvent e) { if (e.Button == MouseButton.Middle || (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(); } /// /// Given a selection target and a function of truth, retrieve the correct ternary state for display. /// public static TernaryState GetStateFromSelection(IEnumerable selection, Func func) { if (selection.Any(func)) return selection.All(func) ? TernaryState.True : TernaryState.Indeterminate; return TernaryState.False; } /// /// 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(); blueprint.Select(); return true; } protected void DeleteSelected() { DeleteItems(SelectedItems.ToArray()); DeselectAll(); } #endregion #region Outline Display /// /// Updates whether this is visible. /// private void updateVisibility() { int count = SelectedItems.Count; SelectionBox.Text = count > 0 ? count.ToString() : string.Empty; SelectionBox.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(INFLATE_SIZE); SelectionBox.Position = selectionRect.Location; SelectionBox.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(CommonStrings.ButtonsDelete, MenuItemType.Destructive, DeleteSelected) { Hotkey = new Hotkey { PlatformAction = PlatformAction.Delete, KeyCombinations = [new KeyCombination(InputKey.Shift, InputKey.MouseRight), new KeyCombination(InputKey.MouseMiddle)] } }); 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 } }