// 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 osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Events; using osu.Game.Graphics; using osuTK; using osuTK.Input; namespace osu.Game.Screens.Edit.Compose.Components { public class SelectionBox : CompositeDrawable { public Func OnRotation; public Func OnScale; public Func OnFlip; public Func OnReverse; public Action OperationStarted; public Action OperationEnded; private bool canReverse; /// /// Whether pattern reversing support should be enabled. /// public bool CanReverse { get => canReverse; set { if (canReverse == value) return; canReverse = value; recreate(); } } private bool canRotate; /// /// Whether rotation support should be enabled. /// public bool CanRotate { get => canRotate; set { if (canRotate == value) return; canRotate = value; recreate(); } } private bool canScaleX; /// /// Whether vertical scale support should be enabled. /// public bool CanScaleX { get => canScaleX; set { if (canScaleX == value) return; canScaleX = value; recreate(); } } private bool canScaleY; /// /// Whether horizontal scale support should be enabled. /// public bool CanScaleY { get => canScaleY; set { if (canScaleY == value) return; canScaleY = value; recreate(); } } private SelectionBoxDragHandleContainer dragHandles; private FillFlowContainer buttons; public const float BORDER_RADIUS = 3; [Resolved] private OsuColour colours { get; set; } [BackgroundDependencyLoader] private void load() { RelativeSizeAxes = Axes.Both; recreate(); } protected override bool OnKeyDown(KeyDownEvent e) { if (e.Repeat || !e.ControlPressed) return false; bool runOperationFromHotkey(Func operation) { operationStarted(); bool result = operation?.Invoke() ?? false; operationEnded(); return result; } switch (e.Key) { case Key.G: return CanReverse && runOperationFromHotkey(OnReverse); case Key.H: return CanScaleX && runOperationFromHotkey(() => OnFlip?.Invoke(Direction.Horizontal) ?? false); case Key.J: return CanScaleY && runOperationFromHotkey(() => OnFlip?.Invoke(Direction.Vertical) ?? false); } return base.OnKeyDown(e); } private void recreate() { if (LoadState < LoadState.Loading) return; InternalChildren = new Drawable[] { new Container { Masking = true, BorderThickness = BORDER_RADIUS, BorderColour = colours.YellowDark, RelativeSizeAxes = Axes.Both, Children = new Drawable[] { new Box { RelativeSizeAxes = Axes.Both, AlwaysPresent = true, Alpha = 0 }, } }, dragHandles = new SelectionBoxDragHandleContainer { RelativeSizeAxes = Axes.Both, // ensures that the centres of all drag handles line up with the middle of the selection box border. Padding = new MarginPadding(BORDER_RADIUS / 2) }, buttons = new FillFlowContainer { Y = 20, AutoSizeAxes = Axes.Both, Direction = FillDirection.Horizontal, Anchor = Anchor.BottomCentre, Origin = Anchor.Centre } }; if (CanScaleX) addXScaleComponents(); if (CanScaleX && CanScaleY) addFullScaleComponents(); if (CanScaleY) addYScaleComponents(); if (CanRotate) addRotationComponents(); if (CanReverse) addButton(FontAwesome.Solid.Backward, "Reverse pattern (Ctrl-G)", () => OnReverse?.Invoke()); } private void addRotationComponents() { addButton(FontAwesome.Solid.Undo, "Rotate 90 degrees counter-clockwise", () => OnRotation?.Invoke(-90)); addButton(FontAwesome.Solid.Redo, "Rotate 90 degrees clockwise", () => OnRotation?.Invoke(90)); addRotateHandle(Anchor.TopLeft); addRotateHandle(Anchor.TopRight); addRotateHandle(Anchor.BottomLeft); addRotateHandle(Anchor.BottomRight); } private void addYScaleComponents() { addButton(FontAwesome.Solid.ArrowsAltV, "Flip vertically (Ctrl-J)", () => OnFlip?.Invoke(Direction.Vertical)); addScaleHandle(Anchor.TopCentre); addScaleHandle(Anchor.BottomCentre); } private void addFullScaleComponents() { addScaleHandle(Anchor.TopLeft); addScaleHandle(Anchor.TopRight); addScaleHandle(Anchor.BottomLeft); addScaleHandle(Anchor.BottomRight); } private void addXScaleComponents() { addButton(FontAwesome.Solid.ArrowsAltH, "Flip horizontally (Ctrl-H)", () => OnFlip?.Invoke(Direction.Horizontal)); addScaleHandle(Anchor.CentreLeft); addScaleHandle(Anchor.CentreRight); } private void addButton(IconUsage icon, string tooltip, Action action) { var button = new SelectionBoxButton(icon, tooltip) { Action = action }; button.OperationStarted += operationStarted; button.OperationEnded += operationEnded; buttons.Add(button); } private void addScaleHandle(Anchor anchor) { var handle = new SelectionBoxScaleHandle { Anchor = anchor, HandleDrag = e => OnScale?.Invoke(e.Delta, anchor) }; handle.OperationStarted += operationStarted; handle.OperationEnded += operationEnded; dragHandles.AddScaleHandle(handle); } private void addRotateHandle(Anchor anchor) { var handle = new SelectionBoxRotationHandle { Anchor = anchor, HandleDrag = e => OnRotation?.Invoke(convertDragEventToAngleOfRotation(e)) }; handle.OperationStarted += operationStarted; handle.OperationEnded += operationEnded; dragHandles.AddRotationHandle(handle); } private int activeOperations; private float convertDragEventToAngleOfRotation(DragEvent e) { // Adjust coordinate system to the center of SelectionBox float startAngle = MathF.Atan2(e.LastMousePosition.Y - DrawHeight / 2, e.LastMousePosition.X - DrawWidth / 2); float endAngle = MathF.Atan2(e.MousePosition.Y - DrawHeight / 2, e.MousePosition.X - DrawWidth / 2); return (endAngle - startAngle) * 180 / MathF.PI; } private void operationEnded() { if (--activeOperations == 0) OperationEnded?.Invoke(); } private void operationStarted() { if (activeOperations++ == 0) OperationStarted?.Invoke(); } } }