using System.Collections; using TMPro; using UnityEngine; using UnityEngine.Events; using UnityEngine.EventSystems; // Pavé numérique à deux mesures : KEYBOARDNUMBERDOUBLE. // // Le même clavier que KeyNumber, mais devant deux champs plutôt qu'un — une // largeur et une profondeur. Les touches s'appliquent au champ qui a le focus, // et la validation rend les deux valeurs d'un coup. // // Les boutons du panneau appellent change, fraction, back, AddValue et close : // ces noms et signatures sont ceux de KeyNumber, un bouton se rebranche donc // sans rien changer d'autre que l'objet visé. // // Hiérarchie attendue : // KEYBOARDNUMBERDOUBLE // NUMBER / FRACTIONS <- les touches // InputMesure1 <- TMP_InputField + TMPSelectFlag // InputMesure2 <- TMP_InputField + TMPSelectFlag public class KeyNumberDouble : MonoBehaviour { [Header("Panneaux")] [Tooltip("Le panneau du clavier. Vide = cet objet.")] [SerializeField] private GameObject keyboard; [Tooltip("Le panneau des fractions, replié à chaque ouverture.")] [SerializeField] private GameObject fractions; [Header("Saisie")] [Tooltip("InputMesure1 : la première mesure, la largeur.")] [SerializeField] private TMP_InputField inputFirst; [SerializeField] private TMPSelectFlag selectFirst; [Tooltip("InputMesure2 : la seconde mesure, la profondeur.")] [SerializeField] private TMP_InputField inputSecond; [SerializeField] private TMPSelectFlag selectSecond; [Header("Validation")] [Tooltip("Appelé à la validation : (largeur et profondeur saisies, direction du bouton).")] public ValuesEvent onValidated; [System.Serializable] public class ValuesEvent : UnityEvent { } // Le champ qui reçoit les touches. Il change quand on en touche un autre. private TMP_InputField _active; // Sur ces plateformes le clavier natif viendrait se superposer au pavé. private static bool UsesNativeKeyboard => Application.isMobilePlatform || Application.platform == RuntimePlatform.WebGLPlayer; // Qui demande la saisie. Sert à ne répondre qu'à sa propre ouverture, quand // plusieurs boutons partagent le même pavé. public Object Caller { get; private set; } public Vector2 PreviousValues { get; private set; } public bool IsOpen => keyboard != null && keyboard.activeSelf; // Vrai une fois les champs écoutés. Awake ne tourne pas sur un objet rangé // éteint dans la scène : la préparation doit pouvoir se faire à l'ouverture. private bool _ready; void Awake() { EnsureReady(); keyboard.SetActive(false); if (fractions != null) fractions.SetActive(false); } private void EnsureReady() { if (_ready) return; _ready = true; if (keyboard == null) keyboard = gameObject; Listen(inputFirst); Listen(inputSecond); } // Toucher un champ le rend actif : les touches y vont désormais. Et le clavier // natif est refusé, il recouvrirait le pavé. private void Listen(TMP_InputField field) { if (field == null) return; field.onSelect.AddListener(_ => { _active = field; RefuseNativeKeyboard(field); }); } // Ouvre le pavé sur deux mesures. caller identifie le demandeur. public void Open(Object caller, float first, float second) { EnsureReady(); Caller = caller; PreviousValues = new Vector2(first, second); // Le pavé peut être rangé en éteignant son propre objet plutôt qu'un // panneau enfant : il est donc remis debout avant tout le reste, sans quoi // rien de ce qui suit ne tourne. if (!gameObject.activeSelf) gameObject.SetActive(true); keyboard.SetActive(true); keyboard.transform.localPosition = Vector3.zero; if (fractions != null) fractions.SetActive(false); Fill(inputFirst, selectFirst, first); Fill(inputSecond, selectSecond, second); // La saisie commence sur la première mesure. _active = inputFirst != null ? inputFirst : inputSecond; if (_active == null || UsesNativeKeyboard) return; // Un parent éteint garde l'objet inerte quoi qu'on fasse ici : le champ est // alors activé sans l'attente d'une frame, plutôt que de lever une erreur. if (isActiveAndEnabled) StartCoroutine(SelectActive()); else _active.ActivateInputField(); } private static void Fill(TMP_InputField field, TMPSelectFlag flag, float value) { if (field != null) field.text = KeyNumber.Format(value); // Le premier chiffre tapé remplace la valeur affichée. if (flag != null) flag.replaceNext = true; } // TMP traite l'activation du champ à la frame suivante : sélectionner dans la // foulée serait annulé par cette activation, d'où l'attente d'une frame. private IEnumerator SelectActive() { if (EventSystem.current != null) EventSystem.current.SetSelectedGameObject(_active.gameObject); _active.ActivateInputField(); yield return null; // Ne pas toucher à caretPosition ici : son setter replie la sélection. _active.selectionAnchorPosition = 0; _active.selectionFocusPosition = _active.text.Length; _active.ForceLabelUpdate(); } // Une touche du pavé : chiffre ou point. public void change(string key) { TMP_InputField field = Active(); if (field == null) return; TMPSelectFlag flag = FlagOf(field); string text = (field.text ?? "").Replace((char)8203, ' ').Replace(" ", ""); // Valeur surlignée dans le champ : le chiffre cliqué la remplace. bool replace = HasSelection(field) || (text == "0" && key != ".") || (flag != null && flag.replaceNext); if (replace) { field.text = key; if (flag != null) flag.replaceNext = false; } else { field.text = text + key; } PlaceCaretAtEnd(field); StartCoroutine(DeselectNextFrame(field)); } // Une touche de fraction : elle s'ajoute à la partie entière saisie. public void fraction(string key) { TMP_InputField field = Active(); if (field == null) return; string[] ratio = key.Split('/'); if (ratio.Length != 2) return; float denominator = KeyNumber.ParseValue(ratio[1]); if (denominator == 0f) return; float whole = Mathf.Floor(KeyNumber.ParseValue(field.text)); field.text = KeyNumber.Format(whole + KeyNumber.ParseValue(ratio[0]) / denominator); TMPSelectFlag flag = FlagOf(field); if (flag != null) flag.replaceNext = false; PlaceCaretAtEnd(field); } public void back() { TMP_InputField field = Active(); if (field == null || field.text.Length == 0) return; // Tout est surligné : la touche efface la valeur entière. if (HasSelection(field)) { field.text = ""; TMPSelectFlag flag = FlagOf(field); if (flag != null) flag.replaceNext = false; } else { field.text = field.text.Substring(0, field.text.Length - 1); } PlaceCaretAtEnd(field); } // Validation. direction vient du bouton : "Left", "Right" ou "Both". public void AddValue(string direction) { Vector2 values = new Vector2( KeyNumber.ParseValue(inputFirst != null ? inputFirst.text : ""), KeyNumber.ParseValue(inputSecond != null ? inputSecond.text : "")); onValidated?.Invoke(values, direction); close(); } public void close() { // Relâcher le focus, sinon la prochaine ouverture repart d'un champ à // moitié actif et l'activation ne resélectionne plus la valeur. if (inputFirst != null) inputFirst.DeactivateInputField(); if (inputSecond != null) inputSecond.DeactivateInputField(); if (EventSystem.current != null) EventSystem.current.SetSelectedGameObject(null); if (keyboard != null) keyboard.SetActive(false); if (fractions != null) fractions.SetActive(false); _active = null; Caller = null; } public void closePNL() { close(); } public void OpenFraction() { if (fractions != null) fractions.SetActive(true); } // Le champ actif, ou la première mesure tant qu'aucun n'a été touché. private TMP_InputField Active() { if (_active == null) _active = inputFirst != null ? inputFirst : inputSecond; return _active; } private TMPSelectFlag FlagOf(TMP_InputField field) { if (field == inputFirst) return selectFirst; if (field == inputSecond) return selectSecond; return null; } // Vrai quand une partie du texte est surlignée dans le champ. private static bool HasSelection(TMP_InputField field) { return field != null && field.selectionAnchorPosition != field.selectionFocusPosition; } // Après une frappe, plus rien n'est surligné et le curseur est au bout : le // chiffre suivant s'ajoute au lieu de tout remplacer une seconde fois. private static void PlaceCaretAtEnd(TMP_InputField field) { if (field == null) return; field.selectionAnchorPosition = field.text.Length; field.selectionFocusPosition = field.text.Length; } private IEnumerator DeselectNextFrame(TMP_InputField field) { yield return null; PlaceCaretAtEnd(field); } private static void RefuseNativeKeyboard(TMP_InputField field) { if (!UsesNativeKeyboard || field == null) return; field.DeactivateInputField(); if (EventSystem.current != null) EventSystem.current.SetSelectedGameObject(null); } }