From ffe82aad25659c6f113ac9db685bcca581b815ab Mon Sep 17 00:00:00 2001
From: HoutarouOreki <TheZjarek@gmail.com>
Date: Tue, 10 Jul 2018 21:57:09 +0200
Subject: [PATCH 01/28] Add basic quick exit functionality

---
 .../Input/Bindings/GlobalActionContainer.cs   |  3 ++
 osu.Game/Screens/Play/Player.cs               | 13 +++++++++
 osu.Game/Screens/Play/QuickExit.cs            | 28 +++++++++++++++++++
 3 files changed, 44 insertions(+)
 create mode 100644 osu.Game/Screens/Play/QuickExit.cs

diff --git a/osu.Game/Input/Bindings/GlobalActionContainer.cs b/osu.Game/Input/Bindings/GlobalActionContainer.cs
index b21deff509..f4419cc6d0 100644
--- a/osu.Game/Input/Bindings/GlobalActionContainer.cs
+++ b/osu.Game/Input/Bindings/GlobalActionContainer.cs
@@ -49,6 +49,7 @@ namespace osu.Game.Input.Bindings
         {
             new KeyBinding(InputKey.Space, GlobalAction.SkipCutscene),
             new KeyBinding(InputKey.Tilde, GlobalAction.QuickRetry),
+            new KeyBinding(new[] { InputKey.Alt, InputKey.Tilde }, GlobalAction.QuickExit),
             new KeyBinding(new[] { InputKey.Control, InputKey.Plus }, GlobalAction.IncreaseScrollSpeed),
             new KeyBinding(new[] { InputKey.Control, InputKey.Minus }, GlobalAction.DecreaseScrollSpeed),
         };
@@ -83,6 +84,8 @@ namespace osu.Game.Input.Bindings
         SkipCutscene,
         [Description("Quick Retry (Hold)")]
         QuickRetry,
+        [Description("Quick Exit (Hold)")]
+        QuickExit,
 
         [Description("Take screenshot")]
         TakeScreenshot,
diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs
index b406bda411..84fcede6b5 100644
--- a/osu.Game/Screens/Play/Player.cs
+++ b/osu.Game/Screens/Play/Player.cs
@@ -71,6 +71,7 @@ namespace osu.Game.Screens.Play
         private APIAccess api;
 
         private SampleChannel sampleRestart;
+        private SampleChannel sampleExit;
 
         protected ScoreProcessor ScoreProcessor;
         protected RulesetContainer RulesetContainer;
@@ -93,6 +94,7 @@ namespace osu.Game.Screens.Play
                 return;
 
             sampleRestart = audio.Sample.Get(@"Gameplay/restart");
+            sampleExit = audio.Sample.Get(@"UI/screen-back");
 
             mouseWheelDisabled = config.GetBindable<bool>(OsuSetting.MouseDisableWheel);
             userAudioOffset = config.GetBindable<double>(OsuSetting.AudioOffset);
@@ -224,6 +226,17 @@ namespace osu.Game.Screens.Play
                         RulesetContainer?.Hide();
                         Restart();
                     },
+                },
+                new QuickExit
+                {
+                    Action = () =>
+                    {
+                        if (!IsCurrentScreen) return;
+
+                        sampleExit?.Play();
+                        ValidForResume = false;
+                        Exit();
+                    }
                 }
             };
 
diff --git a/osu.Game/Screens/Play/QuickExit.cs b/osu.Game/Screens/Play/QuickExit.cs
new file mode 100644
index 0000000000..611b02c543
--- /dev/null
+++ b/osu.Game/Screens/Play/QuickExit.cs
@@ -0,0 +1,28 @@
+// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
+// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCEusing System;
+
+using osu.Framework.Input.Bindings;
+using osu.Game.Input.Bindings;
+using osu.Game.Overlays;
+
+namespace osu.Game.Screens.Play
+{
+    public class QuickExit : HoldToConfirmOverlay, IKeyBindingHandler<GlobalAction>
+    {
+        public bool OnPressed(GlobalAction action)
+        {
+            if (action != GlobalAction.QuickExit) return false;
+
+            BeginConfirm();
+            return true;
+        }
+
+        public bool OnReleased(GlobalAction action)
+        {
+            if (action != GlobalAction.QuickExit) return false;
+
+            AbortConfirm();
+            return true;
+        }
+    }
+}

From 4c6286a3ca1929cbe5c96410ea8121da00f65a40 Mon Sep 17 00:00:00 2001
From: HoutarouOreki <TheZjarek@gmail.com>
Date: Tue, 10 Jul 2018 22:16:08 +0200
Subject: [PATCH 02/28] Fix license header

---
 osu.Game/Screens/Play/QuickExit.cs | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/osu.Game/Screens/Play/QuickExit.cs b/osu.Game/Screens/Play/QuickExit.cs
index 611b02c543..0c3908a44f 100644
--- a/osu.Game/Screens/Play/QuickExit.cs
+++ b/osu.Game/Screens/Play/QuickExit.cs
@@ -1,5 +1,5 @@
 // Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
-// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCEusing System;
+// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
 
 using osu.Framework.Input.Bindings;
 using osu.Game.Input.Bindings;

From 4cc22387d4a3481972ae35688952a392a1fb79fb Mon Sep 17 00:00:00 2001
From: HoutarouOreki <TheZjarek@gmail.com>
Date: Wed, 11 Jul 2018 12:03:05 +0200
Subject: [PATCH 03/28] Avoid interversion key configuration conflicts

---
 osu.Game/Input/Bindings/GlobalActionContainer.cs | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/osu.Game/Input/Bindings/GlobalActionContainer.cs b/osu.Game/Input/Bindings/GlobalActionContainer.cs
index f4419cc6d0..56d8db72fe 100644
--- a/osu.Game/Input/Bindings/GlobalActionContainer.cs
+++ b/osu.Game/Input/Bindings/GlobalActionContainer.cs
@@ -84,8 +84,6 @@ namespace osu.Game.Input.Bindings
         SkipCutscene,
         [Description("Quick Retry (Hold)")]
         QuickRetry,
-        [Description("Quick Exit (Hold)")]
-        QuickExit,
 
         [Description("Take screenshot")]
         TakeScreenshot,
@@ -103,5 +101,8 @@ namespace osu.Game.Input.Bindings
 
         [Description("Select")]
         Select,
+
+        [Description("Quick Exit (Hold)")]
+        QuickExit,
     }
 }

From fde9df39389e88368360ce46ad2e651ac459a778 Mon Sep 17 00:00:00 2001
From: HoutarouOreki <TheZjarek@gmail.com>
Date: Wed, 11 Jul 2018 14:47:34 +0200
Subject: [PATCH 04/28] Same as #3006 + hide gameplay instantly

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

diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs
index 84fcede6b5..e0d5fed212 100644
--- a/osu.Game/Screens/Play/Player.cs
+++ b/osu.Game/Screens/Play/Player.cs
@@ -235,6 +235,8 @@ namespace osu.Game.Screens.Play
 
                         sampleExit?.Play();
                         ValidForResume = false;
+                        RulesetContainer?.Hide();
+                        pauseContainer?.Hide();
                         Exit();
                     }
                 }

From 07183c0069691ee0c39af25e3535b6362af121bd Mon Sep 17 00:00:00 2001
From: HoutarouOreki <TheZjarek@gmail.com>
Date: Sun, 15 Jul 2018 00:52:15 +0200
Subject: [PATCH 05/28] Hide Content instead of particular overlays

---
 osu.Game/Screens/Play/Player.cs | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs
index e0d5fed212..c575824c1c 100644
--- a/osu.Game/Screens/Play/Player.cs
+++ b/osu.Game/Screens/Play/Player.cs
@@ -235,8 +235,7 @@ namespace osu.Game.Screens.Play
 
                         sampleExit?.Play();
                         ValidForResume = false;
-                        RulesetContainer?.Hide();
-                        pauseContainer?.Hide();
+                        Content.Hide();
                         Exit();
                     }
                 }

From a7306248d162f61c48a343ee892435dba7f87428 Mon Sep 17 00:00:00 2001
From: Andrei Zavatski <megaman9919@gmail.com>
Date: Tue, 18 Jun 2019 16:20:35 +0300
Subject: [PATCH 06/28] Implement OverlayTabControl class

An abstraction for OverlayHeaderTabControl
---
 osu.Game/Overlays/OverlayHeaderTabControl.cs | 143 ++---------------
 osu.Game/Overlays/OverlayTabControl.cs       | 153 +++++++++++++++++++
 osu.Game/Overlays/Profile/ProfileHeader.cs   |   6 -
 3 files changed, 162 insertions(+), 140 deletions(-)
 create mode 100644 osu.Game/Overlays/OverlayTabControl.cs

diff --git a/osu.Game/Overlays/OverlayHeaderTabControl.cs b/osu.Game/Overlays/OverlayHeaderTabControl.cs
index dfe7e52420..b2c5fb8f2a 100644
--- a/osu.Game/Overlays/OverlayHeaderTabControl.cs
+++ b/osu.Game/Overlays/OverlayHeaderTabControl.cs
@@ -1,153 +1,28 @@
 // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
 // See the LICENCE file in the repository root for full licence text.
 
