gdharness 1.1.21 → 1.1.22

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 (47) hide show
  1. package/build/cli.js +1036 -525
  2. package/build/godot/addons/gdharness_editor/property_values.gd +484 -65
  3. package/build/godot/addons/gdharness_editor/reading.gd +17 -0
  4. package/build/godot/addons/gdharness_editor/scene_file.gd +229 -0
  5. package/build/godot/addons/gdharness_editor/scene_file.gd.uid +1 -0
  6. package/build/godot/addons/gdharness_editor/serialisation.gd +116 -7
  7. package/build/godot/addons/gdharness_editor/tool_executor.gd +10 -3
  8. package/build/godot/addons/gdharness_editor/tools/animation_tools.gd +374 -242
  9. package/build/godot/addons/gdharness_editor/tools/class_tools.gd +47 -9
  10. package/build/godot/addons/gdharness_editor/tools/filesystem_tools.gd +206 -0
  11. package/build/godot/addons/gdharness_editor/tools/filesystem_tools.gd.uid +1 -0
  12. package/build/godot/addons/gdharness_editor/tools/play_tools.gd +90 -15
  13. package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +373 -140
  14. package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +566 -472
  15. package/build/godot/addons/gdharness_runtime/call_errors.gd +42 -0
  16. package/build/godot/addons/gdharness_runtime/call_errors.gd.uid +1 -0
  17. package/build/godot/addons/gdharness_runtime/reading.gd +17 -0
  18. package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +8 -4
  19. package/build/godot/addons/gdharness_runtime/runtime_changes.gd +440 -0
  20. package/build/godot/addons/gdharness_runtime/runtime_changes.gd.uid +1 -0
  21. package/build/godot/addons/gdharness_runtime/runtime_input.gd +184 -338
  22. package/build/godot/addons/gdharness_runtime/runtime_paths.gd +28 -8
  23. package/build/godot/addons/gdharness_runtime/runtime_queries.gd +7 -299
  24. package/build/godot/addons/gdharness_runtime/runtime_screen.gd +13 -1
  25. package/build/godot/addons/gdharness_runtime/runtime_targets.gd +90 -24
  26. package/build/godot/addons/gdharness_runtime/runtime_typing.gd +351 -0
  27. package/build/godot/addons/gdharness_runtime/runtime_typing.gd.uid +1 -0
  28. package/build/godot/addons/gdharness_runtime/runtime_values.gd +92 -33
  29. package/build/godot/addons/gdharness_runtime/runtime_waits.gd +20 -9
  30. package/build/godot/addons/gdharness_runtime/serialisation.gd +116 -7
  31. package/build/godot/operations/audio_buses.gd +47 -11
  32. package/build/godot/operations/class_cache.gd +18 -2
  33. package/build/godot/operations/dependencies.gd +128 -17
  34. package/build/godot/operations/file_walk.gd +20 -0
  35. package/build/godot/operations/gdscript_analysis.gd +59 -20
  36. package/build/godot/operations/gdscript_authoring.gd +127 -60
  37. package/build/godot/operations/godot_operations.gd +24 -5
  38. package/build/godot/operations/import_pipeline.gd +179 -19
  39. package/build/godot/operations/logger.gd +2 -2
  40. package/build/godot/operations/plugins.gd +88 -70
  41. package/build/godot/operations/project_config.gd +41 -6
  42. package/build/godot/operations/project_diagnostics.gd +25 -10
  43. package/build/godot/operations/reading.gd +17 -0
  44. package/build/godot/operations/resource_files.gd +60 -17
  45. package/build/godot/operations/serialisation.gd +116 -7
  46. package/build/index.js +949 -435
  47. package/package.json +1 -1
@@ -1,7 +1,8 @@
1
1
  extends RefCounted
2
2
 
3
3
  ## Input handed to the running game as if a player had given it: actions, keys, the mouse, and
4
- ## a whole click on a Control or a 3D node found by path.
4
+ ## a whole click on a Control or a 3D node found by path. Typing and choosing from a menu, which
5
+ ## reach the game the way a keyboard does, are in runtime_typing.gd.
5
6
 
6
7
  const Values = preload("runtime_values.gd")
7
8
 
@@ -10,18 +11,12 @@ const Values = preload("runtime_values.gd")
10
11
  ## reports are the same place by construction rather than by agreement.
11
12
  const Queries = preload("runtime_queries.gd")
12
13
  const Read = preload("reading.gd")
13
- const Menus = preload("runtime_menus.gd")
14
14
  const Screen = preload("runtime_screen.gd")
15
15
  const Targets = preload("runtime_targets.gd")
16
- const Words = preload("runtime_words.gd")
17
16
 
18
17
  ## The distance from a capital letter to its small one in Unicode. A keycode holds the capital.
19
18
  const TO_SMALL: int = 32
20
19
 
21
- ## The two characters a field reads as keys rather than as text.
22
- const NEWLINE: int = 10
23
- const TAB: int = 9
24
-
25
20
  ## What a game started without a window gets whatever the project settings say, and the usual
26
21
  ## reason a control is out of reach. Only worth telling somebody when it is what they have.
27
22
  const HEADLESS_VIEWPORT: Vector2 = Vector2(64, 64)
@@ -176,154 +171,6 @@ func inject_key(params: Dictionary) -> Dictionary:
176
171
  }
177
172
 
178
173
 
179
- ## Types [param text] wherever the focus is, one key event per character.
180
- ##
181
- ## A key on its own cannot do this and should not try: which character a key produces is the
182
- ## keyboard layout's business, and shift over a digit is an exclamation mark on one layout and
183
- ## something else on the next. Given the character instead there is nothing to guess, so a field
184
- ## can be filled with anything a player could type, this project's own two typefaces included.
185
- ##
186
- ## A newline and a tab are the two characters a field reads as keys rather than as text, so they
187
- ## are sent as those keys and carry no character of their own: typing a name and submitting it is
188
- ## one call rather than two.
189
- ##
190
- ## Pushed into the viewport for the reason [method click] is, and it matters more here: the focus
191
- ## is what decides where a character lands, so a caller that clicked a field and then typed would
192
- ## otherwise have both waiting in the same queue with nothing said about the order.
193
- ##
194
- ## [code]replace[/code] is for a field that already says something, which is most of them: typing
195
- ## lands at the caret, so a spin box reading 2.1 typed "0.3" at reads 2.10.3 and parses back to
196
- ## 2.1, and the answer said four characters had gone in. What the field holds afterwards is read
197
- ## back now rather than echoed from the request, so a call that typed somewhere unhelpful says so.
198
- func inject_text(params: Dictionary) -> Dictionary:
199
- var text: String = str(params.get("text", ""))
200
- var over: bool = Read.as_bool(params.get("replace", false))
201
- if text.is_empty() and not over:
202
- return {"type": "error", "message": "text needs something to type, or replace to empty a field"}
203
-
204
- var viewport: Viewport = _host.get_tree().root
205
- var focused: Control = viewport.gui_get_focus_owner()
206
- var shut: String = _shut_to_typing(focused)
207
- if not shut.is_empty():
208
- return {"type": "error", "message": shut}
209
-
210
- var replaced: bool = over and _select_everything(focused)
211
- # Emptying a field is a real thing to ask for and the one shape of filling one in that types no
212
- # characters. The selection is standing, so the key a player presses over one is what clears it.
213
- if replaced and text.is_empty():
214
- _press(viewport, _held_down(KEY_DELETE))
215
- for index: int in text.length():
216
- _press(viewport, _typed(text.unicode_at(index)))
217
-
218
- # Where it went, which is the one thing a caller cannot see from here. Null is a game reading
219
- # keys for itself with nothing focused, which is a real thing to be typing at.
220
- var into: Variant = null
221
- if focused != null:
222
- into = str(focused.get_path())
223
-
224
- return {
225
- "type": "input_injected",
226
- "input_type": "text",
227
- "text": text,
228
- "characters": text.length(),
229
- "into": into,
230
- "replaced": replaced,
231
- "holds": _what_it_holds(focused),
232
- }
233
-
234
-
235
- ## Selects everything in [param focused] so the next character typed writes over it, and answers
236
- ## whether there was a field to select in.
237
- ##
238
- ## Through the control rather than through a Ctrl+A, which is the one part of filling a field that
239
- ## cannot honestly be sent as a key: the shortcut is Cmd+A on macOS and is an [InputMap] action a
240
- ## project is free to unbind, and a caller asking for the field to be replaced would then be typing
241
- ## on the end of what was there. The replacement itself is still every character a player types.
242
- static func _select_everything(focused: Control) -> bool:
243
- var field: LineEdit = focused as LineEdit
244
- if field != null:
245
- field.select_all()
246
- return true
247
- var box: TextEdit = focused as TextEdit
248
- if box == null:
249
- return false
250
- box.select_all()
251
- return true
252
-
253
-
254
- ## What the field says now, or null where the focus is not a field at all.
255
- ##
256
- ## A spin box rewrites its own text out of the number it parsed, so this is the only thing that
257
- ## says whether what was typed became what the field means. A secret field answers with its mask,
258
- ## which still says how many characters it took.
259
- static func _what_it_holds(focused: Control) -> Variant:
260
- var field: LineEdit = focused as LineEdit
261
- if field != null:
262
- return Words.masked(field) if field.secret else field.text
263
- var box: TextEdit = focused as TextEdit
264
- if box == null:
265
- return null
266
- return box.text
267
-
268
-
269
- ## Why nothing typed would reach [param focused], or "" when it would.
270
- ##
271
- ## Having the focus is not the same as being edited. Since Godot 4.4 a field is focused and shut
272
- ## until something opens it, which is what a click does and what submitting undoes: press Enter in
273
- ## a box and it keeps the focus and drops every key that arrives afterwards. Typing into one
274
- ## answered that the characters had gone in and put nothing anywhere, which is the shape a refusal
275
- ## exists to prevent. Measured on a spin box in a real game: `has_focus` true, `is_editing` false,
276
- ## three characters reported and the field unchanged.
277
- ##
278
- ## Only a [LineEdit] is refused: it is the one control the engine will say this about, since
279
- ## [TextEdit] has no editing state of its own. A game reading keys for itself is typed at with
280
- ## nothing focused at all, and that is not this tool's business to judge.
281
- static func _shut_to_typing(focused: Control) -> String:
282
- var field: LineEdit = focused as LineEdit
283
- if field == null or field.is_editing():
284
- return ""
285
- return (
286
- "%s has the focus and is not being edited, so nothing typed lands in it. Click it first."
287
- % focused.get_path()
288
- )
289
-
290
-
291
- ## The key press that produces [param glyph], as a keyboard would send it.
292
- ## A whole press: down, then the release, so nothing is left held down behind the caller.
293
- static func _press(viewport: Viewport, down: InputEventKey) -> void:
294
- viewport.push_input(down)
295
- var up: InputEventKey = down.duplicate()
296
- up.pressed = false
297
- viewport.push_input(up)
298
-
299
-
300
- ## A key held down by its keycode, for the ones that stand for an edit rather than a character.
301
- static func _held_down(code: Key) -> InputEventKey:
302
- var event: InputEventKey = InputEventKey.new()
303
- event.pressed = true
304
- event.keycode = code
305
- event.physical_keycode = code
306
- event.key_label = code
307
- return event
308
-
309
-
310
- func _typed(glyph: int) -> InputEventKey:
311
- var event: InputEventKey = InputEventKey.new()
312
- event.pressed = true
313
- if glyph == NEWLINE:
314
- event.keycode = KEY_ENTER
315
- elif glyph == TAB:
316
- event.keycode = KEY_TAB
317
- else:
318
- var capital: int = String.chr(glyph).to_upper().unicode_at(0)
319
- event.keycode = capital as Key
320
- event.shift_pressed = capital != glyph
321
- event.unicode = glyph
322
- event.physical_keycode = event.keycode
323
- event.key_label = event.keycode
324
- return event
325
-
326
-
327
174
  ## The character a key produces, or nothing for a key that produces none.
