diff --git a/osu.Game.Tests/Visual/TestCaseEditorComposeTimeline.cs b/osu.Game.Tests/Visual/TestCaseEditorComposeTimeline.cs index 81b491799b..6c74876e81 100644 --- a/osu.Game.Tests/Visual/TestCaseEditorComposeTimeline.cs +++ b/osu.Game.Tests/Visual/TestCaseEditorComposeTimeline.cs @@ -15,7 +15,7 @@ namespace osu.Game.Tests.Visual [TestFixture] public class TestCaseEditorComposeTimeline : OsuTestCase { - public override IReadOnlyList RequiredTypes => new[] { typeof(ScrollableTimeline), typeof(ScrollingTimelineContainer), typeof(BeatmapWaveformGraph), typeof(TimelineButton) }; + public override IReadOnlyList RequiredTypes => new[] { typeof(TimelineArea), typeof(Timeline), typeof(TimelineButton) }; public TestCaseEditorComposeTimeline() { @@ -27,11 +27,12 @@ namespace osu.Game.Tests.Visual Origin = Anchor.TopCentre, State = Visibility.Visible }, - new ScrollableTimeline + new TimelineArea { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Size = new Vector2(1000, 100) + RelativeSizeAxes = Axes.X, + Size = new Vector2(0.8f, 100) } }; } diff --git a/osu.Game.Tests/Visual/TestCaseWaveform.cs b/osu.Game.Tests/Visual/TestCaseWaveform.cs index b0966cf5c4..983b98016e 100644 --- a/osu.Game.Tests/Visual/TestCaseWaveform.cs +++ b/osu.Game.Tests/Visual/TestCaseWaveform.cs @@ -6,11 +6,11 @@ using OpenTK; using OpenTK.Graphics; using osu.Framework.Allocation; using osu.Framework.Graphics; +using osu.Framework.Graphics.Audio; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Game.Graphics.Sprites; using osu.Game.Overlays; -using osu.Game.Screens.Edit.Screens.Compose.Timeline; namespace osu.Game.Tests.Visual { @@ -40,14 +40,13 @@ namespace osu.Game.Tests.Visual for (int i = 1; i <= 16; i *= 2) { - var newDisplay = new BeatmapWaveformGraph + var newDisplay = new WaveformGraph { RelativeSizeAxes = Axes.Both, Resolution = 1f / i, - Beatmap = Beatmap }; - Beatmap.ValueChanged += b => newDisplay.Beatmap = b; + Beatmap.ValueChanged += b => newDisplay.Waveform = b.Waveform; flow.Add(new Container { diff --git a/osu.Game.Tests/Visual/TestCaseZoomableScrollContainer.cs b/osu.Game.Tests/Visual/TestCaseZoomableScrollContainer.cs new file mode 100644 index 0000000000..70dd67cdbd --- /dev/null +++ b/osu.Game.Tests/Visual/TestCaseZoomableScrollContainer.cs @@ -0,0 +1,142 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using NUnit.Framework; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Primitives; +using osu.Framework.Graphics.Shapes; +using osu.Framework.MathUtils; +using osu.Game.Graphics; +using osu.Game.Graphics.Cursor; +using osu.Game.Screens.Edit.Screens.Compose.Timeline; +using OpenTK; +using OpenTK.Graphics; +using OpenTK.Input; + +namespace osu.Game.Tests.Visual +{ + public class TestCaseZoomableScrollContainer : ManualInputManagerTestCase + { + private readonly ZoomableScrollContainer scrollContainer; + private readonly Drawable innerBox; + + public TestCaseZoomableScrollContainer() + { + Children = new Drawable[] + { + new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.X, + Height = 250, + Width = 0.75f, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = OsuColour.Gray(30) + }, + scrollContainer = new ZoomableScrollContainer { RelativeSizeAxes = Axes.Both } + } + }, + new MenuCursor() + }; + + scrollContainer.Add(innerBox = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = ColourInfo.GradientHorizontal(new Color4(0.8f, 0.6f, 0.4f, 1f), new Color4(0.4f, 0.6f, 0.8f, 1f)) + }); + } + + [Test] + public void TestZoom0() + { + reset(); + AddAssert("Box at 0", () => Precision.AlmostEquals(boxQuad.TopLeft, scrollQuad.TopLeft)); + AddAssert("Box width = 1x", () => Precision.AlmostEquals(boxQuad.Size, scrollQuad.Size)); + } + + [Test] + public void TestZoom10() + { + reset(); + AddStep("Set zoom = 10", () => scrollContainer.Zoom = 10); + AddAssert("Box at 1/2", () => Precision.AlmostEquals(boxQuad.Centre, scrollQuad.Centre)); + AddAssert("Box width = 10x", () => Precision.AlmostEquals(boxQuad.Size.X, 10 * scrollQuad.Size.X)); + } + + [Test] + public void TestMouseZoomInOnceOutOnce() + { + reset(); + + // Scroll in at 0.25 + AddStep("Move mouse to 0.25x", () => InputManager.MoveMouseTo(new Vector2(scrollQuad.TopLeft.X + 0.25f * scrollQuad.Size.X, scrollQuad.Centre.Y))); + AddStep("Press ctrl", () => InputManager.PressKey(Key.LControl)); + AddStep("Scroll by 3", () => InputManager.ScrollBy(new Vector2(3, 0))); + AddStep("Release ctrl", () => InputManager.ReleaseKey(Key.LControl)); + AddAssert("Box not at 0", () => !Precision.AlmostEquals(boxQuad.TopLeft, scrollQuad.TopLeft)); + AddAssert("Box 1/4 at 1/4", () => Precision.AlmostEquals(boxQuad.TopLeft.X + 0.25f * boxQuad.Size.X, scrollQuad.TopLeft.X + 0.25f * scrollQuad.Size.X)); + + // Scroll out at 0.25 + AddStep("Press ctrl", () => InputManager.PressKey(Key.LControl)); + AddStep("Scroll by -3", () => InputManager.ScrollBy(new Vector2(-3, 0))); + AddStep("Release ctrl", () => InputManager.ReleaseKey(Key.LControl)); + AddAssert("Box at 0", () => Precision.AlmostEquals(boxQuad.TopLeft, scrollQuad.TopLeft)); + AddAssert("Box 1/4 at 1/4", () => Precision.AlmostEquals(boxQuad.TopLeft.X + 0.25f * boxQuad.Size.X, scrollQuad.TopLeft.X + 0.25f * scrollQuad.Size.X)); + } + + [Test] + public void TestMouseZoomInTwiceOutTwice() + { + reset(); + + // Scroll in at 0.25 + AddStep("Move mouse to 0.25x", () => InputManager.MoveMouseTo(new Vector2(scrollQuad.TopLeft.X + 0.25f * scrollQuad.Size.X, scrollQuad.Centre.Y))); + AddStep("Press ctrl", () => InputManager.PressKey(Key.LControl)); + AddStep("Scroll by 1", () => InputManager.ScrollBy(new Vector2(1, 0))); + AddStep("Release ctrl", () => InputManager.ReleaseKey(Key.LControl)); + + // Scroll in at 0.6 + AddStep("Move mouse to 0.75x", () => InputManager.MoveMouseTo(new Vector2(scrollQuad.TopLeft.X + 0.75f * scrollQuad.Size.X, scrollQuad.Centre.Y))); + AddStep("Press ctrl", () => InputManager.PressKey(Key.LControl)); + AddStep("Scroll by 1", () => InputManager.ScrollBy(new Vector2(1, 0))); + AddStep("Release ctrl", () => InputManager.ReleaseKey(Key.LControl)); + AddAssert("Box not at 0", () => !Precision.AlmostEquals(boxQuad.TopLeft, scrollQuad.TopLeft)); + + // Very hard to determine actual position, so approximate + AddAssert("Box at correct position (1)", () => Precision.DefinitelyBigger(scrollQuad.TopLeft.X + 0.25f * scrollQuad.Size.X, boxQuad.TopLeft.X + 0.25f * boxQuad.Size.X)); + AddAssert("Box at correct position (2)", () => Precision.DefinitelyBigger(scrollQuad.TopLeft.X + 0.6f * scrollQuad.Size.X, boxQuad.TopLeft.X + 0.3f * boxQuad.Size.X)); + AddAssert("Box at correct position (3)", () => Precision.DefinitelyBigger(boxQuad.TopLeft.X + 0.6f * boxQuad.Size.X, scrollQuad.TopLeft.X + 0.6f * scrollQuad.Size.X)); + + // Scroll out at 0.6 + AddStep("Press ctrl", () => InputManager.PressKey(Key.LControl)); + AddStep("Scroll by -1", () => InputManager.ScrollBy(new Vector2(-1, 0))); + AddStep("Release ctrl", () => InputManager.ReleaseKey(Key.LControl)); + + // Scroll out at 0.25 + AddStep("Move mouse to 0.25x", () => InputManager.MoveMouseTo(new Vector2(scrollQuad.TopLeft.X + 0.25f * scrollQuad.Size.X, scrollQuad.Centre.Y))); + AddStep("Press ctrl", () => InputManager.PressKey(Key.LControl)); + AddStep("Scroll by -1", () => InputManager.ScrollBy(new Vector2(-1, 0))); + AddStep("Release ctrl", () => InputManager.ReleaseKey(Key.LControl)); + AddAssert("Box at 0", () => Precision.AlmostEquals(boxQuad.TopLeft, scrollQuad.TopLeft)); + } + + private void reset() + { + AddStep("Reset", () => + { + scrollContainer.Zoom = 0; + scrollContainer.ScrollTo(0, false); + }); + } + + private Quad scrollQuad => scrollContainer.ScreenSpaceDrawQuad; + private Quad boxQuad => innerBox.ScreenSpaceDrawQuad; + } +} diff --git a/osu.Game/Screens/Edit/Screens/Compose/Compose.cs b/osu.Game/Screens/Edit/Screens/Compose/Compose.cs index b991f7c2c0..a862485fd6 100644 --- a/osu.Game/Screens/Edit/Screens/Compose/Compose.cs +++ b/osu.Game/Screens/Edit/Screens/Compose/Compose.cs @@ -64,7 +64,7 @@ namespace osu.Game.Screens.Edit.Screens.Compose { RelativeSizeAxes = Axes.Both, Padding = new MarginPadding { Right = 5 }, - Child = new ScrollableTimeline { RelativeSizeAxes = Axes.Both } + Child = new TimelineArea { RelativeSizeAxes = Axes.Both } }, new BeatDivisorControl(beatDivisor) { RelativeSizeAxes = Axes.Both } }, diff --git a/osu.Game/Screens/Edit/Screens/Compose/Timeline/BeatmapWaveformGraph.cs b/osu.Game/Screens/Edit/Screens/Compose/Timeline/BeatmapWaveformGraph.cs deleted file mode 100644 index 136ceb47c4..0000000000 --- a/osu.Game/Screens/Edit/Screens/Compose/Timeline/BeatmapWaveformGraph.cs +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE - -using osu.Framework.Graphics; -using osu.Framework.Graphics.Audio; -using osu.Framework.Graphics.Containers; -using osu.Game.Beatmaps; - -namespace osu.Game.Screens.Edit.Screens.Compose.Timeline -{ - public class BeatmapWaveformGraph : CompositeDrawable - { - public WorkingBeatmap Beatmap { set => graph.Waveform = value.Waveform; } - - private readonly WaveformGraph graph; - - public BeatmapWaveformGraph() - { - InternalChild = graph = new WaveformGraph { RelativeSizeAxes = Axes.Both }; - } - - /// - /// Gets or sets the . - /// - public float Resolution - { - get { return graph.Resolution; } - set { graph.Resolution = value; } - } - } -} diff --git a/osu.Game/Screens/Edit/Screens/Compose/Timeline/ScrollableTimeline.cs b/osu.Game/Screens/Edit/Screens/Compose/Timeline/ScrollableTimeline.cs deleted file mode 100644 index 3bebf78b17..0000000000 --- a/osu.Game/Screens/Edit/Screens/Compose/Timeline/ScrollableTimeline.cs +++ /dev/null @@ -1,126 +0,0 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE - -using OpenTK; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; -using osu.Game.Graphics; -using osu.Game.Graphics.UserInterface; - -namespace osu.Game.Screens.Edit.Screens.Compose.Timeline -{ - public class ScrollableTimeline : CompositeDrawable - { - private readonly ScrollingTimelineContainer timelineContainer; - - public ScrollableTimeline() - { - Masking = true; - CornerRadius = 5; - - OsuCheckbox hitObjectsCheckbox; - OsuCheckbox hitSoundsCheckbox; - OsuCheckbox waveformCheckbox; - InternalChildren = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = OsuColour.FromHex("111") - }, - new FillFlowContainer - { - RelativeSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Children = new Drawable[] - { - new Container - { - AutoSizeAxes = Axes.X, - RelativeSizeAxes = Axes.Y, - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = OsuColour.FromHex("222") - }, - new FillFlowContainer - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - AutoSizeAxes = Axes.Y, - Width = 160, - Padding = new MarginPadding { Horizontal = 15 }, - Direction = FillDirection.Vertical, - Spacing = new Vector2(0, 4), - Children = new[] - { - hitObjectsCheckbox = new OsuCheckbox { LabelText = "Hitobjects" }, - hitSoundsCheckbox = new OsuCheckbox { LabelText = "Hitsounds" }, - waveformCheckbox = new OsuCheckbox { LabelText = "Waveform" } - } - } - } - }, - new Container - { - AutoSizeAxes = Axes.X, - RelativeSizeAxes = Axes.Y, - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = OsuColour.FromHex("333") - }, - new Container - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - RelativeSizeAxes = Axes.Y, - AutoSizeAxes = Axes.X, - Masking = true, - Children = new[] - { - new TimelineButton - { - RelativeSizeAxes = Axes.Y, - Height = 0.5f, - Icon = FontAwesome.fa_search_plus, - Action = () => timelineContainer.Zoom++ - }, - new TimelineButton - { - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - RelativeSizeAxes = Axes.Y, - Height = 0.5f, - Icon = FontAwesome.fa_search_minus, - Action = () => timelineContainer.Zoom-- - }, - } - } - } - }, - timelineContainer = new ScrollingTimelineContainer { RelativeSizeAxes = Axes.Y } - } - } - }; - - hitObjectsCheckbox.Current.Value = true; - hitSoundsCheckbox.Current.Value = true; - waveformCheckbox.Current.Value = true; - - timelineContainer.WaveformVisible.BindTo(waveformCheckbox.Current); - } - - protected override void Update() - { - base.Update(); - - timelineContainer.Size = new Vector2(DrawSize.X - timelineContainer.DrawPosition.X, 1); - } - } -} diff --git a/osu.Game/Screens/Edit/Screens/Compose/Timeline/ScrollingTimelineContainer.cs b/osu.Game/Screens/Edit/Screens/Compose/Timeline/ScrollingTimelineContainer.cs deleted file mode 100644 index cc51ae1096..0000000000 --- a/osu.Game/Screens/Edit/Screens/Compose/Timeline/ScrollingTimelineContainer.cs +++ /dev/null @@ -1,151 +0,0 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE - -using System; -using osu.Framework.Allocation; -using OpenTK; -using osu.Framework.Configuration; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Input; -using osu.Game.Beatmaps; -using osu.Game.Graphics; - -namespace osu.Game.Screens.Edit.Screens.Compose.Timeline -{ - public class ScrollingTimelineContainer : ScrollContainer - { - public readonly Bindable HitObjectsVisible = new Bindable(); - public readonly Bindable HitSoundsVisible = new Bindable(); - public readonly Bindable WaveformVisible = new Bindable(); - - private readonly IBindable beatmap = new Bindable(); - - private readonly BeatmapWaveformGraph waveform; - - public ScrollingTimelineContainer() - : base(Direction.Horizontal) - { - Masking = true; - - Add(waveform = new BeatmapWaveformGraph - { - RelativeSizeAxes = Axes.Both, - Colour = OsuColour.FromHex("222"), - Depth = float.MaxValue - }); - - Content.AutoSizeAxes = Axes.None; - Content.RelativeSizeAxes = Axes.Both; - - WaveformVisible.ValueChanged += waveformVisibilityChanged; - - Zoom = 10; - } - - [BackgroundDependencyLoader] - private void load(IBindableBeatmap beatmap) - { - this.beatmap.BindTo(beatmap); - this.beatmap.BindValueChanged(beatmapChanged, true); - } - - private void beatmapChanged(WorkingBeatmap beatmap) => waveform.Beatmap = beatmap; - - private float minZoom = 1; - /// - /// The minimum zoom level allowed. - /// - public float MinZoom - { - get { return minZoom; } - set - { - if (value <= 0) - throw new ArgumentOutOfRangeException(nameof(value)); - if (minZoom == value) - return; - minZoom = value; - - // Update the zoom level - Zoom = Zoom; - } - } - - private float maxZoom = 30; - /// - /// The maximum zoom level allowed. - /// - public float MaxZoom - { - get { return maxZoom; } - set - { - if (value <= 0) - throw new ArgumentOutOfRangeException(nameof(value)); - if (maxZoom == value) - return; - maxZoom = value; - - // Update the zoom level - Zoom = Zoom; - } - } - - private float zoom = 1; - /// - /// The current zoom level. - /// - public float Zoom - { - get { return zoom; } - set - { - value = MathHelper.Clamp(value, MinZoom, MaxZoom); - if (zoom == value) - return; - zoom = value; - - // Make the zoom target default to the center of the graph if it hasn't been set - if (relativeContentZoomTarget == null) - relativeContentZoomTarget = ToSpaceOfOtherDrawable(DrawSize / 2, Content).X / Content.DrawSize.X; - if (localZoomTarget == null) - localZoomTarget = DrawSize.X / 2; - - Content.ResizeWidthTo(Zoom); - - // Update the scroll position to focus on the zoom target - float scrollPos = Content.DrawSize.X * relativeContentZoomTarget.Value - localZoomTarget.Value; - ScrollTo(scrollPos, false); - - relativeContentZoomTarget = null; - localZoomTarget = null; - } - } - - /// - /// Zoom target as a relative position in the space. - /// - private float? relativeContentZoomTarget; - - /// - /// Zoom target as a position in our local space. - /// - private float? localZoomTarget; - - protected override bool OnScroll(InputState state) - { - if (!state.Keyboard.ControlPressed) - return base.OnScroll(state); - - relativeContentZoomTarget = Content.ToLocalSpace(state.Mouse.NativeState.Position).X / Content.DrawSize.X; - localZoomTarget = ToLocalSpace(state.Mouse.NativeState.Position).X; - - Zoom += state.Mouse.ScrollDelta.Y; - - return true; - } - - private void waveformVisibilityChanged(bool visible) => waveform.FadeTo(visible ? 1 : 0, 200, Easing.OutQuint); - } -} diff --git a/osu.Game/Screens/Edit/Screens/Compose/Timeline/Timeline.cs b/osu.Game/Screens/Edit/Screens/Compose/Timeline/Timeline.cs new file mode 100644 index 0000000000..3649b24cd0 --- /dev/null +++ b/osu.Game/Screens/Edit/Screens/Compose/Timeline/Timeline.cs @@ -0,0 +1,57 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Framework.Allocation; +using osu.Framework.Configuration; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Audio; +using osu.Game.Beatmaps; +using osu.Game.Graphics; + +namespace osu.Game.Screens.Edit.Screens.Compose.Timeline +{ + public class Timeline : ZoomableScrollContainer + { + public readonly Bindable WaveformVisible = new Bindable(); + public readonly IBindable Beatmap = new Bindable(); + + public Timeline() + { + ZoomDuration = 200; + ZoomEasing = Easing.OutQuint; + Zoom = 10; + } + + private WaveformGraph waveform; + + [BackgroundDependencyLoader] + private void load(IBindableBeatmap beatmap) + { + Child = waveform = new WaveformGraph + { + RelativeSizeAxes = Axes.Both, + Colour = OsuColour.FromHex("222"), + Depth = float.MaxValue + }; + + WaveformVisible.ValueChanged += visible => waveform.FadeTo(visible ? 1 : 0, 200, Easing.OutQuint); + + Beatmap.BindTo(beatmap); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + Beatmap.BindValueChanged(b => waveform.Waveform = b.Waveform); + waveform.Waveform = Beatmap.Value.Waveform; + } + + protected override void Update() + { + base.Update(); + + // We want time = 0 to be at the centre of the container when scrolled to the start + Content.Margin = new MarginPadding { Horizontal = DrawWidth / 2 }; + } + } +} diff --git a/osu.Game/Screens/Edit/Screens/Compose/Timeline/TimelineArea.cs b/osu.Game/Screens/Edit/Screens/Compose/Timeline/TimelineArea.cs new file mode 100644 index 0000000000..006317e57e --- /dev/null +++ b/osu.Game/Screens/Edit/Screens/Compose/Timeline/TimelineArea.cs @@ -0,0 +1,128 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using OpenTK; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Graphics; +using osu.Game.Graphics.UserInterface; + +namespace osu.Game.Screens.Edit.Screens.Compose.Timeline +{ + public class TimelineArea : CompositeDrawable + { + private readonly Timeline timeline; + + public TimelineArea() + { + Masking = true; + CornerRadius = 5; + + OsuCheckbox hitObjectsCheckbox; + OsuCheckbox hitSoundsCheckbox; + OsuCheckbox waveformCheckbox; + + InternalChildren = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = OsuColour.FromHex("111") + }, + new GridContainer + { + RelativeSizeAxes = Axes.Both, + Content = new[] + { + new Drawable[] + { + new Container + { + RelativeSizeAxes = Axes.Y, + AutoSizeAxes = Axes.X, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = OsuColour.FromHex("222") + }, + new FillFlowContainer + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + AutoSizeAxes = Axes.Y, + Width = 160, + Padding = new MarginPadding { Horizontal = 15 }, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0, 4), + Children = new[] + { + hitObjectsCheckbox = new OsuCheckbox { LabelText = "Hitobjects" }, + hitSoundsCheckbox = new OsuCheckbox { LabelText = "Hitsounds" }, + waveformCheckbox = new OsuCheckbox { LabelText = "Waveform" } + } + } + } + }, + new Container + { + RelativeSizeAxes = Axes.Y, + AutoSizeAxes = Axes.X, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = OsuColour.FromHex("333") + }, + new Container + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + RelativeSizeAxes = Axes.Y, + AutoSizeAxes = Axes.X, + Masking = true, + Children = new[] + { + new TimelineButton + { + RelativeSizeAxes = Axes.Y, + Height = 0.5f, + Icon = FontAwesome.fa_search_plus, + Action = () => timeline.Zoom++ + }, + new TimelineButton + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + RelativeSizeAxes = Axes.Y, + Height = 0.5f, + Icon = FontAwesome.fa_search_minus, + Action = () => timeline.Zoom-- + }, + } + } + } + }, + timeline = new Timeline { RelativeSizeAxes = Axes.Both } + }, + }, + ColumnDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize), + new Dimension(GridSizeMode.AutoSize), + new Dimension(GridSizeMode.Distributed), + } + } + }; + + hitObjectsCheckbox.Current.Value = true; + hitSoundsCheckbox.Current.Value = true; + waveformCheckbox.Current.Value = true; + + timeline.WaveformVisible.BindTo(waveformCheckbox.Current); + } + } +} diff --git a/osu.Game/Screens/Edit/Screens/Compose/Timeline/ZoomableScrollContainer.cs b/osu.Game/Screens/Edit/Screens/Compose/Timeline/ZoomableScrollContainer.cs new file mode 100644 index 0000000000..035e6a0804 --- /dev/null +++ b/osu.Game/Screens/Edit/Screens/Compose/Timeline/ZoomableScrollContainer.cs @@ -0,0 +1,174 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Transforms; +using osu.Framework.Input; +using osu.Framework.MathUtils; +using OpenTK; + +namespace osu.Game.Screens.Edit.Screens.Compose.Timeline +{ + public class ZoomableScrollContainer : ScrollContainer + { + /// + /// The time to zoom into/out of a point. + /// All user scroll input will be overwritten during the zoom transform. + /// + public double ZoomDuration; + + /// + /// The easing with which to transform the zoom. + /// + public Easing ZoomEasing; + + private readonly Container zoomedContent; + protected override Container Content => zoomedContent; + + private float currentZoom = 1; + + public ZoomableScrollContainer() + : base(Direction.Horizontal) + { + base.Content.Add(zoomedContent = new Container { RelativeSizeAxes = Axes.Y }); + } + + private int minZoom = 1; + + /// + /// The minimum zoom level allowed. + /// + public int MinZoom + { + get => minZoom; + set + { + if (value < 1) + throw new ArgumentException($"{nameof(MinZoom)} must be >= 1.", nameof(value)); + minZoom = value; + + if (Zoom < value) + Zoom = value; + } + } + + private int maxZoom = 60; + + /// + /// The maximum zoom level allowed. + /// + public int MaxZoom + { + get => maxZoom; + set + { + if (value < 1) + throw new ArgumentException($"{nameof(MaxZoom)} must be >= 1.", nameof(value)); + maxZoom = value; + + if (Zoom > value) + Zoom = value; + } + } + + /// + /// Gets or sets the content zoom level of this . + /// + public float Zoom + { + get => zoomTarget; + set + { + value = MathHelper.Clamp(value, MinZoom, MaxZoom); + + if (IsLoaded) + setZoomTarget(value, ToSpaceOfOtherDrawable(new Vector2(DrawWidth / 2, 0), zoomedContent).X); + else + currentZoom = zoomTarget = value; + } + } + + protected override void Update() + { + base.Update(); + + zoomedContent.Width = DrawWidth * currentZoom; + } + + protected override bool OnScroll(InputState state) + { + if (!state.Keyboard.ControlPressed) + return base.OnScroll(state); + + setZoomTarget(zoomTarget + state.Mouse.ScrollDelta.X, zoomedContent.ToLocalSpace(state.Mouse.NativeState.Position).X); + return true; + } + + private float zoomTarget = 1; + private void setZoomTarget(float newZoom, float focusPoint) + { + zoomTarget = MathHelper.Clamp(newZoom, MinZoom, MaxZoom); + transformZoomTo(zoomTarget, focusPoint, ZoomDuration, ZoomEasing); + } + + private void transformZoomTo(float newZoom, float focusPoint, double duration = 0, Easing easing = Easing.None) + => this.TransformTo(this.PopulateTransform(new TransformZoom(focusPoint, zoomedContent.DrawWidth, Current), newZoom, duration, easing)); + + private class TransformZoom : Transform + { + /// + /// The focus point in absolute coordinates local to the content. + /// + private readonly float focusPoint; + + /// + /// The size of the content. + /// + private readonly float contentSize; + + /// + /// The scroll offset at the start of the transform. + /// + private readonly float scrollOffset; + + /// + /// Transforms to a new value. + /// + /// The focus point in absolute coordinates local to the content. + /// The size of the content. + /// The scroll offset at the start of the transform. + public TransformZoom(float focusPoint, float contentSize, float scrollOffset) + { + this.focusPoint = focusPoint; + this.contentSize = contentSize; + this.scrollOffset = scrollOffset; + } + + public override string TargetMember => nameof(currentZoom); + + private float valueAt(double time) + { + if (time < StartTime) return StartValue; + if (time >= EndTime) return EndValue; + + return Interpolation.ValueAt(time, StartValue, EndValue, StartTime, EndTime, Easing); + } + + protected override void Apply(ZoomableScrollContainer d, double time) + { + float newZoom = valueAt(time); + + float focusOffset = focusPoint - scrollOffset; + float expectedWidth = d.DrawWidth * newZoom; + float targetOffset = expectedWidth * (focusPoint / contentSize) - focusOffset; + + d.currentZoom = newZoom; + d.ScrollTo(targetOffset, false); + } + + protected override void ReadIntoStartValue(ZoomableScrollContainer d) => StartValue = d.currentZoom; + } + } +} diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index e35b6dbe1b..d87e190352 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -18,7 +18,7 @@ - +