-using osu.Framework.Graphics;
-using osu.Framework.Graphics.Shapes;
+using osu.Framework.Allocation;
 using osu.Framework.Graphics.UserInterface;
-using osu.Framework.Input.Events;
 using osu.Game.Graphics;
-using osu.Game.Graphics.Sprites;
-using osu.Game.Graphics.UserInterface;
-using osuTK;
-using osuTK.Graphics;
 
 namespace osu.Game.Overlays
 {
-    public class OverlayHeaderTabControl : TabControl<string>
+    public class OverlayHeaderTabControl : OverlayTabControl<string>
     {
-        private readonly Box bar;
+        protected override TabItem<string> CreateTabItem(string value) => new OverlayHeaderTabItem(value);
 
-        private Color4 accentColour = Color4.White;
-
-        public Color4 AccentColour
+        [BackgroundDependencyLoader]
+        private void load(OsuColour colours)
         {
-            get => accentColour;
-            set
-            {
-                if (accentColour == value)
-                    return;
-
-                accentColour = value;
-                bar.Colour = value;
-
-                foreach (TabItem<string> tabItem in TabContainer)
-                {
-                    ((HeaderTabItem)tabItem).AccentColour = value;
-                }
-            }
+            AccentColour = colours.Seafoam;
         }
 
-        public new MarginPadding Padding
+        private class OverlayHeaderTabItem : OverlayTabItem<string>
         {
-            get => TabContainer.Padding;
-            set => TabContainer.Padding = value;
-        }
-
-        public OverlayHeaderTabControl()
-        {
-            TabContainer.Masking = false;
-            TabContainer.Spacing = new Vector2(15, 0);
-
-            AddInternal(bar = new Box
-            {
-                RelativeSizeAxes = Axes.X,
-                Height = 2,
-                Anchor = Anchor.BottomLeft,
-                Origin = Anchor.CentreLeft
-            });
-        }
-
-        protected override Dropdown<string> CreateDropdown() => null;
-
-        protected override TabItem<string> CreateTabItem(string value) => new HeaderTabItem(value)
-        {
-            AccentColour = AccentColour
-        };
-
-        private class HeaderTabItem : TabItem<string>
-        {
-            private readonly OsuSpriteText text;
-            private readonly ExpandingBar bar;
-
-            private Color4 accentColour;
-
-            public Color4 AccentColour
-            {
-                get => accentColour;
-                set
-                {
-                    if (accentColour == value)
-                        return;
-
-                    accentColour = value;
-                    bar.Colour = value;
-
-                    updateState();
-                }
-            }
-
-            public HeaderTabItem(string value)
+            public OverlayHeaderTabItem(string value)
                 : base(value)
             {
-                AutoSizeAxes = Axes.X;
-                RelativeSizeAxes = Axes.Y;
-
-                Children = new Drawable[]
-                {
-                    text = new OsuSpriteText
-                    {
-                        Margin = new MarginPadding { Bottom = 10 },
-                        Origin = Anchor.BottomLeft,
-                        Anchor = Anchor.BottomLeft,
-                        Text = value,
-                        Font = OsuFont.GetFont()
-                    },
-                    bar = new ExpandingBar
-                    {
-                        Anchor = Anchor.BottomCentre,
-                        ExpandedSize = 7.5f,
-                        CollapsedSize = 0
-                    },
-                    new HoverClickSounds()
-                };
-            }
-
-            protected override bool OnHover(HoverEvent e)
-            {
-                base.OnHover(e);
-
-                updateState();
-
-                return true;
-            }
-
-            protected override void OnHoverLost(HoverLostEvent e)
-            {
-                base.OnHoverLost(e);
-
-                updateState();
-            }
-
-            protected override void OnActivated() => updateState();
-
-            protected override void OnDeactivated() => updateState();
-
-            private void updateState()
-            {
-                if (Active.Value || IsHovered)
-                {
-                    text.FadeColour(Color4.White, 120, Easing.InQuad);
-                    bar.Expand();
-
-                    if (Active.Value)
-                        text.Font = text.Font.With(weight: FontWeight.Bold);
-                }
-                else
-                {
-                    text.FadeColour(AccentColour, 120, Easing.InQuad);
-                    bar.Collapse();
-                    text.Font = text.Font.With(weight: FontWeight.Medium);
-                }
+                Text.Text = value;
             }
         }
     }
diff --git a/osu.Game/Overlays/OverlayTabControl.cs b/osu.Game/Overlays/OverlayTabControl.cs
new file mode 100644
index 0000000000..8fd53e0f36
--- /dev/null
+++ b/osu.Game/Overlays/OverlayTabControl.cs
@@ -0,0 +1,153 @@
+// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Shapes;
+using osu.Framework.Graphics.UserInterface;
+using osu.Framework.Input.Events;
+using osu.Game.Graphics;
+using osu.Game.Graphics.Sprites;
+using osu.Game.Graphics.UserInterface;
+using osuTK;
+using osuTK.Graphics;
+
+namespace osu.Game.Overlays
+{
+    public abstract class OverlayTabControl<T> : TabControl<T>
+    {
+        private readonly Box bar;
+
+        private Color4 accentColour = Color4.White;
+
+        public Color4 AccentColour
+        {
+            get => accentColour;
+            set
+            {
+                if (accentColour == value)
+                    return;
+
+                accentColour = value;
+                bar.Colour = value;
+
+                foreach (TabItem<T> tabItem in TabContainer)
+                {
+                    ((OverlayTabItem<T>)tabItem).AccentColour = value;
+                }
+            }
+        }
+
+        public new MarginPadding Padding
+        {
+            get => TabContainer.Padding;
+            set => TabContainer.Padding = value;
+        }
+
+        protected OverlayTabControl()
+        {
+            TabContainer.Masking = false;
+            TabContainer.Spacing = new Vector2(15, 0);
+
+            AddInternal(bar = new Box
+            {
+                RelativeSizeAxes = Axes.X,
+                Height = 2,
+                Anchor = Anchor.BottomLeft,
+                Origin = Anchor.CentreLeft
+            });
+        }
+
+        protected override Dropdown<T> CreateDropdown() => null;
+
+        protected override TabItem<T> CreateTabItem(T value) => new OverlayTabItem<T>(value);
+
+        protected class OverlayTabItem<U> : TabItem<U>
+        {
+            private readonly ExpandingBar bar;
+
+            protected readonly OsuSpriteText Text;
+
+            private Color4 accentColour;
+
+            public Color4 AccentColour
+            {
+                get => accentColour;
+                set
+                {
+                    if (accentColour == value)
+                        return;
+
+                    accentColour = value;
+                    bar.Colour = value;
+
+                    updateState();
+                }
+            }
+
+            public OverlayTabItem(U value)
+                : base(value)
+            {
+                AutoSizeAxes = Axes.X;
+                RelativeSizeAxes = Axes.Y;
+
+                Children = new Drawable[]
+                {
+                    Text = new OsuSpriteText
+                    {
+                        Margin = new MarginPadding { Bottom = 10 },
+                        Origin = Anchor.BottomLeft,
+                        Anchor = Anchor.BottomLeft,
+                        Font = OsuFont.GetFont(),
+                    },
+                    bar = new ExpandingBar
+                    {
+                        Anchor = Anchor.BottomCentre,
+                        ExpandedSize = 7.5f,
+                        CollapsedSize = 0
+                    },
+                    new HoverClickSounds()
+                };
+            }
+
+            protected override bool OnHover(HoverEvent e)
+            {
+                base.OnHover(e);
+
+                if (!Active.Value)
+                    OnActivated();
+
+                return true;
+            }
+
+            protected override void OnHoverLost(HoverLostEvent e)
+            {
+                base.OnHoverLost(e);
+
+                if (!Active.Value)
+                    OnDeactivated();
+            }
+
+            protected override void OnActivated()
+            {
+                bar.Expand();
+                Text.FadeColour(Color4.White, 120, Easing.InQuad);
+                Text.Font = Text.Font.With(weight: FontWeight.Bold);
+            }
+
+            protected override void OnDeactivated()
+            {
+                bar.Collapse();
+                Text.FadeColour(AccentColour, 120, Easing.InQuad);
+                Text.Font = Text.Font.With(weight: FontWeight.Medium);
+            }
+
+            private void updateState()
+            {
+                if (Active.Value)
+                    OnActivated();
+                else
+                    OnDeactivated();
+            }
+        }
+    }
+}
diff --git a/osu.Game/Overlays/Profile/ProfileHeader.cs b/osu.Game/Overlays/Profile/ProfileHeader.cs
index 76613c156d..90f51a90a6 100644
--- a/osu.Game/Overlays/Profile/ProfileHeader.cs
+++ b/osu.Game/Overlays/Profile/ProfileHeader.cs
@@ -34,12 +34,6 @@ namespace osu.Game.Overlays.Profile
             centreHeaderContainer.DetailsVisible.BindValueChanged(visible => detailHeaderContainer.Expanded = visible.NewValue, true);
         }
 