328
175
  ##
329
176
  ## Godot's keycodes below [constant KEY_SPECIAL] are the Unicode code points of the keys that
@@ -436,18 +283,15 @@ func inject_mouse_motion(params: Dictionary) -> Dictionary:
436
283
  }
437
284
 
438
285
 
439
- ## Scrolls whatever is holding [param control] until it is on screen, and answers whether
440
- ## anything moved.
441
- ##
442
- ## Innermost container first and outwards, because ensuring visibility inside an inner one moves
443
- ## the control within the outer one, so the outer has to be asked after the inner has finished
444
- ## moving it. A control with no ScrollContainer over it moves nothing and answers false, which is
445
- ## what keeps the refusal below saying the right thing about a control that is simply off screen.
446
286
  ## Whether [param centre] is somewhere a click can reach it: inside the viewport, and inside every
447
287
  ## ScrollContainer between the control and the root, each of which clips what it holds.
448
288
  static func _in_sight(control: Control, viewport: Viewport, centre: Vector2) -> bool:
449
- if not viewport.get_visible_rect().has_point(centre):
450
- return false
289
+ return viewport.get_visible_rect().has_point(centre) and _clipped_by(control, centre) == null
290
+
291
+
292
+ ## The ScrollContainer between [param control] and the root that clips [param centre] away from it,
293
+ ## or null when none does.
294
+ static func _clipped_by(control: Control, centre: Vector2) -> ScrollContainer:
451
295
  var walking: Node = control.get_parent()
