using System; using System.Collections.Generic; using MyInfoMate.Vr.Ui; using UnityEngine; namespace MyInfoMate.Vr.Menu { /// /// Le choix de la langue, posé avant le menu. /// /// Pourquoi le demander, et pourquoi à chaque visite. Un casque en borne passe /// de main en main : la langue n'est pas un réglage de l'appareil, c'est une propriété /// du visiteur en cours. Elle vit donc le temps d'une visite et rien de plus — rien /// n'est écrit sur le disque, et le visiteur suivant est de nouveau accueilli par cet /// écran. C'est la même logique que la session de statistiques (voir /// ), et pour la même raison. /// /// Changer de langue ne coûte aucun appel réseau : l'export porte toutes les /// traductions d'un coup — c'est explicitement l'arbitrage de /// ConfigurationExport.FetchAsync. Le menu se reconstruit simplement avec /// l'autre langue, y compris hors ligne. /// /// Ce sont les mêmes tuiles que le menu, avec un drapeau à la place de la /// photo de section : c'est le premier écran de la visite, et il doit annoncer la /// grammaire de ce qui suit plutôt que d'être un formulaire posé devant. Les drapeaux /// sont ceux de l'app mobile, à l'identique. /// /// L'écran ne s'affiche pas quand la visite n'est offerte que dans une langue : un /// choix à une possibilité n'est pas un choix, c'est un écran de plus avant le contenu. /// public class LanguagePicker : MonoBehaviour { const float RadiusMeters = 2.2f; /// Hauteur du centre de la rangée de tuiles. const float HeightMeters = 1.35f; /// Le même écart que la grille du menu, pour la même raison. const float GapMeters = 0.03f; static float StepDegrees => (MenuItemPanel.WidthMeters + GapMeters) / RadiusMeters * Mathf.Rad2Deg; /// Le code de la langue choisie, tel qu'il part dans les traductions. public event Action Chosen; public static LanguagePicker Create(Transform head, IList languages) { var root = new GameObject("LanguagePicker"); var picker = root.AddComponent(); if (head != null) { var forward = head.forward; forward.y = 0f; if (forward.sqrMagnitude < 0.01f) forward = Vector3.forward; root.transform.position = new Vector3(head.position.x, 0f, head.position.z); root.transform.rotation = Quaternion.LookRotation(forward.normalized); } var selector = root.AddComponent(); selector.Configure(head); selector.Selected += picker.OnSelected; picker.Build(languages); return picker; } void Build(IList languages) { // La consigne est écrite dans chacune des langues proposées : celui // qui ne lit pas le français doit pouvoir comprendre l'écran qui lui demande // s'il lit le français. var prompts = new List(); foreach (var language in languages) { var prompt = Strings.Get(Strings.ChooseLanguage, language); if (!prompts.Contains(prompt)) prompts.Add(prompt); } var heading = Label.Create(transform, "Heading", Label.Anchor.MiddleCenter, 0.075f, Color.white); heading.Text = string.Join("\n", prompts); heading.transform.localPosition = new Vector3( 0f, HeightMeters + MenuItemPanel.HeightMeters * 0.5f + 0.13f + prompts.Count * 0.055f, RadiusMeters); for (var i = 0; i < languages.Count; i++) { var panel = MenuItemPanel.Create( languages[i], Strings.NameOf(languages[i]), transform); // Une langue sans drapeau garde la tuile nue, avec son nom au centre : // c'est déjà lisible, et c'est ce que fait une tuile de section sans photo. var flag = Flag(languages[i]); if (flag != null) panel.SetImage(flag); Place(panel.transform, i, languages.Count); panel.PlayAppear(i * 0.06f); } } /// /// Le drapeau d'une langue, ou null. ⚠️ La texture vient de Resources et est /// partagée : contrairement aux vignettes de section, elle ne se détruit pas /// avec la tuile — Unity la garde pour toute la vie de l'app, et la détruire ferait /// un drapeau vide à la visite suivante. /// static Texture2D Flag(string language) => Resources.Load("Flags/" + Strings.Normalize(language).ToLowerInvariant()); /// /// Les tuiles sur l'arc du menu, centrées sur le regard : c'est la même géométrie /// que , à une rangée. /// void Place(Transform panel, int index, int count) { var angle = (index - (count - 1) * 0.5f) * StepDegrees; var radians = angle * Mathf.Deg2Rad; panel.localPosition = new Vector3( Mathf.Sin(radians) * RadiusMeters, HeightMeters, Mathf.Cos(radians) * RadiusMeters); panel.localRotation = Quaternion.Euler(0f, angle, 0f); } void OnSelected(IAimTarget target) { if (target is MenuItemPanel panel) Chosen?.Invoke(panel.Id); } } }