96 lines
4.5 KiB
GDScript
96 lines
4.5 KiB
GDScript
@tool
|
|
extends RefCounted
|
|
## Pure, headless-testable helpers for the input_simulate `send_text` event:
|
|
## per-codepoint [InputEventKey] synthesis, the submit-Enter pair, the `text_after`
|
|
## truncate/redact, and the focus/diagnostic hint.
|
|
##
|
|
## Runtime-clean by construction — it names only [InputEventKey], [String], and
|
|
## string formatting, never an editor class — so it is safe inside the runtime
|
|
## autoload's preload closure. An editor symbol anywhere in that static graph
|
|
## parse-fails the autoload in an unstripped export (godotengine/godot#91713), and
|
|
## the runtime server preloads this file; keep it editor-free.
|
|
|
|
## Largest `text_after` length echoed back before truncation kicks in.
|
|
const TEXT_AFTER_CAP := 200
|
|
|
|
|
|
## Synthesizes one press+release [InputEventKey] pair per CODEPOINT of [param text],
|
|
## with `unicode` set to the codepoint and `keycode` left 0 (unicode-only).
|
|
##
|
|
## Unicode-only so a typed character drives text entry via the focus owner's
|
|
## `gui_input` without also matching a keycode-based shortcut. [param text] is
|
|
## walked by codepoint ([method String.unicode_at]); a Godot [String] is UTF-32, so
|
|
## each index is a whole codepoint and non-ASCII needs no surrogate handling.
|
|
## Returns press, release, press, release… — twice the codepoint count.
|
|
static func synthesize_text_events(text: String) -> Array[InputEventKey]:
|
|
var events: Array[InputEventKey] = []
|
|
for i in text.length():
|
|
var codepoint := text.unicode_at(i)
|
|
var press := InputEventKey.new()
|
|
press.unicode = codepoint
|
|
press.pressed = true
|
|
events.append(press)
|
|
var release := InputEventKey.new()
|
|
release.unicode = codepoint
|
|
release.pressed = false
|
|
events.append(release)
|
|
return events
|
|
|
|
|
|
## Synthesizes the press+release [InputEventKey] pair for the optional `submit`
|
|
## Enter, keyed on `keycode = KEY_ENTER` (not unicode).
|
|
##
|
|
## Enter is the one keycode exception: `ui_text_submit` / `ui_text_newline` match
|
|
## on KEYCODE, so a unicode-only event would neither fire `text_submitted` on a
|
|
## [LineEdit] nor insert a newline in a multiline [TextEdit].
|
|
static func synthesize_enter() -> Array[InputEventKey]:
|
|
var press := InputEventKey.new()
|
|
press.keycode = KEY_ENTER
|
|
press.pressed = true
|
|
var release := InputEventKey.new()
|
|
release.keycode = KEY_ENTER
|
|
release.pressed = false
|
|
return [press, release]
|
|
|
|
|
|
## Codepoint count [method synthesize_text_events] will type for [param text] — its
|
|
## reported `chars_sent`. A Godot [String] is UTF-32, so this is just its length.
|
|
static func char_count(text: String) -> int:
|
|
return text.length()
|
|
|
|
|
|
## Builds the `text_after` result field from the target's real [param raw] text.
|
|
##
|
|
## Redaction wins over truncation: when [param is_secret] the value is replaced by
|
|
## its length alone (a [LineEdit] with `secret = true`), so the secret characters
|
|
## never reach the response — yet `text_changed` still computes from the real value
|
|
## upstream. A non-secret value longer than [param cap] is truncated with a
|
|
## "...[+N chars]" suffix (same form as execute.code's log truncation); otherwise it
|
|
## is returned verbatim.
|
|
static func format_text_after(raw: String, is_secret: bool, cap: int = TEXT_AFTER_CAP) -> String:
|
|
if is_secret:
|
|
return "[redacted: %d chars]" % raw.length()
|
|
if raw.length() > cap:
|
|
return raw.substr(0, cap) + "...[+%d chars]" % (raw.length() - cap)
|
|
return raw
|
|
|
|
|
|
## Builds the actionable `hint` for a send_text result, or "" on clean success.
|
|
##
|
|
## [param text_changed] is tri-state (true / false / null); null means the target
|
|
## exposes no readable `text` property (a custom `_input` reader), which is not an
|
|
## error and gets no hint. A "none" [param focus_source] steers the caller to
|
|
## `event_data.node_path`; an unchanged editable target flags the likely causes and,
|
|
## when [param tree_paused], notes that a paused tree skips `gui_input`. The
|
|
## node_path-unresolved case is hinted inline by the handler (it holds the path).
|
|
static func build_hint(focus_source: String, text_changed: Variant, focus_path: String,
|
|
focus_class: String, chars_sent: int, tree_paused: bool) -> String:
|
|
if focus_source == "none":
|
|
return "No Control had focus; pass event_data.node_path to focus the target field before typing. (Custom _input handlers may still have received the %d characters.)" % chars_sent
|
|
if text_changed == false:
|
|
var changed_hint := "Focus was on %s (%s) but its text didn't change — non-editable, at max_length, or not a text field. Verify the target or pass node_path." % [focus_path, focus_class]
|
|
if tree_paused:
|
|
changed_hint += " The scene tree is paused (process_mode), so gui_input was skipped."
|
|
return changed_hint
|
|
return ""
|