452
296
  while walking != null:
453
297
  var holder: ScrollContainer = walking as ScrollContainer
@@ -456,20 +300,35 @@ static func _in_sight(control: Control, viewport: Viewport, centre: Vector2) ->
456
300
  if holder != null:
457
301
  var seen: Rect2 = holder.get_global_transform_with_canvas() * Rect2(Vector2.ZERO, holder.size)
458
302
  if not seen.has_point(centre):
459
- return false
303
+ return holder
460
304
  walking = walking.get_parent()
461
- return true
305
+ return null
462
306
 
463
307
 
464
- static func _scroll_into_view(control: Control) -> bool:
308
+ ## Scrolls whatever is holding [param control] until it is on screen, and answers whether the view
309
+ ## moved.
310
+ ##
311
+ ## Innermost container first and outwards, a frame apart. A container moves what it holds on the
312
+ ## next layout pass rather than inside the call, and the next one out works out where the control is
313
+ ## from where it is drawn, so asked in the same frame it scrolled to where the control had been.
314
+ ## Moved means a scroll value changed: a container asked to show what it already shows moves
315
+ ## nothing, and answering true for it said the view had moved when it had not.
316
+ func _scroll_into_view(control: Control) -> bool:
465
317
  var moved: bool = false
466
- var walking: Node = control.get_parent()
318
+ var walking: Variant = control.get_parent()
467
319
  while walking != null:
468
- var holder: ScrollContainer = walking as ScrollContainer
320
+ var step: Node = walking
321
+ var holder: ScrollContainer = step as ScrollContainer
469
322
  if holder != null:
323
+ var before: Vector2i = Vector2i(holder.scroll_horizontal, holder.scroll_vertical)
470
324
  holder.ensure_control_visible(control)
471
- moved = true
472
- walking = walking.get_parent()
325
+ if Vector2i(holder.scroll_horizontal, holder.scroll_vertical) != before:
326
+ moved = true
327
+ await _host.get_tree().process_frame
328
+ # A panel that rebuilds itself can go in the frame this waited.
329
+ if not is_instance_valid(control) or not is_instance_valid(holder):
330
+ return moved
331
+ walking = step.get_parent()
473
332
  return moved
474
333
 
475
334
 
@@ -514,8 +373,9 @@ func click(params: Dictionary) -> Dictionary:
514
373
  "message": "%s is a %s, not a Control or a Node3D" % [node_path, node.get_class()]
515
374
  }
516
375
  var control: Control = node
517
- if not control.is_visible_in_tree():
518
- return {"type": "error", "message": "%s is not visible, so nothing can click it" % node_path}
376
+ var unmoved: String = _takes_no_click(control, node_path)
377
+ if not unmoved.is_empty():
378
+ return {"type": "error", "message": unmoved}
519
379
 
520
380
  # In the control's own viewport first, which is the space its scrolling happens in.
521
381
  var own: Viewport = control.get_viewport()
@@ -532,33 +392,30 @@ func click(params: Dictionary) -> Dictionary:
532
392
  # scrolled past: the button was at y 69 and its container started at y 166.
533
393
  var scrolled: bool = false
534
394
  if not _in_sight(control, own, local):
535
- scrolled = _scroll_into_view(control)
536
- if scrolled:
537
- # A container moves its child on the next layout pass rather than inside the call.
538
- await _host.get_tree().process_frame
539
- local = control.get_global_transform_with_canvas() * (control.size * 0.5)
395
+ scrolled = await _scroll_into_view(control)
396
+ var left: String = Values.afterwards(control)
397
+ if left != "in_tree":
398
+ return {
399
+ "type": "error",
400
+ "message":
401
+ (
402
+ "%s was %s while it was being scrolled into view, so nothing was clicked"
403
+ % [node_path, "freed" if left == "freed" else "taken out of the tree"]
404
+ )
405
+ }
406
+ local = control.get_global_transform_with_canvas() * (control.size * 0.5)
540
407
 
541
408
  var reached: Dictionary = Screen.reach(own, local)
542
409
  var viewport: Viewport = reached["viewport"]
543
410
  var centre: Vector2 = reached["point"]
544
411
  var position: Vector2 = viewport.get_final_transform() * centre
