From 8ec927899f3d04dfb0791b0d6c69ef490d5a21f3 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Fri, 10 Feb 2017 16:26:43 +0900
Subject: [PATCH 1/3] Implement notifications.

---
 .../Tests/TestCaseNotificationManager.cs      | 121 ++++++++
 .../osu.Desktop.VisualTests.csproj            |   1 +
 osu.Game/Graphics/Sprites/OsuSpriteText.cs    |   2 +-
 osu.Game/Graphics/UserInterface/Nub.cs        |   2 +-
 osu.Game/OsuGame.cs                           |  10 +
 osu.Game/Overlays/NotificationManager.cs      | 111 +++++++
 .../Notifications/IHasCompletionTarget.cs     |  12 +
 .../Overlays/Notifications/Notification.cs    | 285 ++++++++++++++++++
 .../Notifications/NotificationSection.cs      | 163 ++++++++++
 .../ProgressCompletionNotification.cs         |  16 +
 .../Notifications/ProgressNotification.cs     | 198 ++++++++++++
 .../Notifications/SimpleNotification.cs       |  66 ++++
 osu.Game/Overlays/OptionsOverlay.cs           |   2 +-
 osu.Game/Overlays/Toolbar/Toolbar.cs          |   6 +-
 osu.Game/Overlays/Toolbar/ToolbarButton.cs    |  11 +-
 .../Toolbar/ToolbarNotificationButton.cs      |  28 ++
 osu.Game/osu.Game.csproj                      |   8 +
 17 files changed, 1033 insertions(+), 9 deletions(-)
 create mode 100644 osu.Desktop.VisualTests/Tests/TestCaseNotificationManager.cs
 create mode 100644 osu.Game/Overlays/NotificationManager.cs
 create mode 100644 osu.Game/Overlays/Notifications/IHasCompletionTarget.cs
 create mode 100644 osu.Game/Overlays/Notifications/Notification.cs
 create mode 100644 osu.Game/Overlays/Notifications/NotificationSection.cs
 create mode 100644 osu.Game/Overlays/Notifications/ProgressCompletionNotification.cs
 create mode 100644 osu.Game/Overlays/Notifications/ProgressNotification.cs
 create mode 100644 osu.Game/Overlays/Notifications/SimpleNotification.cs
 create mode 100644 osu.Game/Overlays/Toolbar/ToolbarNotificationButton.cs

diff --git a/osu.Desktop.VisualTests/Tests/TestCaseNotificationManager.cs b/osu.Desktop.VisualTests/Tests/TestCaseNotificationManager.cs
new file mode 100644
index 0000000000..cc9201c1fd
--- /dev/null
+++ b/osu.Desktop.VisualTests/Tests/TestCaseNotificationManager.cs
@@ -0,0 +1,121 @@
+// 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.Collections.Generic;
+using osu.Framework.Graphics;
+using osu.Framework.GameModes.Testing;
+using osu.Framework.MathUtils;
+using osu.Framework.Timing;
+using osu.Game.Overlays;
+using System.Linq;
+using osu.Game.Overlays.Notifications;
+using osu.Game.Screens.Backgrounds;
+
+namespace osu.Desktop.VisualTests.Tests
+{
+    class TestCaseNotificationManager : TestCase
+    {
+        public override string Name => @"Notification Manager";
+        public override string Description => @"I handle notifications";
+
+        NotificationManager manager;
+
+        public override void Reset()
+        {
+            base.Reset();
+
+            progressingNotifications.Clear();
+
+            AddInternal(new BackgroundModeDefault() { Depth = 10 });
+
+            Content.Add(manager = new NotificationManager
+            {
+                Anchor = Anchor.TopRight,
+                Origin = Anchor.TopRight,
+            });
+
+            AddToggle(@"show", manager.ToggleVisibility);
+
+            AddButton(@"simple #1", sendNotification1);
+            AddButton(@"simple #2", sendNotification2);
+            AddButton(@"progress #1", sendProgress1);
+            AddButton(@"progress #2", sendProgress2);
+            AddButton(@"barrage", () => sendBarrage());
+        }
+
+        private void sendBarrage(int remaining = 100)
+        {
+            switch (RNG.Next(0, 4))
+            {
+                case 0:
+                    sendNotification1();
+                    break;
+                case 1:
+                    sendNotification2();
+                    break;
+                case 2:
+                    sendProgress1();
+                    break;
+                case 3:
+                    sendProgress2();
+                    break;
+            }
+
+            if (remaining > 0)
+            {
+                Delay(80);
+                Schedule(() => sendBarrage(remaining - 1));
+            }
+        }
+
+        protected override void Update()
+        {
+            base.Update();
+
+            progressingNotifications.RemoveAll(n => n.State == ProgressNotificationState.Completed);
+
+            while (progressingNotifications.Count(n => n.State == ProgressNotificationState.Active) < 3)
+            {
+                var p = progressingNotifications.FirstOrDefault(n => n.IsLoaded && n.State == ProgressNotificationState.Queued);
+                if (p == null)
+                    break;
+
+                p.State = ProgressNotificationState.Active;
+            }
+
+            foreach (var n in progressingNotifications.FindAll(n => n.State == ProgressNotificationState.Active))
+            {
+                if (n.Progress < 1)
+                    n.Progress += (float)(Time.Elapsed / 2000) * RNG.NextSingle();
+                else
+                    n.Complete();
+            }
+        }
+
+        private void sendProgress2()
+        {
+            var n = new ProgressNotification(@"Downloading Haitai...");
+            manager.Post(n);
+            progressingNotifications.Add(n);
+        }
+
+        List<ProgressNotification> progressingNotifications = new List<ProgressNotification>();
+
+        private void sendProgress1()
+        {
+            var n = new ProgressNotification(@"Uploading to BSS...");
+            manager.Post(n);
+            progressingNotifications.Add(n);
+        }
+
+        private void sendNotification2()
+        {
+            manager.Post(new SimpleNotification(@"You are amazing"));
+        }
+
+        private void sendNotification1()
+        {
+            manager.Post(new SimpleNotification(@"Welcome to osu!. Enjoy your stay!"));
+        }
+    }
+}
diff --git a/osu.Desktop.VisualTests/osu.Desktop.VisualTests.csproj b/osu.Desktop.VisualTests/osu.Desktop.VisualTests.csproj
index 69df007013..6fa9ab604e 100644
--- a/osu.Desktop.VisualTests/osu.Desktop.VisualTests.csproj
+++ b/osu.Desktop.VisualTests/osu.Desktop.VisualTests.csproj
@@ -176,6 +176,7 @@
     <Compile Include="Tests\TestCaseChatDisplay.cs" />
     <Compile Include="Tests\TestCaseGamefield.cs" />
     <Compile Include="Tests\TestCaseMusicController.cs" />
