gdharness 1.3.9 → 1.3.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/build/cli.js +4563 -3195
  2. package/build/godot/addons/gdharness_editor/animation_reads.gd +115 -4
  3. package/build/godot/addons/gdharness_editor/declared_values.gd +272 -0
  4. package/build/godot/addons/gdharness_editor/file_reads.gd +102 -18
  5. package/build/godot/addons/gdharness_editor/headless_check.gd +49 -2
  6. package/build/godot/addons/gdharness_editor/property_paths.gd +89 -16
  7. package/build/godot/addons/gdharness_editor/property_values.gd +180 -121
  8. package/build/godot/addons/gdharness_editor/reading.gd +51 -1
  9. package/build/godot/addons/gdharness_editor/scene_file.gd +77 -140
  10. package/build/godot/addons/gdharness_editor/scene_uids.gd +107 -0
  11. package/build/godot/addons/gdharness_editor/scene_uids.gd.uid +1 -0
  12. package/build/godot/addons/gdharness_editor/serialisation.gd +11 -22
  13. package/build/godot/addons/gdharness_editor/setting_reads.gd +121 -0
  14. package/build/godot/addons/gdharness_editor/setting_reads.gd.uid +1 -0
  15. package/build/godot/addons/gdharness_editor/tool_executor.gd +28 -5
  16. package/build/godot/addons/gdharness_editor/tools/animation_states.gd +39 -20
  17. package/build/godot/addons/gdharness_editor/tools/animation_tools.gd +49 -362
  18. package/build/godot/addons/gdharness_editor/tools/animation_tracks.gd +464 -0
  19. package/build/godot/addons/gdharness_editor/tools/animation_tracks.gd.uid +1 -0
  20. package/build/godot/addons/gdharness_editor/tools/class_tools.gd +37 -0
  21. package/build/godot/addons/gdharness_editor/tools/navigation_bake.gd +221 -0
  22. package/build/godot/addons/gdharness_editor/tools/navigation_bake.gd.uid +1 -0
  23. package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +147 -492
  24. package/build/godot/addons/gdharness_editor/tools/scene_groups.gd +16 -0
  25. package/build/godot/addons/gdharness_editor/tools/scene_kin.gd +4 -3
  26. package/build/godot/addons/gdharness_editor/tools/scene_paths.gd +2 -1
  27. package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +79 -5
  28. package/build/godot/addons/gdharness_editor/tools/settings_tools.gd +7 -30
  29. package/build/godot/addons/gdharness_editor/tools/signal_tools.gd +183 -37
  30. package/build/godot/addons/gdharness_editor/tools/tileset_tools.gd +485 -0
  31. package/build/godot/addons/gdharness_editor/tools/tileset_tools.gd.uid +1 -0
  32. package/build/godot/addons/gdharness_editor/value_shapes.gd +128 -1
  33. package/build/godot/addons/gdharness_runtime/declared_values.gd +272 -0
  34. package/build/godot/addons/gdharness_runtime/reading.gd +51 -1
  35. package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +35 -3
  36. package/build/godot/addons/gdharness_runtime/runtime_changes.gd +185 -30
  37. package/build/godot/addons/gdharness_runtime/runtime_input.gd +86 -79
  38. package/build/godot/addons/gdharness_runtime/runtime_mouse.gd +17 -0
  39. package/build/godot/addons/gdharness_runtime/runtime_pad.gd +13 -4
  40. package/build/godot/addons/gdharness_runtime/runtime_paths.gd +309 -22
  41. package/build/godot/addons/gdharness_runtime/runtime_queries.gd +163 -20
  42. package/build/godot/addons/gdharness_runtime/runtime_screen.gd +49 -3
  43. package/build/godot/addons/gdharness_runtime/runtime_targets.gd +5 -2
  44. package/build/godot/addons/gdharness_runtime/runtime_values.gd +146 -5
  45. package/build/godot/addons/gdharness_runtime/runtime_waits.gd +279 -11
  46. package/build/godot/addons/gdharness_runtime/runtime_words.gd +27 -0
  47. package/build/godot/addons/gdharness_runtime/serialisation.gd +11 -22
  48. package/build/godot/addons/gdharness_runtime/value_shapes.gd +128 -1
  49. package/build/godot/operations/animation_reads.gd +115 -4
  50. package/build/godot/operations/audio_buses.gd +93 -7
  51. package/build/godot/operations/classdb_queries.gd +119 -7
  52. package/build/godot/operations/declared_values.gd +272 -0
  53. package/build/godot/operations/dependencies.gd +154 -41
  54. package/build/godot/operations/editor_tools.gd +22 -14
  55. package/build/godot/operations/export_presets.gd +602 -4
  56. package/build/godot/operations/file_reads.gd +102 -18
  57. package/build/godot/operations/gdscript_authoring.gd +11 -1
  58. package/build/godot/operations/godot_operations.gd +16 -2
  59. package/build/godot/operations/import_pipeline.gd +20 -221
  60. package/build/godot/operations/input_actions.gd +108 -47
  61. package/build/godot/operations/project_config.gd +83 -69
  62. package/build/godot/operations/project_diagnostics.gd +4 -0
  63. package/build/godot/operations/project_validation.gd +266 -0
  64. package/build/godot/operations/property_paths.gd +89 -16
  65. package/build/godot/operations/property_values.gd +180 -121
  66. package/build/godot/operations/reading.gd +51 -1
  67. package/build/godot/operations/scene_connections.gd +222 -0
  68. package/build/godot/operations/serialisation.gd +11 -22
  69. package/build/godot/operations/setting_reads.gd +121 -0
  70. package/build/godot/operations/value_shapes.gd +128 -1
  71. package/build/godot/tests/listed.gd +20 -13
  72. package/build/keeper.js +1371 -706
  73. package/package.json +1 -1
