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.
- package/build/cli.js +1036 -525
- package/build/godot/addons/gdharness_editor/property_values.gd +484 -65
- package/build/godot/addons/gdharness_editor/reading.gd +17 -0
- package/build/godot/addons/gdharness_editor/scene_file.gd +229 -0
- package/build/godot/addons/gdharness_editor/scene_file.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/serialisation.gd +116 -7
- package/build/godot/addons/gdharness_editor/tool_executor.gd +10 -3
- package/build/godot/addons/gdharness_editor/tools/animation_tools.gd +374 -242
- package/build/godot/addons/gdharness_editor/tools/class_tools.gd +47 -9
- package/build/godot/addons/gdharness_editor/tools/filesystem_tools.gd +206 -0
- package/build/godot/addons/gdharness_editor/tools/filesystem_tools.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/tools/play_tools.gd +90 -15
- package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +373 -140
- package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +566 -472
- package/build/godot/addons/gdharness_runtime/call_errors.gd +42 -0
- package/build/godot/addons/gdharness_runtime/call_errors.gd.uid +1 -0
- package/build/godot/addons/gdharness_runtime/reading.gd +17 -0
- package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +8 -4
- package/build/godot/addons/gdharness_runtime/runtime_changes.gd +440 -0
- package/build/godot/addons/gdharness_runtime/runtime_changes.gd.uid +1 -0
- package/build/godot/addons/gdharness_runtime/runtime_input.gd +184 -338
- package/build/godot/addons/gdharness_runtime/runtime_paths.gd +28 -8
- package/build/godot/addons/gdharness_runtime/runtime_queries.gd +7 -299
- package/build/godot/addons/gdharness_runtime/runtime_screen.gd +13 -1
- package/build/godot/addons/gdharness_runtime/runtime_targets.gd +90 -24
- package/build/godot/addons/gdharness_runtime/runtime_typing.gd +351 -0
- package/build/godot/addons/gdharness_runtime/runtime_typing.gd.uid +1 -0
- package/build/godot/addons/gdharness_runtime/runtime_values.gd +92 -33
- package/build/godot/addons/gdharness_runtime/runtime_waits.gd +20 -9
- package/build/godot/addons/gdharness_runtime/serialisation.gd +116 -7
- package/build/godot/operations/audio_buses.gd +47 -11
- package/build/godot/operations/class_cache.gd +18 -2
- package/build/godot/operations/dependencies.gd +128 -17
- package/build/godot/operations/file_walk.gd +20 -0
- package/build/godot/operations/gdscript_analysis.gd +59 -20
- package/build/godot/operations/gdscript_authoring.gd +127 -60
- package/build/godot/operations/godot_operations.gd +24 -5
- package/build/godot/operations/import_pipeline.gd +179 -19
- package/build/godot/operations/logger.gd +2 -2
- package/build/godot/operations/plugins.gd +88 -70
- package/build/godot/operations/project_config.gd +41 -6
- package/build/godot/operations/project_diagnostics.gd +25 -10
- package/build/godot/operations/reading.gd +17 -0
- package/build/godot/operations/resource_files.gd +60 -17
- package/build/godot/operations/serialisation.gd +116 -7
- package/build/index.js +949 -435
- 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
|
-
|
|
450
|
-
|
|
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
|
|
303
|
+
return holder
|
|
460
304
|
walking = walking.get_parent()
|
|
461
|
-
return
|
|
305
|
+
return null
|
|
462
306
|
|
|
463
307
|
|
|
464
|
-
|
|
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:
|
|
318
|
+
var walking: Variant = control.get_parent()
|
|
467
319
|
while walking != null:
|
|
468
|
-
var
|
|
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
|
-
|
|
472
|
-
|
|
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
|
-
|
|
518
|
-
|
|
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
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
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
|
-
#
|
|
547
|
-
#
|
|
548
|
-
#
|
|
549
|
-
|
|
550
|
-
if not
|
|
551
|
-
|
|
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 =
|
|
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
|
|
827
|
-
var arriving: Viewport =
|
|
828
|
-
var reached_point: Vector2 =
|
|
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
|
|
839
|
-
var hovered_path: Variant =
|
|
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":
|
|
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.
|