-        [BackgroundDependencyLoader]
-        private void load(OsuColour colours)
-        {
-            TabControl.AccentColour = colours.Seafoam;
-        }
-
         protected override Drawable CreateBackground() =>
             new Container
             {

From 3c3757f12eab580b2709aba027011a3371d40e36 Mon Sep 17 00:00:00 2001
From: Andrei Zavatski <megaman9919@gmail.com>
Date: Tue, 18 Jun 2019 16:23:57 +0300
Subject: [PATCH 07/28] Make ProfileTabControl based on OverlayTabControl

---
 osu.Game/Overlays/UserProfileOverlay.cs | 33 +++++++++----------------
 1 file changed, 11 insertions(+), 22 deletions(-)

diff --git a/osu.Game/Overlays/UserProfileOverlay.cs b/osu.Game/Overlays/UserProfileOverlay.cs
index 8a133a1d1e..6c9b757c13 100644
--- a/osu.Game/Overlays/UserProfileOverlay.cs
+++ b/osu.Game/Overlays/UserProfileOverlay.cs
@@ -8,12 +8,10 @@ using osu.Framework.Graphics.Shapes;
 using osu.Framework.Graphics.UserInterface;
 using osu.Game.Graphics;
 using osu.Game.Graphics.Containers;
-using osu.Game.Graphics.UserInterface;
 using osu.Game.Online.API.Requests;
 using osu.Game.Overlays.Profile;
 using osu.Game.Overlays.Profile.Sections;
 using osu.Game.Users;
-using osuTK;
 
 namespace osu.Game.Overlays
 {
@@ -141,31 +139,28 @@ namespace osu.Game.Overlays
             }
         }
 
-        private class ProfileTabControl : PageTabControl<ProfileSection>
+        private class ProfileTabControl : OverlayTabControl<ProfileSection>
         {
-            private readonly Box bottom;
-
             public ProfileTabControl()
             {
                 TabContainer.RelativeSizeAxes &= ~Axes.X;
                 TabContainer.AutoSizeAxes |= Axes.X;
                 TabContainer.Anchor |= Anchor.x1;
                 TabContainer.Origin |= Anchor.x1;
-                AddInternal(bottom = new Box
-                {
-                    RelativeSizeAxes = Axes.X,
-                    Height = 1,
-                    Anchor = Anchor.BottomCentre,
-                    Origin = Anchor.BottomCentre,
-                    EdgeSmoothness = new Vector2(1)
-                });
             }
 
-            protected override TabItem<ProfileSection> CreateTabItem(ProfileSection value) => new ProfileTabItem(value);
+            protected override TabItem<ProfileSection> CreateTabItem(ProfileSection value) => new ProfileTabItem(value)
+            {
+                AccentColour = AccentColour
+            };
 
-            protected override Dropdown<ProfileSection> CreateDropdown() => null;
+            [BackgroundDependencyLoader]
+            private void load(OsuColour colours)
+            {
+                AccentColour = colours.Seafoam;
+            }
 
-            private class ProfileTabItem : PageTabItem
+            private class ProfileTabItem : OverlayTabItem<ProfileSection>
             {
                 public ProfileTabItem(ProfileSection value)
                     : base(value)
@@ -173,12 +168,6 @@ namespace osu.Game.Overlays
                     Text.Text = value.Title;
                 }
             }
-
-            [BackgroundDependencyLoader]
-            private void load(OsuColour colours)
-            {
-                bottom.Colour = colours.Yellow;
-            }
         }
     }
 }

From 1c44fa84fb4939a05ff61c333d0993908a26fec6 Mon Sep 17 00:00:00 2001
From: Andrei Zavatski <megaman9919@gmail.com>
Date: Wed, 19 Jun 2019 12:15:32 +0300
Subject: [PATCH 08/28] Fix colour issues

---
 osu.Game/Overlays/OverlayHeaderTabControl.cs | 11 +++--------
 osu.Game/Overlays/Profile/ProfileHeader.cs   |  6 ++++++
 2 files changed, 9 insertions(+), 8 deletions(-)

diff --git a/osu.Game/Overlays/OverlayHeaderTabControl.cs b/osu.Game/Overlays/OverlayHeaderTabControl.cs
index b2c5fb8f2a..5b56771dc1 100644
--- a/osu.Game/Overlays/OverlayHeaderTabControl.cs
+++ b/osu.Game/Overlays/OverlayHeaderTabControl.cs
@@ -1,21 +1,16 @@
 // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
 // See the LICENCE file in the repository root for full licence text.
 
-using osu.Framework.Allocation;
 using osu.Framework.Graphics.UserInterface;
-using osu.Game.Graphics;
 
 namespace osu.Game.Overlays
 {
     public class OverlayHeaderTabControl : OverlayTabControl<string>
     {
-        protected override TabItem<string> CreateTabItem(string value) => new OverlayHeaderTabItem(value);
-
-        [BackgroundDependencyLoader]
-        private void load(OsuColour colours)
+        protected override TabItem<string> CreateTabItem(string value) => new OverlayHeaderTabItem(value)
         {
-            AccentColour = colours.Seafoam;
-        }
+            AccentColour = AccentColour,
+        };
 
         private class OverlayHeaderTabItem : OverlayTabItem<string>
         {
diff --git a/osu.Game/Overlays/Profile/ProfileHeader.cs b/osu.Game/Overlays/Profile/ProfileHeader.cs
index 90f51a90a6..76613c156d 100644
--- a/osu.Game/Overlays/Profile/ProfileHeader.cs
+++ b/osu.Game/Overlays/Profile/ProfileHeader.cs
@@ -34,6 +34,12 @@ namespace osu.Game.Overlays.Profile
             centreHeaderContainer.DetailsVisible.BindValueChanged(visible => detailHeaderContainer.Expanded = visible.NewValue, true);
         }
 
+        [BackgroundDependencyLoader]
+        private void load(OsuColour colours)
+        {
+            TabControl.AccentColour = colours.Seafoam;
+        }
+
         protected override Drawable CreateBackground() =>
             new Container
             {

From 29305ca0eb2737395178d588873157f0c69b4e34 Mon Sep 17 00:00:00 2001
From: Andrei Zavatski <megaman9919@gmail.com>
Date: Fri, 21 Jun 2019 13:24:12 +0300
Subject: [PATCH 09/28] fix broken layout

---
 osu.Game/Overlays/OverlayTabControl.cs | 22 ++++++++++++++++------
 1 file changed, 16 insertions(+), 6 deletions(-)

diff --git a/osu.Game/Overlays/OverlayTabControl.cs b/osu.Game/Overlays/OverlayTabControl.cs
index 8fd53e0f36..20649c8a74 100644
--- a/osu.Game/Overlays/OverlayTabControl.cs
+++ b/osu.Game/Overlays/OverlayTabControl.cs
@@ -114,7 +114,7 @@ namespace osu.Game.Overlays
                 base.OnHover(e);
 
                 if (!Active.Value)
-                    OnActivated();
+                    HoverAction();
 
                 return true;
             }
@@ -124,20 +124,18 @@ namespace osu.Game.Overlays
                 base.OnHoverLost(e);
 
                 if (!Active.Value)
-                    OnDeactivated();
+                    UnhoverAction();
             }
 
             protected override void OnActivated()
             {
-                bar.Expand();
-                Text.FadeColour(Color4.White, 120, Easing.InQuad);
+                HoverAction();
                 Text.Font = Text.Font.With(weight: FontWeight.Bold);
             }
 
             protected override void OnDeactivated()
             {
-                bar.Collapse();
-                Text.FadeColour(AccentColour, 120, Easing.InQuad);
+                UnhoverAction();
                 Text.Font = Text.Font.With(weight: FontWeight.Medium);
             }
 
@@ -148,6 +146,18 @@ namespace osu.Game.Overlays
                 else
                     OnDeactivated();
             }
+
+            protected virtual void HoverAction()
+            {
+                bar.Expand();
+                Text.FadeColour(Color4.White, 120, Easing.InQuad);
+            }
+
+            protected virtual void UnhoverAction()
+            {
+                bar.Collapse();
+                Text.FadeColour(AccentColour, 120, Easing.InQuad);
+            }
         }
     }
 }

From 16a4805f1fe95aaa9348c9321402cd0cc612a6ce Mon Sep 17 00:00:00 2001
From: Unknown <aergwyn@t-online.de>
Date: Fri, 21 Jun 2019 15:04:10 +0200
Subject: [PATCH 10/28] add OsuNumberBox with basic tests

---
 .../UserInterface/TestSceneNumberBox.cs       | 55 +++++++++++++++++++
 .../Graphics/UserInterface/OsuNumberBox.cs    | 10 ++++
 2 files changed, 65 insertions(+)
 create mode 100644 osu.Game.Tests/Visual/UserInterface/TestSceneNumberBox.cs
 create mode 100644 osu.Game/Graphics/UserInterface/OsuNumberBox.cs

diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneNumberBox.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneNumberBox.cs
new file mode 100644
index 0000000000..f73450db60
--- /dev/null
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneNumberBox.cs
@@ -0,0 +1,55 @@
+// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Collections.Generic;
+using NUnit.Framework;
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Game.Graphics.UserInterface;
+
+namespace osu.Game.Tests.Visual.UserInterface
+{
+    [TestFixture]
+    public class TestSceneNumberBox : OsuTestScene
+    {
+        public override IReadOnlyList<Type> RequiredTypes => new[]
+        {
+            typeof(OsuNumberBox),
+        };
+
+        private OsuNumberBox numberBox;
+
+        [BackgroundDependencyLoader]
+        private void load()
+        {
+            Child = new Container
+            {
+                Anchor = Anchor.Centre,
+                Origin = Anchor.Centre,
+                RelativeSizeAxes = Axes.X,
+                Padding = new MarginPadding { Horizontal = 250 },
+                Child = numberBox = new OsuNumberBox
+                {
+                    Anchor = Anchor.Centre,
+                    Origin = Anchor.Centre,
+                    RelativeSizeAxes = Axes.X,
+                    PlaceholderText = "Insert numbers here"
+                }
+            };
+
+            clearInput();
+            AddStep("enter numbers", () => numberBox.Text = "987654321");
+            expectedValue("987654321");
+            clearInput();
+            AddStep("enter text + single number", () => numberBox.Text = "1 hello 2 world 3");
+            expectedValue("123");
+            clearInput();
+        }
+
+        private void clearInput() => AddStep("clear input", () => numberBox.Text = null);
+
+        private void expectedValue(string value) => AddAssert("expect number", () => numberBox.Text == value);
+    }
+}
diff --git a/osu.Game/Graphics/UserInterface/OsuNumberBox.cs b/osu.Game/Graphics/UserInterface/OsuNumberBox.cs
new file mode 100644
index 0000000000..36288c745a
--- /dev/null
+++ b/osu.Game/Graphics/UserInterface/OsuNumberBox.cs
@@ -0,0 +1,10 @@
+// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+namespace osu.Game.Graphics.UserInterface
+{
+    public class OsuNumberBox : OsuTextBox
+    {
+        protected override bool CanAddCharacter(char character) => char.IsNumber(character);
+    }
+}

From 1bc1e2459ed865edf05dc0c69a1d1e3bd4639596 Mon Sep 17 00:00:00 2001
From: Unknown <aergwyn@t-online.de>
Date: Fri, 21 Jun 2019 15:04:34 +0200
Subject: [PATCH 11/28] add SettingsNumberBox and use it in tournament tools

---
 .../Screens/Editors/RoundEditorScreen.cs        |  2 +-
 .../Screens/Editors/TeamEditorScreen.cs         |  2 +-
 osu.Game/Overlays/Settings/SettingsNumberBox.cs | 17 +++++++++++++++++
 3 files changed, 19 insertions(+), 2 deletions(-)
 create mode 100644 osu.Game/Overlays/Settings/SettingsNumberBox.cs

