diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 4dd42b7fd2..4ea9fae183 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -189,19 +189,14 @@ namespace osu.Game /// public readonly IBindable OverlayActivationMode = new Bindable(); - /// - /// Whether the back button is currently displayed. - /// - private readonly IBindable backButtonVisibility = new BindableBool(); - IBindable ILocalUserPlayInfo.PlayingState => UserPlayingState; protected readonly Bindable UserPlayingState = new Bindable(); protected OsuScreenStack ScreenStack; - protected BackButton BackButton; - protected ScreenFooter ScreenFooter; + protected BackButton BackButton => screenStackFooter.BackButton; + protected ScreenFooter ScreenFooter => screenStackFooter.Footer; protected SettingsOverlay Settings; @@ -233,6 +228,8 @@ namespace osu.Game private RealmDetachedBeatmapStore detachedBeatmapStore; + private ScreenStackFooter screenStackFooter; + private readonly string[] args; private readonly List focusedOverlays = new List(); @@ -1132,12 +1129,6 @@ namespace osu.Game { backReceptor = new ScreenFooter.BackReceptor(), ScreenStack = new OsuScreenStack { RelativeSizeAxes = Axes.Both }, - BackButton = new BackButton(backReceptor) - { - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - Action = handleBackButton, - }, logoContainer = new Container { RelativeSizeAxes = Axes.Both }, // TODO: what is this? why is this? // TODO: this is being screen scaled even though it's probably AN OVERLAY. @@ -1150,7 +1141,7 @@ namespace osu.Game { Depth = -1, RelativeSizeAxes = Axes.Both, - Child = ScreenFooter = new ScreenFooter(backReceptor) + Child = screenStackFooter = new ScreenStackFooter(ScreenStack, backReceptor) { // TODO: this is really really weird and should not exist. RequestLogoInFront = inFront => ScreenContainer.ChangeChildDepth(logoContainer, inFront ? float.MinValue : 0), @@ -1324,14 +1315,6 @@ namespace osu.Game if (mode.NewValue != OverlayActivation.All) CloseAllOverlays(); }; - backButtonVisibility.ValueChanged += visible => - { - if (visible.NewValue) - BackButton.Show(); - else - BackButton.Hide(); - }; - // Importantly, this should be run after binding PostNotification to the import handlers so they can present the import after game startup. handleStartupImport(); } @@ -1723,13 +1706,12 @@ namespace osu.Game if (current != null) { - backButtonVisibility.UnbindFrom(current.BackButtonVisibility); OverlayActivationMode.UnbindFrom(current.OverlayActivationMode); configUserActivity.UnbindFrom(current.Activity); } // Bind to new screen. - if (newScreen != null) + if (newScreen is OsuScreen newOsuScreen) { OverlayActivationMode.BindTo(newScreen.OverlayActivationMode); configUserActivity.BindTo(newScreen.Activity); @@ -1742,45 +1724,6 @@ namespace osu.Game else Toolbar.Show(); - var newOsuScreen = (OsuScreen)newScreen; - - if (newScreen.ShowFooter) - { - // the legacy back button should never display while the new footer is in use, as it - // contains its own local back button. - ((BindableBool)backButtonVisibility).Value = false; - - BackButton.Hide(); - ScreenFooter.Show(); - - if (newOsuScreen.IsLoaded) - updateFooterButtons(); - else - { - // ensure the current buttons are immediately disabled on screen change (so they can't be pressed). - ScreenFooter.SetButtons(Array.Empty()); - - newOsuScreen.OnLoadComplete += _ => updateFooterButtons(); - } - - void updateFooterButtons() - { - var buttons = newScreen.CreateFooterButtons(); - - newOsuScreen.LoadComponentsAgainstScreenDependencies(buttons); - - ScreenFooter.SetButtons(buttons); - ScreenFooter.Show(); - } - } - else - { - backButtonVisibility.BindTo(newScreen.BackButtonVisibility); - - ScreenFooter.SetButtons(Array.Empty()); - ScreenFooter.Hide(); - } - skinEditor.SetTarget(newOsuScreen); } } diff --git a/osu.Game/Screens/Footer/ScreenStackFooter.cs b/osu.Game/Screens/Footer/ScreenStackFooter.cs new file mode 100644 index 0000000000..807dcc3fe0 --- /dev/null +++ b/osu.Game/Screens/Footer/ScreenStackFooter.cs @@ -0,0 +1,220 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Screens; +using osu.Game.Graphics.UserInterface; + +namespace osu.Game.Screens.Footer +{ + public partial class ScreenStackFooter : CompositeDrawable + { + /// + /// Called when logo tracking begins, intended to bring the osu! logo to the frontmost visually. + /// + public Action? RequestLogoInFront { private get; init; } + + /// + /// The back button was pressed. + /// + public Action? BackButtonPressed { private get; init; } + + /// + /// The (legacy) back button. + /// + public readonly BackButton BackButton; + + /// + /// The footer. + /// + public readonly ScreenFooter Footer; + + /// + /// Whether the legacy back button is currently displayed. + /// + private readonly IBindable backButtonVisibility = new BindableBool(); + + private readonly ScreenStackTracker screenTracker; + + public ScreenStackFooter(ScreenStack screenStack, ScreenFooter.BackReceptor? backReceptor = null) + { + RelativeSizeAxes = Axes.Both; + + InternalChildren = new Drawable[] + { + BackButton = new BackButton(backReceptor) + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Action = () => BackButtonPressed?.Invoke(), + }, + Footer = new ScreenFooter(backReceptor) + { + RequestLogoInFront = v => RequestLogoInFront?.Invoke(v), + BackButtonPressed = () => BackButtonPressed?.Invoke() + } + }; + + screenTracker = new ScreenStackTracker(screenStack); + screenTracker.ScreenChanged += onScreenChanged; + + backButtonVisibility.ValueChanged += onBackButtonVisibilityChanged; + } + + private void onScreenChanged(IScreen lastScreen, IScreen newScreen) + { + unbindScreen(lastScreen); + bindScreen(newScreen); + } + + private void onBackButtonVisibilityChanged(ValueChangedEvent visible) + { + if (visible.NewValue) + BackButton.Show(); + else + BackButton.Hide(); + } + + private void unbindScreen(IScreen screen) + { + if (screen is not OsuScreen osuScreen) + return; + + backButtonVisibility.UnbindFrom(osuScreen.BackButtonVisibility); + } + + private void bindScreen(IScreen screen) + { + if (screen is not OsuScreen osuScreen) + { + ((BindableBool)backButtonVisibility).Value = true; + + Footer.SetButtons([]); + Footer.Hide(); + return; + } + + if (osuScreen.ShowFooter) + { + // the legacy back button should never display while the new footer is in use, as it + // contains its own local back button. + ((BindableBool)backButtonVisibility).Value = false; + + Footer.Show(); + + if (osuScreen.IsLoaded) + updateFooterButtons(); + else + { + // ensure the current buttons are immediately disabled on screen change (so they can't be pressed). + Footer.SetButtons([]); + + osuScreen.OnLoadComplete += _ => updateFooterButtons(); + } + + void updateFooterButtons() + { + var buttons = osuScreen.CreateFooterButtons(); + + osuScreen.LoadComponentsAgainstScreenDependencies(buttons); + + Footer.SetButtons(buttons); + Footer.Show(); + } + } + else + { + backButtonVisibility.BindTo(osuScreen.BackButtonVisibility); + + Footer.SetButtons([]); + Footer.Hide(); + } + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + screenTracker.Dispose(); + } + + /// + /// Recursively represents a single screen stack and any nested subscreen stack. + /// + private class ScreenStackTracker : IDisposable + { + /// + /// Invoked when the leading screen changes. + /// + /// + /// This differs from and + /// because lastScreen and newScreen may be subscreens of the current screen stack. + ///
+ /// As such, no assumptions may be made as to the relation of screens to this entry's . + ///
+ public event ScreenChangedDelegate? ScreenChanged; + + /// + /// The screen stack tracked by this entry. + /// + private readonly ScreenStack stack; + + /// + /// An entry corresponding to the subscreen stack of the current screen, if any. + /// + private ScreenStackTracker? subScreenTracker; + + /// + /// The screen which should be bound to the screen footer - the most nested subscreen. + /// + private IScreen leadingScreen => subScreenTracker?.leadingScreen ?? stack.CurrentScreen; + + public ScreenStackTracker(ScreenStack stack) + { + this.stack = stack; + + stack.ScreenPushed += onParentScreenChanged; + stack.ScreenExited += onParentScreenChanged; + } + + private void onParentScreenChanged(IScreen lastScreen, IScreen newScreen) + { + // The screen which we will be UNBINDING from the screen footer later on. + IScreen lastLeadingScreen = subScreenTracker?.leadingScreen ?? lastScreen; + + // Subscreens are attached to a parent screen, so when the parent changes the subscreen must also. + subScreenTracker?.Dispose(); + subScreenTracker = null; + + // Check if we've switched to a screen that has a subscreen. + if (newScreen is IHasSubScreenStack newStack) + { + subScreenTracker = new ScreenStackTracker(newStack.SubScreenStack); + subScreenTracker.ScreenChanged += onSubScreenScreenChanged; + } + + ScreenChanged?.Invoke(lastLeadingScreen, leadingScreen); + } + + private void onSubScreenScreenChanged(IScreen lastScreen, IScreen newScreen) + { + ScreenChanged?.Invoke(lastScreen, newScreen); + } + + public void Dispose() + { + stack.ScreenPushed -= onParentScreenChanged; + stack.ScreenExited -= onParentScreenChanged; + + if (subScreenTracker != null) + { + subScreenTracker.ScreenChanged -= onSubScreenScreenChanged; + subScreenTracker.Dispose(); + } + } + } + } +}