gdharness 1.1.11 → 1.1.13
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 +119 -19
- package/build/godot/addons/gdharness_runtime/error_report.gd +61 -15
- package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +13 -2
- package/build/godot/addons/gdharness_runtime/runtime_input.gd +77 -134
- package/build/godot/addons/gdharness_runtime/runtime_menus.gd +81 -0
- package/build/godot/addons/gdharness_runtime/runtime_menus.gd.uid +1 -0
- package/build/godot/addons/gdharness_runtime/runtime_queries.gd +16 -6
- package/build/godot/addons/gdharness_runtime/runtime_screen.gd +50 -0
- package/build/godot/addons/gdharness_runtime/runtime_screen.gd.uid +1 -0
- package/build/godot/addons/gdharness_runtime/runtime_targets.gd +232 -18
- package/build/godot/addons/gdharness_runtime/runtime_waits.gd +4 -3
- package/build/index.js +120 -17
- package/package.json +1 -1
|
@@ -10,6 +10,8 @@ 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 Menus = preload("runtime_menus.gd")
|
|
14
|
+
const Screen = preload("runtime_screen.gd")
|
|
13
15
|
const Targets = preload("runtime_targets.gd")
|
|
14
16
|
const Words = preload("runtime_words.gd")
|
|
15
17
|
|
|
@@ -410,36 +412,6 @@ func inject_mouse_motion(params: Dictionary) -> Dictionary:
|
|
|
410
412
|
}
|
|
411
413
|
|
|
412
414
|
|
|
413
|
-
## The viewport a click aimed at [param control] has to be pushed into.
|
|
414
|
-
##
|
|
415
|
-
## Its own, unless that is an embedded [Window]. A ConfirmationDialog is a Window, and under
|
|
416
|
-
## `gui_embed_subwindows` a Window is drawn inside its parent rather than given one by the
|
|
417
|
-
## desktop: pushing into its own viewport delivers nothing, measured, with the pointer reading as
|
|
418
|
-
## over no control at all. What reaches it is the parent, which is what the engine does with a
|
|
419
|
-
## real pointer. Walked rather than stepped once, because a dialog can open a dialog.
|
|
420
|
-
static func _clicking_viewport(control: Control) -> Viewport:
|
|
421
|
-
var viewport: Viewport = control.get_viewport()
|
|
422
|
-
var window: Window = viewport as Window
|
|
423
|
-
while window != null and window.is_embedded() and window.get_parent() != null:
|
|
424
|
-
viewport = window.get_parent().get_viewport()
|
|
425
|
-
window = viewport as Window
|
|
426
|
-
return viewport
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
## Where [param control]'s centre is in the viewport [method _clicking_viewport] names.
|
|
430
|
-
##
|
|
431
|
-
## The transform is against the control's own viewport, so every embedded window between it and
|
|
432
|
-
## that one contributes its offset. The same walk, because the two answers have to agree about
|
|
433
|
-
## which viewport they are describing.
|
|
434
|
-
static func _centre_of(control: Control) -> Vector2:
|
|
435
|
-
var centre: Vector2 = control.get_global_transform_with_canvas() * (control.size * 0.5)
|
|
436
|
-
var window: Window = control.get_viewport() as Window
|
|
437
|
-
while window != null and window.is_embedded() and window.get_parent() != null:
|
|
438
|
-
centre += Vector2(window.position)
|
|
439
|
-
window = window.get_parent().get_viewport() as Window
|
|
440
|
-
return centre
|
|
441
|
-
|
|
442
|
-
|
|
443
415
|
## Scrolls whatever is holding [param control] until it is on screen, and answers whether
|
|
444
416
|
## anything moved.
|
|
445
417
|
##
|
|
@@ -521,8 +493,9 @@ func click(params: Dictionary) -> Dictionary:
|
|
|
521
493
|
if not control.is_visible_in_tree():
|
|
522
494
|
return {"type": "error", "message": "%s is not visible, so nothing can click it" % node_path}
|
|
523
495
|
|
|
524
|
-
|
|
525
|
-
var
|
|
496
|
+
# In the control's own viewport first, which is the space its scrolling happens in.
|
|
497
|
+
var own: Viewport = control.get_viewport()
|
|
498
|
+
var local: Vector2 = control.get_global_transform_with_canvas() * (control.size * 0.5)
|
|
526
499
|
|
|
527
500
|
# A control out of sight inside a ScrollContainer is not out of reach, it is one scroll away,
|
|
528
501
|
# which is what a person does without thinking about it before they click. Refusing it
|
|
@@ -534,19 +507,23 @@ func click(params: Dictionary) -> Dictionary:
|
|
|
534
507
|
# the click went to that instead and said so. Measured on a hall whose staff panel had been
|
|
535
508
|
# scrolled past: the button was at y 69 and its container started at y 166.
|
|
536
509
|
var scrolled: bool = false
|
|
537
|
-
if not _in_sight(control,
|
|
510
|
+
if not _in_sight(control, own, local):
|
|
538
511
|
scrolled = _scroll_into_view(control)
|
|
539
512
|
if scrolled:
|
|
540
513
|
# A container moves its child on the next layout pass rather than inside the call.
|
|
541
514
|
await _host.get_tree().process_frame
|
|
542
|
-
|
|
515
|
+
local = control.get_global_transform_with_canvas() * (control.size * 0.5)
|
|
543
516
|
|
|
517
|
+
var reached: Dictionary = Screen.reach(own, local)
|
|
518
|
+
var viewport: Viewport = reached["viewport"]
|
|
519
|
+
var centre: Vector2 = reached["point"]
|
|
544
520
|
var position: Vector2 = viewport.get_final_transform() * centre
|
|
545
521
|
# The GUI only delivers to what is inside the viewport, so a centre outside it would be a
|
|
546
522
|
# click that silently reached nothing. A game with no window has a 64 by 64 viewport
|
|
547
523
|
# whatever the project settings say, which is the usual reason to be here and is not
|
|
548
|
-
# something the caller can read off the rect on its own.
|
|
549
|
-
|
|
524
|
+
# something the caller can read off the rect on its own. Its own viewport as well, since a
|
|
525
|
+
# control outside its SubViewport is drawn nowhere.
|
|
526
|
+
if not viewport.get_visible_rect().has_point(centre) or not own.get_visible_rect().has_point(local):
|
|
550
527
|
var why: String = _no_window_note(viewport)
|
|
551
528
|
if scrolled:
|
|
552
529
|
why = ". It was scrolled as far as what holds it goes and is still out there"
|
|
@@ -583,20 +560,17 @@ func click(params: Dictionary) -> Dictionary:
|
|
|
583
560
|
hovered_path = str(hovered.get_path())
|
|
584
561
|
var landed: bool = hovered == control or (hovered != null and control.is_ancestor_of(hovered))
|
|
585
562
|
|
|
586
|
-
viewport
|
|
563
|
+
_press_button(viewport, _button(position, button, true, double))
|
|
587
564
|
await _host.get_tree().process_frame
|
|
588
|
-
viewport
|
|
565
|
+
_press_button(viewport, _button(position, button, false, false))
|
|
589
566
|
# The release is what a button acts on, and a queue_free it causes lands at the end of
|
|
590
567
|
# this frame; the frame passes so the answer describes the control as the click left it.
|
|
591
568
|
await _host.get_tree().process_frame
|
|
592
569
|
|
|
593
570
|
# What became of the control: still in the tree, taken out of it, or freed. A button that
|
|
594
571
|
# opened another screen is the second or the third, and the caller wants to hear that
|
|
595
|
-
# rather than guess it from a tree that has changed shape.
|
|
596
|
-
|
|
597
|
-
var afterwards: String = "freed"
|
|
598
|
-
if is_instance_valid(control):
|
|
599
|
-
afterwards = "in_tree" if control.is_inside_tree() else "removed"
|
|
572
|
+
# rather than guess it from a tree that has changed shape.
|
|
573
|
+
var afterwards: String = _afterwards(control)
|
|
600
574
|
|
|
601
575
|
var answer: Dictionary = {
|
|
602
576
|
"type": "clicked",
|
|
@@ -655,7 +629,7 @@ func choose(params: Dictionary) -> Dictionary:
|
|
|
655
629
|
return standing
|
|
656
630
|
var node: Node = standing["node"]
|
|
657
631
|
|
|
658
|
-
var menu: PopupMenu =
|
|
632
|
+
var menu: PopupMenu = Menus.menu_of(node)
|
|
659
633
|
if menu == null:
|
|
660
634
|
return {
|
|
661
635
|
"type": "error",
|
|
@@ -672,15 +646,15 @@ func choose(params: Dictionary) -> Dictionary:
|
|
|
672
646
|
"message":
|
|
673
647
|
(
|
|
674
648
|
"%s needs the item named, by text or index. It holds: %s"
|
|
675
|
-
% [node_path, ", ".join(
|
|
649
|
+
% [node_path, ", ".join(Menus.items_of(menu))]
|
|
676
650
|
)
|
|
677
651
|
}
|
|
678
652
|
|
|
679
|
-
var index: int =
|
|
653
|
+
var index: int = Menus.wanted_item(menu, params)
|
|
680
654
|
if index < 0:
|
|
681
655
|
return {
|
|
682
656
|
"type": "error",
|
|
683
|
-
"message": "%s has no such item. It holds: %s" % [node_path, ", ".join(
|
|
657
|
+
"message": "%s has no such item. It holds: %s" % [node_path, ", ".join(Menus.items_of(menu))]
|
|
684
658
|
}
|
|
685
659
|
if menu.is_item_separator(index):
|
|
686
660
|
var heading: String = Words.item_says(menu, index).strip_edges()
|
|
@@ -705,9 +679,22 @@ func choose(params: Dictionary) -> Dictionary:
|
|
|
705
679
|
# Shown first, because a menu nobody has opened has no focus to move and Enter would go to
|
|
706
680
|
# whatever is behind it. An OptionButton opens its own; a bare PopupMenu is popped where it
|
|
707
681
|
# already sits, which leaves a menu that was already open where it is.
|
|
708
|
-
var opened: bool =
|
|
682
|
+
var opened: bool = Menus.open_the_menu(node, menu)
|
|
709
683
|
await _host.get_tree().process_frame
|
|
710
684
|
|
|
685
|
+
# Read before the press, because the press can take the menu away: a game that rebuilds its
|
|
686
|
+
# settings screen on item_selected frees the button and its menu inside the pick, and reading
|
|
687
|
+
# the item afterwards was an engine error in the game's log and an empty answer to a pick that
|
|
688
|
+
# had taken.
|
|
689
|
+
var answer: Dictionary = {
|
|
690
|
+
"type": "chosen",
|
|
691
|
+
"path": node_path,
|
|
692
|
+
"index": index,
|
|
693
|
+
"text": Words.item_says(menu, index),
|
|
694
|
+
"id": menu.get_item_id(index),
|
|
695
|
+
"opened": opened,
|
|
696
|
+
"menu": str(menu.get_path()),
|
|
697
|
+
}
|
|
711
698
|
menu.scroll_to_item(index)
|
|
712
699
|
menu.set_focused_item(index)
|
|
713
700
|
# Through Input rather than pushed at the menu, which is how a keyboard reaches an open one: a
|
|
@@ -718,96 +705,49 @@ func choose(params: Dictionary) -> Dictionary:
|
|
|
718
705
|
Input.parse_input_event(_accept(false))
|
|
719
706
|
await _host.get_tree().process_frame
|
|
720
707
|
|
|
721
|
-
|
|
722
|
-
"type": "chosen",
|
|
723
|
-
"path": node_path,
|
|
724
|
-
"index": index,
|
|
725
|
-
"text": Words.item_says(menu, index),
|
|
726
|
-
"id": menu.get_item_id(index),
|
|
727
|
-
"opened": opened,
|
|
728
|
-
"menu": str(menu.get_path()),
|
|
729
|
-
}
|
|
708
|
+
answer["control_afterwards"] = _afterwards(node)
|
|
730
709
|
# What the button in front of the menu reads now, which is the answer to "did it take": a menu
|
|
731
|
-
# item that fired changes the thing holding it, and nothing else about the press says so.
|
|
732
|
-
|
|
733
|
-
if
|
|
710
|
+
# item that fired changes the thing holding it, and nothing else about the press says so. A
|
|
711
|
+
# button the pick took away has nothing to read, and that it went is the answer.
|
|
712
|
+
if answer["control_afterwards"] == "in_tree" and node is OptionButton:
|
|
713
|
+
var chooser: OptionButton = node
|
|
734
714
|
answer["selected"] = chooser.get_selected()
|
|
735
715
|
answer["shows"] = Words.said_by(chooser)
|
|
736
716
|
return answer
|
|
737
717
|
|
|
738
718
|
|
|
739
|
-
##
|
|
740
|
-
##
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
719
|
+
## Sends a click's press or release into [param viewport], through Input when that is the game's
|
|
720
|
+
## own window.
|
|
721
|
+
##
|
|
722
|
+
## Through Input so that Input knows a button is held, which is what a real mouse tells it. A
|
|
723
|
+
## PopupMenu opened by a press asks Input whether a button is down, and when one is, it lets the
|
|
724
|
+
## release that ends that press go by rather than choosing the item under it. Pushed straight into
|
|
725
|
+
## the viewport, the press left Input thinking nothing was held, so a dropdown near the bottom of
|
|
726
|
+
## the screen, whose menu opens over it and under the pointer, had its first item chosen by the
|
|
727
|
+
## click meant to open it. Games read that state too, through Input.is_mouse_button_pressed.
|
|
728
|
+
##
|
|
729
|
+
## Any other viewport is pushed into directly: Input reaches only the game's window. That is a
|
|
730
|
+
## viewport with no place on the screen, since `Screen.reach` carries every other one there.
|
|
731
|
+
func _press_button(viewport: Viewport, event: InputEventMouseButton) -> void:
|
|
732
|
+
if viewport == _host.get_tree().root:
|
|
733
|
+
Input.parse_input_event(event)
|
|
734
|
+
# Delivered now, as a push is, rather than at the next frame: the answer is read a frame
|
|
735
|
+
# after the release, and a button that frees itself on it has to be gone by then.
|
|
736
|
+
Input.flush_buffered_events()
|
|
737
|
+
else:
|
|
738
|
+
viewport.push_input(event)
|
|
750
739
|
|
|
751
740
|
|
|
752
|
-
##
|
|
753
|
-
##
|
|
754
|
-
##
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
return asked if asked >= 0 and asked < menu.get_item_count() else -1
|
|
763
|
-
var wanted: String = str(params.get("text", ""))
|
|
764
|
-
if wanted.is_empty():
|
|
765
|
-
return -1
|
|
766
|
-
# The items before the separators, because a heading can carry the same words as an item under
|
|
767
|
-
# it, and finding the heading first refused the choice the caller meant. A separator is still
|
|
768
|
-
# looked for after, so a heading asked for by name is refused as one rather than as missing.
|
|
769
|
-
var order: Array[int] = []
|
|
770
|
-
for separators: bool in [false, true]:
|
|
771
|
-
for index: int in menu.get_item_count():
|
|
772
|
-
if menu.is_item_separator(index) == separators:
|
|
773
|
-
order.append(index)
|
|
774
|
-
var shown: Array[String] = []
|
|
775
|
-
var held: Array[String] = []
|
|
776
|
-
for index: int in order:
|
|
777
|
-
shown.append(Words.item_says(menu, index))
|
|
778
|
-
held.append(menu.get_item_text(index))
|
|
779
|
-
for words: Array[String] in [shown, held]:
|
|
780
|
-
for at: int in words.size():
|
|
781
|
-
if words[at] == wanted:
|
|
782
|
-
return order[at]
|
|
783
|
-
for at: int in words.size():
|
|
784
|
-
if words[at].nocasecmp_to(wanted) == 0:
|
|
785
|
-
return order[at]
|
|
786
|
-
return -1
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
## What the menu shows, for a refusal that names the choices rather than the miss: a titled
|
|
790
|
-
## separator marked as the heading it is, and one with no title left out, since it shows nothing.
|
|
791
|
-
static func _items_of(menu: PopupMenu) -> Array[String]:
|
|
792
|
-
var said: Array[String] = []
|
|
793
|
-
for index: int in menu.get_item_count():
|
|
794
|
-
var words: String = Words.item_says(menu, index)
|
|
795
|
-
if not menu.is_item_separator(index):
|
|
796
|
-
said.append("%d: %s" % [index, words])
|
|
797
|
-
elif not words.strip_edges().is_empty():
|
|
798
|
-
said.append("%d: %s (a heading)" % [index, words])
|
|
799
|
-
return said
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
## Opens the menu if it is not already, and answers whether anything opened.
|
|
803
|
-
static func _open_the_menu(node: Node, menu: PopupMenu) -> bool:
|
|
804
|
-
if menu.visible:
|
|
805
|
-
return false
|
|
806
|
-
if node.has_method("show_popup"):
|
|
807
|
-
node.call("show_popup")
|
|
808
|
-
return true
|
|
809
|
-
menu.popup()
|
|
810
|
-
return true
|
|
741
|
+
## What became of [param node] once an input it was given has run: still in the tree, taken out
|
|
742
|
+
## of it, or freed. Untyped, because a freed reference handed to a typed parameter is itself the
|
|
743
|
+
## engine error this is here to avoid.
|
|
744
|
+
static func _afterwards(node: Variant) -> String:
|
|
745
|
+
if not is_instance_valid(node):
|
|
746
|
+
return "freed"
|
|
747
|
+
if not node is Node:
|
|
748
|
+
return "removed"
|
|
749
|
+
var placed: Node = node
|
|
750
|
+
return "in_tree" if placed.is_inside_tree() else "removed"
|
|
811
751
|
|
|
812
752
|
|
|
813
753
|
func _accept(pressed: bool) -> InputEventKey:
|
|
@@ -859,13 +799,16 @@ func _click_in_the_world(node_path: String, item: Node3D, params: Dictionary) ->
|
|
|
859
799
|
)
|
|
860
800
|
}
|
|
861
801
|
|
|
862
|
-
var
|
|
802
|
+
var reached: Dictionary = Screen.reach(viewport, aim)
|
|
803
|
+
var arriving: Viewport = reached["viewport"]
|
|
804
|
+
var reached_point: Vector2 = reached["point"]
|
|
805
|
+
var position: Vector2 = arriving.get_final_transform() * reached_point
|
|
863
806
|
var button: int = _resolve_mouse_button(params.get("button", MOUSE_BUTTON_LEFT))
|
|
864
807
|
if button < 0:
|
|
865
808
|
return _no_such_button(params.get("button"))
|
|
866
809
|
var double: bool = Read.as_bool(params.get("double", false))
|
|
867
810
|
|
|
868
|
-
|
|
811
|
+
arriving.push_input(_motion(position, _arrive(position)))
|
|
869
812
|
# Read before the press, for the reason the Control click reads it: what the caller needs to
|
|
870
813
|
# know is whether a panel is sitting over the room, and the press is what would change it.
|
|
871
814
|
var hovered: Control = viewport.gui_get_hovered_control()
|
|
@@ -873,9 +816,9 @@ func _click_in_the_world(node_path: String, item: Node3D, params: Dictionary) ->
|
|
|
873
816
|
if hovered != null:
|
|
874
817
|
hovered_path = str(hovered.get_path())
|
|
875
818
|
|
|
876
|
-
|
|
819
|
+
_press_button(arriving, _button(position, button, true, double))
|
|
877
820
|
await _host.get_tree().process_frame
|
|
878
|
-
|
|
821
|
+
_press_button(arriving, _button(position, button, false, false))
|
|
879
822
|
await _host.get_tree().process_frame
|
|
880
823
|
|
|
881
824
|
return {
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
extends RefCounted
|
|
2
|
+
|
|
3
|
+
## The menu a choice is made from and the item in it, for a choice made by what an item says or by
|
|
4
|
+
## where it is in the list.
|
|
5
|
+
|
|
6
|
+
const Read = preload("reading.gd")
|
|
7
|
+
const Words = preload("runtime_words.gd")
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
## The menu [param node] is, or the one it holds. An [OptionButton] and a [MenuButton] both keep
|
|
11
|
+
## theirs as an internal child, which is a node a caller cannot name and should not have to.
|
|
12
|
+
static func menu_of(node: Node) -> PopupMenu:
|
|
13
|
+
var menu: PopupMenu = node as PopupMenu
|
|
14
|
+
if menu != null:
|
|
15
|
+
return menu
|
|
16
|
+
if node.has_method("get_popup"):
|
|
17
|
+
var held: Variant = node.call("get_popup")
|
|
18
|
+
if held is PopupMenu:
|
|
19
|
+
return held
|
|
20
|
+
return null
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
## Which item was asked for: `text`, matched exactly and then case-insensitively, or `index`.
|
|
24
|
+
## Minus one when neither names one that is there.
|
|
25
|
+
##
|
|
26
|
+
## `text` is matched against the words the item shows before the ones it holds. In a game with
|
|
27
|
+
## translations they differ, and the shown ones are what a caller has read off the screen: an item
|
|
28
|
+
## held as `ACT_ENGAGE` and shown as "Hire" refused "Hire" and listed the keys. The held words
|
|
29
|
+
## still count after them, for a caller that has the key from the source.
|
|
30
|
+
static func wanted_item(menu: PopupMenu, params: Dictionary) -> int:
|
|
31
|
+
if params.has("index"):
|
|
32
|
+
var asked: int = Read.as_int(params.get("index", -1), -1)
|
|
33
|
+
return asked if asked >= 0 and asked < menu.get_item_count() else -1
|
|
34
|
+
var wanted: String = str(params.get("text", ""))
|
|
35
|
+
if wanted.is_empty():
|
|
36
|
+
return -1
|
|
37
|
+
# The items before the separators, because a heading can carry the same words as an item under
|
|
38
|
+
# it, and finding the heading first refused the choice the caller meant. A separator is still
|
|
39
|
+
# looked for after, so a heading asked for by name is refused as one rather than as missing.
|
|
40
|
+
var order: Array[int] = []
|
|
41
|
+
for separators: bool in [false, true]:
|
|
42
|
+
for index: int in menu.get_item_count():
|
|
43
|
+
if menu.is_item_separator(index) == separators:
|
|
44
|
+
order.append(index)
|
|
45
|
+
var shown: Array[String] = []
|
|
46
|
+
var held: Array[String] = []
|
|
47
|
+
for index: int in order:
|
|
48
|
+
shown.append(Words.item_says(menu, index))
|
|
49
|
+
held.append(menu.get_item_text(index))
|
|
50
|
+
for words: Array[String] in [shown, held]:
|
|
51
|
+
for at: int in words.size():
|
|
52
|
+
if words[at] == wanted:
|
|
53
|
+
return order[at]
|
|
54
|
+
for at: int in words.size():
|
|
55
|
+
if words[at].nocasecmp_to(wanted) == 0:
|
|
56
|
+
return order[at]
|
|
57
|
+
return -1
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
## What the menu shows, for a refusal that names the choices rather than the miss: a titled
|
|
61
|
+
## separator marked as the heading it is, and one with no title left out, since it shows nothing.
|
|
62
|
+
static func items_of(menu: PopupMenu) -> Array[String]:
|
|
63
|
+
var said: Array[String] = []
|
|
64
|
+
for index: int in menu.get_item_count():
|
|
65
|
+
var words: String = Words.item_says(menu, index)
|
|
66
|
+
if not menu.is_item_separator(index):
|
|
67
|
+
said.append("%d: %s" % [index, words])
|
|
68
|
+
elif not words.strip_edges().is_empty():
|
|
69
|
+
said.append("%d: %s (a heading)" % [index, words])
|
|
70
|
+
return said
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
## Opens the menu if it is not already, and answers whether anything opened.
|
|
74
|
+
static func open_the_menu(node: Node, menu: PopupMenu) -> bool:
|
|
75
|
+
if menu.visible:
|
|
76
|
+
return false
|
|
77
|
+
if node.has_method("show_popup"):
|
|
78
|
+
node.call("show_popup")
|
|
79
|
+
return true
|
|
80
|
+
menu.popup()
|
|
81
|
+
return true
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
uid://iv31ebsnrf1i
|
|
@@ -5,6 +5,7 @@ extends RefCounted
|
|
|
5
5
|
|
|
6
6
|
const Paths = preload("runtime_paths.gd")
|
|
7
7
|
const Read = preload("reading.gd")
|
|
8
|
+
const Screen = preload("runtime_screen.gd")
|
|
8
9
|
const Values = preload("runtime_values.gd")
|
|
9
10
|
const Words = preload("runtime_words.gd")
|
|
10
11
|
|
|
@@ -423,21 +424,30 @@ func get_rect(params: Dictionary) -> Dictionary:
|
|
|
423
424
|
return standing
|
|
424
425
|
var node: Node = standing["node"]
|
|
425
426
|
|
|
427
|
+
# In the window's pixels, which is what a mouse event names, through every embedded window and
|
|
428
|
+
# SubViewport between the node and the window: multiplied by its own viewport's transform alone,
|
|
429
|
+
# a control in a dialog lost the dialog's offset and one in a SubViewport was given its place in
|
|
430
|
+
# the SubViewport, and a mouse event sent there missed it.
|
|
426
431
|
if node is Control:
|
|
427
432
|
var control: Control = node
|
|
428
|
-
var to_window: Transform2D = control.get_viewport().get_final_transform()
|
|
429
433
|
var canvas_rect: Rect2 = control.get_global_rect()
|
|
430
434
|
return {
|
|
431
435
|
"type": "rect",
|
|
432
436
|
"path": node_path,
|
|
433
437
|
"visible": control.is_visible_in_tree(),
|
|
434
438
|
"canvas": _values.serialize(canvas_rect),
|
|
435
|
-
"window":
|
|
439
|
+
"window":
|
|
440
|
+
_values.serialize(
|
|
441
|
+
Screen.rect_in_window(
|
|
442
|
+
control.get_viewport(),
|
|
443
|
+
control.get_global_transform_with_canvas() * Rect2(Vector2.ZERO, control.size)
|
|
444
|
+
)
|
|
445
|
+
),
|
|
436
446
|
}
|
|
437
447
|
if node is Node2D:
|
|
438
448
|
var item: Node2D = node
|
|
439
449
|
var canvas_position: Vector2 = item.get_global_transform_with_canvas().origin
|
|
440
|
-
var window_position: Vector2 = item.get_viewport()
|
|
450
|
+
var window_position: Vector2 = Screen.in_window(item.get_viewport(), canvas_position)
|
|
441
451
|
return {
|
|
442
452
|
"type": "point",
|
|
443
453
|
"path": node_path,
|
|
@@ -470,7 +480,7 @@ func _in_the_frame(node_path: String, item: Node3D) -> Dictionary:
|
|
|
470
480
|
"message": "%s is not in a viewport with a current Camera3D, so nothing is drawing it" % node_path
|
|
471
481
|
}
|
|
472
482
|
|
|
473
|
-
var
|
|
483
|
+
var drawn_in: Viewport = item.get_viewport()
|
|
474
484
|
var answer: Dictionary = {
|
|
475
485
|
"type": "point",
|
|
476
486
|
"path": node_path,
|
|
@@ -481,12 +491,12 @@ func _in_the_frame(node_path: String, item: Node3D) -> Dictionary:
|
|
|
481
491
|
if found.has("aim"):
|
|
482
492
|
var aim: Vector2 = found["aim"]
|
|
483
493
|
answer["canvas"] = _values.serialize(aim)
|
|
484
|
-
answer["window"] = _values.serialize(
|
|
494
|
+
answer["window"] = _values.serialize(Screen.in_window(drawn_in, aim))
|
|
485
495
|
if found.has("rect"):
|
|
486
496
|
var covered: Rect2 = found["rect"]
|
|
487
497
|
answer["covers"] = {
|
|
488
498
|
"canvas": _values.serialize(covered),
|
|
489
|
-
"window": _values.serialize(
|
|
499
|
+
"window": _values.serialize(Screen.rect_in_window(drawn_in, covered)),
|
|
490
500
|
}
|
|
491
501
|
return answer
|
|
492
502
|
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
extends RefCounted
|
|
2
|
+
|
|
3
|
+
## Where a point in some viewport is on the game's own screen, for a click to be sent there and for
|
|
4
|
+
## an answer to say where something is drawn.
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
## The viewport a pointer at [param point], in [param viewport]'s own coordinates, arrives through,
|
|
8
|
+
## and where it is there: {"viewport", "point"}.
|
|
9
|
+
##
|
|
10
|
+
## A real pointer arrives at the outermost viewport showing this one and is handed down, so that is
|
|
11
|
+
## where a click is sent. An embedded [Window], a ConfirmationDialog for one, is drawn inside its
|
|
12
|
+
## parent under `gui_embed_subwindows`: pushing into its own viewport delivered nothing, measured,
|
|
13
|
+
## with the pointer reading as over no control at all. A [SubViewport] shown by a
|
|
14
|
+
## [SubViewportContainer] is drawn inside the container, scaled by its shrink when it stretches, and
|
|
15
|
+
## pushing into it read the point as the container's: the click on a dropdown inside one landed on
|
|
16
|
+
## nothing. Walked rather than stepped once, because a dialog can open a dialog and a container can
|
|
17
|
+
## sit inside another viewport. A SubViewport drawn some other way, onto a mesh for one, has no
|
|
18
|
+
## place on the screen to carry the point to, and the point stays in it.
|
|
19
|
+
static func reach(viewport: Viewport, point: Vector2) -> Dictionary:
|
|
20
|
+
while true:
|
|
21
|
+
var window: Window = viewport as Window
|
|
22
|
+
if window != null and window.is_embedded() and window.get_parent() != null:
|
|
23
|
+
point += Vector2(window.position)
|
|
24
|
+
viewport = window.get_parent().get_viewport()
|
|
25
|
+
continue
|
|
26
|
+
var shown_by: SubViewportContainer = viewport.get_parent() as SubViewportContainer
|
|
27
|
+
if viewport is SubViewport and shown_by != null:
|
|
28
|
+
var scaled: Vector2 = point * float(shown_by.stretch_shrink) if shown_by.stretch else point
|
|
29
|
+
point = shown_by.get_global_transform_with_canvas() * scaled
|
|
30
|
+
viewport = shown_by.get_viewport()
|
|
31
|
+
continue
|
|
32
|
+
break
|
|
33
|
+
return {"viewport": viewport, "point": point}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
## Where [param point], in [param viewport]'s own coordinates, is in the pixels of the window the
|
|
37
|
+
## game is shown in, which is what a mouse event names.
|
|
38
|
+
static func in_window(viewport: Viewport, point: Vector2) -> Vector2:
|
|
39
|
+
var reached: Dictionary = reach(viewport, point)
|
|
40
|
+
var outermost: Viewport = reached["viewport"]
|
|
41
|
+
var there: Vector2 = reached["point"]
|
|
42
|
+
return outermost.get_final_transform() * there
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
## [param rect], in [param viewport]'s own coordinates, in the window's pixels: the box its two
|
|
46
|
+
## corners make once carried there, which is the rect itself for anything not turned.
|
|
47
|
+
static func rect_in_window(viewport: Viewport, rect: Rect2) -> Rect2:
|
|
48
|
+
var start: Vector2 = in_window(viewport, rect.position)
|
|
49
|
+
var end: Vector2 = in_window(viewport, rect.end)
|
|
50
|
+
return Rect2(start.min(end), (end - start).abs())
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
uid://be3gcwt5krw7k
|