gdharness 1.2.39 → 1.2.40
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 +1418 -1400
- package/build/godot/addons/gdharness_runtime/runtime_input.gd +15 -2
- package/build/godot/addons/gdharness_runtime/runtime_items.gd +175 -0
- package/build/godot/addons/gdharness_runtime/runtime_items.gd.uid +1 -0
- package/build/godot/addons/gdharness_runtime/runtime_paths.gd +43 -10
- package/build/godot/addons/gdharness_runtime/runtime_targets.gd +50 -14
- package/build/godot/addons/gdharness_runtime/runtime_words.gd +48 -18
- package/build/index.js +7954 -7937
- package/package.json +1 -1
|
@@ -12,6 +12,7 @@ const Values = preload("runtime_values.gd")
|
|
|
12
12
|
const Queries = preload("runtime_queries.gd")
|
|
13
13
|
const Read = preload("reading.gd")
|
|
14
14
|
const Hover = preload("runtime_hover.gd")
|
|
15
|
+
const Items = preload("runtime_items.gd")
|
|
15
16
|
const Screen = preload("runtime_screen.gd")
|
|
16
17
|
const Targets = preload("runtime_targets.gd")
|
|
17
18
|
const Mouse = preload("runtime_mouse.gd")
|
|
@@ -509,6 +510,7 @@ func click(params: Dictionary) -> Dictionary:
|
|
|
509
510
|
var node_path: String = str(params.get("path", ""))
|
|
510
511
|
var wanted: String = str(params.get("says", ""))
|
|
511
512
|
var found: Variant = null
|
|
513
|
+
var item: Variant = null
|
|
512
514
|
if not wanted.is_empty():
|
|
513
515
|
var picked: Dictionary = Targets.control_saying(
|
|
514
516
|
_host.get_tree().root, "/root" if node_path.is_empty() else node_path, wanted, params.get("index")
|
|
@@ -517,6 +519,7 @@ func click(params: Dictionary) -> Dictionary:
|
|
|
517
519
|
return picked
|
|
518
520
|
node_path = picked["path"]
|
|
519
521
|
found = picked["found"]
|
|
522
|
+
item = picked.get("item")
|
|
520
523
|
if node_path.is_empty():
|
|
521
524
|
return {"type": "error", "message": "click needs a path, or says to find the control by its words"}
|
|
522
525
|
|
|
@@ -541,6 +544,16 @@ func click(params: Dictionary) -> Dictionary:
|
|
|
541
544
|
# other words, a link among them. Said either way, so a press on the middle is never read as one
|
|
542
545
|
# on the words.
|
|
543
546
|
var aim: Vector2 = control.size * 0.5
|
|
547
|
+
var item_scrolled: bool = false
|
|
548
|
+
if item != null:
|
|
549
|
+
var found_by_words: Dictionary = found
|
|
550
|
+
var placed_item: Dictionary = await Items.in_view(
|
|
551
|
+
_host.get_tree(), control, node_path, item, wanted, found_by_words
|
|
552
|
+
)
|
|
553
|
+
if placed_item.has("message"):
|
|
554
|
+
return placed_item
|
|
555
|
+
aim = placed_item["point"]
|
|
556
|
+
item_scrolled = placed_item["scrolled"]
|
|
544
557
|
var rich: RichTextLabel = control as RichTextLabel
|
|
545
558
|
if found != null and rich != null:
|
|
546
559
|
var placed: Dictionary = Targets.point_on_words(rich, wanted)
|
|
@@ -568,9 +581,9 @@ func click(params: Dictionary) -> Dictionary:
|
|
|
568
581
|
# off the top of its container is inside the viewport, behind whatever is drawn up there, so
|
|
569
582
|
# the click went to that instead and said so. Measured on a hall whose staff panel had been
|
|
570
583
|
# scrolled past: the button was at y 69 and its container started at y 166.
|
|
571
|
-
var scrolled: bool =
|
|
584
|
+
var scrolled: bool = item_scrolled
|
|
572
585
|
if not _in_sight(control, own, local):
|
|
573
|
-
scrolled = await _scroll_into_view(control)
|
|
586
|
+
scrolled = await _scroll_into_view(control) or item_scrolled
|
|
574
587
|
var left: String = Values.afterwards(control)
|
|
575
588
|
if left != "in_tree":
|
|
576
589
|
return {
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
extends RefCounted
|
|
2
|
+
|
|
3
|
+
## Where a control drawing a list draws one of its items, for a click by words: a tab, a list entry,
|
|
4
|
+
## a tree row or a menu bar title. Each is drawn by the control rather than held as a node, so a click
|
|
5
|
+
## on the control's path presses its middle, which is whichever item happens to sit there.
|
|
6
|
+
|
|
7
|
+
const Says = preload("runtime_says.gd")
|
|
8
|
+
const Values = preload("runtime_values.gd")
|
|
9
|
+
const Words = preload("runtime_words.gd")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
## Where [param control] draws [param item], one of [method Words.items_of], in the control's own
|
|
13
|
+
## space, or an empty rectangle while it draws it nowhere a pointer reaches: a tab scrolled off its
|
|
14
|
+
## bar, an entry or a row scrolled out of its list, a menu bar the operating system draws. A tree
|
|
15
|
+
## row is the cell saying [param wanted] when one does, and the whole row otherwise.
|
|
16
|
+
##
|
|
17
|
+
## Each one checked against the control's own answer to which item is under a point, where it has
|
|
18
|
+
## one, since an entry's rectangle is kept in the list's unscrolled space and a row's is computed
|
|
19
|
+
## from the scroll as of the last draw.
|
|
20
|
+
static func item_rect(control: Control, item: Variant, wanted: String) -> Rect2:
|
|
21
|
+
var bounds: Rect2 = Rect2(Vector2.ZERO, control.size)
|
|
22
|
+
var bar: TabBar = control as TabBar
|
|
23
|
+
if bar != null:
|
|
24
|
+
var tab: int = item
|
|
25
|
+
var placed: Rect2 = bar.get_tab_rect(tab)
|
|
26
|
+
return placed if bar.get_tab_idx_at_point(placed.get_center()) == tab else Rect2()
|
|
27
|
+
var list: ItemList = control as ItemList
|
|
28
|
+
if list != null:
|
|
29
|
+
var entry: int = item
|
|
30
|
+
var scrolled: Vector2 = Vector2(list.get_h_scroll_bar().value, list.get_v_scroll_bar().value)
|
|
31
|
+
var placed: Rect2 = list.get_item_rect(entry, false)
|
|
32
|
+
placed.position -= scrolled
|
|
33
|
+
var reached: bool = bounds.has_point(placed.get_center())
|
|
34
|
+
return (
|
|
35
|
+
placed if reached and list.get_item_at_position(placed.get_center(), true) == entry else Rect2()
|
|
36
|
+
)
|
|
37
|
+
var tree: Tree = control as Tree
|
|
38
|
+
if tree != null:
|
|
39
|
+
var row: TreeItem = item
|
|
40
|
+
var column: int = tree_column(tree, row, wanted)
|
|
41
|
+
var placed: Rect2 = tree.get_item_area_rect(row, column)
|
|
42
|
+
var centre: Vector2 = placed.get_center()
|
|
43
|
+
if not bounds.has_point(centre) or tree.get_item_at_position(centre) != row:
|
|
44
|
+
return Rect2()
|
|
45
|
+
return placed if column == -1 or tree.get_column_at_position(centre) == column else Rect2()
|
|
46
|
+
var menus: MenuBar = control as MenuBar
|
|
47
|
+
if menus != null and not menus.is_native_menu():
|
|
48
|
+
var menu: int = item
|
|
49
|
+
return _menu_title_rect(menus, menu)
|
|
50
|
+
return Rect2()
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
## The column of [param row] whose cell says [param wanted], or -1 when only the row as a whole
|
|
54
|
+
## does, its cells read together.
|
|
55
|
+
static func tree_column(tree: Tree, row: TreeItem, wanted: String) -> int:
|
|
56
|
+
for column: int in tree.columns:
|
|
57
|
+
if Says.matches(Words.cell_says(tree, row, column), wanted):
|
|
58
|
+
return column
|
|
59
|
+
return -1
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
## [param item] of [param control] as an answer names it: what it is, its number where the control
|
|
63
|
+
## numbers its items, and what it says.
|
|
64
|
+
static func item_named(control: Control, item: Variant, says: String) -> Dictionary:
|
|
65
|
+
if control is Tree:
|
|
66
|
+
return {"kind": "row", "says": says}
|
|
67
|
+
var kind: String = "tab" if control is TabBar else ("menu" if control is MenuBar else "item")
|
|
68
|
+
return {"kind": kind, "index": item, "says": says}
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
## Where in [param control] to press for [param item], the one of its items a click by
|
|
72
|
+
## [param wanted] matched, as {"point", "scrolled"}, or a refusal saying why there is nowhere.
|
|
73
|
+
##
|
|
74
|
+
## Scrolled into the control's own view first when it is drawn nowhere yet, the way a player brings
|
|
75
|
+
## a tab or an entry into view before pressing it. A disabled item is refused as a disabled button
|
|
76
|
+
## is. A tree row is pressed on the cell saying the words, whose column goes into [param found].
|
|
77
|
+
static func in_view(
|
|
78
|
+
scene: SceneTree, control: Control, node_path: String, item: Variant, wanted: String, found: Dictionary
|
|
79
|
+
) -> Dictionary:
|
|
80
|
+
var named: Dictionary = found.get("item", {})
|
|
81
|
+
var naming: String = 'the %s "%s" of %s' % [named.get("kind", "item"), named.get("says", ""), node_path]
|
|
82
|
+
var index: int = item if item is int else -1
|
|
83
|
+
var bar: TabBar = control as TabBar
|
|
84
|
+
var list: ItemList = control as ItemList
|
|
85
|
+
var menus: MenuBar = control as MenuBar
|
|
86
|
+
if (
|
|
87
|
+
(bar != null and bar.is_tab_disabled(index))
|
|
88
|
+
or (list != null and list.is_item_disabled(index))
|
|
89
|
+
or (menus != null and menus.is_menu_disabled(index))
|
|
90
|
+
):
|
|
91
|
+
return {"type": "error", "message": "%s is disabled, so clicking it presses nothing" % naming}
|
|
92
|
+
var placed: Rect2 = item_rect(control, item, wanted)
|
|
93
|
+
var scrolled: bool = false
|
|
94
|
+
if placed.size == Vector2.ZERO:
|
|
95
|
+
scrolled = _scroll_to(control, item)
|
|
96
|
+
if scrolled:
|
|
97
|
+
# A tree places its rows by the scroll it last drew with.
|
|
98
|
+
await scene.process_frame
|
|
99
|
+
var left: String = Values.afterwards(control)
|
|
100
|
+
if left != "in_tree" or (typeof(item) == TYPE_OBJECT and not is_instance_valid(item)):
|
|
101
|
+
return {
|
|
102
|
+
"type": "error",
|
|
103
|
+
"message":
|
|
104
|
+
"%s went while it was being scrolled into view, so nothing was clicked" % naming
|
|
105
|
+
}
|
|
106
|
+
placed = item_rect(control, item, wanted)
|
|
107
|
+
if placed.size == Vector2.ZERO:
|
|
108
|
+
var tried: String = " even scrolled to" if scrolled else ""
|
|
109
|
+
return {
|
|
110
|
+
"type": "error",
|
|
111
|
+
"message": "%s is drawn nowhere a pointer reaches%s, so nothing was clicked" % [naming, tried]
|
|
112
|
+
}
|
|
113
|
+
var tree: Tree = control as Tree
|
|
114
|
+
if tree != null:
|
|
115
|
+
var row: TreeItem = item
|
|
116
|
+
var column: int = tree_column(tree, row, wanted)
|
|
117
|
+
if column != -1:
|
|
118
|
+
named["column"] = column
|
|
119
|
+
return {"point": placed.get_center(), "scrolled": scrolled}
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
## Where [param menus] draws the title of [param menu], worked out as the engine works it out,
|
|
123
|
+
## since the bar keeps it to itself: each title shaped in the bar's font, padded by its normal style
|
|
124
|
+
## and spaced by its separation, counting only the menus it shows.
|
|
125
|
+
static func _menu_title_rect(menus: MenuBar, menu: int) -> Rect2:
|
|
126
|
+
var style: StyleBox = menus.get_theme_stylebox("normal")
|
|
127
|
+
var separation: int = menus.get_theme_constant("h_separation")
|
|
128
|
+
var offset: int = 0
|
|
129
|
+
for before: int in menu:
|
|
130
|
+
if not menus.is_menu_hidden(before):
|
|
131
|
+
offset += int(_menu_title_size(menus, before, style).x + separation)
|
|
132
|
+
var size: Vector2 = _menu_title_size(menus, menu, style)
|
|
133
|
+
var x: float = menus.size.x - offset - size.x if menus.is_layout_rtl() else float(offset)
|
|
134
|
+
return Rect2(Vector2(x, 0), size)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
static func _menu_title_size(menus: MenuBar, menu: int, style: StyleBox) -> Vector2:
|
|
138
|
+
var line: TextLine = TextLine.new()
|
|
139
|
+
var _shaped: bool = line.add_string(
|
|
140
|
+
menus.atr(menus.get_menu_title(menu)),
|
|
141
|
+
menus.get_theme_font("font"),
|
|
142
|
+
menus.get_theme_font_size("font_size"),
|
|
143
|
+
menus.language
|
|
144
|
+
)
|
|
145
|
+
return line.get_size() + style.get_minimum_size()
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
## Scrolls [param control] until it draws [param item], and answers whether it asked anything to
|
|
149
|
+
## move. A tab bar and a tree have a call for it; a list is scrolled by its own bar, by as little as
|
|
150
|
+
## brings the whole entry in.
|
|
151
|
+
static func _scroll_to(control: Control, item: Variant) -> bool:
|
|
152
|
+
var index: int = item if item is int else -1
|
|
153
|
+
var bar: TabBar = control as TabBar
|
|
154
|
+
if bar != null:
|
|
155
|
+
var before: int = bar.get_tab_offset()
|
|
156
|
+
bar.ensure_tab_visible(index)
|
|
157
|
+
return bar.get_tab_offset() != before
|
|
158
|
+
var tree: Tree = control as Tree
|
|
159
|
+
if tree != null:
|
|
160
|
+
var row: TreeItem = item
|
|
161
|
+
var was: float = tree.get_scroll().y
|
|
162
|
+
tree.scroll_to_item(row)
|
|
163
|
+
return tree.get_scroll().y != was
|
|
164
|
+
var list: ItemList = control as ItemList
|
|
165
|
+
if list == null:
|
|
166
|
+
return false
|
|
167
|
+
var rows: VScrollBar = list.get_v_scroll_bar()
|
|
168
|
+
var entry: Rect2 = list.get_item_rect(index, false)
|
|
169
|
+
var top: float = entry.position.y - list.get_theme_stylebox("panel").get_offset().y
|
|
170
|
+
var was_at: float = rows.value
|
|
171
|
+
if top < rows.value:
|
|
172
|
+
rows.value = top
|
|
173
|
+
elif top + entry.size.y > rows.value + rows.page:
|
|
174
|
+
rows.value = top + entry.size.y - rows.page
|
|
175
|
+
return rows.value != was_at
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
uid://mynpfgtjyipe
|
|
@@ -254,12 +254,17 @@ static func can_read(holder: Variant, named: String) -> bool:
|
|
|
254
254
|
## reports an engine error for one of another type, so a step into a map typed by an enum read the
|
|
255
255
|
## right entry and left four errors in the game's log for each of the two walks, and the run was
|
|
256
256
|
## counted as unclean for a read.
|
|
257
|
+
##
|
|
258
|
+
## A step written in double quotes is the key inside them, so a key holding a colon is one step:
|
|
259
|
+
## Alchemill keys its stock by namespaced ids, "stock:\"base:dew\"", and split at that colon the
|
|
260
|
+
## step named a key "base" the map had never had.
|
|
257
261
|
static func _key_in(map: Dictionary, named: String) -> Array:
|
|
258
|
-
var
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
262
|
+
var word: String = key_written(named)
|
|
263
|
+
var spellings: Array = [word, StringName(word)]
|
|
264
|
+
if word.is_valid_int():
|
|
265
|
+
spellings.append(int(word))
|
|
266
|
+
if word.is_valid_float():
|
|
267
|
+
spellings.append(float(word))
|
|
263
268
|
var typed: int = map.get_typed_key_builtin() if map.is_typed_key() else TYPE_NIL
|
|
264
269
|
for key: Variant in spellings:
|
|
265
270
|
if typed != TYPE_NIL and typeof(key) != typed:
|
|
@@ -269,9 +274,35 @@ static func _key_in(map: Dictionary, named: String) -> Array:
|
|
|
269
274
|
return []
|
|
270
275
|
|
|
271
276
|
|
|
277
|
+
## The key a step names: the words inside the quotes of one written in double quotes, read as JSON
|
|
278
|
+
## reads a string so a quote inside is written \", and the step itself otherwise.
|
|
279
|
+
static func key_written(named: String) -> String:
|
|
280
|
+
if named.length() < 2 or not named.begins_with('"') or not named.ends_with('"'):
|
|
281
|
+
return named
|
|
282
|
+
var reader: JSON = JSON.new()
|
|
283
|
+
if reader.parse(named) != OK or not reader.data is String:
|
|
284
|
+
return named
|
|
285
|
+
return reader.data
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
## [param key] written as a step that names it: in double quotes when it holds what a bare step
|
|
289
|
+
## cannot, a colon a path splits at, a bracket that reads as a call, a quote, or nothing at all.
|
|
290
|
+
static func step_naming(key: Variant) -> String:
|
|
291
|
+
var text: String = str(key)
|
|
292
|
+
if not (key is String or key is StringName):
|
|
293
|
+
return text
|
|
294
|
+
if text.is_empty():
|
|
295
|
+
return JSON.stringify(text)
|
|
296
|
+
for character: String in [":", "(", ")", '"']:
|
|
297
|
+
if text.contains(character):
|
|
298
|
+
return JSON.stringify(text)
|
|
299
|
+
return text
|
|
300
|
+
|
|
301
|
+
|
|
272
302
|
## [param reaching] split into its steps at each colon, except one inside a call's brackets or a
|
|
273
|
-
## quoted argument: `get_node("/root/Main"):name` is two steps, and
|
|
274
|
-
## argument holding one became a step that names
|
|
303
|
+
## quoted argument or key: `get_node("/root/Main"):name` is two steps, and so is
|
|
304
|
+
## `stock:"base:dew"`; split at every colon a quoted argument holding one became a step that names
|
|
305
|
+
## nothing.
|
|
275
306
|
static func steps_of(reaching: String) -> PackedStringArray:
|
|
276
307
|
var steps: PackedStringArray = []
|
|
277
308
|
var step: String = ""
|
|
@@ -569,17 +600,19 @@ static func nothing_there(holder: Variant, named: String, called_as: String) ->
|
|
|
569
600
|
var keys: Array = map.keys()
|
|
570
601
|
# A key reading as the step and still not reached is one of a type a step cannot spell, and
|
|
571
602
|
# saying the map has no key 3 while listing 3 among its keys contradicts itself.
|
|
603
|
+
var word: String = key_written(named)
|
|
572
604
|
for key: Variant in keys:
|
|
573
|
-
if str(key) ==
|
|
605
|
+
if str(key) == word:
|
|
574
606
|
return (
|
|
575
607
|
"%s has a key that reads %s, but it is %s, which a path step cannot name"
|
|
576
608
|
% [called_as, named, type_string(typeof(key))]
|
|
577
609
|
)
|
|
578
610
|
# Eight of them, because a map keyed by something unexpected is told by the first few and a
|
|
579
|
-
# map of two hundred would bury the sentence saying which key was missing.
|
|
611
|
+
# map of two hundred would bury the sentence saying which key was missing. Each written as a
|
|
612
|
+
# step reaching it, so the list says how to name a key holding a colon.
|
|
580
613
|
var some: Array[String] = []
|
|
581
614
|
for key: Variant in keys.slice(0, 8):
|
|
582
|
-
some.append(
|
|
615
|
+
some.append(step_naming(key))
|
|
583
616
|
var rest: String = ""
|
|
584
617
|
if keys.size() > 8:
|
|
585
618
|
rest = " and %d more" % [keys.size() - 8]
|
|
@@ -2,6 +2,7 @@ extends RefCounted
|
|
|
2
2
|
|
|
3
3
|
## Which control a click means when it names the control by the words on it rather than by path.
|
|
4
4
|
|
|
5
|
+
const Items = preload("runtime_items.gd")
|
|
5
6
|
const Queries = preload("runtime_queries.gd")
|
|
6
7
|
const Read = preload("reading.gd")
|
|
7
8
|
const Says = preload("runtime_says.gd")
|
|
@@ -24,7 +25,8 @@ const WHOLE: int = 1
|
|
|
24
25
|
|
|
25
26
|
## The control a click by [param wanted] words presses, found under [param root_path] in the tree
|
|
26
27
|
## [param root] heads, or the refusal saying why there is none or which of several:
|
|
27
|
-
## {"path", "found"} or {"type", "message"}
|
|
28
|
+
## {"path", "found"} or {"type", "message"}, with `item` as well when the match is an item of a
|
|
29
|
+
## control drawing a list, which [method Items.item_rect] places.
|
|
28
30
|
##
|
|
29
31
|
## Matched the way a find matches `says`, against a control's own words, and only among controls
|
|
30
32
|
## the player can see. A match inside a button presses the button, because the label is what says
|
|
@@ -57,6 +59,10 @@ static func control_saying(root: Node, root_path: String, wanted: String, which:
|
|
|
57
59
|
# that only says them as part of more stands in for.
|
|
58
60
|
var exact_out_of_reach: Array[Node] = []
|
|
59
61
|
var out_of_reach_as: Array[String] = []
|
|
62
|
+
# The item of a control drawing a list that matched, null for a control matched as a whole, and
|
|
63
|
+
# the point in the control a pointer would go to for it.
|
|
64
|
+
var items: Array = []
|
|
65
|
+
var aims: Array[Vector2] = []
|
|
60
66
|
var pending: Array[Node] = [reached["node"]]
|
|
61
67
|
while not pending.is_empty():
|
|
62
68
|
var node: Node = pending.pop_front()
|
|
@@ -66,6 +72,26 @@ static func control_saying(root: Node, root_path: String, wanted: String, which:
|
|
|
66
72
|
if _says_exactly(Words.said_by(node), wanted):
|
|
67
73
|
exact_out_of_reach.append(node)
|
|
68
74
|
out_of_reach_as.append("hidden")
|
|
75
|
+
elif Words.draws_a_list(node):
|
|
76
|
+
# Each item a match of its own, so the words of one tab are matched as a whole and the
|
|
77
|
+
# index picks between two items saying them as it does between two buttons.
|
|
78
|
+
var drawing: Control = node
|
|
79
|
+
for entry: Dictionary in Words.items_of(drawing):
|
|
80
|
+
var words: String = entry["says"]
|
|
81
|
+
if not Says.matches(words, wanted):
|
|
82
|
+
continue
|
|
83
|
+
var whole: bool = _says_exactly(words, wanted)
|
|
84
|
+
var drawn_at_now: Rect2 = Items.item_rect(drawing, entry["item"], wanted)
|
|
85
|
+
matched.append(drawing)
|
|
86
|
+
said.append(words)
|
|
87
|
+
exact.append(whole)
|
|
88
|
+
inside.append(not whole and _inside_a_word(words, wanted))
|
|
89
|
+
items.append(entry["item"])
|
|
90
|
+
# An item drawn nowhere yet, scrolled out of its list, is judged at the list's middle,
|
|
91
|
+
# which is where the click scrolls it towards.
|
|
92
|
+
aims.append(
|
|
93
|
+
drawing.size * 0.5 if drawn_at_now.size == Vector2.ZERO else drawn_at_now.get_center()
|
|
94
|
+
)
|
|
69
95
|
else:
|
|
70
96
|
var control: Control = node
|
|
71
97
|
var target: Control = _pressed_through(control)
|
|
@@ -78,6 +104,8 @@ static func control_saying(root: Node, root_path: String, wanted: String, which:
|
|
|
78
104
|
said.append(words)
|
|
79
105
|
exact.append(whole)
|
|
80
106
|
inside.append(buried)
|
|
107
|
+
items.append(null)
|
|
108
|
+
aims.append(target.size * 0.5)
|
|
81
109
|
elif (whole and not exact[at]) or (inside[at] and not buried):
|
|
82
110
|
said[at] = words
|
|
83
111
|
exact[at] = whole
|
|
@@ -93,13 +121,13 @@ static func control_saying(root: Node, root_path: String, wanted: String, which:
|
|
|
93
121
|
var outside: int = 0
|
|
94
122
|
var open: Array[int] = []
|
|
95
123
|
for index: int in matched.size():
|
|
96
|
-
if _off_screen(matched[index]):
|
|
124
|
+
if _off_screen(matched[index], aims[index]):
|
|
97
125
|
outside += 1
|
|
98
126
|
if exact[index]:
|
|
99
127
|
exact_out_of_reach.append(matched[index])
|
|
100
128
|
out_of_reach_as.append("outside what the screen shows")
|
|
101
129
|
continue
|
|
102
|
-
var over: Node = _cover_of(matched[index], drawn, areas)
|
|
130
|
+
var over: Node = _cover_of(matched[index], drawn, areas, aims[index])
|
|
103
131
|
if over == null:
|
|
104
132
|
open.append(index)
|
|
105
133
|
else:
|
|
@@ -125,12 +153,14 @@ static func control_saying(root: Node, root_path: String, wanted: String, which:
|
|
|
125
153
|
var lines: Array[String] = []
|
|
126
154
|
var wholes: Array[bool] = []
|
|
127
155
|
var insides: Array[bool] = []
|
|
156
|
+
var picked_items: Array = []
|
|
128
157
|
for index: int in open:
|
|
129
158
|
targets.append(matched[index])
|
|
130
159
|
ranks.append(_rank(matched[index], exact[index]))
|
|
131
160
|
lines.append(said[index])
|
|
132
161
|
wholes.append(exact[index])
|
|
133
162
|
insides.append(inside[index])
|
|
163
|
+
picked_items.append(items[index])
|
|
134
164
|
|
|
135
165
|
var under: String = "" if root_path == "/root" else " under %s" % root_path
|
|
136
166
|
if targets.is_empty():
|
|
@@ -257,7 +287,11 @@ static func control_saying(root: Node, root_path: String, wanted: String, which:
|
|
|
257
287
|
found["picked"] = "%s; %s" % [_described(1, ranks[0], wanted, under), others]
|
|
258
288
|
if covered > 0:
|
|
259
289
|
found["covered"] = covered
|
|
260
|
-
|
|
290
|
+
var answer: Dictionary = {"path": str(targets[index].get_path()), "found": found}
|
|
291
|
+
if picked_items[index] != null:
|
|
292
|
+
answer["item"] = picked_items[index]
|
|
293
|
+
found["item"] = Items.item_named(targets[index], picked_items[index], lines[index])
|
|
294
|
+
return answer
|
|
261
295
|
|
|
262
296
|
|
|
263
297
|
## How good a match [param target] is for a click: a control the pointer can reach over one it
|
|
@@ -451,7 +485,7 @@ static func _pressed_through(control: Control) -> Control:
|
|
|
451
485
|
## What a pointer at [param target]'s centre would reach instead of it, drawn over it, or null.
|
|
452
486
|
static func drawn_over(target: Control) -> Node:
|
|
453
487
|
var drawn: Array[Node] = _pointer_takers(target.get_tree().root)
|
|
454
|
-
return _cover_of(target, drawn, _areas_of(drawn))
|
|
488
|
+
return _cover_of(target, drawn, _areas_of(drawn), target.size * 0.5)
|
|
455
489
|
|
|
456
490
|
|
|
457
491
|
## Why [param node], which is not visible in the tree, is not, as a parenthesis to follow its path:
|
|
@@ -631,7 +665,8 @@ static func _areas_of(drawn: Array[Node]) -> Array[Rect2]:
|
|
|
631
665
|
return areas
|
|
632
666
|
|
|
633
667
|
|
|
634
|
-
## What a pointer at [param target]
|
|
668
|
+
## What a pointer at [param aim] in [param target], its centre or the middle of the item a click is
|
|
669
|
+
## for, reaches instead of it, or null when nothing does.
|
|
635
670
|
##
|
|
636
671
|
## The engine's own order for which control takes the pointer: an embedded window over everything
|
|
637
672
|
## in the viewport that holds it, then the higher canvas layer, then the later in the tree. A
|
|
@@ -650,10 +685,10 @@ static func _areas_of(drawn: Array[Node]) -> Array[Rect2]:
|
|
|
650
685
|
## point is passed over before anything costlier is asked of it. A window's box is in the space of
|
|
651
686
|
## the viewport it is embedded in, which is not the target's when the target is in another window,
|
|
652
687
|
## so a window is judged by [method _window_over] instead.
|
|
653
|
-
static func _cover_of(target: Control, drawn: Array[Node], areas: Array[Rect2]) -> Node:
|
|
688
|
+
static func _cover_of(target: Control, drawn: Array[Node], areas: Array[Rect2], aim: Vector2) -> Node:
|
|
654
689
|
var viewport: Viewport = target.get_viewport()
|
|
655
690
|
var moves: Dictionary[ScrollContainer, Vector2] = _scrolls_for(target)
|
|
656
|
-
var point: Vector2 =
|
|
691
|
+
var point: Vector2 = target.get_global_transform_with_canvas() * aim + _moved_by(target, moves)
|
|
657
692
|
var layer: int = _layer_of(target)
|
|
658
693
|
for at: int in drawn.size():
|
|
659
694
|
var other: Node = drawn[at]
|
|
@@ -770,13 +805,14 @@ static func _where_it_lands(target: Control, point: Vector2) -> Dictionary[Scrol
|
|
|
770
805
|
return moves
|
|
771
806
|
|
|
772
807
|
|
|
773
|
-
## Whether a click could not reach [param target] however it scrolled:
|
|
774
|
-
## would bring it, outside its viewport or the one showing that on the screen, or
|
|
775
|
-
## container the click does not scroll. A drawer parked off the side of the
|
|
776
|
-
## outside the viewport of a game with no window, was listed among the controls
|
|
777
|
-
|
|
808
|
+
## Whether a click could not reach [param aim] in [param target] however it scrolled: the point,
|
|
809
|
+
## where the click would bring it, outside its viewport or the one showing that on the screen, or
|
|
810
|
+
## clipped away by a container the click does not scroll. A drawer parked off the side of the
|
|
811
|
+
## screen, or anything outside the viewport of a game with no window, was listed among the controls
|
|
812
|
+
## on screen.
|
|
813
|
+
static func _off_screen(target: Control, aim: Vector2) -> bool:
|
|
778
814
|
var moves: Dictionary[ScrollContainer, Vector2] = _scrolls_for(target)
|
|
779
|
-
var point: Vector2 =
|
|
815
|
+
var point: Vector2 = target.get_global_transform_with_canvas() * aim + _moved_by(target, moves)
|
|
780
816
|
if _clipped_away(target, point, moves) or not target.get_viewport().get_visible_rect().has_point(point):
|
|
781
817
|
return true
|
|
782
818
|
var reached: Dictionary = Screen.reach(target.get_viewport(), point)
|
|
@@ -23,7 +23,7 @@ static func said_by(node: Node) -> String:
|
|
|
23
23
|
var label: Label = node as Label
|
|
24
24
|
if label != null:
|
|
25
25
|
return _label_says(label)
|
|
26
|
-
if
|
|
26
|
+
if draws_a_list(node):
|
|
27
27
|
return "\n".join(lines_said_by(node))
|
|
28
28
|
var own: String = _text_of(node)
|
|
29
29
|
# A control showing a picture rather than words is named for a player who cannot see it, and that
|
|
@@ -41,37 +41,54 @@ static func said_by(node: Node) -> String:
|
|
|
41
41
|
## every label on it and none of those, and a wait for a tab's title or a list item was never met.
|
|
42
42
|
static func lines_said_by(node: Node) -> Array[String]:
|
|
43
43
|
var lines: Array[String] = []
|
|
44
|
+
if not draws_a_list(node):
|
|
45
|
+
_keep(_text_of(node), lines)
|
|
46
|
+
return lines
|
|
47
|
+
var tree: Tree = node as Tree
|
|
48
|
+
if tree != null and tree.column_titles_visible:
|
|
49
|
+
var titles: Array[String] = []
|
|
50
|
+
for column: int in tree.columns:
|
|
51
|
+
_keep(node.atr(tree.get_column_title(column)), titles)
|
|
52
|
+
_keep(" ".join(titles), lines)
|
|
53
|
+
for entry: Dictionary in items_of(node):
|
|
54
|
+
lines.append(str(entry["says"]))
|
|
55
|
+
return lines
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
## Each item [param node] draws as one of a list, as {"says", "item"}: a tab, a menu title, a list
|
|
59
|
+
## entry or a menu item by its index, and a tree row by its TreeItem. Empty for a node drawing none.
|
|
60
|
+
## Held apart rather than joined, so a click on a tab's title presses that tab: the bar's middle is
|
|
61
|
+
## whichever tab happens to sit there.
|
|
62
|
+
static func items_of(node: Node) -> Array[Dictionary]:
|
|
63
|
+
var items: Array[Dictionary] = []
|
|
44
64
|
if node is TabBar:
|
|
45
65
|
var bar: TabBar = node
|
|
46
66
|
for tab: int in bar.tab_count:
|
|
47
67
|
if not bar.is_tab_hidden(tab):
|
|
48
|
-
|
|
68
|
+
_keep_item(node.atr(bar.get_tab_title(tab)), tab, items)
|
|
49
69
|
elif node is MenuBar:
|
|
50
70
|
var menus: MenuBar = node
|
|
51
71
|
for menu: int in menus.get_menu_count():
|
|
52
72
|
if not menus.is_menu_hidden(menu):
|
|
53
|
-
|
|
73
|
+
_keep_item(node.atr(menus.get_menu_title(menu)), menu, items)
|
|
54
74
|
elif node is ItemList:
|
|
55
75
|
var list: ItemList = node
|
|
56
76
|
for item: int in list.item_count:
|
|
57
|
-
|
|
77
|
+
_keep_item(
|
|
78
|
+
_as_drawn(node, list.get_item_text(item), list.get_item_auto_translate_mode(item)),
|
|
79
|
+
item,
|
|
80
|
+
items
|
|
81
|
+
)
|
|
58
82
|
elif node is PopupMenu:
|
|
59
83
|
# A separator too, by its title: the popup draws a titled one as a heading over the items
|
|
60
84
|
# below it, and one without a title says nothing and is dropped as an empty line.
|
|
61
85
|
var menu: PopupMenu = node
|
|
62
86
|
for item: int in menu.item_count:
|
|
63
|
-
|
|
87
|
+
_keep_item(item_says(menu, item), item, items)
|
|
64
88
|
elif node is Tree:
|
|
65
89
|
var tree: Tree = node
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
for column: int in tree.columns:
|
|
69
|
-
_keep(node.atr(tree.get_column_title(column)), titles)
|
|
70
|
-
_keep(" ".join(titles), lines)
|
|
71
|
-
_tree_rows(tree, tree.get_root(), tree.hide_root, lines)
|
|
72
|
-
else:
|
|
73
|
-
_keep(_text_of(node), lines)
|
|
74
|
-
return lines
|
|
90
|
+
_tree_rows(tree, tree.get_root(), tree.hide_root, items)
|
|
91
|
+
return items
|
|
75
92
|
|
|
76
93
|
|
|
77
94
|
## What item [param index] of [param menu] shows: the translation where the item has one. A menu is
|
|
@@ -86,7 +103,9 @@ static func masked(field: LineEdit) -> String:
|
|
|
86
103
|
return field.secret_character.repeat(field.text.length())
|
|
87
104
|
|
|
88
105
|
|
|
89
|
-
|
|
106
|
+
## Whether [param node] draws a list of items rather than holding words in a `text`; see
|
|
107
|
+
## [method items_of].
|
|
108
|
+
static func draws_a_list(node: Node) -> bool:
|
|
90
109
|
return node is TabBar or node is MenuBar or node is ItemList or node is PopupMenu or node is Tree
|
|
91
110
|
|
|
92
111
|
|
|
@@ -96,6 +115,12 @@ static func _keep(line: String, into: Array[String]) -> void:
|
|
|
96
115
|
into.append(trimmed)
|
|
97
116
|
|
|
98
117
|
|
|
118
|
+
static func _keep_item(line: String, item: Variant, into: Array[Dictionary]) -> void:
|
|
119
|
+
var trimmed: String = line.strip_edges()
|
|
120
|
+
if not trimmed.is_empty():
|
|
121
|
+
into.append({"says": trimmed, "item": item})
|
|
122
|
+
|
|
123
|
+
|
|
99
124
|
## [param words] as [param node] draws them, which for a game with translations is the translation
|
|
100
125
|
## rather than the key it holds: a control set to `UI_HIRE` shows "Hire", and a wait for "Hire" was
|
|
101
126
|
## never met. [param mode] is an item's own setting, which the engine reads before the node's.
|
|
@@ -113,14 +138,14 @@ static func _as_drawn(
|
|
|
113
138
|
## The rows of a tree from [param item] down, a row being its columns' text side by side, and
|
|
114
139
|
## nothing under a collapsed row or a hidden one, since neither is drawn. A hidden root is only the
|
|
115
140
|
## place its children hang from.
|
|
116
|
-
static func _tree_rows(tree: Tree, item: TreeItem, skip: bool, into: Array[
|
|
141
|
+
static func _tree_rows(tree: Tree, item: TreeItem, skip: bool, into: Array[Dictionary]) -> void:
|
|
117
142
|
if item == null or not item.visible:
|
|
118
143
|
return
|
|
119
144
|
if not skip:
|
|
120
145
|
var cells: Array[String] = []
|
|
121
146
|
for column: int in tree.columns:
|
|
122
|
-
_keep(
|
|
123
|
-
|
|
147
|
+
_keep(cell_says(tree, item, column), cells)
|
|
148
|
+
_keep_item(" ".join(cells), item, into)
|
|
124
149
|
if item.collapsed:
|
|
125
150
|
return
|
|
126
151
|
var child: TreeItem = item.get_first_child()
|
|
@@ -129,6 +154,11 @@ static func _tree_rows(tree: Tree, item: TreeItem, skip: bool, into: Array[Strin
|
|
|
129
154
|
child = child.get_next()
|
|
130
155
|
|
|
131
156
|
|
|
157
|
+
## What [param column] of the row [param item] of [param tree] shows.
|
|
158
|
+
static func cell_says(tree: Tree, item: TreeItem, column: int) -> String:
|
|
159
|
+
return _as_drawn(tree, item.get_text(column), item.get_auto_translate_mode(column))
|
|
160
|
+
|
|
161
|
+
|
|
132
162
|
## What a node with a `text` says, as it is drawn.
|
|
133
163
|
static func _text_of(node: Node) -> String:
|
|
134
164
|
var label: Label = node as Label
|