@@ -78,12 +78,16 @@ func set_property(params: Dictionary) -> Dictionary:
78
78
  var unwritable: String = GdhPaths.unwritable_copy(chain)
79
79
  if not unwritable.is_empty():
80
80
  return {"type": "error", "message": unwritable}
81
- # The write alone, setter included, which is where a property's cost is.
81
+ # The write alone, setter included, which is where a property's cost is. Listened to as a call
82
+ # is, since a setter refusing a value says why only in what it reports.
83
+ var caught: GdhCallErrors = GdhCallErrors.new()
84
+ OS.add_logger(caught)
82
85
  var began: int = Time.get_ticks_usec()
83
86
  var written: Variant = GdhPaths.write(holder, named, given)
84
87
  if copied:
85
88
  GdhPaths.write_back(chain, written)
86
89
  var elapsed: int = Time.get_ticks_usec() - began
90
+ OS.remove_logger(caught)
87
91
  var now: Variant = GdhPaths.read_under(holder, named)
88
92
  if copied:
89
93
  var again: Dictionary = GdhPaths.walk_to(target, node_path, property)
@@ -122,20 +126,20 @@ func set_property(params: Dictionary) -> Dictionary:
122
126
  )
123
127
  % [named, str(slot["class"]), GdhValues.class_of(refused_object)]
124
128
  )
125
- return {
126
- "type": "error",
127
- "message":
128
- (
129
- "%s.%s was given %s and reads %s afterwards, as it did before: %s."
130
- % [
131
- reached["called"],
132
- named,
133
- JSON.stringify(_values.serialize(given), "", false),
134
- JSON.stringify(_values.serialize(now), "", false),
135
- why,
136
- ]
137
- )
138
- }
129
+ var refused: String = (
130
+ "%s.%s was given %s and reads %s afterwards, as it did before: %s."
131
+ % [
132
+ reached["called"],
133
+ named,
134
+ JSON.stringify(_values.serialize(given), "", false),
135
+ JSON.stringify(_values.serialize(now), "", false),
136
+ why,
137
+ ]
138
+ )
139
+ if not caught.raised.is_empty():
140
+ var reported: PackedStringArray = PackedStringArray(caught.raised.slice(0, ERRORS_KEPT))
141
+ refused += " While it was written the game reported: %s" % "; ".join(reported)
142
+ return {"type": "error", "message": refused}
139
143
 
140
144
  var answer: Dictionary = {
141
145
  "type": "property_set",
@@ -147,6 +151,8 @@ func set_property(params: Dictionary) -> Dictionary:
147
151
  }
148
152
  if already and unmoved:
149
153
  answer["unchanged"] = true
154
+ _keep_reported(answer, "errors", caught.raised)
155
+ _keep_reported(answer, "warnings", caught.warned)
150
156
  # The engine stores a number past an export range, or one no enum value has, as given; only the
151
157
  # inspector holds a property to them, so the game now holds a value its own script never expected.
152
158
  if holder is Object:
@@ -169,6 +175,33 @@ func _argument_for(
169
175
  var holder: Object = reached["holder"]
170
176
  var called: String = reached["called"]
171
177
  var wants: int = _values.parameter_type(holder, named, index)
178
+ var choices: Dictionary = _values.parameter_enum(holder, named, index)
179
+ var values: Dictionary = choices.get("values", {})
180
+ if not choices.is_empty() and sent is String and not str(sent).strip_edges().is_valid_int():
181
+ var chosen: Variant = GdhDeclared.enum_named(values, str(sent))
182
+ if chosen == null:
183
+ return {
184
+ "type": "error",
185
+ "message":
186
+ GdhDeclared.enum_argument_refusal(
187
+ "%s.%s" % [called, named], index, str(choices["enum"]), str(sent), values
188
+ )
189
+ }
190
+ return {"value": chosen}
191
+ # A packed list's elements are fitted one by one, and one that cannot become what the list holds
192
+ # is named: converted whole, a word or a fraction among them became 0 or was cut, and the call ran.
193
+ var packed: Variant = GdhValues.packed_template(wants)
194
+ if packed != null and sent is Array:
195
+ var elements: Dictionary = _values.typed_like(sent, packed, _element_object.bind(node, node_path))
196
+ if elements.has("message"):
197
+ return {
198
+ "type": "error",
199
+ "message":
200
+ (
201
+ "%s.%s takes %s as argument %d: %s"
202
+ % [called, named, type_string(wants), index + 1, elements["message"]]
203
+ )
204
+ }
172
205
  if GdhValues.cuts(sent, wants):
173
206
  return {
174
207
  "type": "error",
@@ -259,6 +292,14 @@ func _value_for(
259
292
  return {"value": by_name[1]}
260
293
  if not str(by_name[1]).is_empty():
261
294
  return {"type": "error", "message": by_name[1]}
295
+ var packed: Variant = GdhValues.packed_template(wanted)
296
+ if packed != null and value is Array:
297
+ var elements: Dictionary = _values.typed_like(value, packed, _element_object.bind(node, node_path))
298
+ if elements.has("message"):
299
+ return {
300
+ "type": "error",
301
+ "message": "%s.%s holds %s: %s" % [called, named, type_string(wanted), elements["message"]]
302
+ }
262
303
  var holds_objects: bool = (
263
304
  declared == TYPE_OBJECT or (declared == TYPE_NIL and (old_value == null or old_value is Object))
264
305
  )
@@ -335,6 +376,52 @@ static func held_already(given: Variant, held: Variant) -> bool:
335
376
  return PackedFloat32Array([given])[0] == PackedFloat32Array([held])[0]
336
377
 
337
378
 
379
+ ## What to do instead of the call [param method] makes on [param sort], the list or map [param
380
+ ## reached] ends on, which a path does not make. A method of the list itself is the likelier aim
381
+ ## than one of what it holds when the list has it: told to call append() on an element, a caller
382
+ ## adding to a list has nowhere to go.
383
+ static func _instead_of_a_call(reached: Dictionary, method: String, sort: String) -> String:
384
+ var own: String = GdhPaths.method_of(str(reached["name"]))
385
+ if own.is_empty():
386
+ own = str(reached["name"])
387
+ var steps: PackedStringArray = GdhPaths.steps_of(method)
388
+ var _shortened: int = steps.resize(steps.size() - 1)
389
+ var holding: String = ":".join(steps)
390
+ var through_a_call: bool = false
391
+ for step: String in steps:
392
+ through_a_call = through_a_call or not GdhPaths.method_of(step).is_empty()
393
+ var the: String = "the " + sort.trim_prefix("a ")
394
+ match GdhPaths.container_method(reached["holder"], own):
395
+ "changes":
396
+ if through_a_call:
397
+ return (
398
+ (
399
+ "%s() changes %s: to change it, call a method of the game that changes it, since %s"
400
+ + " is returned by a call, and runtime_invoke set does not write into what a call returns"
401
+ )
402
+ % [own, the, reached["called"]]
403
+ )
404
+ return (
405
+ (
406
+ "%s() changes %s: to change it, give %s its new value whole with runtime_invoke set,"
407
+ + " property %s, or call a method of the game that changes it"
408
+ )
409
+ % [own, the, reached["called"], holding]
410
+ )
411
+ "reads":
412
+ return (
413
+ (
414
+ "To look in %s with %s(), read %s whole with runtime_inspect property, property %s, and"
415
+ + " look in what comes back"
416
+ )
417
+ % [the, own, reached["called"], holding]
418
+ )
419
+ return (
420
+ "A method of what %s holds is called on the element, as in %s:0:%s"
421
+ % [sort, reached["called"], reached["name"]]
422
+ )
423
+
424
+
338
425
  func call_method(params: Dictionary) -> Dictionary:
339
426
  var node_path: String = str(params.get("path", ""))
340
427
  var method: String = str(params.get("method", ""))
@@ -343,7 +430,33 @@ func call_method(params: Dictionary) -> Dictionary:
343
430
  if node_path.is_empty() or method.is_empty():
344
431
  return {"type": "error", "message": "Node path and method required"}
345
432
 
346
- var standing: Dictionary = GdhValues.target_at(_host.get_tree().root, node_path)
433
+ var root: Node = _host.get_tree().root
434
+ var standing: Dictionary = GdhValues.class_at(root, node_path)
435
+ if standing.is_empty():
436
+ standing = GdhValues.target_at(root, node_path)
437
+ if standing.has("message") and node_path.is_valid_ascii_identifier():
438
+ var classes: Array[String] = []
439
+ for declared: Dictionary in ProjectSettings.get_global_class_list():
440
+ classes.append(str(declared.get("class", "")))
441
+ var close: PackedStringArray = GdhRead.nearest(node_path, PackedStringArray(classes))
442
+ standing["message"] = (
443
+ (
444
+ "%s. No global class is called %s either%s; a static function is called with its"
445
+ + " class's name or its script's res:// path as the path."
446
+ )
447
+ % [
448
+ str(standing["message"]).trim_suffix("."),
449
+ node_path,
450
+ (
451
+ ""
452
+ if close.is_empty()
453
+ else (
454
+ ", and the nearest %s %s"
455
+ % ["is" if close.size() == 1 else "are", ", ".join(close)]
456
+ )
457
+ ),
458
+ ]
459
+ )
347
460
  if standing.has("message"):
348
461
  return standing
349
462
  var node: Node = standing["node"]
@@ -368,13 +481,24 @@ func call_method(params: Dictionary) -> Dictionary:
368
481
  var spelled: String = str(reached["name"])
369
482
  if GdhPaths.method_of(spelled).is_empty():
370
483
  spelled += "()"
371
- # A vector or a transform has no element to call a method on: it is read by its components.
372
- if GdhPaths.composite(reached["holder"]):
484
+ # A vector or a transform has no element to call a method on, so a call it does not answer is
485
+ # refused without the advice about elements below.
486
+ var answers: bool = GdhPaths.can_read(reached["holder"], spelled)
487
+ var called_on: Variant = reached["holder"]
488
+ if (
489
+ (GdhPaths.composite(called_on) or GdhPaths.textual(called_on))
490
+ and not (args.is_empty() and answers)
491
+ ):
373
492
  return {
374
493
  "type": "error",
375
- "message": GdhPaths.nothing_there(reached["holder"], spelled, str(reached["called"]))
494
+ "message":
495
+ (
496
+ "%s:%s takes no arguments" % [reached["called"], spelled]
497
+ if answers
498
+ else GdhPaths.nothing_there(reached["holder"], spelled, str(reached["called"]))
499
+ )
376
500
  }
377
- if args.is_empty() and GdhPaths.can_read(reached["holder"], spelled):
501
+ if args.is_empty() and answers:
378
502
  var read_from: int = Time.get_ticks_usec()
379
503
  var answered: Variant = GdhPaths.read_under(reached["holder"], spelled)
380
504
  var read_for: int = Time.get_ticks_usec() - read_from
@@ -394,20 +518,19 @@ func call_method(params: Dictionary) -> Dictionary:
394
518
  if GdhPaths.can_read(reached["holder"], spelled)
395
519
  else GdhPaths.nothing_there(reached["holder"], spelled, str(reached["called"]))
396
520
  )
397
- return {
398
- "type": "error",
399
- "message":
400
- (
401
- "%s. A method of what %s holds is called on the element, as in %s:0:%s"
402
- % [why, sort, reached["called"], reached["name"]]
403
- )
404
- }
521
+ return {"type": "error", "message": "%s. %s" % [why, _instead_of_a_call(reached, method, sort)]}
405
522
  var holder: Object = reached["holder"]
406
523
  var named: String = reached["name"]
407
524
  # The method a call ends on is the one being called whether or not it carries the brackets a
408
525
  # step along the way would: "get_viewport():gui_get_focus_owner()" is the same call as without
409
526
  # the last pair.
410
- var not_called: String = _not_a_method_to_call(holder, named, str(reached["called"]))
527
+ var not_called: String = _instance_method(
528
+ holder,
529
+ named if GdhPaths.method_of(named).is_empty() else GdhPaths.method_of(named),
530
+ str(reached["called"])
531
+ )
532
+ if not_called.is_empty():
533
+ not_called = _not_a_method_to_call(holder, named, str(reached["called"]))
411
534
  if not not_called.is_empty():
412
535
  return {"type": "error", "message": not_called}
413
536
  if not GdhPaths.method_of(named).is_empty():
@@ -424,6 +547,9 @@ func call_method(params: Dictionary) -> Dictionary:
424
547
  # The object arguments checked only against the engine class, by position from 1, for the refusal
425
548
  # the engine may still make: see [method GdhValues.slot_declared].
426
549
  var by_engine_class: Dictionary = {}
550
+ # The enum arguments given a number none of their values has, said beside the answer: the engine
551
+ # passes any number on, and a write of the same number to a property already said so.
552
+ var outside: PackedStringArray = []
427
553
  for index: int in args.size():
428
554
  var wants: int = _values.parameter_type(holder, named, index)
429
555
  if wants == TYPE_OBJECT and (args[index] is String or args[index] is Dictionary):
@@ -444,6 +570,14 @@ func call_method(params: Dictionary) -> Dictionary:
444
570
  if argument.has("message"):
445
571
  return argument
446
572
  deserialized_args.append(argument["value"])
573
+ var choices: Dictionary = _values.parameter_enum(holder, named, index)
574
+ if not choices.is_empty():
575
+ var values: Dictionary = choices["values"]
576
+ var stray: String = GdhDeclared.outside_enum_argument(
577
+ index, str(choices["enum"]), argument["value"], values
578
+ )
579
+ if not stray.is_empty():
580
+ var _noted: bool = outside.append(stray)
447
581
 
448
582
  # Timed around the call alone, since the bridge's round trip is a second or more and swamps what
449
583
  # one click's work costs; the frame metrics read whichever frame is last, which is not this one.
@@ -491,6 +625,8 @@ func call_method(params: Dictionary) -> Dictionary:
491
625
  }
492
626
  if awaited:
493
627
  answer["awaited"] = true
628
+ if not outside.is_empty():
629
+ answer["outside_declared"] = " ".join(outside)
494
630
  _keep_reported(answer, "errors", caught.raised)
495
631
  _keep_reported(answer, "warnings", caught.warned)
496
632
  # Main.explode, ended at a null it called into, answered result 0 as if it had counted something.
@@ -532,6 +668,25 @@ static func _not_a_method_to_call(holder: Object, named: String, called: String)
532
668
  return GdhPaths.with_methods_offered("%s has no method %s" % [called, method], holder, method)
533
669
 
534
670
 
671
+ ## Why [param method] cannot be called on [param holder], a script named by its class or its path,
672
+ ## or "": a function that is not static runs on an instance of the class, which the script is not.
673
+ static func _instance_method(holder: Object, method: String, called: String) -> String:
674
+ if not holder is Script:
675
+ return ""
676
+ var script: Script = holder
677
+ for entry: Dictionary in script.get_script_method_list():
678
+ if str(entry.get("name", "")) != method or GdhRead.as_int(entry.get("flags", 0)) & METHOD_FLAG_STATIC:
679
+ continue
680
+ return (
681
+ (
682
+ "%s.%s is not static, so it runs on an instance of the class: call it on a node carrying %s,"
683
+ + " or on an object a path from one reaches"
684
+ )
685
+ % [called, method, script.resource_path]
686
+ )
687
+ return ""
688
+
689
+
535
690
  ## Whether a call's `properties` asks for its result to be read: true, or a list of names.
536
691
  static func _reads_the_result(asked: Variant) -> bool:
537
692
  return asked is Array or (asked is bool and asked)
@@ -613,7 +768,7 @@ static func _argument_count_refused(holder: Object, method: String, given: int)
613
768
  # of the game's frame; a call giving exactly the arguments the method declares needs no walk.
614
769
  if given == holder.get_method_argument_count(method):
615
770
  return ""
616
- for entry: Dictionary in holder.get_method_list():
771
+ for entry: Dictionary in GdhValues.methods_of(holder):
617
772
  if str(entry.get("name", "")) != method:
618
773
  continue
619
774
  var parameters: Array = entry.get("args", [])
@@ -148,7 +148,7 @@ func inject_action(params: Dictionary) -> Dictionary:
148
148
  return {"type": "error", "message": holding}
149
149
  var frames: int = holding
150
150
 
151
- var focused: String = _focus_owner()
151
+ var focused: String = GdhValues.focus_path(_host)
152
152
  _say_action(action, whole or held, strength)
153
153
  var shortcuts: Dictionary = {"shortcuts": [], "shortcut_pressed": null}
154
154
  if whole or held:
@@ -159,38 +159,21 @@ func inject_action(params: Dictionary) -> Dictionary:
159
159
  # a listener that acts on the release both get their own frame to do it in.
160
160
  lasted = await GdhValues.hold(_host.get_tree(), frames)
161
161
  _say_action(action, false, strength)
162
- await _host.get_tree().process_frame
163
-
164
- return _focus_noted(
165
- {
166
- "type": "input_injected",
167
- "input_type": "action",
168
- "action": action,
169
- "shortcuts": shortcuts["shortcuts"],
170
- "shortcut_pressed": shortcuts["shortcut_pressed"],
171
- "pressed": held and not whole,
172
- "whole": whole,
173
- "held_frames": GdhValues.held_answer(whole, frames),
174
- "held_for": lasted,
175
- },
176
- focused
177
- )
178
-
179
-
180
- ## The path of the Control holding the keyboard focus, or "" when none does. Read before a key or an
181
- ## action goes in, since pressing a focused button can move the focus.
182
- func _focus_owner() -> String:
183
- var holding: Control = _host.get_viewport().gui_get_focus_owner()
184
- return "" if holding == null else str(holding.get_path())
185
-
162
+ # The engine hands a parsed event on at the next frame, so the focus is read after one.
163
+ await _host.get_tree().process_frame
186
164
 
187
- ## [param said] with [param focused], the Control that had the focus, under focused. A key or an action
188
- ## reaches that Control before the game's _unhandled_input, so Space on a focused Button pressed it
189
- ## and the game's own handler counted the key as well, with nothing in the answer saying why.
190
- static func _focus_noted(said: Dictionary, focused: String) -> Dictionary:
191
- if not focused.is_empty():
192
- said["focused"] = focused
193
- return said
165
+ var answer: Dictionary = {
166
+ "type": "input_injected",
167
+ "input_type": "action",
168
+ "action": action,
169
+ "shortcuts": shortcuts["shortcuts"],
170
+ "shortcut_pressed": shortcuts["shortcut_pressed"],
171
+ "pressed": held and not whole,
172
+ "whole": whole,
173
+ "held_frames": GdhValues.held_answer(whole, frames),
174
+ "held_for": lasted,
175
+ }
176
+ return GdhValues.focus_noted(answer, focused, _host)
194
177
 
195
178
 
196
179
  func _say_action(action: String, down: bool, strength: float) -> void:
@@ -246,7 +229,7 @@ func inject_key(params: Dictionary) -> Dictionary:
246
229
  # keys are the code points themselves, which is what makes this a mapping and not a table.
247
230
  event.unicode = GdhValues.glyph_of(event.keycode, event.shift_pressed)
248
231
 
249
- var focused: String = _focus_owner()
232
+ var focused: String = GdhValues.focus_path(_host)
250
233
  Input.parse_input_event(event)
251
234
  var lasted: Variant = null
252
235
  if whole:
@@ -254,26 +237,24 @@ func inject_key(params: Dictionary) -> Dictionary:
254
237
  var up: InputEventKey = event.duplicate()
255
238
  up.pressed = false
256
239
  Input.parse_input_event(up)
257
- await _host.get_tree().process_frame
240
+ await _host.get_tree().process_frame
258
241
 
259
- return _focus_noted(
260
- {
261
- "type": "input_injected",
262
- "input_type": "key",
263
- "held_frames": GdhValues.held_answer(whole, frames),
264
- "held_for": lasted,
265
- "keycode": event.keycode,
266
- "physical_keycode": event.physical_keycode,
267
- "shift": event.shift_pressed,
268
- "ctrl": event.ctrl_pressed,
269
- "alt": event.alt_pressed,
270
- "unicode": event.unicode,
271
- "actions": GdhValues.actions_of(event),
272
- "pressed": held and not whole,
273
- "whole": whole,
274
- },
275
- focused
276
- )
242
+ var answer: Dictionary = {
243
+ "type": "input_injected",
244
+ "input_type": "key",
245
+ "held_frames": GdhValues.held_answer(whole, frames),
246
+ "held_for": lasted,
247
+ "keycode": event.keycode,
248
+ "physical_keycode": event.physical_keycode,
249
+ "shift": event.shift_pressed,
250
+ "ctrl": event.ctrl_pressed,
251
+ "alt": event.alt_pressed,
252
+ "unicode": event.unicode,
253
+ "actions": GdhValues.actions_of(event),
254
+ "pressed": held and not whole,
255
+ "whole": whole,
256
+ }
257
+ return GdhValues.focus_noted(answer, focused, _host)
277
258
 
278
259
 
279
260
  ## Clicks a mouse button at a position, and lets go of it again unless asked to hold it.
@@ -314,16 +295,22 @@ func inject_mouse_click(params: Dictionary) -> Dictionary:
314
295
  # its drag from the press, which is where a real pointer would be.
315
296
  var _moved: Vector2 = _arrive(position)
316
297
  var supplied: bool = _follow_in_the_window(position)
317
- var press: InputEventMouseButton = _button(position, button, whole or held, double)
298
+ # A double click's first click goes whole before it, as [method _click_button] sends one.
299
+ if double and held and not notch:
300
+ Input.parse_input_event(GdhMouse.button_event(position, button, true, false))
301
+ await _host.get_tree().process_frame
302
+ Input.parse_input_event(GdhMouse.button_event(position, button, false, false))
303
+ await _host.get_tree().process_frame
304
+ var press: InputEventMouseButton = GdhMouse.button_event(position, button, whole or held, double)
318
305
  Input.parse_input_event(press)
319
306
  var lasted: Variant = null
320
307
  if notch:
321
- Input.parse_input_event(_button(position, button, false, false))
308
+ Input.parse_input_event(GdhMouse.button_event(position, button, false, false))
322
309
  elif whole:
323
310
  # A frame between the halves at least, as inject_action has, so a control that acts on the
324
311
  # press and one that acts on the release each get a frame to do it in.
325
312
  lasted = await GdhValues.hold(_host.get_tree(), frames)
326
- Input.parse_input_event(_button(position, button, false, false))
313
+ Input.parse_input_event(GdhMouse.button_event(position, button, false, false))
327
314
  # Input hands the events on at the next frame, so the control under the pointer is read after it.
328
315
  await _host.get_tree().process_frame
329
316
  var hovered: Control = _hover.hovered_in(_host.get_viewport())
@@ -424,7 +411,7 @@ func inject_mouse_drag(params: Dictionary) -> Dictionary:
424
411
  var end: Vector2 = points[points.size() - 1]
425
412
  var _moved: Vector2 = _arrive(start)
426
413
  var supplied: bool = _follow_in_the_window(start)
427
- Input.parse_input_event(_button(start, button, true, false))
414
+ Input.parse_input_event(GdhMouse.button_event(start, button, true, false))
428
415
  var travelled: Vector2 = Vector2.ZERO
429
416
  for step: int in range(1, frames + 1):
430
417
  await _host.get_tree().process_frame
@@ -434,7 +421,7 @@ func inject_mouse_drag(params: Dictionary) -> Dictionary:
434
421
  supplied = _follow_in_the_window(at) or supplied
435
422
  Input.parse_input_event(_motion(at, relative))
436
423
  await _host.get_tree().process_frame
437
- Input.parse_input_event(_button(end, button, false, false))
424
+ Input.parse_input_event(GdhMouse.button_event(end, button, false, false))
438
425
  await _host.get_tree().process_frame
439
426
 
440
427
  var said: Dictionary = _pointer_noted(
@@ -555,6 +542,7 @@ func click(params: Dictionary) -> Dictionary:
555
542
  # other words, a link among them. Said either way, so a press on the middle is never read as one
556
543
  # on the words.
557
544
  var aim: Vector2 = control.size * 0.5
545
+ var at_centre: bool = true
558
546
  var item_scrolled: bool = false
559
547
  if item != null:
560
548
  var found_by_words: Dictionary = found
@@ -564,6 +552,7 @@ func click(params: Dictionary) -> Dictionary:
564
552
  if placed_item.has("message"):
565
553
  return placed_item
566
554
  aim = placed_item["point"]
555
+ at_centre = false
567
556
  item_scrolled = placed_item["scrolled"]
568
557
  var rich: RichTextLabel = control as RichTextLabel
569
558
  if found != null and rich != null:
@@ -571,6 +560,7 @@ func click(params: Dictionary) -> Dictionary:
571
560
  var found_here: Dictionary = found
572
561
  if placed.has("point"):
573
562
  aim = placed["point"]
563
+ at_centre = false
574
564
  found_here["pressedOn"] = "the words"
575
565
  elif placed.has("unplaced"):
576
566
  found_here["pressedOn"] = "the middle of the label"
@@ -607,6 +597,20 @@ func click(params: Dictionary) -> Dictionary:
607
597
  }
608
598
  local = control.get_global_transform_with_canvas() * aim
609
599
 
600
+ # A control taller or wider than what shows it has its centre out of sight at every scroll, and
601
+ # a player clicks the part they can see. Refused, it was called a control nothing could click.
602
+ var on_part: bool = false
603
+ if at_centre and not _in_sight(control, own, local):
604
+ var part: Variant = GdhScreen.shown_part(control)
605
+ if part is Rect2:
606
+ var seen: Rect2 = part
607
+ var middle: Vector2 = seen.get_center()
608
+ # The box of a turned control is wider than the control, so its middle may be off it.
609
+ var back: Vector2 = control.get_global_transform_with_canvas().affine_inverse() * middle
610
+ if seen.has_area() and Rect2(Vector2.ZERO, control.size).has_point(back):
611
+ local = middle
612
+ on_part = true
613
+
610
614
  var reached: Dictionary = GdhScreen.reach(own, local)
611
615
  var viewport: Viewport = reached["viewport"]
612
616
  var centre: Vector2 = reached["point"]
@@ -643,6 +647,8 @@ func click(params: Dictionary) -> Dictionary:
643
647
  if hovered != null:
644
648
  hovered_path = str(hovered.get_path())
645
649
  var landed: bool = hovered == control or (hovered != null and control.is_ancestor_of(hovered))
650
+ # Asked only of a miss, before the press restacks windows: hovered missed a window over it.
651
+ var cover: Node = null if landed else GdhTargets.drawn_over(control)
646
652
 
647
653
  # The link a rich label says was pressed, which is its own word for what the press reached.
648
654
  var links: Array = []
@@ -658,10 +664,7 @@ func click(params: Dictionary) -> Dictionary:
658
664
  var was_pressed: Variant = null
659
665
  if toggle != null and toggle.toggle_mode:
660
666
  was_pressed = toggle.button_pressed
661
- var press: InputEventMouseButton = _button(position, button, true, double)
662
- _press_button(viewport, press)
663
- await _host.get_tree().process_frame
664
- _press_button(viewport, _button(position, button, false, false))
667
+ var press: InputEventMouseButton = await _click_button(viewport, position, button, double)
665
668
  # The release is what a button acts on, and a queue_free it causes lands at the end of
666
669
  # this frame; the frame passes so the answer describes the control as the click left it.
667
670
  await _host.get_tree().process_frame
@@ -685,12 +688,16 @@ func click(params: Dictionary) -> Dictionary:
685
688
  "control_afterwards": afterwards,
686
689
  "scrolled_into_view": scrolled,
687
690
  }