+    <Compile Include="Tests\TestCaseNotificationManager.cs" />
     <Compile Include="Tests\TestCasePlayer.cs" />
     <Compile Include="Tests\TestCaseHitObjects.cs" />
     <Compile Include="Tests\TestCaseKeyCounter.cs" />
diff --git a/osu.Game/Graphics/Sprites/OsuSpriteText.cs b/osu.Game/Graphics/Sprites/OsuSpriteText.cs
index 5bc76b74b4..f5749846be 100644
--- a/osu.Game/Graphics/Sprites/OsuSpriteText.cs
+++ b/osu.Game/Graphics/Sprites/OsuSpriteText.cs
@@ -9,7 +9,7 @@ using OpenTK.Graphics;
 
 namespace osu.Game.Graphics.Sprites
 {
-    class OsuSpriteText : SpriteText
+    public class OsuSpriteText : SpriteText
     {
         public const float FONT_SIZE = 16;
 
diff --git a/osu.Game/Graphics/UserInterface/Nub.cs b/osu.Game/Graphics/UserInterface/Nub.cs
index c8174ef546..61b59d6b51 100644
--- a/osu.Game/Graphics/UserInterface/Nub.cs
+++ b/osu.Game/Graphics/UserInterface/Nub.cs
@@ -13,7 +13,7 @@ using osu.Framework.Graphics.UserInterface;
 
 namespace osu.Game.Graphics.UserInterface
 {
-    class Nub : CircularContainer, IStateful<CheckBoxState>
+    public class Nub : CircularContainer, IStateful<CheckBoxState>
     {
         public const float COLLAPSED_SIZE = 20;
         public const float EXPANDED_SIZE = 40;
diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs
index 97c9dbb2e0..e8233366d9 100644
--- a/osu.Game/OsuGame.cs
+++ b/osu.Game/OsuGame.cs
@@ -35,6 +35,8 @@ namespace osu.Game
 
         private MusicController musicController;
 
+        private NotificationManager notificationManager;
+
         private MainMenu mainMenu => modeStack?.ChildGameMode as MainMenu;
         private Intro intro => modeStack as Intro;
 
@@ -117,8 +119,16 @@ namespace osu.Game
                 Origin = Anchor.TopRight,
             }).Preload(this, overlayContent.Add);
 
+            (notificationManager = new NotificationManager
+            {
+                Depth = -2,
+                Anchor = Anchor.TopRight,
+                Origin = Anchor.TopRight,
+            }).Preload(this, overlayContent.Add);
+
             Dependencies.Cache(options);
             Dependencies.Cache(musicController);
+            Dependencies.Cache(notificationManager);
 
             (Toolbar = new Toolbar
             {
diff --git a/osu.Game/Overlays/NotificationManager.cs b/osu.Game/Overlays/NotificationManager.cs
new file mode 100644
index 0000000000..e4eb718639
--- /dev/null
+++ b/osu.Game/Overlays/NotificationManager.cs
@@ -0,0 +1,111 @@
+// 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.Linq;
+using osu.Framework.Allocation;
+using osu.Framework.Extensions.IEnumerableExtensions;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Primitives;
+using osu.Framework.Graphics.Sprites;
+using osu.Framework.Graphics.Transformations;
+using osu.Game.Graphics;
+using osu.Game.Overlays.Notifications;
+using OpenTK.Graphics;
+
+namespace osu.Game.Overlays
+{
+    public class NotificationManager : FocusedOverlayContainer
+    {
+        private const float width = 320;
+
+        public const float TRANSITION_LENGTH = 600;
+
+        private ScrollContainer scrollContainer;
+        private FlowContainer<NotificationSection> sections;
+
+        [BackgroundDependencyLoader(permitNulls: true)]
+        private void load(OsuColour colours)
+        {
+            Width = width;
+            RelativeSizeAxes = Axes.Y;
+
+            Children = new Drawable[]
+            {
+                new Box
+                {
+                    RelativeSizeAxes = Axes.Both,
+                    Colour = Color4.Black,
+                    Alpha = 0.6f,
+                },
+                scrollContainer = new ScrollContainer()
+                {
+                    Margin = new MarginPadding { Top = Toolbar.Toolbar.HEIGHT },
+                    Children = new[]
+                    {
+                        sections = new FlowContainer<NotificationSection>
+                        {
+                            Direction = FlowDirection.VerticalOnly,
+                            AutoSizeAxes = Axes.Y,
+                            RelativeSizeAxes = Axes.X,
+                            Children = new []
+                            {
+                                new NotificationSection
+                                {
+                                    Title = @"Notifications",
+                                    ClearText = @"Clear All",
+                                    AcceptTypes = new [] { typeof(SimpleNotification) },
+                                },
+                                new NotificationSection
+                                {
+                                    Title = @"Running Tasks",
+                                    ClearText = @"Cancel All",
+                                    AcceptTypes = new [] { typeof(ProgressNotification) },
+                                },
+                            }
+                        }
+                    }
+                }
+            };
+        }
+
+        int runningDepth = 0;
+
+        public void Post(Notification notification)
+        {
+            ++runningDepth;
+            notification.Depth = notification.DisplayOnTop ? runningDepth : -runningDepth;
+
+            var hasCompletionTarget = notification as IHasCompletionTarget;
+            if (hasCompletionTarget != null)
+                hasCompletionTarget.CompletionTarget = Post;
+
+            var ourType = notification.GetType();
+            sections.Children.FirstOrDefault(s => s.AcceptTypes.Any(accept => ourType == accept || ourType.IsSubclassOf(accept)))?.Add(notification);
+        }
+
+        protected override void PopIn()
+        {
+            base.PopIn();
+
+            scrollContainer.MoveToX(0, TRANSITION_LENGTH, EasingTypes.OutQuint);
+            MoveToX(0, TRANSITION_LENGTH, EasingTypes.OutQuint);
+            FadeTo(1, TRANSITION_LENGTH / 2);
+        }
+
+        private void markAllRead()
+        {
+            sections.Children.ForEach(s => s.MarkAllRead());
+        }
+
+        protected override void PopOut()
+        {
+            base.PopOut();
+
+            markAllRead();
+
+            MoveToX(width, TRANSITION_LENGTH, EasingTypes.OutQuint);
+            FadeTo(0, TRANSITION_LENGTH / 2);
+        }
+    }
+}
\ No newline at end of file
diff --git a/osu.Game/Overlays/Notifications/IHasCompletionTarget.cs b/osu.Game/Overlays/Notifications/IHasCompletionTarget.cs
new file mode 100644
index 0000000000..7fba75c4fc
--- /dev/null
+++ b/osu.Game/Overlays/Notifications/IHasCompletionTarget.cs
@@ -0,0 +1,12 @@
+// 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;
+
+namespace osu.Game.Overlays.Notifications
+{
+    public interface IHasCompletionTarget
+    {
+        Action<Notification> CompletionTarget { get; set; }
+    }
+}
\ No newline at end of file
diff --git a/osu.Game/Overlays/Notifications/Notification.cs b/osu.Game/Overlays/Notifications/Notification.cs
new file mode 100644
index 0000000000..9270e4e792
--- /dev/null
+++ b/osu.Game/Overlays/Notifications/Notification.cs
@@ -0,0 +1,285 @@
+// 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 osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Colour;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Primitives;
+using osu.Framework.Graphics.Sprites;
+using osu.Framework.Graphics.Transformations;
+using osu.Framework.Input;
+using osu.Game.Graphics;
+using OpenTK;
+using OpenTK.Graphics;
+
+namespace osu.Game.Overlays.Notifications
+{
+    public abstract class Notification : Container
+    {
+        /// <summary>
+        /// Use requested close.
+        /// </summary>
+        public Action Closed;
+
+        /// <summary>
+        /// Run on user activating the notification. Return true to close.
+        /// </summary>
+        public Func<bool> Activated;
+
+        /// <summary>
+        /// Should we show at the top of our section on display?
+        /// </summary>
+        public virtual bool DisplayOnTop => true;
+
+        protected NotificationLight Light;
+        private CloseButton closeButton;
+        protected Container IconContent;
+        private Container content;
+
+        protected override Container<Drawable> Content => content;
+
+        protected Container NotificationContent;
+
+        private bool read;
+
+        public virtual bool Read
+        {
+            get { return read; }
+            set
+            {
+                read = value;
+            }
+        }
+
+        [BackgroundDependencyLoader]
+        private void load(OsuColour colours)
+        {
+            RelativeSizeAxes = Axes.X;
+            AutoSizeAxes = Axes.Y;
+
+            AddInternal(new Drawable[]
+            {
+                Light = new NotificationLight
+                {
+                    Margin = new MarginPadding { Right = 5 },
+                    Anchor = Anchor.CentreLeft,
+                    Origin = Anchor.CentreRight,
+                },
+                NotificationContent = new Container
+                {
+                    CornerRadius = 8,
+                    Masking = true,
+                    RelativeSizeAxes = Axes.X,
+                    AutoSizeAxes = Axes.Y,
+                    Children = new Drawable[]
+                    {
+                        new Box
+                        {
+                            RelativeSizeAxes = Axes.Both,
+                            Colour = Color4.White,
+                        },
+                        new Container
+                        {
+                            RelativeSizeAxes = Axes.X,
+                            Padding = new MarginPadding(5),
+                            Height = 60,
+                            Children = new Drawable[]
+                            {
+                                IconContent = new Container
+                                {
+                                    Size = new Vector2(40),
+                                    Colour = Color4.DarkGray,
+                                    Masking = true,
+                                    CornerRadius = 5,
+                                },
+                                content = new Container
+                                {
+                                    RelativeSizeAxes = Axes.X,
+                                    AutoSizeAxes = Axes.Y,
+                                    Padding = new MarginPadding
+                                    {
+                                        Top = 5,
+                                        Left = 45,
+                                        Right = 30
+                                    },
+                                }
+                            }
+                        },
+                        closeButton = new CloseButton
+                        {
+                            Alpha = 0,
+                            Action = Close,
+                            Anchor = Anchor.CentreRight,
+                            Origin = Anchor.CentreRight,
+                            Margin = new MarginPadding
+                            {
+                                Right = 5
+                            },
+                        }
+                    }
+                }
+            });
+        }
+
+        protected override bool OnHover(InputState state)
+        {
+            closeButton.FadeIn(75);
+            return base.OnHover(state);
+        }
+
+        protected override void OnHoverLost(InputState state)
+        {
+            closeButton.FadeOut(75);
+            base.OnHoverLost(state);
+        }
+
+        protected override bool OnClick(InputState state)
+        {
+            if (Activated?.Invoke() ?? true)
+                Close();
+
+            return true;
+        }
+
+        protected override void LoadComplete()
+        {
+            base.LoadComplete();
+            FadeInFromZero(200);
+            NotificationContent.MoveToX(DrawSize.X);
+            NotificationContent.MoveToX(0, 500, EasingTypes.OutQuint);
+        }
+
+        private bool wasClosed;
+
+        public virtual void Close()
+        {
+            if (wasClosed) return;
+            wasClosed = true;
+
+            Closed?.Invoke();
+            FadeOut(100);
+            Expire();
+        }
+
+        class CloseButton : ClickableContainer
+        {
+            private Color4 hoverColour;
+
+            public CloseButton()
+            {
+                Colour = OsuColour.Gray(0.2f);
+                AutoSizeAxes = Axes.Both;
+
+                Children = new[]
+                {
+                    new TextAwesome
+                    {
+                        Anchor = Anchor.Centre,
+                        Icon = FontAwesome.fa_times_circle,
+                    }
+                };
+            }
+
+            [BackgroundDependencyLoader]
+            private void load(OsuColour colours)
+            {
+                hoverColour = colours.Yellow;
+            }
+
+            protected override bool OnHover(InputState state)
+            {
+                FadeColour(hoverColour, 200);
+                return base.OnHover(state);
+            }
+
+            protected override void OnHoverLost(InputState state)
+            {
+                FadeColour(OsuColour.Gray(0.2f), 200);
+                base.OnHoverLost(state);
+            }
+        }
+
+        public class NotificationLight : Container
+        {
+            private bool pulsate;
+            private Container pulsateLayer;
+
+            public bool Pulsate
+            {
+                get { return pulsate; }
+                set
+                {
+                    pulsate = value;
+
+                    pulsateLayer.ClearTransformations();
+                    pulsateLayer.Alpha = 1;
+
+                    if (pulsate)
+                    {
+                        const float length = 1000;
+                        pulsateLayer.Transforms.Add(new TransformAlpha
+                        {
+                            StartTime = Time.Current,
+                            EndTime = Time.Current + length,
+                            StartValue = 1,
+                            EndValue = 0.4f,
+                            Easing = EasingTypes.In
+                        });
+                        pulsateLayer.Transforms.Add(new TransformAlpha
+                        {
+                            StartTime = Time.Current + length,
+                            EndTime = Time.Current + length * 2,
+                            StartValue = 0.4f,
+                            EndValue = 1,
+                            Easing = EasingTypes.Out
+                        });
+
+                        //todo: figure why we can't add arbitrary delays at the end of loop.
+                        pulsateLayer.Loop(length * 2);
+                    }
+                }
+            }
+
+            public new SRGBColour Colour
+            {
+                set
+                {
+                    base.Colour = value;
+                    pulsateLayer.EdgeEffect = new EdgeEffect
+                    {
+                        Colour = ((Color4)value).Opacity(0.5f), //todo: avoid cast
+                        Type = EdgeEffectType.Glow,
+                        Radius = 12,
+                        Roundness = 12,
+                    };
+                }
+            }
+
+            [BackgroundDependencyLoader]
+            private void load(OsuColour colours)
+            {
+                Size = new Vector2(6, 15);
+
+                Children = new[]
+                {
+                    pulsateLayer = new CircularContainer
+                    {
+                        Anchor = Anchor.CentreLeft,
+                        Origin = Anchor.CentreLeft,
+                        Masking = true,
+                        RelativeSizeAxes = Axes.Both,
+                        Children = new[]
+                        {
+                            new Box
+                            {
+                                RelativeSizeAxes = Axes.Both,
+                            },
+                        }
+                    }
+                };
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/osu.Game/Overlays/Notifications/NotificationSection.cs b/osu.Game/Overlays/Notifications/NotificationSection.cs
new file mode 100644
index 0000000000..526b568c12
--- /dev/null
+++ b/osu.Game/Overlays/Notifications/NotificationSection.cs
@@ -0,0 +1,163 @@
+// 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.Extensions.IEnumerableExtensions;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Primitives;
+using osu.Framework.Graphics.Transformations;
+using osu.Game.Graphics;
+using osu.Game.Graphics.Sprites;
+using OpenTK;
+
+namespace osu.Game.Overlays.Notifications
+{
+    public class NotificationSection : FlowContainer
+    {
+        private OsuSpriteText titleText;
+        private OsuSpriteText countText;
+
+        private ClearAllButton clearButton;
+
+        private FlowContainer<Notification> notifications;
+
+        public void Add(Notification notification)
+        {
+            notifications.Add(notification);
+        }
+
+        public IEnumerable<Type> AcceptTypes;
+
+        private string clearText;
+        public string ClearText
+        {
+            get { return clearText; }
+            set
+            {
+                clearText = value;
+                if (clearButton != null) clearButton.Text = clearText;
+            }
+        }
+
+        private string title;
+
+
+        public string Title
+        {
+            get { return title; }
+            set
+            {
+                title = value;
+                if (titleText != null) titleText.Text = title.ToUpper();
+            }
+        }
+
+        [BackgroundDependencyLoader]
+        private void load(OsuColour colours)
+        {
+            RelativeSizeAxes = Axes.X;
+            AutoSizeAxes = Axes.Y;
+            Direction = FlowDirection.VerticalOnly;
+
+            Padding = new MarginPadding
+            {
+                Top = 10,
+                Bottom = 5,
+                Right = 20,
+                Left = 20,
+            };
+
+            AddInternal(new Drawable[]
+            {
+                new Container
+                {
+                    RelativeSizeAxes = Axes.X,
+                    AutoSizeAxes = Axes.Y,
+                    Children = new Drawable[]
+                    {
+                        clearButton = new ClearAllButton
+                        {
+                            Text = clearText,
+                            Anchor = Anchor.TopRight,
+                            Origin = Anchor.TopRight,
+                            Action = clearAll
+                        },
+                        new FlowContainer
+                        {
+                            Margin = new MarginPadding
+                            {
+                                Bottom = 5
+                            },
+                            Spacing = new Vector2(5, 0),
+                            AutoSizeAxes = Axes.Both,
+                            Children = new Drawable[]
+                            {
+                                titleText = new OsuSpriteText
+                                {
+                                    Text = title.ToUpper(),
+                                    Font = @"Exo2.0-Black",
+                                },
+                                countText = new OsuSpriteText
+                                {
+                                    Text = "3",
+                                    Colour = colours.Yellow,
+                                    Font = @"Exo2.0-Black",
+                                },
+                            }
+                        },
+                    },
+                },
+                notifications = new FlowContainer<Notification>
+                {
+                    AutoSizeAxes = Axes.Y,
+                    RelativeSizeAxes = Axes.X,
+                    LayoutDuration = 150,
+                    LayoutEasing = EasingTypes.OutQuart,
+                    Spacing = new Vector2(3),
+                }
+            });
+        }
+
+        private void clearAll()
+        {
+            notifications.Children.ForEach(c => c.Close());
+        }
+
+        protected override void Update()
+        {
+            base.Update();
+
+            countText.Text = notifications.Children.Count(c => c.Alpha > 0.99f).ToString();
+        }
+
+        class ClearAllButton : ClickableContainer
+        {
+            private OsuSpriteText text;
+
+            public ClearAllButton()
+            {
+                AutoSizeAxes = Axes.Both;
+
+                Children = new[]
+                {
+                    text = new OsuSpriteText()
+                };
+            }
+
+            public string Text
+            {
+                get { return text.Text; }
+                set { text.Text = value.ToUpper(); }
+            }
+        }
+
+        public void MarkAllRead()
+        {
+            notifications.Children.ForEach(n => n.Read = true);
+        }
+    }
+}
\ No newline at end of file
diff --git a/osu.Game/Overlays/Notifications/ProgressCompletionNotification.cs b/osu.Game/Overlays/Notifications/ProgressCompletionNotification.cs
new file mode 100644
index 0000000000..1861e410d2
--- /dev/null
+++ b/osu.Game/Overlays/Notifications/ProgressCompletionNotification.cs
@@ -0,0 +1,16 @@
+// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
+// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
+
+namespace osu.Game.Overlays.Notifications
+{
+    public class ProgressCompletionNotification : SimpleNotification
+    {
+        private ProgressNotification progressNotification;
+
+        public ProgressCompletionNotification(ProgressNotification progressNotification)
+            : base(@"Task has completed!")
+        {
+            this.progressNotification = progressNotification;
+        }
+    }
+}
\ No newline at end of file
diff --git a/osu.Game/Overlays/Notifications/ProgressNotification.cs b/osu.Game/Overlays/Notifications/ProgressNotification.cs
new file mode 100644
index 0000000000..a4cac41d33
--- /dev/null
+++ b/osu.Game/Overlays/Notifications/ProgressNotification.cs
@@ -0,0 +1,198 @@
+// 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.Diagnostics;
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Sprites;
+using osu.Framework.Graphics.Transformations;
+using osu.Game.Graphics;
+using OpenTK;
+using OpenTK.Graphics;
+
+namespace osu.Game.Overlays.Notifications
+{
+    public class ProgressNotification : Notification, IHasCompletionTarget
+    {
+        private string text;
+
+        private float progress;
+        public float Progress
+        {
+            get { return progress; }
+            set
+            {
+                Debug.Assert(state == ProgressNotificationState.Active);
+                progress = value;
+                progressBar.Progress = progress;
+            }
+        }
+
+        public ProgressNotificationState State
+        {
+            get { return state; }
+            set
+            {
+                state = value;
+                switch (state)
+                {
+                    case ProgressNotificationState.Queued:
+                        Light.Colour = colourQueued;
+                        Light.Pulsate = false;
+                        progressBar.Active = false;
+                        break;
+                    case ProgressNotificationState.Active:
+                        Light.Colour = colourActive;
+                        Light.Pulsate = true;
+                        progressBar.Active = true;
+                        break;
+                    case ProgressNotificationState.Cancelled:
+                        Light.Colour = colourCancelled;
+                        Light.Pulsate = false;
+                        progressBar.Active = false;
+                        break;
+                }
+            }
+        }
+
+        private ProgressNotificationState state;
+
+        public Action Completed;
+
+        public override bool DisplayOnTop => false;
+
+        private ProgressBar progressBar;
+        private Color4 colourQueued;
+        private Color4 colourActive;
+        private Color4 colourCancelled;
+
+        public ProgressNotification(string text)
+        {
+            this.text = text;
+        }
+
+        [BackgroundDependencyLoader]
+        private void load(OsuColour colours)
+        {
+            colourQueued = colours.YellowDark;
+            colourActive = colours.Blue;
+            colourCancelled = colours.Red;
+
+            IconContent.Add(new Box
+            {
+                RelativeSizeAxes = Axes.Both,
+            });
+
+            Content.Add(new SpriteText
+            {
+                TextSize = 16,
+                Colour = OsuColour.Gray(128),
+                AutoSizeAxes = Axes.Y,
+                RelativeSizeAxes = Axes.X,
+                Text = text
+            });
+
+            NotificationContent.Add(progressBar = new ProgressBar
+            {
+                Origin = Anchor.BottomLeft,
+                Anchor = Anchor.BottomLeft,
+                RelativeSizeAxes = Axes.X,
+            });
+
+            State = ProgressNotificationState.Queued;
+        }
+
+        public void Complete()
+        {
+            state = ProgressNotificationState.Completed;
+
+            NotificationContent.MoveToY(-DrawSize.Y / 2, 200, EasingTypes.OutQuint);
+            FadeTo(0.01f, 200); //don't completely fade out or our scheduled task won't run.
+
+            Delay(100);
+            Schedule(() =>
+            {
+                CompletionTarget?.Invoke(new ProgressCompletionNotification(this));
+                base.Close();
+            });
+        }
+
+        public override void Close()
+        {
+            switch (State)
+            {
+                case ProgressNotificationState.Cancelled:
+                    base.Close();
+                    break;
+                case ProgressNotificationState.Active:
+                case ProgressNotificationState.Queued:
+                    State = ProgressNotificationState.Cancelled;
+                    break;
+            }
+        }
+
+        public Action<Notification> CompletionTarget { get; set; }
+
+        class ProgressBar : Container
+        {
+            private Box box;
+
+            private Color4 colourActive;
+            private Color4 colourInactive;
+
+            private float progress;
+            public float Progress
+            {
+                get { return progress; }
+                set
+                {
+                    if (progress == value) return;
+
+                    progress = value;
+                    box.ResizeTo(new Vector2(progress, 1), 100, EasingTypes.OutQuad);
+                }
+            }
+
+            private bool active;
+
+            public bool Active
+            {
+                get { return active; }
+                set
+                {
+                    active = value;
+                    FadeColour(active ? colourActive : colourInactive, 100);
+                }
+            }
+
+
+            [BackgroundDependencyLoader]
+            private void load(OsuColour colours)
+            {
+                colourActive = colours.Blue;
+                Colour = colourInactive = OsuColour.Gray(0.5f);
+                
+                Height = 5;
+
+                Children = new[]
+                {
+                    box = new Box
+                    {
+                        RelativeSizeAxes = Axes.Both,
+                        Width = 0,
+                    }
+                };
+            }
+        }
+    }
+
+    public enum ProgressNotificationState
+    {
+        Queued,
+        Active,
+        Completed,
+        Cancelled
+    }
+}
\ No newline at end of file
diff --git a/osu.Game/Overlays/Notifications/SimpleNotification.cs b/osu.Game/Overlays/Notifications/SimpleNotification.cs
new file mode 100644
index 0000000000..7c6666082e
--- /dev/null
+++ b/osu.Game/Overlays/Notifications/SimpleNotification.cs
@@ -0,0 +1,66 @@
+// 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.Graphics;
+using osu.Framework.Graphics.Colour;
+using osu.Framework.Graphics.Sprites;
+using osu.Game.Graphics;
+
+namespace osu.Game.Overlays.Notifications
+{
+    public class SimpleNotification : Notification
+    {
+        private string text;
+
+        public SimpleNotification(string text)
+        {
+            this.text = text;
+        }
+
+        [BackgroundDependencyLoader]
+        private void load(OsuColour colours)
+        {
+            IconContent.Add(new Drawable[]
+            {
+                new Box
+                {
+                    RelativeSizeAxes = Axes.Both,
+                    ColourInfo = ColourInfo.GradientVertical(OsuColour.Gray(0.2f), OsuColour.Gray(0.5f))
+                },
+                new TextAwesome
+                {
+                    Anchor = Anchor.Centre,
+                    Icon = FontAwesome.fa_info_circle,
+                }
+            });
+
+            Content.Add(new SpriteText
+            {
+                TextSize = 16,
+                Colour = OsuColour.Gray(128),
+                AutoSizeAxes = Axes.Y,
+                RelativeSizeAxes = Axes.X,
+                Text = text
+            });
+
+            Light.Colour = colours.Green;
+        }
+
+        public override bool Read
+        {
+            get
+            {
+                return base.Read;
+            }
+
+            set
+            {
+                if (base.Read = value)
+                    Light.FadeOut(100);
+                else
+                    Light.FadeIn(100);
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/osu.Game/Overlays/OptionsOverlay.cs b/osu.Game/Overlays/OptionsOverlay.cs
index 1fce450104..a93bbc10d3 100644
--- a/osu.Game/Overlays/OptionsOverlay.cs
+++ b/osu.Game/Overlays/OptionsOverlay.cs
@@ -90,7 +90,7 @@ namespace osu.Game.Overlays
                                 {
                                     Text = "settings",
                                     TextSize = 40,
-                                    Margin = new MarginPadding { Left = CONTENT_MARGINS, Top = 30 },
+                                    Margin = new MarginPadding { Left = CONTENT_MARGINS, Top = Toolbar.Toolbar.TOOLTIP_HEIGHT },
                                 },
                                 new OsuSpriteText
                                 {
diff --git a/osu.Game/Overlays/Toolbar/Toolbar.cs b/osu.Game/Overlays/Toolbar/Toolbar.cs
index 503a3e0bf5..dc8b8d1e9a 100644
--- a/osu.Game/Overlays/Toolbar/Toolbar.cs
+++ b/osu.Game/Overlays/Toolbar/Toolbar.cs
@@ -17,6 +17,7 @@ namespace osu.Game.Overlays.Toolbar
     public class Toolbar : OverlayContainer
     {
         public const float HEIGHT = 40;
+        public const float TOOLTIP_HEIGHT = 30;
 
         public Action OnHome;
         public Action<PlayMode> OnPlayModeChange;
@@ -73,10 +74,7 @@ namespace osu.Game.Overlays.Toolbar
                             Icon = FontAwesome.fa_search
                         },
                         userArea = new ToolbarUserArea(),
-                        new ToolbarButton
-                        {
-                            Icon = FontAwesome.fa_bars
-                        },
+                        new ToolbarNotificationButton(),
                     }
                 }
             };
diff --git a/osu.Game/Overlays/Toolbar/ToolbarButton.cs b/osu.Game/Overlays/Toolbar/ToolbarButton.cs
index 160e4460d9..67b0039971 100644
--- a/osu.Game/Overlays/Toolbar/ToolbarButton.cs
+++ b/osu.Game/Overlays/Toolbar/ToolbarButton.cs
@@ -56,6 +56,8 @@ namespace osu.Game.Overlays.Toolbar
             }
         }
 
+        protected virtual Anchor TooltipAnchor => Anchor.TopLeft;
+
         public Action Action;
         protected TextAwesome DrawableIcon;
         protected SpriteText DrawableText;
@@ -107,19 +109,24 @@ namespace osu.Game.Overlays.Toolbar
                 {
                     Direction = FlowDirection.VerticalOnly,
                     RelativeSizeAxes = Axes.Both, //stops us being considered in parent's autosize
-                    Anchor = Anchor.BottomLeft,
-                    Position = new Vector2(5, 5),
+                    Anchor = (TooltipAnchor & Anchor.x0) > 0 ? Anchor.BottomLeft : Anchor.BottomRight,
+                    Origin = TooltipAnchor,
+                    Position = new Vector2((TooltipAnchor & Anchor.x0) > 0 ? 5 : -5, 5),
                     Alpha = 0,
                     Children = new[]
                     {
                         tooltip1 = new OsuSpriteText
                         {
+                            Anchor = TooltipAnchor,
+                            Origin = TooltipAnchor,
                             Shadow = true,
                             TextSize = 22,
                             Font = @"Exo2.0-Bold",
                         },
                         tooltip2 = new OsuSpriteText
                         {
+                            Anchor = TooltipAnchor,
+                            Origin = TooltipAnchor,
                             Shadow = true,
                             TextSize = 16
                         }
diff --git a/osu.Game/Overlays/Toolbar/ToolbarNotificationButton.cs b/osu.Game/Overlays/Toolbar/ToolbarNotificationButton.cs
new file mode 100644
index 0000000000..973f9f2d8a
--- /dev/null
+++ b/osu.Game/Overlays/Toolbar/ToolbarNotificationButton.cs
@@ -0,0 +1,28 @@
+// 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.Graphics;
+using osu.Game.Graphics;
+
+namespace osu.Game.Overlays.Toolbar
+{
+    class ToolbarNotificationButton : ToolbarOverlayToggleButton
+    {
+        protected override Anchor TooltipAnchor => Anchor.TopRight;
+
+        public ToolbarNotificationButton()
+        {
+            Icon = FontAwesome.fa_bars;
+            TooltipMain = "Notifications";
+            TooltipSub = "Waiting for 'ya";
+        }
+
+        [BackgroundDependencyLoader]
+        private void load(NotificationManager notificationManager)
+        {
+            StateContainer = notificationManager;
+            Action = notificationManager.ToggleVisibility;
+        }
+    }
+}
\ No newline at end of file
diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj
index 2ead6aad48..0477d85b43 100644
--- a/osu.Game/osu.Game.csproj
+++ b/osu.Game/osu.Game.csproj
@@ -100,6 +100,13 @@
     <Compile Include="Beatmaps\Timing\SampleChange.cs" />
     <Compile Include="Beatmaps\Timing\TimingChange.cs" />
     <Compile Include="Configuration\OsuConfigManager.cs" />
+    <Compile Include="Overlays\Notifications\IHasCompletionTarget.cs" />
+    <Compile Include="Overlays\Notifications\Notification.cs" />
+    <Compile Include="Overlays\NotificationManager.cs" />
+    <Compile Include="Overlays\Notifications\NotificationSection.cs" />
+    <Compile Include="Overlays\Notifications\ProgressCompletionNotification.cs" />
+    <Compile Include="Overlays\Notifications\ProgressNotification.cs" />
+    <Compile Include="Overlays\Notifications\SimpleNotification.cs" />
     <Compile Include="Overlays\Options\OptionDropDown.cs" />
     <Compile Include="Overlays\Options\OptionLabel.cs" />
     <Compile Include="Graphics\UserInterface\OsuDropDownHeader.cs" />
@@ -107,6 +114,7 @@
     <Compile Include="Graphics\UserInterface\OsuDropDownMenuItem.cs" />
     <Compile Include="Overlays\Toolbar\ToolbarHomeButton.cs" />
     <Compile Include="Overlays\Toolbar\ToolbarMusicButton.cs" />
+    <Compile Include="Overlays\Toolbar\ToolbarNotificationButton.cs" />
     <Compile Include="Overlays\Toolbar\ToolbarSettingsButton.cs" />
     <Compile Include="Overlays\Toolbar\ToolbarOverlayToggleButton.cs" />
     <Compile Include="Overlays\Toolbar\ToolbarUserArea.cs" />

From dd8ec70bd5fdefa4e3a4ba4d3951a3461754d711 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Fri, 10 Feb 2017 16:57:49 +0900
Subject: [PATCH 2/3] Remove excess newline.

---
 osu.Game/Overlays/Notifications/NotificationSection.cs | 1 -
 1 file changed, 1 deletion(-)

diff --git a/osu.Game/Overlays/Notifications/NotificationSection.cs b/osu.Game/Overlays/Notifications/NotificationSection.cs
index 526b568c12..ec286302b8 100644
--- a/osu.Game/Overlays/Notifications/NotificationSection.cs
+++ b/osu.Game/Overlays/Notifications/NotificationSection.cs
@@ -45,7 +45,6 @@ namespace osu.Game.Overlays.Notifications
 
         private string title;
 
-
         public string Title
         {
             get { return title; }

From 398ac6f459b266a92d79f76e0ab3b38e96e140b5 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Fri, 10 Feb 2017 16:58:40 +0900
Subject: [PATCH 3/3] Add assert to ensure complete is only called once.

---
 osu.Game/Overlays/Notifications/ProgressNotification.cs | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/osu.Game/Overlays/Notifications/ProgressNotification.cs b/osu.Game/Overlays/Notifications/ProgressNotification.cs
index a4cac41d33..4cb1557c32 100644
--- a/osu.Game/Overlays/Notifications/ProgressNotification.cs
+++ b/osu.Game/Overlays/Notifications/ProgressNotification.cs
@@ -106,6 +106,8 @@ namespace osu.Game.Overlays.Notifications
 
         public void Complete()
         {
+            Debug.Assert(state != ProgressNotificationState.Completed);
+
             state = ProgressNotificationState.Completed;
 
             NotificationContent.MoveToY(-DrawSize.Y / 2, 200, EasingTypes.OutQuint);