diff --git a/osu.Game.Tournament/Screens/Editors/RoundEditorScreen.cs b/osu.Game.Tournament/Screens/Editors/RoundEditorScreen.cs
index 1dc91abe6d..b036350879 100644
--- a/osu.Game.Tournament/Screens/Editors/RoundEditorScreen.cs
+++ b/osu.Game.Tournament/Screens/Editors/RoundEditorScreen.cs
@@ -183,7 +183,7 @@ namespace osu.Game.Tournament.Screens.Editors
                                 AutoSizeAxes = Axes.Both,
                                 Children = new Drawable[]
                                 {
-                                    new SettingsTextBox
+                                    new SettingsNumberBox
                                     {
                                         LabelText = "Beatmap ID",
                                         RelativeSizeAxes = Axes.None,
diff --git a/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs b/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs
index 93745acabf..a4479f3cfd 100644
--- a/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs
+++ b/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs
@@ -231,7 +231,7 @@ namespace osu.Game.Tournament.Screens.Editors
                                 AutoSizeAxes = Axes.Both,
                                 Children = new Drawable[]
                                 {
-                                    new SettingsTextBox
+                                    new SettingsNumberBox
                                     {
                                         LabelText = "User ID",
                                         RelativeSizeAxes = Axes.None,
diff --git a/osu.Game/Overlays/Settings/SettingsNumberBox.cs b/osu.Game/Overlays/Settings/SettingsNumberBox.cs
new file mode 100644
index 0000000000..cb7e63ae6f
--- /dev/null
+++ b/osu.Game/Overlays/Settings/SettingsNumberBox.cs
@@ -0,0 +1,17 @@
+// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Graphics;
+using osu.Game.Graphics.UserInterface;
+
+namespace osu.Game.Overlays.Settings
+{
+    public class SettingsNumberBox : SettingsItem<string>
+    {
+        protected override Drawable CreateControl() => new OsuNumberBox
+        {
+            Margin = new MarginPadding { Top = 5 },
+            RelativeSizeAxes = Axes.X,
+        };
+    }
+}

From e2f82ac29b242b6d7621faeb347b2cc9d1704d85 Mon Sep 17 00:00:00 2001
From: Lucas A <game4allyt@gmail.com>
Date: Sat, 22 Jun 2019 17:53:00 +0200
Subject: [PATCH 12/28] Move CatcherSprite to its own file + Make CatcherSprite
 a SkinReloadableDrawable

---
 osu.Game.Rulesets.Catch/UI/CatcherArea.cs   | 22 +---------
 osu.Game.Rulesets.Catch/UI/CatcherSprite.cs | 46 +++++++++++++++++++++
 2 files changed, 48 insertions(+), 20 deletions(-)
 create mode 100644 osu.Game.Rulesets.Catch/UI/CatcherSprite.cs

diff --git a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs
index 90052d9b11..18121bba4b 100644
--- a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs
+++ b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs
@@ -7,7 +7,6 @@ using osu.Framework.Allocation;
 using osu.Framework.Graphics;
 using osu.Framework.Graphics.Containers;
 using osu.Framework.Graphics.Sprites;
-using osu.Framework.Graphics.Textures;
 using osu.Framework.Input.Bindings;
 using osu.Framework.MathUtils;
 using osu.Game.Beatmaps;
@@ -141,7 +140,7 @@ namespace osu.Game.Rulesets.Catch.UI
             [BackgroundDependencyLoader]
             private void load()
             {
-                Children = new Drawable[]
+                Children = new[]
                 {
                     caughtFruit = new Container<DrawableHitObject>
                     {
@@ -212,7 +211,7 @@ namespace osu.Game.Rulesets.Catch.UI
                 Scheduler.AddDelayed(beginTrail, HyperDashing ? 25 : 50);
             }
 
-            private Sprite createCatcherSprite() => new CatcherSprite();
+            private Drawable createCatcherSprite() => new CatcherSprite();
 
             /// <summary>
             /// Add a caught fruit to the catcher's stack.
@@ -444,23 +443,6 @@ namespace osu.Game.Rulesets.Catch.UI
 
                 fruit.Expire();
             }
-
-            private class CatcherSprite : Sprite
-            {
-                public CatcherSprite()
-                {
-                    Size = new Vector2(CATCHER_SIZE);
-
-                    // Sets the origin roughly to the centre of the catcher's plate to allow for correct scaling.
-                    OriginPosition = new Vector2(-0.02f, 0.06f) * CATCHER_SIZE;
-                }
-
-                [BackgroundDependencyLoader]
-                private void load(TextureStore textures)
-                {
-                    Texture = textures.Get(@"Play/Catch/fruit-catcher-idle");
-                }
-            }
         }
     }
 }
diff --git a/osu.Game.Rulesets.Catch/UI/CatcherSprite.cs b/osu.Game.Rulesets.Catch/UI/CatcherSprite.cs
new file mode 100644
index 0000000000..24e5ec500a
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/UI/CatcherSprite.cs
@@ -0,0 +1,46 @@
+// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Sprites;
+using osu.Framework.Graphics.Textures;
+using osu.Game.Skinning;
+using osuTK;
+
+namespace osu.Game.Rulesets.Catch.UI
+{
+    public class CatcherSprite : SkinReloadableDrawable
+    {
+        private Drawable catcher;
+
+        public CatcherSprite()
+        {
+            Size = new Vector2(CatcherArea.CATCHER_SIZE);
+
+            // Sets the origin roughly to the centre of the catcher's plate to allow for correct scaling.
+            OriginPosition = new Vector2(-0.02f, 0.06f) * CatcherArea.CATCHER_SIZE;
+        }
+
+        [BackgroundDependencyLoader]
+        private void load(TextureStore textures)
+        {
+            InternalChild = new Container()
+            {
+                RelativeSizeAxes = Axes.Both,
+                Child = catcher = new SkinnableDrawable("fruit-catcher-idle", _ => new Sprite()
+                {
+                    Texture = textures.Get(@"Play/Catch/fruit-catcher-idle"),
+                    RelativeSizeAxes = Axes.Both,
+                    Size = Vector2.One,
+                }, restrictSize: true)
+                {
+                    RelativeSizeAxes = Axes.Both,
+                    Anchor = Anchor.TopCentre,
+                    Origin = Anchor.TopCentre,
+                }
+            };
+        }
+    }
+}

From b6cae57694b813b1649c5e99efce25003dfce02f Mon Sep 17 00:00:00 2001
From: Lucas A <game4allyt@gmail.com>
Date: Sat, 22 Jun 2019 18:14:29 +0200
Subject: [PATCH 13/28] Add TestSceneCatcherSkinning

---
 .../TestSceneCatcherSkinning.cs               | 99 +++++++++++++++++++
 1 file changed, 99 insertions(+)
 create mode 100644 osu.Game.Rulesets.Catch.Tests/TestSceneCatcherSkinning.cs

diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherSkinning.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherSkinning.cs
new file mode 100644
index 0000000000..fa9486b087
--- /dev/null
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherSkinning.cs
@@ -0,0 +1,99 @@
+// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using NUnit.Framework;
+using osu.Framework.Allocation;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics;
+using osu.Game.Rulesets.Catch.UI;
+using osu.Game.Tests.Visual;
+using System;
+using System.Collections.Generic;
+using osu.Game.Skinning;
+using osu.Framework.Graphics.Shapes;
+using osu.Framework.Graphics.Sprites;
+using osuTK.Graphics;
+using osu.Framework.Audio.Sample;
+using osu.Framework.Graphics.Textures;
+
+namespace osu.Game.Rulesets.Catch.Tests
+{
+    [TestFixture]
+    public class TestSceneCatcherSkinning : OsuTestScene
+    {
+        public override IReadOnlyList<Type> RequiredTypes => new[]
+        {
+            typeof(CatcherSprite),
+        };
+
+        private Container container;
+
+        public TestSceneCatcherSkinning()
+        {
+            Child = container = new Container
+            {
+                Anchor = Anchor.Centre,
+                Origin = Anchor.Centre,
+            };
+        }
+
+        [BackgroundDependencyLoader]
+        private void load()
+        {
+            AddStep("show default catcher implementation", () =>
+            {
+                container.Clear();
+                container.Child = new CatcherSprite();
+            });
+
+            AddStep("show custom catcher implementation", () =>
+            {
+                container.Clear();
+                container.Child = new CatchCustomSkinSourceContainer
+                {
+                    Child = new CatcherSprite()
+                };
+            });
+        }
+
+        private class CatcherCustomSkin : Container
+        {
+            public CatcherCustomSkin()
+            {
+                RelativeSizeAxes = Axes.Both;
+
+                Children = new Drawable[]
+                {
+                    new Box
+                    {
+                        RelativeSizeAxes = Axes.Both,
+                        Colour = Color4.Blue
+                    },
+                    new SpriteText
+                    {
+                        Text = "custom"
+                    }
+                };
+            }
+        }
+
+        private class CatchCustomSkinSourceContainer : Container, ISkinSource
+        {
+            public event Action SourceChanged;
+
+            public Drawable GetDrawableComponent(string componentName) => new CatcherCustomSkin();
+
+            public SampleChannel GetSample(string sampleName) => throw new NotImplementedException();
+
+            public Texture GetTexture(string componentName) => throw new NotImplementedException();
+
+            public TValue GetValue<TConfiguration, TValue>(Func<TConfiguration, TValue> query) where TConfiguration : SkinConfiguration => throw new NotImplementedException();
+
+            protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
+            {
+                ((DependencyContainer)parent).CacheAs<ISkinSource>(this);
+                return base.CreateChildDependencies(parent);
+            }
+        }
+    }
+}

From 332ac0b82ba974f65ac4fb9d6f6cfa6c6c1bdbd3 Mon Sep 17 00:00:00 2001
From: Lucas A <game4allyt@gmail.com>
Date: Sat, 22 Jun 2019 18:23:20 +0200
Subject: [PATCH 14/28] Fix CI inspections

---
 osu.Game.Rulesets.Catch.Tests/TestSceneCatcherSkinning.cs | 8 ++++++--
 osu.Game.Rulesets.Catch/UI/CatcherArea.cs                 | 1 -
 osu.Game.Rulesets.Catch/UI/CatcherSprite.cs               | 6 ++----
 3 files changed, 8 insertions(+), 7 deletions(-)

diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherSkinning.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherSkinning.cs
index fa9486b087..e1ec88d4a9 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherSkinning.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherSkinning.cs
@@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Catch.Tests
             typeof(CatcherSprite),
         };
 
-        private Container container;
+        private readonly Container container;
 
         public TestSceneCatcherSkinning()
         {
@@ -79,7 +79,11 @@ namespace osu.Game.Rulesets.Catch.Tests
 
         private class CatchCustomSkinSourceContainer : Container, ISkinSource
         {
-            public event Action SourceChanged;
+            public event Action SourceChanged
+            {
+                add { }
+                remove { }
+            }
 
             public Drawable GetDrawableComponent(string componentName) => new CatcherCustomSkin();
 
diff --git a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs
index 18121bba4b..0b06e958e6 100644
--- a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs
+++ b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs
@@ -6,7 +6,6 @@ using System.Linq;
 using osu.Framework.Allocation;
 using osu.Framework.Graphics;
 using osu.Framework.Graphics.Containers;
-using osu.Framework.Graphics.Sprites;
 using osu.Framework.Input.Bindings;
 using osu.Framework.MathUtils;
 using osu.Game.Beatmaps;
diff --git a/osu.Game.Rulesets.Catch/UI/CatcherSprite.cs b/osu.Game.Rulesets.Catch/UI/CatcherSprite.cs
index 24e5ec500a..1308a9b7a1 100644
--- a/osu.Game.Rulesets.Catch/UI/CatcherSprite.cs
+++ b/osu.Game.Rulesets.Catch/UI/CatcherSprite.cs
@@ -13,8 +13,6 @@ namespace osu.Game.Rulesets.Catch.UI
 {
     public class CatcherSprite : SkinReloadableDrawable
     {
-        private Drawable catcher;
-
         public CatcherSprite()
         {
             Size = new Vector2(CatcherArea.CATCHER_SIZE);
@@ -26,10 +24,10 @@ namespace osu.Game.Rulesets.Catch.UI
         [BackgroundDependencyLoader]
         private void load(TextureStore textures)
         {
-            InternalChild = new Container()
+            InternalChild = new Container
             {
                 RelativeSizeAxes = Axes.Both,
-                Child = catcher = new SkinnableDrawable("fruit-catcher-idle", _ => new Sprite()
+                Child = new SkinnableDrawable("fruit-catcher-idle", _ => new Sprite
                 {
                     Texture = textures.Get(@"Play/Catch/fruit-catcher-idle"),
                     RelativeSizeAxes = Axes.Both,

From 4cd8b6a2bd48d7ffc57a335dae4ec9e4f9bdc2a8 Mon Sep 17 00:00:00 2001
From: smoogipoo <smoogipoo@smgi.me>
Date: Mon, 24 Jun 2019 13:37:06 +0900
Subject: [PATCH 15/28] Fix unsafe caching of dependencies

---
 osu.Game.Rulesets.Catch.Tests/TestSceneCatcherSkinning.cs | 7 +------
 1 file changed, 1 insertion(+), 6 deletions(-)

diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherSkinning.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherSkinning.cs
index e1ec88d4a9..a56a1e31c1 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherSkinning.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherSkinning.cs
@@ -77,6 +77,7 @@ namespace osu.Game.Rulesets.Catch.Tests
             }
         }
 
+        [Cached(typeof(ISkinSource))]
         private class CatchCustomSkinSourceContainer : Container, ISkinSource
         {
             public event Action SourceChanged
@@ -92,12 +93,6 @@ namespace osu.Game.Rulesets.Catch.Tests
             public Texture GetTexture(string componentName) => throw new NotImplementedException();
 
             public TValue GetValue<TConfiguration, TValue>(Func<TConfiguration, TValue> query) where TConfiguration : SkinConfiguration => throw new NotImplementedException();
-
-            protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
-            {
-                ((DependencyContainer)parent).CacheAs<ISkinSource>(this);
-                return base.CreateChildDependencies(parent);
-            }
         }
     }
 }

From 2a4bd612d7e31a9a44467d6144f83d24c9c10052 Mon Sep 17 00:00:00 2001
From: smoogipoo <smoogipoo@smgi.me>
Date: Mon, 24 Jun 2019 13:47:37 +0900
Subject: [PATCH 16/28] Rename test scene

---
 .../{TestSceneCatcherSkinning.cs => TestSceneCatcher.cs}      | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)
 rename osu.Game.Rulesets.Catch.Tests/{TestSceneCatcherSkinning.cs => TestSceneCatcher.cs} (96%)

diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherSkinning.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs
similarity index 96%
rename from osu.Game.Rulesets.Catch.Tests/TestSceneCatcherSkinning.cs
rename to osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs
index a56a1e31c1..e4c01cb668 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherSkinning.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs
@@ -19,7 +19,7 @@ using osu.Framework.Graphics.Textures;
 namespace osu.Game.Rulesets.Catch.Tests
 {
     [TestFixture]
-    public class TestSceneCatcherSkinning : OsuTestScene
+    public class TestSceneCatcher : OsuTestScene
     {
         public override IReadOnlyList<Type> RequiredTypes => new[]
         {
@@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Catch.Tests
 
         private readonly Container container;
 
-        public TestSceneCatcherSkinning()
+        public TestSceneCatcher()
         {
             Child = container = new Container
             {

From 06b087db701a97033fa15c678b73265afd1d1f41 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Mon, 24 Jun 2019 14:39:20 +0900
Subject: [PATCH 17/28] Add SkinnableSprite implementation

---
 osu.Game/Skinning/SkinnableDrawable.cs | 26 +++++++++++++++++-------
 osu.Game/Skinning/SkinnableSprite.cs   | 28 ++++++++++++++++++++++++++
 2 files changed, 47 insertions(+), 7 deletions(-)
 create mode 100644 osu.Game/Skinning/SkinnableSprite.cs

diff --git a/osu.Game/Skinning/SkinnableDrawable.cs b/osu.Game/Skinning/SkinnableDrawable.cs
index 3ca58dc625..64ef50111c 100644
--- a/osu.Game/Skinning/SkinnableDrawable.cs
+++ b/osu.Game/Skinning/SkinnableDrawable.cs
@@ -23,6 +23,8 @@ namespace osu.Game.Skinning
         /// </summary>
         protected Drawable Drawable { get; private set; }
 
+        protected virtual T CreateDefault() => createDefault(componentName);
+
         private readonly Func<string, T> createDefault;
 
         private readonly string componentName;
@@ -37,34 +39,44 @@ namespace osu.Game.Skinning
         /// <param name="allowFallback">A conditional to decide whether to allow fallback to the default implementation if a skinned element is not present.</param>
         /// <param name="restrictSize">Whether a user-skin drawable should be limited to the size of our parent.</param>
         public SkinnableDrawable(string name, Func<string, T> defaultImplementation, Func<ISkinSource, bool> allowFallback = null, bool restrictSize = true)
+            : this(name, allowFallback, restrictSize)
+        {
+            createDefault = defaultImplementation;
+        }
+
+        protected SkinnableDrawable(string name, Func<ISkinSource, bool> allowFallback = null, bool restrictSize = true)
             : base(allowFallback)
         {
             componentName = name;
-            createDefault = defaultImplementation;
             this.restrictSize = restrictSize;
 
             RelativeSizeAxes = Axes.Both;
         }
 
+        protected virtual bool ApplySizeToDefault => false;
+
         protected override void SkinChanged(ISkinSource skin, bool allowFallback)
         {
             Drawable = skin.GetDrawableComponent(componentName);
 
+            bool isDefault = false;
+
+            if (Drawable == null && allowFallback)
+            {
+                Drawable = CreateDefault();
+                isDefault = true;
+            }
+
             if (Drawable != null)
             {
-                if (restrictSize)
+                if (restrictSize && (!isDefault || ApplySizeToDefault))
                 {
                     Drawable.RelativeSizeAxes = Axes.Both;
                     Drawable.Size = Vector2.One;
                     Drawable.Scale = Vector2.One;
                     Drawable.FillMode = FillMode.Fit;
                 }
-            }
-            else if (allowFallback)
-                Drawable = createDefault(componentName);
 
-            if (Drawable != null)
-            {
                 Drawable.Origin = Anchor.Centre;
                 Drawable.Anchor = Anchor.Centre;
 
diff --git a/osu.Game/Skinning/SkinnableSprite.cs b/osu.Game/Skinning/SkinnableSprite.cs
new file mode 100644
index 0000000000..4cf054b704
--- /dev/null
+++ b/osu.Game/Skinning/SkinnableSprite.cs
@@ -0,0 +1,28 @@
+// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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.Sprites;
+using osu.Framework.Graphics.Textures;
+
+namespace osu.Game.Skinning
+{
+    public class SkinnableSprite : SkinnableDrawable<Sprite>
+    {
+        protected override bool ApplySizeToDefault => true;
+
+        private readonly string defaultName;
+
+        protected override Sprite CreateDefault() => new Sprite { Texture = textures.Get(defaultName) };
+
+        [Resolved]
+        private TextureStore textures { get; set; }
+
+        public SkinnableSprite(string name, string defaultName, Func<ISkinSource, bool> allowFallback = null, bool restrictSize = true)
+            : base(name, allowFallback, restrictSize)
+        {
+            this.defaultName = defaultName;
+        }
+    }
+}

From d9f701176995af7b005034438e97b87878bed7f8 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Mon, 24 Jun 2019 14:39:46 +0900
Subject: [PATCH 18/28] Use SkinnableSprite to reduce complexity of
 implementation

---
 osu.Game.Rulesets.Catch/UI/CatcherSprite.cs | 21 +++++----------------
 osu.Game/Skinning/SkinnableDrawable.cs      |  4 ++--
 osu.Game/Skinning/SkinnableSprite.cs        |  7 ++-----
 3 files changed, 9 insertions(+), 23 deletions(-)

diff --git a/osu.Game.Rulesets.Catch/UI/CatcherSprite.cs b/osu.Game.Rulesets.Catch/UI/CatcherSprite.cs
index 1308a9b7a1..df396279fc 100644
--- a/osu.Game.Rulesets.Catch/UI/CatcherSprite.cs
+++ b/osu.Game.Rulesets.Catch/UI/CatcherSprite.cs
@@ -1,11 +1,9 @@
-// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
+// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
 // See the LICENCE file in the repository root for full licence text.
 
 using osu.Framework.Allocation;
 using osu.Framework.Graphics;
 using osu.Framework.Graphics.Containers;
-using osu.Framework.Graphics.Sprites;
-using osu.Framework.Graphics.Textures;
 using osu.Game.Skinning;
 using osuTK;
 
@@ -22,22 +20,13 @@ namespace osu.Game.Rulesets.Catch.UI
         }
 
         [BackgroundDependencyLoader]
-        private void load(TextureStore textures)
+        private void load()
         {
-            InternalChild = new Container
+            InternalChild = new SkinnableSprite(@"Play/Catch/fruit-catcher-idle")
             {
                 RelativeSizeAxes = Axes.Both,
-                Child = new SkinnableDrawable("fruit-catcher-idle", _ => new Sprite
-                {
-                    Texture = textures.Get(@"Play/Catch/fruit-catcher-idle"),
-                    RelativeSizeAxes = Axes.Both,
-                    Size = Vector2.One,
-                }, restrictSize: true)
-                {
-                    RelativeSizeAxes = Axes.Both,
-                    Anchor = Anchor.TopCentre,
-                    Origin = Anchor.TopCentre,
-                }
+                Anchor = Anchor.TopCentre,
+                Origin = Anchor.TopCentre,
             };
         }
     }
diff --git a/osu.Game/Skinning/SkinnableDrawable.cs b/osu.Game/Skinning/SkinnableDrawable.cs
index 64ef50111c..dcbb63435a 100644
--- a/osu.Game/Skinning/SkinnableDrawable.cs
+++ b/osu.Game/Skinning/SkinnableDrawable.cs
@@ -23,7 +23,7 @@ namespace osu.Game.Skinning
         /// </summary>
         protected Drawable Drawable { get; private set; }
 
-        protected virtual T CreateDefault() => createDefault(componentName);
+        protected virtual T CreateDefault(string name) => createDefault(name);
 
         private readonly Func<string, T> createDefault;
 
@@ -63,7 +63,7 @@ namespace osu.Game.Skinning
 
             if (Drawable == null && allowFallback)
             {
-                Drawable = CreateDefault();
+                Drawable = CreateDefault(componentName);
                 isDefault = true;
             }
 
diff --git a/osu.Game/Skinning/SkinnableSprite.cs b/osu.Game/Skinning/SkinnableSprite.cs
index 4cf054b704..72b5740bd6 100644
--- a/osu.Game/Skinning/SkinnableSprite.cs
+++ b/osu.Game/Skinning/SkinnableSprite.cs
@@ -12,17 +12,14 @@ namespace osu.Game.Skinning
     {
         protected override bool ApplySizeToDefault => true;
 
-        private readonly string defaultName;
-
-        protected override Sprite CreateDefault() => new Sprite { Texture = textures.Get(defaultName) };
+        protected override Sprite CreateDefault(string name) => new Sprite { Texture = textures.Get(name) };
 
         [Resolved]
         private TextureStore textures { get; set; }
 
-        public SkinnableSprite(string name, string defaultName, Func<ISkinSource, bool> allowFallback = null, bool restrictSize = true)
+        public SkinnableSprite(string name, Func<ISkinSource, bool> allowFallback = null, bool restrictSize = true)
             : base(name, allowFallback, restrictSize)
         {
-            this.defaultName = defaultName;
         }
     }
 }

From 9baf8160d1e07256d36fe4ec7daee20f0be3259c Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Mon, 24 Jun 2019 14:40:08 +0900
Subject: [PATCH 19/28] Remove pointless SkinReloadableDrawable specification

---
 osu.Game.Rulesets.Catch/UI/CatcherSprite.cs | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/osu.Game.Rulesets.Catch/UI/CatcherSprite.cs b/osu.Game.Rulesets.Catch/UI/CatcherSprite.cs
index df396279fc..c0c1952064 100644
--- a/osu.Game.Rulesets.Catch/UI/CatcherSprite.cs
+++ b/osu.Game.Rulesets.Catch/UI/CatcherSprite.cs
@@ -1,4 +1,4 @@
-// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
+// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
 // See the LICENCE file in the repository root for full licence text.
 
 using osu.Framework.Allocation;
@@ -9,7 +9,7 @@ using osuTK;
 
 namespace osu.Game.Rulesets.Catch.UI
 {
-    public class CatcherSprite : SkinReloadableDrawable
+    public class CatcherSprite : CompositeDrawable
     {
         public CatcherSprite()
         {

From 9ab520494773355c523d14f5eb3892c395377675 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Mon, 24 Jun 2019 15:09:28 +0900
Subject: [PATCH 20/28] Make test a bit more sane

---
 .../TestSceneCatcher.cs                       | 27 ++++++++++++-------
 1 file changed, 17 insertions(+), 10 deletions(-)

diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs
index e4c01cb668..33f93cdb4a 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs
@@ -40,15 +40,10 @@ namespace osu.Game.Rulesets.Catch.Tests
         [BackgroundDependencyLoader]
         private void load()
         {
-            AddStep("show default catcher implementation", () =>
-            {
-                container.Clear();
-                container.Child = new CatcherSprite();
-            });
+            AddStep("show default catcher implementation", () => { container.Child = new CatcherSprite(); });
 
             AddStep("show custom catcher implementation", () =>
             {
-                container.Clear();
                 container.Child = new CatchCustomSkinSourceContainer
                 {
                     Child = new CatcherSprite()
@@ -86,13 +81,25 @@ namespace osu.Game.Rulesets.Catch.Tests
                 remove { }
             }
 
-            public Drawable GetDrawableComponent(string componentName) => new CatcherCustomSkin();
+            public Drawable GetDrawableComponent(string componentName)
+            {
+                switch (componentName)
+                {
+                    case "Play/Catch/fruit-catcher-idle":
+                        return new CatcherCustomSkin();
+                }
 
-            public SampleChannel GetSample(string sampleName) => throw new NotImplementedException();
+                return null;
+            }
 
-            public Texture GetTexture(string componentName) => throw new NotImplementedException();
+            public SampleChannel GetSample(string sampleName) =>
+                throw new NotImplementedException();
 
-            public TValue GetValue<TConfiguration, TValue>(Func<TConfiguration, TValue> query) where TConfiguration : SkinConfiguration => throw new NotImplementedException();
+            public Texture GetTexture(string componentName) =>
+                throw new NotImplementedException();
+
+            public TValue GetValue<TConfiguration, TValue>(Func<TConfiguration, TValue> query) where TConfiguration : SkinConfiguration =>
+                throw new NotImplementedException();
         }
     }
 }

From 3f22c0a311abfc79bf32104a0b99c44fa30412c8 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Mon, 24 Jun 2019 14:39:20 +0900
Subject: [PATCH 21/28] Add SkinnableSprite implementation

---
 osu.Game/Skinning/SkinnableDrawable.cs | 26 +++++++++++++++++++-------
 osu.Game/Skinning/SkinnableSprite.cs   | 25 +++++++++++++++++++++++++
 2 files changed, 44 insertions(+), 7 deletions(-)
 create mode 100644 osu.Game/Skinning/SkinnableSprite.cs

diff --git a/osu.Game/Skinning/SkinnableDrawable.cs b/osu.Game/Skinning/SkinnableDrawable.cs
index 3ca58dc625..dcbb63435a 100644
--- a/osu.Game/Skinning/SkinnableDrawable.cs
+++ b/osu.Game/Skinning/SkinnableDrawable.cs
@@ -23,6 +23,8 @@ namespace osu.Game.Skinning
         /// </summary>
         protected Drawable Drawable { get; private set; }
 
+        protected virtual T CreateDefault(string name) => createDefault(name);
+
         private readonly Func<string, T> createDefault;
 
         private readonly string componentName;
@@ -37,34 +39,44 @@ namespace osu.Game.Skinning
         /// <param name="allowFallback">A conditional to decide whether to allow fallback to the default implementation if a skinned element is not present.</param>
         /// <param name="restrictSize">Whether a user-skin drawable should be limited to the size of our parent.</param>
         public SkinnableDrawable(string name, Func<string, T> defaultImplementation, Func<ISkinSource, bool> allowFallback = null, bool restrictSize = true)
+            : this(name, allowFallback, restrictSize)
+        {
+            createDefault = defaultImplementation;
+        }
+
+        protected SkinnableDrawable(string name, Func<ISkinSource, bool> allowFallback = null, bool restrictSize = true)
             : base(allowFallback)
         {
             componentName = name;
-            createDefault = defaultImplementation;
             this.restrictSize = restrictSize;
 
             RelativeSizeAxes = Axes.Both;
         }
 
+        protected virtual bool ApplySizeToDefault => false;
+
         protected override void SkinChanged(ISkinSource skin, bool allowFallback)
         {
             Drawable = skin.GetDrawableComponent(componentName);
 
+            bool isDefault = false;
+
+            if (Drawable == null && allowFallback)
+            {
+                Drawable = CreateDefault(componentName);
+                isDefault = true;
+            }
+
             if (Drawable != null)
             {
-                if (restrictSize)
+                if (restrictSize && (!isDefault || ApplySizeToDefault))
                 {
                     Drawable.RelativeSizeAxes = Axes.Both;
                     Drawable.Size = Vector2.One;
                     Drawable.Scale = Vector2.One;
                     Drawable.FillMode = FillMode.Fit;
                 }
-            }
-            else if (allowFallback)
-                Drawable = createDefault(componentName);
 
-            if (Drawable != null)
-            {
                 Drawable.Origin = Anchor.Centre;
                 Drawable.Anchor = Anchor.Centre;
 
diff --git a/osu.Game/Skinning/SkinnableSprite.cs b/osu.Game/Skinning/SkinnableSprite.cs
new file mode 100644
index 0000000000..72b5740bd6
--- /dev/null
+++ b/osu.Game/Skinning/SkinnableSprite.cs
@@ -0,0 +1,25 @@
+// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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.Sprites;
+using osu.Framework.Graphics.Textures;
+
+namespace osu.Game.Skinning
+{
+    public class SkinnableSprite : SkinnableDrawable<Sprite>
+    {
+        protected override bool ApplySizeToDefault => true;
+
+        protected override Sprite CreateDefault(string name) => new Sprite { Texture = textures.Get(name) };
+
+        [Resolved]
+        private TextureStore textures { get; set; }
+
+        public SkinnableSprite(string name, Func<ISkinSource, bool> allowFallback = null, bool restrictSize = true)
+            : base(name, allowFallback, restrictSize)
+        {
+        }
+    }
+}

From aca9289d89df4c0984a0416ad689cff0ddff6f16 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Mon, 24 Jun 2019 15:17:56 +0900
Subject: [PATCH 22/28] Use SkinnableSprite for approach circle

---
 .../Objects/Drawables/Pieces/ApproachCircle.cs                 | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/ApproachCircle.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/ApproachCircle.cs
index 8ee065aaea..9981585f9e 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/ApproachCircle.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/ApproachCircle.cs
@@ -4,7 +4,6 @@
 using osu.Framework.Allocation;
 using osu.Framework.Graphics;
 using osu.Framework.Graphics.Containers;
-using osu.Framework.Graphics.Sprites;
 using osu.Framework.Graphics.Textures;
 using osu.Game.Skinning;
 
@@ -25,7 +24,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
         [BackgroundDependencyLoader]
         private void load(TextureStore textures)
         {
-            Child = new SkinnableDrawable("Play/osu/approachcircle", name => new Sprite { Texture = textures.Get(name) });
+            Child = new SkinnableSprite("Play/osu/approachcircle");
         }
     }
 }

From 9593e66a968d075066512f2a5540d2cfe6b37cc5 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Mon, 24 Jun 2019 15:25:01 +0900
Subject: [PATCH 23/28] Add some more xmldoc

---
 osu.Game/Skinning/SkinnableDrawable.cs | 13 ++++++++++---
 osu.Game/Skinning/SkinnableSprite.cs   |  5 ++++-
 2 files changed, 14 insertions(+), 4 deletions(-)

diff --git a/osu.Game/Skinning/SkinnableDrawable.cs b/osu.Game/Skinning/SkinnableDrawable.cs
index dcbb63435a..0a7c398990 100644
--- a/osu.Game/Skinning/SkinnableDrawable.cs
+++ b/osu.Game/Skinning/SkinnableDrawable.cs
@@ -15,6 +15,10 @@ namespace osu.Game.Skinning
         }
     }
 
+    /// <summary>
+    /// A drawable which can be skinned via an <see cref="ISkinSource"/>.
+    /// </summary>
+    /// <typeparam name="T">The type of drawable.</typeparam>
     public class SkinnableDrawable<T> : SkinReloadableDrawable
         where T : Drawable
     {
@@ -32,7 +36,7 @@ namespace osu.Game.Skinning
         private readonly bool restrictSize;
 
         /// <summary>
-        ///
+        /// Create a new skinnable drawable.
         /// </summary>
         /// <param name="name">The namespace-complete resource name for this skinnable element.</param>
         /// <param name="defaultImplementation">A function to create the default skin implementation of this element.</param>
@@ -53,7 +57,10 @@ namespace osu.Game.Skinning
             RelativeSizeAxes = Axes.Both;
         }
 
-        protected virtual bool ApplySizeToDefault => false;
+        /// <summary>
+        /// Whether to apply size restrictions (specified via <see cref="restrictSize"/>) to the default implementation.
+        /// </summary>
+        protected virtual bool ApplySizeRestrictionsToDefault => false;
 
         protected override void SkinChanged(ISkinSource skin, bool allowFallback)
         {
@@ -69,7 +76,7 @@ namespace osu.Game.Skinning
 
             if (Drawable != null)
             {
-                if (restrictSize && (!isDefault || ApplySizeToDefault))
+                if (restrictSize && (!isDefault || ApplySizeRestrictionsToDefault))
                 {
                     Drawable.RelativeSizeAxes = Axes.Both;
                     Drawable.Size = Vector2.One;
diff --git a/osu.Game/Skinning/SkinnableSprite.cs b/osu.Game/Skinning/SkinnableSprite.cs
index 72b5740bd6..9771f4cc19 100644
--- a/osu.Game/Skinning/SkinnableSprite.cs
+++ b/osu.Game/Skinning/SkinnableSprite.cs
@@ -8,9 +8,12 @@ using osu.Framework.Graphics.Textures;
 
 namespace osu.Game.Skinning
 {
+    /// <summary>
+    /// A skinnable element which uses a stable sprite and can therefore share implementation logic.
+    /// </summary>
     public class SkinnableSprite : SkinnableDrawable<Sprite>
     {
-        protected override bool ApplySizeToDefault => true;
+        protected override bool ApplySizeRestrictionsToDefault => true;
 
         protected override Sprite CreateDefault(string name) => new Sprite { Texture = textures.Get(name) };
 

From 06eaba766b93261b0ecdbc5732202ec40955247a Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Mon, 24 Jun 2019 15:27:46 +0900
Subject: [PATCH 24/28] Move method below ctor

---
 osu.Game/Skinning/SkinnableDrawable.cs | 8 ++++----
 osu.Game/Skinning/SkinnableSprite.cs   | 4 ++--
 2 files changed, 6 insertions(+), 6 deletions(-)

diff --git a/osu.Game/Skinning/SkinnableDrawable.cs b/osu.Game/Skinning/SkinnableDrawable.cs
index 0a7c398990..995cb15136 100644
--- a/osu.Game/Skinning/SkinnableDrawable.cs
+++ b/osu.Game/Skinning/SkinnableDrawable.cs
@@ -27,10 +27,6 @@ namespace osu.Game.Skinning
         /// </summary>
         protected Drawable Drawable { get; private set; }
 
-        protected virtual T CreateDefault(string name) => createDefault(name);
-
-        private readonly Func<string, T> createDefault;
-
         private readonly string componentName;
 
         private readonly bool restrictSize;
@@ -57,6 +53,10 @@ namespace osu.Game.Skinning
             RelativeSizeAxes = Axes.Both;
         }
 
+        private readonly Func<string, T> createDefault;
+
+        protected virtual T CreateDefault(string name) => createDefault(name);
+
         /// <summary>
         /// Whether to apply size restrictions (specified via <see cref="restrictSize"/>) to the default implementation.
         /// </summary>
diff --git a/osu.Game/Skinning/SkinnableSprite.cs b/osu.Game/Skinning/SkinnableSprite.cs
index 9771f4cc19..ceb1ed0f70 100644
--- a/osu.Game/Skinning/SkinnableSprite.cs
+++ b/osu.Game/Skinning/SkinnableSprite.cs
@@ -15,8 +15,6 @@ namespace osu.Game.Skinning
     {
         protected override bool ApplySizeRestrictionsToDefault => true;
 
-        protected override Sprite CreateDefault(string name) => new Sprite { Texture = textures.Get(name) };
-
         [Resolved]
         private TextureStore textures { get; set; }
 
@@ -24,5 +22,7 @@ namespace osu.Game.Skinning
             : base(name, allowFallback, restrictSize)
         {
         }
+
+        protected override Sprite CreateDefault(string name) => new Sprite { Texture = textures.Get(name) };
     }
 }

From 2473a9487a0738c6e405d36caf72bb8da831e792 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Mon, 24 Jun 2019 17:38:11 +0900
Subject: [PATCH 25/28] Add missing using

---
 osu.Game/Overlays/UserProfileOverlay.cs | 1 +
 1 file changed, 1 insertion(+)

diff --git a/osu.Game/Overlays/UserProfileOverlay.cs b/osu.Game/Overlays/UserProfileOverlay.cs
index 5b8c2bb357..058d84d32f 100644
--- a/osu.Game/Overlays/UserProfileOverlay.cs
+++ b/osu.Game/Overlays/UserProfileOverlay.cs
@@ -13,6 +13,7 @@ using osu.Game.Online.API.Requests;
 using osu.Game.Overlays.Profile;
 using osu.Game.Overlays.Profile.Sections;
 using osu.Game.Users;
+using osuTK;
 
 namespace osu.Game.Overlays
 {

From fad1ced1b57303be23a28ee5e4077100950148fe Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Mon, 24 Jun 2019 18:15:27 +0900
Subject: [PATCH 26/28] Add correct conditionals to allow exit

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

diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs
index c3e351a0ca..c3a9ffdaba 100644
--- a/osu.Game/Screens/Play/Player.cs
+++ b/osu.Game/Screens/Play/Player.cs
@@ -177,6 +177,16 @@ namespace osu.Game.Screens.Play
                         Restart();
                     },
                 },
+                new HotkeyExitOverlay
+                {
+                    Action = () =>
+                    {
+                        if (!this.IsCurrentScreen()) return;
+
+                        fadeOut(true);
+                        performUserRequestedExit();
+                    },
+                },
                 failAnimation = new FailAnimation(DrawableRuleset) { OnComplete = onFailComplete, }
             };
 
@@ -245,6 +255,11 @@ namespace osu.Game.Screens.Play
         {
             if (!this.IsCurrentScreen()) return;
 
+            // if a restart has been requested, cancel any pending completion (user has shown intent to restart).
+            onCompletionEvent = null;
+
+            ValidForResume = false;
+
             this.Exit();
         }
 

From 2ea5165803928c0fe4e11f452012cee6fdea7636 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Mon, 24 Jun 2019 18:19:17 +0900
Subject: [PATCH 27/28] Change case to match; change hotkey to be more globally
 usable (previous has conflict on macOS)

---
 osu.Game/Input/Bindings/GlobalActionContainer.cs | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/osu.Game/Input/Bindings/GlobalActionContainer.cs b/osu.Game/Input/Bindings/GlobalActionContainer.cs
index cb913e386b..904aa9c8c0 100644
--- a/osu.Game/Input/Bindings/GlobalActionContainer.cs
+++ b/osu.Game/Input/Bindings/GlobalActionContainer.cs
@@ -50,7 +50,7 @@ namespace osu.Game.Input.Bindings
         {
             new KeyBinding(InputKey.Space, GlobalAction.SkipCutscene),
             new KeyBinding(InputKey.Tilde, GlobalAction.QuickRetry),
-            new KeyBinding(new[] { InputKey.Alt, InputKey.Tilde }, GlobalAction.QuickExit),
+            new KeyBinding(new[] { InputKey.Control, InputKey.Tilde }, GlobalAction.QuickExit),
             new KeyBinding(new[] { InputKey.Control, InputKey.Plus }, GlobalAction.IncreaseScrollSpeed),
             new KeyBinding(new[] { InputKey.Control, InputKey.Minus }, GlobalAction.DecreaseScrollSpeed),
         };
@@ -95,6 +95,9 @@ namespace osu.Game.Input.Bindings
         [Description("Quick retry (hold)")]
         QuickRetry,
 
+        [Description("Quick exit (Hold)")]
+        QuickExit,
+
         [Description("Take screenshot")]
         TakeScreenshot,
 
@@ -112,8 +115,5 @@ namespace osu.Game.Input.Bindings
 
         [Description("Select")]
         Select,
-
-        [Description("Quick Exit (Hold)")]
-        QuickExit,
     }
 }

From f91467eddc6be5dc731d6236bbb040a4590529ed Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Mon, 24 Jun 2019 18:29:30 +0900
Subject: [PATCH 28/28] Fix licence header

---
 osu.Game/Screens/Play/HotkeyExitOverlay.cs | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/osu.Game/Screens/Play/HotkeyExitOverlay.cs b/osu.Game/Screens/Play/HotkeyExitOverlay.cs
index 391cc2b753..c18aecda55 100644
--- a/osu.Game/Screens/Play/HotkeyExitOverlay.cs
+++ b/osu.Game/Screens/Play/HotkeyExitOverlay.cs
@@ -1,5 +1,5 @@
-// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
-// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
+// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
 
 using osu.Framework.Input.Bindings;
 using osu.Game.Input.Bindings;