545
- # The GUI only delivers to what is inside the viewport, so a centre outside it would be a
546
- # click that silently reached nothing. A game with no window has a 64 by 64 viewport
547
- # whatever the project settings say, which is the usual reason to be here and is not
548
- # something the caller can read off the rect on its own. Its own viewport as well, since a
549
- # control outside its SubViewport is drawn nowhere.
550
- if not viewport.get_visible_rect().has_point(centre) or not own.get_visible_rect().has_point(local):
551
- var why: String = _no_window_note(viewport)
552
- if scrolled:
553
- why = ". It was scrolled as far as what holds it goes and is still out there"
554
- return {
555
- "type": "error",
556
- "message":
557
- (
558
- "%s has its centre at %s, outside the viewport %s, so nothing can click it%s"
559
- % [node_path, centre, viewport.get_visible_rect(), why]
560
- )
561
- }
412
+ # The GUI only delivers to what is inside the viewport, so a centre outside it would be a click
413
+ # that silently reached nothing: outside the viewport it is drawn in, outside the one that shows
414
+ # that on the screen, or clipped away by a ScrollContainer that would not scroll it into view.
415
+ # Each is said of the viewport or container at fault, with its own rect and the point in it.
416
+ var out_there: String = _out_of_reach(control, node_path, local, reached, scrolled)
417
+ if not out_there.is_empty():
418
+ return {"type": "error", "message": out_there}
562
419
  var button: int = _resolve_mouse_button(params.get("button", MOUSE_BUTTON_LEFT))
563
420
  if button < 0:
564
421
  return _no_such_button(params.get("button"))
@@ -594,7 +451,7 @@ func click(params: Dictionary) -> Dictionary:
594
451
  # What became of the control: still in the tree, taken out of it, or freed. A button that
595
452
  # opened another screen is the second or the third, and the caller wants to hear that
596
453
  # rather than guess it from a tree that has changed shape.
597
- var afterwards: String = _afterwards(control)
454
+ var afterwards: String = Values.afterwards(control)
598
455
 
