Files
unity/Assets/Game/Scripts/UI/SettingsScreen.cs
T

445 lines
20 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.UIElements;
namespace Ironspore
{
// Owns a settings draft and its UI. It does not write files or mutate gameplay state.
public sealed class SettingsScreen
{
readonly SettingsService settings;
readonly Func<bool> backPressed;
readonly List<VisualElement> focusOrder = new List<VisualElement>();
readonly Dictionary<Button, Action> buttonActions = new Dictionary<Button, Action>();
VisualElement parent, screen;
ScrollView scroll;
Label countdown;
Action closed;
GameSettingsData draft;
BindingSlot capture;
string messageKey;
object[] messageArgs = Array.Empty<object>();
int tab, lastKeyFrame = -1;
bool open;
static string L(string key, params object[] args) => GameLocalization.Text(key, args);
public SettingsScreen(SettingsService settings, Func<bool> backPressed)
{
this.settings = settings;
this.backPressed = backPressed;
}
public void Open(VisualElement parent, Action closed)
{
this.parent = parent;
this.closed = closed;
draft = settings.Current;
messageKey = null;
open = true;
Draw();
}
public void Close()
{
if (!open)
return;
open = false;
capture = null;
settings.DiscardDraft();
screen?.RemoveFromHierarchy();
screen = null;
scroll = null;
closed = null;
focusOrder.Clear();
buttonActions.Clear();
}
public void RefreshLocale()
{
if (!open) return;
string focusedName = (screen.focusController.focusedElement as VisualElement)?.name;
Draw();
if (!string.IsNullOrEmpty(focusedName)) FocusAfterDispatch(screen.Q(focusedName));
}
public void CancelInput() => settings.Bindings.Cancel();
void Draw()
{
Vector2 offset = scroll?.scrollOffset ?? Vector2.zero;
screen?.RemoveFromHierarchy();
focusOrder.Clear();
buttonActions.Clear();
screen = Element(parent, "settings-screen", "settings-screen");
screen.RegisterCallback<KeyDownEvent>(evt =>
{
if (HandleKey(evt.keyCode, evt.shiftKey))
evt.StopImmediatePropagation();
}, TrickleDown.TrickleDown);
screen.RegisterCallback<PointerDownEvent>(CapturePointer, TrickleDown.TrickleDown);
var panel = Element(screen, "settings-panel");
var header = Element(panel, "settings-header");
var title = Element(header, "settings-title-group");
Text(title, L("settings.title"), "heading");
Text(title, L("settings.help"), "settings-help");
var locales = GameLocalization.Catalog.locales;
var names = new List<string>();
foreach (var locale in locales)
names.Add(locale.nativeName);
int languageIndex = Array.FindIndex(locales, x => x.code == GameLocalization.CurrentCode);
var language = Dropdown(header, "settings.language", names, languageIndex, index => GameLocalization.SelectLocale(locales[index].code));
language.name = "settings-language";
language.AddToClassList("settings-language");
var body = Element(panel, "settings-body");
var navigation = Element(body, "settings-navigation");
string[] tabs = { "settings.audio", "settings.display", "settings.controls" };
for (int i = 0; i < tabs.Length; i++)
{
int choice = i;
var button = AddButton(navigation, L(tabs[i]), () => { tab = choice; scroll = null; Draw(); }, "settings-tab");
button.name = "settings-tab-" + i;
if (tab == i)
button.AddToClassList("selected");
}
scroll = new ScrollView(ScrollViewMode.Vertical) { horizontalScrollerVisibility = ScrollerVisibility.Hidden };
scroll.AddToClassList("settings-scroll");
body.Add(scroll);
if (tab == 0)
Audio(scroll);
else if (tab == 1)
Display(scroll);
else
Controls(scroll);
var renderedScroll = scroll;
renderedScroll.schedule.Execute(() => { if (renderedScroll.panel != null) renderedScroll.scrollOffset = offset; });
var status = Text(panel, string.IsNullOrEmpty(messageKey) ? "" : L(messageKey, messageArgs), "settings-status");
status.name = "settings-status";
var footer = Element(panel, "settings-footer");
var reset = AddButton(footer, L("settings.reset"), Reset, "button");
reset.name = "settings-reset";
var footerActions = Element(footer, "settings-footer-actions");
AddButton(footerActions, L("settings.back"), Back, "button").name = "settings-back";
AddButton(footerActions, L("settings.apply"), Apply, "primary").name = "settings-apply";
if (settings.ConfirmingDisplay)
{
header.SetEnabled(false);
body.SetEnabled(false);
footer.SetEnabled(false);
var dialog = Dialog("settings.display.confirm");
countdown = Text(dialog, L("settings.display.countdown", settings.ConfirmationSeconds), "body");
FocusAfterDispatch(AddButton(dialog, L("settings.display.keep"), ConfirmDisplay, "primary"));
AddButton(dialog, L("settings.display.revert"), RevertDisplay, "button");
}
else if (capture != null)
{
header.SetEnabled(false);
body.SetEnabled(false);
footer.SetEnabled(false);
var dialog = Dialog("settings.bind.waiting");
Text(dialog, L("settings.bind.prompt", L(capture.LabelKey)), "body");
var cancel = AddButton(dialog, L("settings.bind.cancel"), settings.Bindings.Cancel, "button");
cancel.name = "settings-capture-cancel";
FocusAfterDispatch(cancel);
}
else
FocusAfterDispatch(navigation.Q<Button>("settings-tab-" + tab));
}
static void FocusAfterDispatch(VisualElement element)
{
// Popup and button dispatch may restore focus to the old, detached control afterwards.
if (element != null)
element.schedule.Execute(() => { if (element.panel != null && element.enabledInHierarchy) element.Focus(); });
}
void Audio(VisualElement content)
{
Text(content, L("settings.audio"), "section");
Slider(content, "settings.volume", draft.masterVolume, 0, 1, .01f,
value => draft.masterVolume = value, value => L("settings.percent", Mathf.RoundToInt(value * 100)));
Text(content, L("settings.volumeHelp"), "hint");
}
void Display(VisualElement content)
{
Text(content, L("settings.display"), "section");
string[] tiers = QualitySettings.names;
var qualityNames = new List<string>();
foreach (string tier in tiers)
{
switch (tier)
{
case "Very Low": qualityNames.Add(L("settings.quality.veryLow")); break;
case "Low": qualityNames.Add(L("settings.quality.low")); break;
case "Medium": qualityNames.Add(L("settings.quality.medium")); break;
case "High": qualityNames.Add(L("settings.quality.high")); break;
case "Very High": qualityNames.Add(L("settings.quality.veryHigh")); break;
case "Ultra": qualityNames.Add(L("settings.quality.ultra")); break;
default: qualityNames.Add(tier); break;
}
}
Dropdown(content, "settings.quality", qualityNames, draft.qualityLevel, index => draft.qualityLevel = index).name = "settings-quality";
var resolutions = settings.Resolutions();
var choices = new List<string>();
int selected = 0;
for (int i = 0; i < resolutions.Count; i++)
{
var size = resolutions[i];
choices.Add(size.x + " × " + size.y);
if (size.x == draft.resolutionWidth && size.y == draft.resolutionHeight)
selected = i;
}
Dropdown(content, "settings.resolution", choices, selected, index =>
{
draft.resolutionWidth = resolutions[index].x;
draft.resolutionHeight = resolutions[index].y;
}).name = "settings-resolution";
Dropdown(content, "settings.windowMode", new List<string> { L("settings.windowed"), L("settings.fullscreen") }, draft.fullscreen ? 1 : 0,
index => draft.fullscreen = index == 1).name = "settings-window-mode";
Text(content, L("settings.displayHelp"), "hint");
}
void Controls(VisualElement content)
{
Text(content, L("settings.mouse"), "section");
Slider(content, "settings.sensitivity", draft.mouseSensitivity, .1f, 5, .05f,
value => draft.mouseSensitivity = value, value => L("settings.multiplier", value.ToString("0.00", GameLocalization.Culture)));
Slider(content, "settings.aimSensitivity", draft.aimSensitivity, .1f, 2, .05f,
value => draft.aimSensitivity = value, value => L("settings.multiplier", value.ToString("0.00", GameLocalization.Culture)));
Text(content, L("settings.bindings"), "section");
Text(content, L("settings.bind.help"), "hint");
foreach (var slot in settings.Bindings.Slots)
{
var row = Element(content, "settings-binding-row");
Text(row, L(slot.LabelKey), "settings-binding-label");
var button = AddButton(row, settings.Bindings.Display(slot), () => BeginRebind(slot), "settings-binding");
button.name = "binding-" + slot.Action.name.ToLowerInvariant() + "-" + slot.Index;
button.tooltip = L("settings.bind.prompt", L(slot.LabelKey));
}
}
void BeginRebind(BindingSlot slot)
{
capture = slot;
settings.Bindings.Begin(slot, (result, conflict) =>
{
if (!open)
return;
capture = null;
draft.bindingOverrides = settings.Bindings.Save();
string key = result == BindingChange.Success ? "settings.bind.changed"
: result == BindingChange.Cancelled ? "settings.bind.cancelled"
: result == BindingChange.Conflict ? "settings.bind.conflict"
: result == BindingChange.Reserved ? "settings.bind.reserved" : "settings.bind.unsupported";
Message(key, result == BindingChange.Conflict ? L(conflict.LabelKey) : L(slot.LabelKey));
Draw();
});
Draw();
}
void Apply()
{
var result = settings.Apply(draft);
if (result == SettingsApplyResult.Saved)
{
draft = settings.Current;
Message("settings.saved");
}
else if (result == SettingsApplyResult.Failed)
Message("settings.saveFailed");
Draw();
}
void ConfirmDisplay()
{
bool saved = settings.ConfirmDisplay();
draft = settings.Current;
Message(saved ? "settings.saved" : "settings.saveFailed");
Draw();
}
void RevertDisplay()
{
settings.CancelDisplay();
var saved = settings.Current;
draft.resolutionWidth = saved.resolutionWidth;
draft.resolutionHeight = saved.resolutionHeight;
draft.fullscreen = saved.fullscreen;
Message("settings.display.reverted");
Draw();
}
void Reset()
{
draft = settings.Defaults;
settings.Bindings.Reset();
Message("settings.resetHint");
Draw();
}
void Back()
{
if (settings.Bindings.IsRebinding)
{
settings.Bindings.Cancel();
return;
}
if (settings.ConfirmingDisplay)
{
RevertDisplay();
return;
}
var callback = closed;
Close();
callback?.Invoke();
}
public void Tick()
{
if (!open)
return;
if (settings.Tick())
RevertDisplay();
if (settings.ConfirmingDisplay && countdown != null)
countdown.text = L("settings.display.countdown", settings.ConfirmationSeconds);
if (settings.Bindings.SuppressMenuInput)
return;
var keyboard = Keyboard.current;
if (keyboard != null)
{
if (keyboard.escapeKey.wasPressedThisFrame) HandleKey(KeyCode.Escape, false);
else if (keyboard.tabKey.wasPressedThisFrame) HandleKey(KeyCode.Tab, keyboard.shiftKey.isPressed);
else if (keyboard.enterKey.wasPressedThisFrame || keyboard.numpadEnterKey.wasPressedThisFrame) HandleKey(KeyCode.Return, false);
else if (keyboard.spaceKey.wasPressedThisFrame) HandleKey(KeyCode.Space, false);
else if (keyboard.leftArrowKey.wasPressedThisFrame) HandleKey(KeyCode.LeftArrow, false);
else if (keyboard.rightArrowKey.wasPressedThisFrame) HandleKey(KeyCode.RightArrow, false);
else if (keyboard.upArrowKey.wasPressedThisFrame) HandleKey(KeyCode.UpArrow, false);
else if (keyboard.downArrowKey.wasPressedThisFrame) HandleKey(KeyCode.DownArrow, false);
}
if (backPressed() && lastKeyFrame != Time.frameCount)
HandleKey(KeyCode.Escape, false);
}
bool HandleKey(KeyCode key, bool shift)
{
if (settings.Bindings.IsRebinding)
{
if (key == KeyCode.Escape) settings.Bindings.Cancel();
else if (key != KeyCode.None) settings.Bindings.CapturePath(SettingsInputCapture.KeyboardPath(key));
return true;
}
if (settings.Bindings.SuppressMenuInput)
return true;
if (lastKeyFrame == Time.frameCount)
return true;
var focused = screen.focusController.focusedElement as VisualElement;
int index = focusOrder.FindIndex(x => x == focused || (focused != null && x.Contains(focused)));
if (key == KeyCode.Escape)
Back();
else if (key == KeyCode.Tab)
{
int direction = shift ? -1 : 1;
for (int i = 0; i < focusOrder.Count; i++)
{
index = (index + direction + focusOrder.Count) % focusOrder.Count;
if (focusOrder[index].enabledInHierarchy)
{
focusOrder[index].Focus();
if (scroll.Contains(focusOrder[index])) scroll.ScrollTo(focusOrder[index]);
break;
}
}
}
else if ((key == KeyCode.Return || key == KeyCode.KeypadEnter || key == KeyCode.Space) && focused is Button button && buttonActions.TryGetValue(button, out var action))
action();
else if (index >= 0 && (key == KeyCode.LeftArrow || key == KeyCode.RightArrow || key == KeyCode.UpArrow || key == KeyCode.DownArrow))
{
int direction = key == KeyCode.LeftArrow || key == KeyCode.DownArrow ? -1 : 1;
if (focusOrder[index] is Slider slider)
slider.value += direction * (float)slider.userData;
else if (focusOrder[index] is DropdownField dropdown)
dropdown.index = (dropdown.index + direction + dropdown.choices.Count) % dropdown.choices.Count;
else
return false;
}
else
return false;
lastKeyFrame = Time.frameCount;
return true;
}
void CapturePointer(PointerDownEvent evt)
{
if (!settings.Bindings.IsRebinding)
return;
var cancel = screen.Q<Button>("settings-capture-cancel");
var target = evt.target as VisualElement;
if (cancel != null && target != null && (target == cancel || cancel.Contains(target)))
settings.Bindings.Cancel();
else
settings.Bindings.CapturePath(SettingsInputCapture.MousePath(evt.button));
evt.StopImmediatePropagation();
}
void Message(string key, params object[] args) { messageKey = key; messageArgs = args; }
VisualElement Dialog(string titleKey)
{
var shade = Element(screen, "settings-dialog-shade");
var dialog = Element(shade, "settings-dialog");
Text(dialog, L(titleKey), "heading");
return dialog;
}
static VisualElement Element(VisualElement parent, string css, string name = null)
{
var element = new VisualElement { name = name };
element.AddToClassList(css);
parent.Add(element);
return element;
}
static Label Text(VisualElement parent, string value, string css)
{
var label = new Label(value);
label.AddToClassList(css);
parent.Add(label);
return label;
}
Button AddButton(VisualElement parent, string label, Action action, string css)
{
var button = new Button(action) { text = label };
button.AddToClassList(css);
parent.Add(button);
focusOrder.Add(button);
buttonActions.Add(button, action);
return button;
}
DropdownField Dropdown(VisualElement parent, string key, List<string> choices, int index, Action<int> changed)
{
var dropdown = new DropdownField(L(key), choices, Mathf.Clamp(index, 0, choices.Count - 1));
dropdown.AddToClassList("settings-dropdown");
dropdown.RegisterValueChangedCallback(evt => changed(dropdown.index));
parent.Add(dropdown);
focusOrder.Add(dropdown);
return dropdown;
}
void Slider(VisualElement parent, string key, float value, float min, float max, float step, Action<float> changed, Func<float, string> format)
{
var row = Element(parent, "settings-slider-row");
var heading = Element(row, "settings-slider-heading");
Text(heading, L(key), "settings-label");
var readout = Text(heading, format(value), "settings-value");
var slider = new Slider(min, max) { value = value, name = key, userData = step };
slider.AddToClassList("settings-slider");
slider.RegisterValueChangedCallback(evt => { changed(evt.newValue); readout.text = format(evt.newValue); });
row.Add(slider);
focusOrder.Add(slider);
}
}
}