From 5e5ad9a8999dcb80969cb53beed10c0e63098097 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Tue, 16 May 2017 12:53:50 +0900
Subject: [PATCH 1/5] Add basic on-screen display

---
 .../Tests/TestCaseOnScreenDisplay.cs          |  47 ++++
 .../osu.Desktop.VisualTests.csproj            |   1 +
 osu.Game/OsuGame.cs                           |   1 +
 osu.Game/Overlays/OnScreenDisplay.cs          | 262 ++++++++++++++++++
 osu.Game/osu.Game.csproj                      |   1 +
 5 files changed, 312 insertions(+)
 create mode 100644 osu.Desktop.VisualTests/Tests/TestCaseOnScreenDisplay.cs
 create mode 100644 osu.Game/Overlays/OnScreenDisplay.cs

diff --git a/osu.Desktop.VisualTests/Tests/TestCaseOnScreenDisplay.cs b/osu.Desktop.VisualTests/Tests/TestCaseOnScreenDisplay.cs
new file mode 100644
index 0000000000..3cefb8a3d2
--- /dev/null
+++ b/osu.Desktop.VisualTests/Tests/TestCaseOnScreenDisplay.cs
@@ -0,0 +1,47 @@
+// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
+// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
+
+using osu.Framework.Allocation;
+using osu.Framework.Configuration;
+using osu.Framework.Testing;
+using osu.Game.Overlays;
+
+namespace osu.Desktop.VisualTests.Tests
+{
+    internal class TestCaseOnScreenDisplay : TestCase
+    {
+        private FrameworkConfigManager config;
+        private Bindable<FrameSync> frameSyncMode;
+
+        public override string Description => @"Make it easier to see setting changes";
+
+        public override void Reset()
+        {
+            base.Reset();
+
+            Add(new OnScreenDisplay());
+
+            frameSyncMode = config.GetBindable<FrameSync>(FrameworkSetting.FrameSync);
+
+            FrameSync initial = frameSyncMode.Value;
+
+            AddRepeatStep(@"Change frame limiter", setNextMode, 3);
+
+            AddStep(@"Restore frame limiter", () => frameSyncMode.Value = initial);
+        }
+
+        private void setNextMode()
+        {
+            var nextMode = frameSyncMode.Value + 1;
+            if (nextMode > FrameSync.Unlimited)
+                nextMode = FrameSync.VSync;
+            frameSyncMode.Value = nextMode;
+        }
+
+        [BackgroundDependencyLoader]
+        private void load(FrameworkConfigManager config)
+        {
+            this.config = config;
+        }
+    }
+}
diff --git a/osu.Desktop.VisualTests/osu.Desktop.VisualTests.csproj b/osu.Desktop.VisualTests/osu.Desktop.VisualTests.csproj
index 9b07ebf90b..dbb1750b72 100644
--- a/osu.Desktop.VisualTests/osu.Desktop.VisualTests.csproj
+++ b/osu.Desktop.VisualTests/osu.Desktop.VisualTests.csproj
@@ -195,6 +195,7 @@
     <Compile Include="Tests\TestCaseMenuOverlays.cs" />
     <Compile Include="Tests\TestCaseMusicController.cs" />
     <Compile Include="Tests\TestCaseNotificationManager.cs" />
+    <Compile Include="Tests\TestCaseOnScreenDisplay.cs" />
     <Compile Include="Tests\TestCasePlayer.cs" />
     <Compile Include="Tests\TestCaseHitObjects.cs" />
     <Compile Include="Tests\TestCaseKeyCounter.cs" />
diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs
index 9ce725661b..dfa94b2bfe 100644
--- a/osu.Game/OsuGame.cs
+++ b/osu.Game/OsuGame.cs
@@ -146,6 +146,7 @@ namespace osu.Game
                 },
                 volume = new VolumeControl(),
                 overlayContent = new Container{ RelativeSizeAxes = Axes.Both },
+                new OnScreenDisplay(),
                 new GlobalHotkeys //exists because UserInputManager is at a level below us.
                 {
                     Handler = globalHotkeyPressed
diff --git a/osu.Game/Overlays/OnScreenDisplay.cs b/osu.Game/Overlays/OnScreenDisplay.cs
new file mode 100644
index 0000000000..1c837eb602
--- /dev/null
+++ b/osu.Game/Overlays/OnScreenDisplay.cs
@@ -0,0 +1,262 @@
+// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
+// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using osu.Framework.Allocation;
+using osu.Framework.Configuration;
+using osu.Framework.Extensions;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Primitives;
+using osu.Framework.Graphics.Sprites;
+using osu.Game.Graphics;
+using OpenTK;
+using OpenTK.Graphics;
+using osu.Framework.Extensions.Color4Extensions;
+
+namespace osu.Game.Overlays
+{
+    public class OnScreenDisplay : Container
+    {
+        private readonly Container box;
+
+        public override bool HandleInput => false;
+
+        private readonly SpriteText textLine1;
+        private readonly SpriteText textLine2;
+        private readonly SpriteText textLine3;
+
+        private const float height = 110;
+        private const float height_contracted = height * 0.9f;
+
+        private readonly FillFlowContainer<OptionLight> optionLights;
+
+        public OnScreenDisplay()
+        {
+            RelativeSizeAxes = Axes.Both;
+
+            Children = new Drawable[]
+            {
+                box = new Container
+                {
+                    Origin = Anchor.Centre,
+                    RelativePositionAxes = Axes.Both,
+                    Position = new Vector2(0.5f, 0.75f),
+                    Masking = true,
+                    AutoSizeAxes = Axes.X,
+                    Height = height_contracted,
+                    Alpha = 0,
+                    CornerRadius = 20,
+                    Children = new Drawable[]
+                    {
+                        new Box
+                        {
+                            RelativeSizeAxes = Axes.Both,
+                            Colour = Color4.Black,
+                            Alpha = 0.7f,
+                        },
+                        new Container // purely to add a minimum width
+                        {
+                            Anchor = Anchor.Centre,
+                            Origin = Anchor.Centre,
+                            Width = 240,
+                            RelativeSizeAxes = Axes.Y,
+                        },
+                        textLine1 = new SpriteText
+                        {
+                            Padding = new MarginPadding(10),
+                            Font = @"Exo2.0-Black",
+                            Spacing = new Vector2(1, 0),
+                            TextSize = 14,
+                            Anchor = Anchor.TopCentre,
+                            Origin = Anchor.TopCentre,
+                        },
+                        textLine2 = new SpriteText
+                        {
+                            TextSize = 24,
+                            Font = @"Exo2.0-Light",
+                            Anchor = Anchor.Centre,
+                            Origin = Anchor.BottomCentre,
+                        },
+                        new FillFlowContainer
+                        {
+                            Anchor = Anchor.BottomCentre,
+                            Origin = Anchor.BottomCentre,
+                            AutoSizeAxes = Axes.Both,
+                            Direction = FillDirection.Vertical,
+                            Children = new Drawable[]
+                            {
+                                optionLights = new FillFlowContainer<OptionLight>
+                                {
+                                    Padding = new MarginPadding { Top = 20, Bottom = 5 },
+                                    Spacing = new Vector2(5, 0),
+                                    Direction = FillDirection.Horizontal,
+                                    Anchor = Anchor.TopCentre,
+                                    Origin = Anchor.TopCentre,
+                                    AutoSizeAxes = Axes.Both
+                                },
+                                textLine3 = new SpriteText
+                                {
+                                    Padding = new MarginPadding { Bottom = 15 },
+                                    Font = @"Exo2.0-Bold",
+                                    TextSize = 12,
+                                    Alpha = 0.3f,
+                                    Anchor = Anchor.TopCentre,
+                                    Origin = Anchor.TopCentre,
+                                },
+                            }
+                        }
+                    }
+                },
+            };
+        }
+
+        [BackgroundDependencyLoader]
+        private void load(FrameworkConfigManager frameworkConfig)
+        {
+            trackSetting(frameworkConfig.GetBindable<FrameSync>(FrameworkSetting.FrameSync), v => display(v, "Frame Limiter", v.GetDescription(), "Ctrl+F7"));
+            trackSetting(frameworkConfig.GetBindable<string>(FrameworkSetting.AudioDevice), v => display(v, "Audio Device", v, v));
+            trackSetting(frameworkConfig.GetBindable<bool>(FrameworkSetting.ShowLogOverlay), v => display(v, "Debug Logs", v ? "visible" : "hidden", "Ctrl+F10"));
+
+            Action displayResolution = delegate { display(null, "Screen Resolution", frameworkConfig.Get<int>(FrameworkSetting.Width) + "x" + frameworkConfig.Get<int>(FrameworkSetting.Height)); };
+
+            trackSetting(frameworkConfig.GetBindable<int>(FrameworkSetting.Width), v => displayResolution());
+            trackSetting(frameworkConfig.GetBindable<int>(FrameworkSetting.Height), v => displayResolution());
+
+            trackSetting(frameworkConfig.GetBindable<WindowMode>(FrameworkSetting.WindowMode), v => display(v, "Screen Mode", v.ToString(), "Alt+Enter"));
+        }
+
+        private readonly List<IBindable> references = new List<IBindable>();
+
+        private void trackSetting<T>(Bindable<T> bindable, Bindable<T>.BindableValueChanged<T> action)
+        {
+            // we need to keep references as we bind
+            references.Add(bindable);
+
+            bindable.ValueChanged += action;
+        }
+
+        private void display(object rawValue, string settingName, string settingValue, string shortcut = @"")
+        {
+            Schedule(() =>
+            {
+                textLine1.Text = settingName.ToUpper();
+                textLine2.Text = settingValue;
+                textLine3.Text = shortcut.ToUpper();
+
+                box.FadeIn(500, EasingTypes.OutQuint);
+                box.ResizeHeightTo(height, 500, EasingTypes.OutQuint);
+
+                using (box.BeginDelayedSequence(500))
+                {
+                    box.FadeOutFromOne(1500, EasingTypes.InQuint);
+                    box.ResizeHeightTo(height_contracted, 1500, EasingTypes.InQuint);
+                }
+
+                int optionCount = 0;
+                int selectedOption = -1;
+
+                if (rawValue is bool)
+                {
+                    optionCount = 1;
+                    if ((bool)rawValue) selectedOption = 0;
+                }
+                else if (rawValue is Enum)
+                {
+                    var values = Enum.GetValues(rawValue.GetType());
+                    optionCount = values.Length;
+                    selectedOption = Convert.ToInt32(rawValue);
+                }
+
+                textLine2.Origin = optionCount > 0 ? Anchor.BottomCentre : Anchor.Centre;
+                textLine2.Y = optionCount > 0 ? 0 : 5;
+
+                if (optionLights.Children.Count() != optionCount)
+                {
+                    optionLights.Clear();
+                    for (int i = 0; i < optionCount; i++)
+                        optionLights.Add(new OptionLight());
+                }
+
+                for (int i = 0; i < optionCount; i++)
+                    optionLights.Children.Skip(i).First().Glowing = i == selectedOption;
+            });
+        }
+
+        private class OptionLight : Container
+        {
+            private Color4 glowingColour, idleColour;
+
+            private const float transition_speed = 300;
+
+            private const float glow_strength = 0.4f;
+
+            private readonly Box fill;
+
+            public OptionLight()
+            {
+                Children = new[]
+                {
+                    fill = new Box
+                    {
+                        RelativeSizeAxes = Axes.Both,
+                        Alpha = 1,
+                    },
+                };
+            }
+
+            private bool glowing;
+
+            public bool Glowing
+            {
+                set
+                {
+                    glowing = value;
+                    if (!IsLoaded) return;
+
+                    updateGlow();
+                }
+            }
+
+            private void updateGlow()
+            {
+                if (glowing)
+                {
+                    fill.FadeColour(glowingColour, transition_speed, EasingTypes.OutQuint);
+                    FadeEdgeEffectTo(glow_strength, transition_speed, EasingTypes.OutQuint);
+                }
+                else
+                {
+                    FadeEdgeEffectTo(0, transition_speed, EasingTypes.OutQuint);
+                    fill.FadeColour(idleColour, transition_speed, EasingTypes.OutQuint);
+                }
+            }
+
+            [BackgroundDependencyLoader]
+            private void load(OsuColour colours)
+            {
+                fill.Colour = idleColour = Color4.White.Opacity(0.4f);
+                glowingColour = Color4.White;
+
+                Size = new Vector2(25, 5);
+
+                Masking = true;
+                CornerRadius = 3;
+
+                EdgeEffect = new EdgeEffect
+                {
+                    Colour = colours.BlueDark.Opacity(glow_strength),
+                    Type = EdgeEffectType.Glow,
+                    Radius = 8,
+                };
+
+                FadeEdgeEffectTo(0);
+
+                updateGlow();
+                Flush(true);
+            }
+        }
+    }
+}
diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj
index 1f866f0e17..2e94b3d658 100644
--- a/osu.Game/osu.Game.csproj
+++ b/osu.Game/osu.Game.csproj
@@ -79,6 +79,7 @@
     <Compile Include="Overlays\Music\FilterControl.cs" />
     <Compile Include="Overlays\Music\PlaylistItem.cs" />
     <Compile Include="Overlays\Music\PlaylistList.cs" />
+    <Compile Include="Overlays\OnScreenDisplay.cs" />
     <Compile Include="Overlays\Settings\SettingsHeader.cs" />
     <Compile Include="Overlays\Settings\Sections\Audio\MainMenuSettings.cs" />
     <Compile Include="Overlays\Toolbar\ToolbarChatButton.cs" />

From 694b85c4b0399801c288ce9ab71c07819850c880 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Tue, 16 May 2017 16:29:18 +0900
Subject: [PATCH 2/5] Update framework

---
 osu-framework | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/osu-framework b/osu-framework
index 7146c07159..30cd231fc6 160000
--- a/osu-framework
+++ b/osu-framework
@@ -1 +1 @@
-Subproject commit 7146c07159d2cf3d07a8d371fa50ef8b200ba038
+Subproject commit 30cd231fc6b3e971c70456433dbcf03591141f28

From b016d53b85e94c1088652c97b99cdca6dfe79f74 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Tue, 16 May 2017 17:57:40 +0900
Subject: [PATCH 3/5] Fix TwoLayerButton not capturing MouseDown events

Caused clicks to pass through the skip button to the progress bar.
---
 osu.Game/Graphics/UserInterface/TwoLayerButton.cs | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/osu.Game/Graphics/UserInterface/TwoLayerButton.cs b/osu.Game/Graphics/UserInterface/TwoLayerButton.cs
index 601970e36d..237aaa44a6 100644
--- a/osu.Game/Graphics/UserInterface/TwoLayerButton.cs
+++ b/osu.Game/Graphics/UserInterface/TwoLayerButton.cs
@@ -216,6 +216,11 @@ namespace osu.Game.Graphics.UserInterface
                 });
         }
 