599
456
  var answer: Dictionary = {
600
457
  "type": "clicked",
@@ -612,13 +469,70 @@ func click(params: Dictionary) -> Dictionary:
612
469
  return answer
613
470
 
614
471
 
472
+ ## Why [param control] would take no click a player gave it, or "" when it would.
473
+ ##
474
+ ## The last two answered landed, since the pointer reaches the control, while the press did nothing,
475
+ ## measured on 4.7.2: a disabled button keeps its mouse filter and ignores the press, and a control
476
+ ## that does not process while the game is paused is hovered and never handed the event.
477
+ func _takes_no_click(control: Control, node_path: String) -> String:
478
+ if not control.is_visible_in_tree():
479
+ return "%s is not visible, so nothing can click it" % node_path
480
+ var button: BaseButton = control as BaseButton
481
+ if button != null and button.disabled:
482
+ return "%s is disabled, so clicking it presses nothing" % node_path
483
+ if not control.can_process():
484
+ if _host.get_tree().paused:
485
+ return (
486
+ "the game is paused and %s does not process while it is, so clicking it does nothing"
487
+ % node_path
488
+ )
489
+ return "%s has its processing disabled, so clicking it does nothing" % node_path
490
+ return ""
491
+
492
+
493
+ ## Why a click at [param control]'s centre would reach nothing, or "" when it would reach it: the
494
+ ## centre outside the viewport it is drawn in, at [param local]; outside the viewport showing that
495
+ ## one on the screen, which [param reached] holds with the centre there; or clipped away by a
496
+ ## ScrollContainer holding it. A refusal names the viewport or container at fault and the point in
497
+ ## that one's own space, since naming another printed a point inside the rect it was said to be
498
+ ## outside. The note on a game with no window goes only on the viewport that is the window's.
499
+ func _out_of_reach(
500
+ control: Control, node_path: String, local: Vector2, reached: Dictionary, scrolled: bool
501
+ ) -> String:
502
+ var after: String = (
503
+ ". It was scrolled as far as what holds it goes and is still out there" if scrolled else ""
504
+ )
505
+ for place: Array in [[control.get_viewport(), local], [reached["viewport"], reached["point"]]]:
506
+ var viewport: Viewport = place[0]
507
+ var point: Vector2 = place[1]
508
+ if viewport.get_visible_rect().has_point(point):
509
+ continue
510
+ var which: String = "" if viewport == _host.get_tree().root else " of %s" % viewport.get_path()
511
+ return (
512
+ "%s has its centre at %s, outside the viewport%s %s, so nothing can click it%s%s"
513
+ % [node_path, point, which, viewport.get_visible_rect(), after, _no_window_note(viewport)]
514
+ )
515
+ var holder: ScrollContainer = _clipped_by(control, local)
516
+ if holder == null:
517
+ return ""
518
+ return (
519
+ "%s has its centre at %s, outside the part of %s that shows it, so nothing can click it%s"
520
+ % [
521
+ node_path,
522
+ local,
523
+ holder.get_path(),
524
+ after if scrolled else ". It could not be scrolled into view",
525
+ ]
526
+ )
527
+
528
+
615
529
  ## What to add to a refusal about a point outside the viewport, when the reason is that nobody
616
530
  ## gave this game a window. The rect on its own does not say it, and it is the usual reason.
617
531
  ##
618
532
  ## The size is only claimed where it is true: the rect is printed beside this, so naming 64 by 64
619
533
  ## over a viewport somebody has resized says two different things in one sentence.
620
534
  func _no_window_note(viewport: Viewport) -> String:
621
- if _host.get_tree().root.can_draw():
535
+ if viewport != _host.get_tree().root or _host.get_tree().root.can_draw():
622
536
  return ""
623
537
  if viewport.get_visible_rect().size == HEADLESS_VIEWPORT:
624
538
  return (
@@ -628,118 +542,6 @@ func _no_window_note(viewport: Viewport) -> String:
628
542
  return ". This game has no window: run it with a window to reach this control"
629
543
 
630
544
 
631
- ## Chooses an item out of a menu, by what it says or by where it is in the list.
632
- ##
633
- ## A menu's items are drawn rather than built, so there is no node under the pointer to aim at and
634
- ## no rectangle to ask for: [PopupMenu] exposes their text, their ids and which one has the focus,
635
- ## and nothing about where any of them is. So a click cannot reach one, and a whole click on the
636
- ## [OptionButton] in front of it opens the menu on the press and closes it again on the release.
637
- ## Every language picker, every filter and every dropdown in a game was unreachable, and the way
638
- ## past it was to call `select` and emit `item_selected`, which sets a number and runs none of the
639
- ## engine's own path.
640
- ##
641
- ## Chosen the way a keyboard chooses: the item takes the focus and then Enter presses it, which is
642
- ## the same route through [PopupMenu] a pointer takes and which needs no geometry, so it works in a
643
- ## game with no window as well.
644
- ##
645
- ## [param path] may be the menu or the button in front of it. Naming the button is what a caller
646
- ## has, since the menu is an internal child with a generated name that changes between runs.
647
- func choose(params: Dictionary) -> Dictionary:
648
- var node_path: String = str(params.get("path", ""))
649
- if node_path.is_empty():
650
- return {"type": "error", "message": "Node path required"}
651
- var standing: Dictionary = Values.node_at(_host.get_tree().root, node_path)
652
- if standing.has("message"):
653
- return standing
654
- var node: Node = standing["node"]
655
-
656
- var menu: PopupMenu = Menus.menu_of(node)
657
- if menu == null:
658
- return {
659
- "type": "error",
660
- "message":
661
- (
662
- "%s is a %s, which is neither a PopupMenu nor something holding one"
663
- % [node_path, node.get_class()]
664
- )
665
- }
666
-
667
- if not params.has("index") and str(params.get("text", "")).is_empty():
668
- return {
669
- "type": "error",
670
- "message":
671
- (
672
- "%s needs the item named, by text or index. It holds: %s"
673
- % [node_path, ", ".join(Menus.items_of(menu))]
674
- )
675
- }
676
-
677
- var index: int = Menus.wanted_item(menu, params)
678
- if index < 0:
679
- return {
680
- "type": "error",
681
- "message": "%s has no such item. It holds: %s" % [node_path, ", ".join(Menus.items_of(menu))]
682
- }
683
- if menu.is_item_separator(index):
684
- var heading: String = Words.item_says(menu, index).strip_edges()
685
- return {
686
- "type": "error",
687
- "message":
688
- (
689
- "%s item %d is a separator, not a choice" % [node_path, index]
690
- if heading.is_empty()
691
- else (
692
- "%s item %d, %s, is a separator heading the items below it, not a choice"
693
- % [node_path, index, heading]
694
- )
695
- )
696
- }
697
- if menu.is_item_disabled(index):
698
- return {
699
- "type": "error",
700
- "message": "%s item %d, %s, is disabled" % [node_path, index, Words.item_says(menu, index)]
701
- }
702
-
703
- # Shown first, because a menu nobody has opened has no focus to move and Enter would go to
704
- # whatever is behind it. An OptionButton opens its own; a bare PopupMenu is popped where it
705
- # already sits, which leaves a menu that was already open where it is.
706
- var opened: bool = Menus.open_the_menu(node, menu)
707
- await _host.get_tree().process_frame
708
-
709
- # Read before the press, because the press can take the menu away: a game that rebuilds its
710
- # settings screen on item_selected frees the button and its menu inside the pick, and reading
711
- # the item afterwards was an engine error in the game's log and an empty answer to a pick that
712
- # had taken.
713
- var answer: Dictionary = {
714
- "type": "chosen",
715
- "path": node_path,
716
- "index": index,
717
- "text": Words.item_says(menu, index),
718
- "id": menu.get_item_id(index),
719
- "opened": opened,
720
- "menu": str(menu.get_path()),
721
- }
722
- menu.scroll_to_item(index)
723
- menu.set_focused_item(index)
724
- # Through Input rather than pushed at the menu, which is how a keyboard reaches an open one: a
725
- # popup is a Window, it takes the focus when it opens, and Input delivers to whichever window
726
- # has it. Pushed straight at the menu the event arrived and nothing happened.
727
- Input.parse_input_event(_accept(true))
728
- await _host.get_tree().process_frame
729
- Input.parse_input_event(_accept(false))
730
- await _host.get_tree().process_frame
731
-
732
- answer["control_afterwards"] = _afterwards(node)
733
- # What the button in front of the menu reads now, which is the answer to "did it take": a menu
734
- # item that fired changes the thing holding it, and nothing else about the press says so. A
735
- # button the pick took away has nothing to read, and that it went is the answer.
736
- if answer["control_afterwards"] == "in_tree" and node is OptionButton:
737
- var chooser: OptionButton = node
738
- answer["selected"] = chooser.get_selected()
739
- answer["shows"] = Words.said_by(chooser)
740
- return answer
741
-
742
-
743
545
  ## Sends a click's press or release into [param viewport], through Input when that is the game's
744
546
  ## own window.
745
547
  ##
@@ -762,27 +564,6 @@ func _press_button(viewport: Viewport, event: InputEventMouseButton) -> void:
762
564
  viewport.push_input(event)
763
565
 
764
566
 
765
- ## What became of [param node] once an input it was given has run: still in the tree, taken out
766
- ## of it, or freed. Untyped, because a freed reference handed to a typed parameter is itself the
767
- ## engine error this is here to avoid.
768
- static func _afterwards(node: Variant) -> String:
769
- if not is_instance_valid(node):
770
- return "freed"
771
- if not node is Node:
772
- return "removed"
773
- var placed: Node = node
774
- return "in_tree" if placed.is_inside_tree() else "removed"
775
-
776
-
777
- func _accept(pressed: bool) -> InputEventKey:
778
- var event: InputEventKey = InputEventKey.new()
779
- event.keycode = KEY_ENTER
780
- event.physical_keycode = KEY_ENTER
781
- event.key_label = KEY_ENTER
782
- event.pressed = pressed
783
- return event
784
-
785
-
786
567
  ## A whole click aimed at where a 3D node is drawn, for a game that picks with a ray out of the
787
568
  ## cursor rather than with a Control.
788
569
  ##
@@ -823,9 +604,21 @@ func _click_in_the_world(node_path: String, item: Node3D, params: Dictionary) ->
823
604
  )
824
605
  }
