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.
@@ -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": "Node path required"}
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
- return {
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, header and
153
- # every other line kept; a ConfigFile of project.godot drops the comments on the way out.
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 is how project.godot marks an autoload as enabled.
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: