// 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.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input; using osu.Framework.Input.Bindings; using osu.Framework.Input.States; using osu.Game.Audio; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; using osuTK; namespace osu.Game.Screens.Edit.Compose.Components { /// /// A component which outlines s and handles movement of selections. /// public class SelectionHandler : CompositeDrawable, IKeyBindingHandler, IHasContextMenu { public const float BORDER_RADIUS = 2; public IEnumerable SelectedBlueprints => selectedBlueprints; private readonly List selectedBlueprints; public IEnumerable SelectedHitObjects => selectedBlueprints.Select(b => b.HitObject); private Drawable content; private OsuSpriteText selectionDetailsText; [Resolved(CanBeNull = true)] protected EditorBeatmap EditorBeatmap { get; private set; } [Resolved(CanBeNull = true)] private IEditorChangeHandler changeHandler { get; set; } public SelectionHandler() { selectedBlueprints = new List(); RelativeSizeAxes = Axes.Both; AlwaysPresent = true; Alpha = 0; } [BackgroundDependencyLoader] private void load(OsuColour colours) { InternalChild = content = new Container { Children = new Drawable[] { new Container { RelativeSizeAxes = Axes.Both, Masking = true, BorderThickness = BORDER_RADIUS, BorderColour = colours.YellowDark, Child = new Box { RelativeSizeAxes = Axes.Both, AlwaysPresent = true, Alpha = 0 } }, 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) } } } } }; } #region User Input Handling /// /// Handles the selected s being moved. /// /// /// Just returning true is enough to allow updates to take place. /// Custom implementation is only required if other attributes are to be considered, like changing columns. /// /// The move event. /// /// Whether any s could be moved. /// Returning true will also propagate StartTime changes provided by the closest . /// public virtual bool HandleMovement(MoveSelectionEvent moveEvent) => true; 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 void HandleSelected(SelectionBlueprint blueprint) { selectedBlueprints.Add(blueprint); EditorBeatmap.SelectedHitObjects.Add(blueprint.HitObject); UpdateVisibility(); } /// /// Handle a blueprint becoming deselected. /// /// The blueprint. internal void HandleDeselected(SelectionBlueprint blueprint) { selectedBlueprints.Remove(blueprint); EditorBeatmap.SelectedHitObjects.Remove(blueprint.HitObject); UpdateVisibility(); } /// /// Handle a blueprint requesting selection. /// /// The blueprint. /// The input state at the point of selection. internal void HandleSelectionRequested(SelectionBlueprint blueprint, InputState state) { if (state.Keyboard.ControlPressed) { if (blueprint.IsSelected) blueprint.Deselect(); else blueprint.Select(); } else { if (blueprint.IsSelected) return; DeselectAll?.Invoke(); blueprint.Select(); } } private void deleteSelected() { changeHandler?.BeginChange(); foreach (var h in selectedBlueprints.ToList()) EditorBeatmap?.Remove(h.HitObject); changeHandler?.EndChange(); } #endregion #region Outline Display /// /// Updates whether this is visible. /// internal void UpdateVisibility() { int count = selectedBlueprints.Count; selectionDetailsText.Text = count > 0 ? count.ToString() : string.Empty; if (count > 0) Show(); else Hide(); } protected override void Update() { base.Update(); if (selectedBlueprints.Count == 0) return; // Move the rectangle to cover the hitobjects var topLeft = new Vector2(float.MaxValue, float.MaxValue); var bottomRight = new Vector2(float.MinValue, float.MinValue); foreach (var blueprint in selectedBlueprints) { topLeft = Vector2.ComponentMin(topLeft, ToLocalSpace(blueprint.SelectionQuad.TopLeft)); bottomRight = Vector2.ComponentMax(bottomRight, ToLocalSpace(blueprint.SelectionQuad.BottomRight)); } topLeft -= new Vector2(5); bottomRight += new Vector2(5); content.Size = bottomRight - topLeft; content.Position = topLeft; } #endregion #region Sample Changes /// /// Adds a hit sample to all selected s. /// /// The name of the hit sample. public void AddHitSample(string sampleName) { changeHandler?.BeginChange(); foreach (var h in SelectedHitObjects) { // Make sure there isn't already an existing sample if (h.Samples.Any(s => s.Name == sampleName)) continue; h.Samples.Add(new HitSampleInfo { Name = sampleName }); } changeHandler?.EndChange(); } /// /// Removes a hit sample from all selected s. /// /// The name of the hit sample. public void RemoveHitSample(string sampleName) { changeHandler?.BeginChange(); foreach (var h in SelectedHitObjects) h.SamplesBindable.RemoveAll(s => s.Name == sampleName); changeHandler?.EndChange(); } #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.AddRange(new[] { new OsuMenuItem("Sound") { Items = new[] { createHitSampleMenuItem("Whistle", HitSampleInfo.HIT_WHISTLE), createHitSampleMenuItem("Clap", HitSampleInfo.HIT_CLAP), createHitSampleMenuItem("Finish", HitSampleInfo.HIT_FINISH) } }, 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(); private MenuItem createHitSampleMenuItem(string name, string sampleName) { return new TernaryStateMenuItem(name, MenuItemType.Standard, setHitSampleState) { State = { Value = getHitSampleState() } }; void setHitSampleState(TernaryState state) { switch (state) { case TernaryState.False: RemoveHitSample(sampleName); break; case TernaryState.True: AddHitSample(sampleName); break; } } TernaryState getHitSampleState() { int countExisting = SelectedHitObjects.Count(h => h.Samples.Any(s => s.Name == sampleName)); if (countExisting == 0) return TernaryState.False; if (countExisting < SelectedHitObjects.Count()) return TernaryState.Indeterminate; return TernaryState.True; } } #endregion } }