825
606
 
826
- var reached: Dictionary = Screen.reach(viewport, aim)
827
- var arriving: Viewport = reached["viewport"]
828
- var reached_point: Vector2 = reached["point"]
607
+ var out: Array[Dictionary] = Screen.steps_out(viewport, aim)
608
+ var arriving: Viewport = out.back()["viewport"]
609
+ var reached_point: Vector2 = out.back()["point"]
610
+ # Checked where the pointer arrives as well as where the node is drawn: a room in a SubViewport
611
+ # larger than the container or the window showing it has points inside the SubViewport that are
612
+ # outside everything on the screen, and a click at one reached nothing.
613
+ if arriving != viewport and not arriving.get_visible_rect().has_point(reached_point):
614
+ return {
615
+ "type": "error",
616
+ "message":
617
+ (
618
+ "%s is drawn at %s, which is %s on the screen, outside the viewport %s, so nothing can click it%s"
619
+ % [node_path, aim, reached_point, arriving.get_visible_rect(), _no_window_note(arriving)]
620
+ )
621
+ }
829
622
  var position: Vector2 = arriving.get_final_transform() * reached_point
830
623
  var button: int = _resolve_mouse_button(params.get("button", MOUSE_BUTTON_LEFT))
831
624
  if button < 0:
@@ -835,10 +628,8 @@ func _click_in_the_world(node_path: String, item: Node3D, params: Dictionary) ->
835
628
  arriving.push_input(_motion(position, _arrive(position)))
