using System.Collections; using System.Globalization; using TMPro; using UnityEngine; using UnityEngine.Events; using UnityEngine.EventSystems; // Pavé numérique du devis. // // Ouvert sur un TMP_Text (la mesure à modifier), il laisse saisir un nombre // entier, décimal ou avec fraction, puis réécrit la valeur dans ce texte et // prévient par onValidated. // // Les boutons du prefab KEYBOARDNUMBER appellent change, fraction, back, // AddValue et close : ces noms et signatures ne bougent pas. public class KeyNumber : MonoBehaviour { [Header("Panneaux")] [SerializeField] private GameObject keyboard; // le panneau du clavier [SerializeField] private GameObject fractions; // le panneau des fractions [Header("Saisie")] [SerializeField] private TMP_InputField inputValue; // InputMesure [SerializeField] private TMPSelectFlag selectFlag; // le drapeau posé sur InputMesure [Header("Validation")] [Tooltip("Appelé à la validation : (valeur saisie, direction du bouton).")] public ValueEvent onValidated; [System.Serializable] public class ValueEvent : UnityEvent { } // Le texte en cours de modification et sa valeur avant ouverture. public TMP_Text Target { get; private set; } public float PreviousValue { get; private set; } // Sur ces plateformes le clavier natif viendrait se superposer au pavé. private static bool UsesNativeKeyboard => Application.isMobilePlatform || Application.platform == RuntimePlatform.WebGLPlayer; void Awake() { if (keyboard != null) keyboard.SetActive(false); if (fractions != null) fractions.SetActive(false); if (inputValue != null) inputValue.onSelect.AddListener(_ => RefuseNativeKeyboard()); } // À brancher sur le OnClick du texte à modifier. public void OpenPro(TMP_Text target) { Open(target); } public void Open(TMP_Text target) { Target = target; PreviousValue = ParseValue(target != null ? target.text : ""); if (keyboard != null) { keyboard.SetActive(true); keyboard.transform.localPosition = Vector3.zero; } if (fractions != null) fractions.SetActive(false); if (inputValue != null) inputValue.text = Format(PreviousValue); // Le premier chiffre tapé remplace la valeur affichée. if (selectFlag != null) selectFlag.replaceNext = true; if (inputValue != null && !UsesNativeKeyboard) StartCoroutine(SelectValue()); } // 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 SelectValue() { if (EventSystem.current != null) EventSystem.current.SetSelectedGameObject(inputValue.gameObject); inputValue.ActivateInputField(); yield return null; // Ne pas toucher à caretPosition ici : son setter replie la sélection. inputValue.selectionAnchorPosition = 0; inputValue.selectionFocusPosition = inputValue.text.Length; inputValue.ForceLabelUpdate(); } // Une touche du pavé : chiffre ou point. public void change(string key) { if (inputValue == null) return; string text = (inputValue.text ?? "").Replace((char)8203, ' ').Replace(" ", ""); // Valeur surlignée dans le champ : le chiffre cliqué la remplace. bool replace = HasSelection || (text == "0" && key != ".") || (selectFlag != null && selectFlag.replaceNext); if (replace) { inputValue.text = key; if (selectFlag != null) selectFlag.replaceNext = false; } else { inputValue.text = text + key; } PlaceCaretAtEnd(); StartCoroutine(DeselectNextFrame()); } // Une touche de fraction : elle s'ajoute à la partie entière saisie. public void fraction(string key) { if (inputValue == null) return; string[] ratio = key.Split('/'); if (ratio.Length != 2) return; float denominator = ParseValue(ratio[1]); if (denominator == 0f) return; float whole = Mathf.Floor(ParseValue(inputValue.text)); inputValue.text = Format(whole + ParseValue(ratio[0]) / denominator); if (selectFlag != null) selectFlag.replaceNext = false; PlaceCaretAtEnd(); } public void back() { if (inputValue == null || inputValue.text.Length == 0) return; // Tout est surligné : la touche efface la valeur entière. if (HasSelection) { inputValue.text = ""; if (selectFlag != null) selectFlag.replaceNext = false; } else { inputValue.text = inputValue.text.Substring(0, inputValue.text.Length - 1); } PlaceCaretAtEnd(); } // Validation. direction vient du bouton : "Left", "Right" ou "Both". public void AddValue(string direction) { float value = ParseValue(inputValue != null ? inputValue.text : ""); if (Target != null) { // Si la mesure est portée par un champ de saisie, c'est lui qui pilote // le texte : l'écrire deux fois écraserait le format. TMP_InputField field = Target.GetComponentInParent(); if (field != null) field.text = Format(value); else Target.text = Format(value) + _G.Sys; } onValidated?.Invoke(value, 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 (inputValue != null) inputValue.DeactivateInputField(); if (EventSystem.current != null) EventSystem.current.SetSelectedGameObject(null); if (keyboard != null) keyboard.SetActive(false); if (fractions != null) fractions.SetActive(false); Target = null; } public void closePNL() { close(); } public void OpenFraction() { if (fractions != null) fractions.SetActive(true); } // Accepte "12", "12.5", "12 1/2", "1/2", avec ou sans symbole d'unité. // Culture invariante : sur un Windows français, "12.5" se lit quand même. public static float ParseValue(string text) { if (string.IsNullOrEmpty(text)) return 0f; text = text.Replace((char)8203, ' ') .Replace("''", " ").Replace("\"", " ") .Replace("cm", " ").Replace(",", ".") .Trim(); float total = 0f; foreach (string part in text.Split(' ')) { if (part.Length == 0) continue; string[] ratio = part.Split('/'); if (ratio.Length == 2) { if (TryParse(ratio[0], out float numerator) && TryParse(ratio[1], out float denominator) && denominator != 0f) { total += numerator / denominator; } continue; } if (TryParse(part, out float value)) total += value; } return total; } public static string Format(float value) { return value.ToString("0.###", CultureInfo.InvariantCulture); } private static bool TryParse(string text, out float value) { return float.TryParse(text, NumberStyles.Float, CultureInfo.InvariantCulture, out value); } // Vrai quand une partie du texte est surlignée dans le champ. private bool HasSelection => inputValue != null && inputValue.selectionAnchorPosition != inputValue.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 void PlaceCaretAtEnd() { if (inputValue == null) return; int end = inputValue.text.Length; inputValue.caretPosition = end; inputValue.selectionAnchorPosition = end; inputValue.selectionFocusPosition = end; } private void RefuseNativeKeyboard() { if (!UsesNativeKeyboard || inputValue == null) return; inputValue.DeactivateInputField(); if (EventSystem.current != null) EventSystem.current.SetSelectedGameObject(null); } // Le champ garde le focus après un clic sur une touche, ce qui rouvrirait le // clavier natif : on le relâche à la frame suivante. private IEnumerator DeselectNextFrame() { yield return null; if (inputValue != null) inputValue.DeactivateInputField(); if (EventSystem.current != null) EventSystem.current.SetSelectedGameObject(null); } }