+        protected override bool OnMouseDown(InputState state, MouseDownEventArgs args)
+        {
+            return true;
+        }
+
         protected override bool OnClick(InputState state)
         {
             var flash = new Box

From 6487bf45cff7f69e61cedb5718ee88b1317d82c8 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Tue, 16 May 2017 17:55:35 +0900
Subject: [PATCH 4/5] Eagerly attempt to pause the game when the window is not
 focused

---
 osu.Game/Screens/Play/Player.cs | 9 +++++++++
 1 file changed, 9 insertions(+)

diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs
index de390b63b3..8ee0de12b9 100644
--- a/osu.Game/Screens/Play/Player.cs
+++ b/osu.Game/Screens/Play/Player.cs
@@ -224,6 +224,15 @@ namespace osu.Game.Screens.Play
             };
         }
 
+        protected override void Update()
+        {
+            // eagerly pause when we lose window focus (if we are locally playing).
+            if (!Game.IsActive && !HitRenderer.HasReplayLoaded)
+                Pause();
+
+            base.Update();
+        }
+
         private void initializeSkipButton()
         {
             const double skip_required_cutoff = 3000;

From 712bd21e2524b90bc253ee8fb3f1d6cb345f70fc Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Tue, 16 May 2017 18:25:03 +0900
Subject: [PATCH 5/5] Fix duplicate channels being created on connection loss

Resolves #763
---
 osu.Game/Overlays/ChatOverlay.cs | 17 ++++++++++++++---
 1 file changed, 14 insertions(+), 3 deletions(-)

diff --git a/osu.Game/Overlays/ChatOverlay.cs b/osu.Game/Overlays/ChatOverlay.cs
index e6dffbfe63..4748eb7de6 100644
--- a/osu.Game/Overlays/ChatOverlay.cs
+++ b/osu.Game/Overlays/ChatOverlay.cs
@@ -284,10 +284,21 @@ namespace osu.Game.Overlays
         {
             if (channel == null) return;
 
-            careChannels.Add(channel);
-            channelTabs.AddItem(channel);
+            var existing = careChannels.Find(c => c.Id == channel.Id);
 
-            // we need to get a good number of messages initially for each channel we care about.
+            if (existing != null)
+            {
+                // if we already have this channel loaded, we don't want to make a second one.
+                channel = existing;
+            }
+            else
+            {
+
+                careChannels.Add(channel);
+                channelTabs.AddItem(channel);
+            }
+
+            // let's fetch a small number of messages to bring us up-to-date with the backlog.
             fetchInitialMessages(channel);
 
             if (CurrentChannel == null)