836
629
  # Read before the press, for the reason the Control click reads it: what the caller needs to
837
630
  # know is whether a panel is sitting over the room, and the press is what would change it.
838
- var hovered: Control = viewport.gui_get_hovered_control()
839
- var hovered_path: Variant = null
840
- if hovered != null:
841
- hovered_path = str(hovered.get_path())
631
+ var through: Dictionary = _through_the_interface(out)
632
+ var hovered_path: Variant = through["hovered"]
842
633
 
843
634
  _press_button(arriving, _button(position, button, true, double))
844
635
  await _host.get_tree().process_frame
@@ -855,11 +646,66 @@ func _click_in_the_world(node_path: String, item: Node3D, params: Dictionary) ->
855
646
  # The interface did not take it, so it reached the game's own input. As close to "it
856
647
  # landed" as anything outside the game can get, and said in the same word the Control
857
648
  # click says it in.
858
- "landed": hovered == null,
649
+ "landed": through["landed"],
859
650
  "camera": found["camera"],
860
651
  }
861
652
 
862
653
 
654
+ ## Whether a pointer on its way out through [param out], the steps [method Screen.steps_out]
655
+ ## answers, gets past the interface in every viewport, as {"landed", "hovered"}: the path of the
656
+ ## control that took it, or of the one under it that let it by, or null.
657
+ ##
658
+ ## In the viewport the node is drawn in, a control under the pointer takes it unless it and every
659
+ ## control holding it let the pointer pass. In each viewport outside that one, the pointer has to be
660
+ ## over what shows the one inside: the SubViewportContainer, with nothing drawn over it, or inside
661
+ ## the embedded window. Read in the first viewport alone, a click beside a container that is smaller
662
+ ## than what it shows, or under a panel drawn over the container, answered landed.
663
+ static func _through_the_interface(out: Array[Dictionary]) -> Dictionary:
664
+ var first_under: Variant = null
665
+ for step: Dictionary in out:
666
+ var viewport: Viewport = step["viewport"]
667
+ var through: Variant = step["through"]
668
+ var hovered: Control = viewport.gui_get_hovered_control()
669
+ if through is Window:
670
+ var window: Window = through
671
+ var point: Vector2 = step["point"]
672
+ if not Rect2(window.position, window.size).has_point(point):
673
+ return {"landed": false, "hovered": _path_or_null(hovered)}
674
+ continue
675
+ if through is SubViewportContainer:
676
+ if hovered != through:
677
+ return {"landed": false, "hovered": _path_or_null(hovered)}
678
+ continue
679
+ if hovered != null and not _passes_on(hovered):
680
+ return {"landed": false, "hovered": str(hovered.get_path())}
681
+ if hovered != null and first_under == null:
682
+ first_under = str(hovered.get_path())
683
+ return {"landed": true, "hovered": first_under}
684
+
685
+
686
+ ## Whether a pointer over [param control] goes on past the interface: a control that lets it pass
687
+ ## hands it to the one holding it, and one that stops it takes it, up to a control drawn on its own
688
+ ## or something that is not a control. Measured on 4.7.2: a HUD over the room letting the pointer
689
+ ## pass answered not landed while the game's own input took the press. A script on a control that
690
+ ## lets it pass can still take the event for itself, which nothing outside it can see.
691
+ static func _passes_on(control: Control) -> bool:
692
+ var walk: Node = control
693
+ while walk is Control:
694
+ var at: Control = walk
695
+ if at.get_mouse_filter_with_override() == Control.MOUSE_FILTER_STOP:
696
+ return false
697
+ if at.top_level:
698
+ break
699
+ walk = at.get_parent()
700
+ return true
701
+
702
+
703
+ static func _path_or_null(node: Node) -> Variant:
704
+ if node == null:
705
+ return null
706
+ return str(node.get_path())
707
+
708
+
863
709
  ## The buttons a real pointer event carries are the ones held as it happens, and Input keeps that
864
710
  ## from every button event it is given, injected ones included, so a motion between a held click
865
711
  ## and its release is a drag to a control that reads the mask rather than remembering the click.