gdharness 1.1.8 → 1.1.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.
- package/build/cli.js +929 -1245
- package/build/godot/addons/gdharness_runtime/runtime_input.gd +21 -2
- package/build/godot/addons/gdharness_runtime/runtime_queries.gd +6 -0
- package/build/godot/addons/gdharness_runtime/runtime_targets.gd +113 -0
- package/build/godot/addons/gdharness_runtime/runtime_targets.gd.uid +1 -0
- package/build/godot/operations/project_config.gd +4 -3
- package/build/index.js +97 -159
- package/package.json +1 -1
|
@@ -10,6 +10,7 @@ const Values = preload("runtime_values.gd")
|
|
|
10
10
|
## reports are the same place by construction rather than by agreement.
|
|
11
11
|
const Queries = preload("runtime_queries.gd")
|
|
12
12
|
const Read = preload("reading.gd")
|
|
13
|
+
const Targets = preload("runtime_targets.gd")
|
|
13
14
|
const Words = preload("runtime_words.gd")
|
|
14
15
|
|
|
15
16
|
## The distance from a capital letter to its small one in Unicode. A keycode holds the capital.
|
|
@@ -484,10 +485,25 @@ static func _scroll_into_view(control: Control) -> bool:
|
|
|
484
485
|
## A control out of sight inside a ScrollContainer is scrolled to first; the answer says so under
|
|
485
486
|
## `scrolled_into_view`, because the view having moved is a thing that happened to the screen and
|
|
486
487
|
## the caller is the only one who can tell whether that matters.
|
|
488
|
+
##
|
|
489
|
+
## `says` names the control by the words on it instead, under `path` when that is given as well,
|
|
490
|
+
## found and pressed in the same frame. A find and then a click by path was two round trips over a
|
|
491
|
+
## generated path copied whole, and a panel that rebuilt itself between them freed the button, so
|
|
492
|
+
## the click landed on nothing.
|
|
487
493
|
func click(params: Dictionary) -> Dictionary:
|
|
488
494
|
var node_path: String = str(params.get("path", ""))
|
|
495
|
+
var wanted: String = str(params.get("says", ""))
|
|
496
|
+
var found: Variant = null
|
|
497
|
+
if not wanted.is_empty():
|
|
498
|
+
var picked: Dictionary = Targets.control_saying(
|
|
499
|
+
_host.get_tree().root, "/root" if node_path.is_empty() else node_path, wanted, params.get("index")
|
|
500
|
+
)
|
|
501
|
+
if picked.has("message"):
|
|
502
|
+
return picked
|
|
503
|
+
node_path = picked["path"]
|
|
504
|
+
found = picked["found"]
|
|
489
505
|
if node_path.is_empty():
|
|
490
|
-
return {"type": "error", "message": "
|
|
506
|
+
return {"type": "error", "message": "click needs a path, or says to find the control by its words"}
|
|
491
507
|
|
|
492
508
|
var standing: Dictionary = Values.node_at(_host.get_tree().root, node_path)
|
|
493
509
|
if standing.has("message"):
|
|
@@ -582,7 +598,7 @@ func click(params: Dictionary) -> Dictionary:
|
|
|
582
598
|
if is_instance_valid(control):
|
|
583
599
|
afterwards = "in_tree" if control.is_inside_tree() else "removed"
|
|
584
600
|
|
|
585
|
-
|
|
601
|
+
var answer: Dictionary = {
|
|
586
602
|
"type": "clicked",
|
|
587
603
|
"path": node_path,
|
|
588
604
|
"position": _values.serialize(position),
|
|
@@ -593,6 +609,9 @@ func click(params: Dictionary) -> Dictionary:
|
|
|
593
609
|
"control_afterwards": afterwards,
|
|
594
610
|
"scrolled_into_view": scrolled,
|
|
595
611
|
}
|
|
612
|
+
if found != null:
|
|
613
|
+
answer["found"] = found
|
|
614
|
+
return answer
|
|
596
615
|
|
|
597
616
|
|
|
598
617
|
## What to add to a refusal about a point outside the viewport, when the reason is that nobody
|
|
@@ -259,6 +259,12 @@ static func _says(said: String, wanted: String) -> bool:
|
|
|
259
259
|
return said.containsn(words) if _is_literal(words) else said.matchn(words)
|
|
260
260
|
|
|
261
261
|
|
|
262
|
+
## Whether [param node]'s own words are what [param wanted] asks for, by the rules a find uses, so a
|
|
263
|
+
## click that names its control by its words finds the one a find would have answered with.
|
|
264
|
+
static func says(node: Node, wanted: String) -> bool:
|
|
265
|
+
return _says(Words.said_by(node), wanted)
|
|
266
|
+
|
|
267
|
+
|
|
262
268
|
## [param wanted], a glob, open at both ends so it matches its words anywhere in a text; "" when
|
|
263
269
|
## it is not a glob, or already open at both ends, and so has nothing to suggest.
|
|
264
270
|
static func _widened(wanted: String) -> String:
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
extends RefCounted
|
|
2
|
+
|
|
3
|
+
## Which control a click means when it names the control by the words on it rather than by path.
|
|
4
|
+
|
|
5
|
+
const Queries = preload("runtime_queries.gd")
|
|
6
|
+
const Read = preload("reading.gd")
|
|
7
|
+
const Values = preload("runtime_values.gd")
|
|
8
|
+
const Words = preload("runtime_words.gd")
|
|
9
|
+
|
|
10
|
+
## How many of the controls saying the same words a refusal names, with the index for each.
|
|
11
|
+
const LISTED_CANDIDATES: int = 10
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
## The control a click by [param wanted] words presses, found under [param root_path] in the tree
|
|
15
|
+
## [param root] heads, or the refusal saying why there is none or which of several:
|
|
16
|
+
## {"path", "found"} or {"type", "message"}.
|
|
17
|
+
##
|
|
18
|
+
## Matched the way a find matches `says`, against a control's own words, and only among controls
|
|
19
|
+
## the player can see. A match inside a button presses the button, because the label is what says
|
|
20
|
+
## the words and the button is what takes the click: aimed at the label, the pointer lands on the
|
|
21
|
+
## button and the answer would call the click a miss. Several matches are refused rather than
|
|
22
|
+
## guessed, each named with the [param which] that picks it, counting from 0.
|
|
23
|
+
static func control_saying(root: Node, root_path: String, wanted: String, which: Variant) -> Dictionary:
|
|
24
|
+
var reached: Dictionary = Values.node_at(root, root_path)
|
|
25
|
+
if reached.has("message"):
|
|
26
|
+
return reached
|
|
27
|
+
var targets: Array[Control] = []
|
|
28
|
+
# The words of the control that matched, which a button pressed on a label's behalf has none of.
|
|
29
|
+
var said: Array[String] = []
|
|
30
|
+
var hidden: int = 0
|
|
31
|
+
var pending: Array[Node] = [reached["node"]]
|
|
32
|
+
while not pending.is_empty():
|
|
33
|
+
var node: Node = pending.pop_front()
|
|
34
|
+
if node is Control and Queries.says(node, wanted):
|
|
35
|
+
if not Queries.shown(node):
|
|
36
|
+
hidden += 1
|
|
37
|
+
else:
|
|
38
|
+
var matched: Control = node
|
|
39
|
+
var target: Control = _pressed_through(matched)
|
|
40
|
+
if not targets.has(target):
|
|
41
|
+
targets.append(target)
|
|
42
|
+
said.append(Words.said_by(node))
|
|
43
|
+
var children: Array[Node] = node.get_children(true)
|
|
44
|
+
for index: int in range(children.size() - 1, -1, -1):
|
|
45
|
+
pending.push_front(children[index])
|
|
46
|
+
|
|
47
|
+
var under: String = "" if root_path == "/root" else " under %s" % root_path
|
|
48
|
+
if targets.is_empty():
|
|
49
|
+
return {
|
|
50
|
+
"type": "error",
|
|
51
|
+
"message":
|
|
52
|
+
(
|
|
53
|
+
'no control on screen%s says "%s", so nothing was clicked%s'
|
|
54
|
+
% [under, wanted, _hidden_note(hidden)]
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
if which == null and targets.size() > 1:
|
|
58
|
+
return {
|
|
59
|
+
"type": "error",
|
|
60
|
+
"message":
|
|
61
|
+
(
|
|
62
|
+
'%d controls on screen%s say "%s", so which to click is not clear; pass index, counting from 0: %s'
|
|
63
|
+
% [targets.size(), under, wanted, _candidates(targets, said)]
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
var index: int = 0 if which == null else Read.as_int(which, -1)
|
|
67
|
+
if index < 0 or index >= targets.size():
|
|
68
|
+
var plural: String = "" if targets.size() == 1 else "s"
|
|
69
|
+
return {
|
|
70
|
+
"type": "error",
|
|
71
|
+
"message":
|
|
72
|
+
(
|
|
73
|
+
'index %s is not one of the %d control%s on screen%s saying "%s": %s'
|
|
74
|
+
% [str(which), targets.size(), plural, under, wanted, _candidates(targets, said)]
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
"path": str(targets[index].get_path()),
|
|
79
|
+
"found": {"says": wanted, "index": index, "of": targets.size()},
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
## What to add to a refusal that found nothing on screen, when words matched controls out of sight.
|
|
84
|
+
static func _hidden_note(hidden: int) -> String:
|
|
85
|
+
if hidden == 0:
|
|
86
|
+
return ""
|
|
87
|
+
return "; %d hidden control%s" % [hidden, " says it" if hidden == 1 else "s say it"]
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
## [param control], or the button holding it when it is not one itself, which is what a click on
|
|
91
|
+
## its words means.
|
|
92
|
+
static func _pressed_through(control: Control) -> Control:
|
|
93
|
+
var walk: Node = control
|
|
94
|
+
while walk != null:
|
|
95
|
+
if walk is BaseButton:
|
|
96
|
+
var pressed: BaseButton = walk
|
|
97
|
+
return pressed
|
|
98
|
+
walk = walk.get_parent()
|
|
99
|
+
return control
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
## The controls a click by words could mean, each with its index, its path and the words that
|
|
103
|
+
## matched, [param said] holding those in the same order.
|
|
104
|
+
static func _candidates(targets: Array[Control], said: Array[String]) -> String:
|
|
105
|
+
var named: Array[String] = []
|
|
106
|
+
for index: int in mini(targets.size(), LISTED_CANDIDATES):
|
|
107
|
+
var words: String = said[index].replace("\n", " ")
|
|
108
|
+
if words.length() > 60:
|
|
109
|
+
words = words.substr(0, 57) + "..."
|
|
110
|
+
named.append('%d %s ("%s")' % [index, targets[index].get_path(), words])
|
|
111
|
+
if targets.size() > LISTED_CANDIDATES:
|
|
112
|
+
named.append("and %d more" % (targets.size() - LISTED_CANDIDATES))
|
|
113
|
+
return "; ".join(named)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
uid://be1ydcqg2ag4t
|
|
@@ -149,12 +149,13 @@ func add_autoload(params: Dictionary) -> Dictionary:
|
|
|
149
149
|
if not FileAccess.file_exists(path):
|
|
150
150
|
return _log.failure("Autoload file does not exist: " + path)
|
|
151
151
|
|
|
152
|
-
# Through ProjectSettings so the file is saved the way the editor saves it,
|
|
153
|
-
#
|
|
152
|
+
# Through ProjectSettings so the file is saved the way the editor saves it, which includes
|
|
153
|
+
# putting Godot's header back and dropping any comment somebody wrote into the file by hand.
|
|
154
154
|
var setting: String = "autoload/" + name
|
|
155
155
|
var was_updated: bool = ProjectSettings.has_setting(setting)
|
|
156
156
|
|
|
157
|
-
# An asterisk in front of the path
|
|
157
|
+
# An asterisk in front of the path makes the autoload a global name as well. Without it the
|
|
158
|
+
# node still comes up under the root; only the name stops resolving.
|
|
158
159
|
ProjectSettings.set_setting(setting, ("*" if enabled else "") + path)
|
|
159
160
|
var err: Error = ProjectSettings.save()
|
|
160
161
|
if err != OK:
|