// 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.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Input; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Framework.Localisation; using osu.Game.Database; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Input.Bindings; using osu.Game.Localisation; using osuTK; namespace osu.Game.Overlays.Settings.Sections.Input { public partial class KeyBindingConflictPopover : OsuPopover { public Action? BindingConflictResolved { get; init; } private ConflictingKeyBindingPreview newPreview = null!; private ConflictingKeyBindingPreview existingPreview = null!; private HoverableRoundedButton keepExistingButton = null!; private HoverableRoundedButton applyNewButton = null!; [Resolved] private RealmAccess realm { get; set; } = null!; [Resolved] private OsuColour colours { get; set; } = null!; private readonly KeyBindingRow.KeyBindingConflictInfo conflictInfo; protected override string PopInSampleName => @"UI/generic-error"; public KeyBindingConflictPopover(KeyBindingRow.KeyBindingConflictInfo conflictInfo) { this.conflictInfo = conflictInfo; } [BackgroundDependencyLoader] private void load() { Child = new FillFlowContainer { Width = 250, AutoSizeAxes = Axes.Y, Direction = FillDirection.Vertical, Spacing = new Vector2(10), Children = new Drawable[] { new OsuTextFlowContainer { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Text = InputSettingsStrings.KeyBindingConflictDetected, Margin = new MarginPadding { Bottom = 10 } }, existingPreview = new ConflictingKeyBindingPreview( conflictInfo.Existing.Action, conflictInfo.Existing.CombinationWhenChosen, conflictInfo.Existing.CombinationWhenNotChosen), newPreview = new ConflictingKeyBindingPreview( conflictInfo.New.Action, conflictInfo.New.CombinationWhenChosen, conflictInfo.New.CombinationWhenNotChosen), new Container { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Margin = new MarginPadding { Top = 10 }, Children = new[] { keepExistingButton = new HoverableRoundedButton { Text = InputSettingsStrings.KeepExistingBinding, RelativeSizeAxes = Axes.X, Width = 0.48f, Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, Action = Hide }, applyNewButton = new HoverableRoundedButton { Text = InputSettingsStrings.ApplyNewBinding, BackgroundColour = colours.DangerousButtonColour, RelativeSizeAxes = Axes.X, Width = 0.48f, Anchor = Anchor.CentreRight, Origin = Anchor.CentreRight, Action = applyNew } } } } }; } private void applyNew() { // only "apply new" needs to cause actual realm changes, since the flow in `KeyBindingsSubsection` does not actually make db changes // if it detects a binding conflict. // the temporary visual changes will be reverted by calling `Hide()` / `BindingConflictResolved`. realm.Write(r => { var existingBinding = r.Find(conflictInfo.Existing.ID); existingBinding!.KeyCombinationString = conflictInfo.Existing.CombinationWhenNotChosen.ToString(); var newBinding = r.Find(conflictInfo.New.ID); newBinding!.KeyCombinationString = conflictInfo.Existing.CombinationWhenChosen.ToString(); }); Hide(); } protected override void PopOut() { base.PopOut(); // workaround for `VisibilityContainer.PopOut()` being called in `LoadAsyncComplete()` if (IsLoaded) BindingConflictResolved?.Invoke(); } protected override void LoadComplete() { base.LoadComplete(); keepExistingButton.IsHoveredBindable.BindValueChanged(_ => updatePreviews()); applyNewButton.IsHoveredBindable.BindValueChanged(_ => updatePreviews()); updatePreviews(); } private void updatePreviews() { if (!keepExistingButton.IsHovered && !applyNewButton.IsHovered) { existingPreview.IsChosen.Value = newPreview.IsChosen.Value = null; return; } existingPreview.IsChosen.Value = keepExistingButton.IsHovered; newPreview.IsChosen.Value = applyNewButton.IsHovered; } public override bool OnPressed(KeyBindingPressEvent e) { if (e.Action == GlobalAction.Select && !e.Repeat) { applyNew(); return true; } return base.OnPressed(e); } private partial class ConflictingKeyBindingPreview : CompositeDrawable { private readonly object action; private readonly KeyCombination combinationWhenChosen; private readonly KeyCombination combinationWhenNotChosen; private OsuSpriteText newBindingText = null!; public Bindable IsChosen { get; } = new Bindable(); [Resolved] private ReadableKeyCombinationProvider keyCombinationProvider { get; set; } = null!; [Resolved] private OsuColour colours { get; set; } = null!; public ConflictingKeyBindingPreview(object action, KeyCombination combinationWhenChosen, KeyCombination combinationWhenNotChosen) { this.action = action; this.combinationWhenChosen = combinationWhenChosen; this.combinationWhenNotChosen = combinationWhenNotChosen; } [BackgroundDependencyLoader] private void load(OverlayColourProvider colourProvider) { RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; InternalChild = new Container { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, CornerRadius = 5, Masking = true, Children = new Drawable[] { new Box { RelativeSizeAxes = Axes.Both, Colour = colourProvider.Background5 }, new GridContainer { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) }, ColumnDimensions = new[] { new Dimension(), new Dimension(GridSizeMode.AutoSize), }, Content = new[] { new Drawable[] { new OsuSpriteText { Text = action.GetLocalisableDescription(), Margin = new MarginPadding(7.5f), }, new Container { AutoSizeAxes = Axes.Both, CornerRadius = 5, Masking = true, Anchor = Anchor.CentreRight, Origin = Anchor.CentreRight, X = -5, Children = new[] { new Box { RelativeSizeAxes = Axes.Both, Colour = colourProvider.Background6 }, Empty().With(d => d.Width = 80), // poor man's min-width newBindingText = new OsuSpriteText { Font = OsuFont.Numeric.With(size: 10), Margin = new MarginPadding(5), Anchor = Anchor.Centre, Origin = Anchor.Centre } } }, } } } } }; } protected override void LoadComplete() { base.LoadComplete(); IsChosen.BindValueChanged(_ => updateState(), true); } private void updateState() { LocalisableString keyCombinationText; switch (IsChosen.Value) { case true: keyCombinationText = keyCombinationProvider.GetReadableString(combinationWhenChosen); newBindingText.Colour = colours.Green1; break; case false: keyCombinationText = keyCombinationProvider.GetReadableString(combinationWhenNotChosen); newBindingText.Colour = colours.Red1; break; case null: keyCombinationText = keyCombinationProvider.GetReadableString(combinationWhenChosen); newBindingText.Colour = Colour4.White; break; } if (LocalisableString.IsNullOrEmpty(keyCombinationText)) keyCombinationText = InputSettingsStrings.ActionHasNoKeyBinding; newBindingText.Text = keyCombinationText; } } private partial class HoverableRoundedButton : RoundedButton { public BindableBool IsHoveredBindable { get; set; } = new BindableBool(); protected override bool OnHover(HoverEvent e) { IsHoveredBindable.Value = IsHovered; return base.OnHover(e); } protected override void OnHoverLost(HoverLostEvent e) { IsHoveredBindable.Value = IsHovered; base.OnHoverLost(e); } } } }