691
+ if on_part:
692
+ answer["pressed_on"] = "the middle of the part on screen, since no scroll brings its centre into view"
688
693
  # A Label and a TextureRect ignore the pointer unless told otherwise, so the click goes to what is
689
694
  # under them, and landed false with that control as hovered said nothing of why.
690
695
  if not landed and control.mouse_filter == Control.MOUSE_FILTER_IGNORE:
691
696
  answer["why_not_landed"] = (
692
697
  "%s's mouse_filter is ignore, so the pointer passes through it to what is under it" % node_path
693
698
  )
699
+ elif cover != null:
700
+ answer["why_not_landed"] = "%s is drawn over it at the point clicked" % cover.get_path()
694
701
  if found != null:
695
702
  answer["found"] = found
696
703
  if was_pressed != null:
@@ -795,6 +802,23 @@ func _no_window_note(viewport: Viewport) -> String:
795
802
  ##
796
803
  ## Any other viewport is pushed into directly: Input reaches only the game's window. That is a
797
804
  ## viewport with no place on the screen, since `GdhScreen.reach` carries every other one there.
805
+ ## A press and its release a frame apart, answering with the press. A double click arrives as the
806
+ ## engine gets one from the system: a whole click, then a second press marked as a double click and
807
+ ## its release. Sent as the marked press alone, it pressed a button once where a player's presses it
808
+ ## twice.
809
+ func _click_button(viewport: Viewport, position: Vector2, button: int, double: bool) -> InputEventMouseButton:
810
+ if double:
811
+ _press_button(viewport, GdhMouse.button_event(position, button, true, false))
812
+ await _host.get_tree().process_frame
813
+ _press_button(viewport, GdhMouse.button_event(position, button, false, false))
814
+ await _host.get_tree().process_frame
815
+ var press: InputEventMouseButton = GdhMouse.button_event(position, button, true, double)
816
+ _press_button(viewport, press)
817
+ await _host.get_tree().process_frame
818
+ _press_button(viewport, GdhMouse.button_event(position, button, false, false))
819
+ return press
820
+
821
+
798
822
  func _press_button(viewport: Viewport, event: InputEventMouseButton) -> void:
799
823
  if viewport == _host.get_tree().root:
800
824
  Input.parse_input_event(event)
@@ -877,9 +901,7 @@ func _click_in_the_world(node_path: String, item: Node3D, params: Dictionary) ->
877
901
  var through: Dictionary = _through_the_interface(out)
878
902
  var hovered_path: Variant = through["hovered"]
879
903
 
880
- _press_button(arriving, _button(position, button, true, double))
881
- await _host.get_tree().process_frame
882
- _press_button(arriving, _button(position, button, false, false))
904
+ var _press: InputEventMouseButton = await _click_button(arriving, position, button, double)
883
905
  await _host.get_tree().process_frame
884
906
 
885
907
  return _pointer_noted(
@@ -975,18 +997,3 @@ func _motion(position: Vector2, relative: Vector2) -> InputEventMouseMotion:
975
997
  event.screen_velocity = event.velocity
976
998
  event.button_mask = Input.get_mouse_button_mask()
977
999
  return event
978
-
979
-
980
- ## A button event carries the buttons held once it has happened: the pressed one among them, the
981
- ## released one out. A wheel step is a button with no mask, as it is from a real mouse.
982
- func _button(position: Vector2, button: int, pressed: bool, double: bool) -> InputEventMouseButton:
983
- var event: InputEventMouseButton = InputEventMouseButton.new()
984
- event.position = position
985
- event.global_position = position
986
- event.button_index = button as MouseButton
987
- event.pressed = pressed
988
- event.double_click = double
989
- var held: int = Input.get_mouse_button_mask()
990
- var own: int = GdhMouse.mask_of(button)
991
- event.button_mask = (held | own) if pressed else (held & ~own)
992
- return event
@@ -65,6 +65,23 @@ static func mask_of(button: int) -> int:
65
65
  return 1 << (button - 1)
66
66
 
67
67
 
68
+ ## A button event carries the buttons held once it has happened: the pressed one among them, the
69
+ ## released one out. A wheel step is a button with no mask, as it is from a real mouse.
70
+ static func button_event(
71
+ position: Vector2, button: int, pressed: bool, double: bool
72
+ ) -> InputEventMouseButton:
73
+ var event: InputEventMouseButton = InputEventMouseButton.new()
74
+ event.position = position
75
+ event.global_position = position
76
+ event.button_index = button as MouseButton
77
+ event.pressed = pressed
78
+ event.double_click = double
79
+ var held: int = Input.get_mouse_button_mask()
80
+ var own: int = mask_of(button)
81
+ event.button_mask = (held | own) if pressed else (held & ~own)
82
+ return event
83
+
84
+
68
85
  ## [param answer], saying so when one of [param points] lies outside [param viewport]: the GUI picks
69
86
  ## what is under the pointer only inside it, so such a press or motion reaches no control, and its
70
87
  ## answer read the same as one that landed.