Fix and tidy the Character select, Shortcuts, Log and QR popups
Character select
- A card only picks its character when it is clicked. Before, every card
watched every click, so clicking below the panel could pick a card
scrolled out of view there.
- Six columns of fixed-size cards that scroll for any number of
characters, with a gap between the cards and the scrollbar.
- Pictures are read as PNG, JPG or WebP by their first bytes, so JPGs no
longer print an error. Names are split into words ("Paper Mario") and
the card under the mouse is highlighted.
Shortcuts
- Alt+Z restarts the song; it was listed as "Reset".
- Grouped by what the keys do, in sentence case, and the panel fits the
list. A test checks that every button shortcut is listed.
Log
- Scrolls to the newest row, alternates row colours and says "Nothing
has happened yet" when empty.
QR
- A title, and the code is set up in one place.
This commit is contained in:
1 parent
b2d547b650
commit
8d21b0617d
23 files changed
+4692
-5347
No files matched your search
@@ -61,18 +61,18 @@ unique_name_in_owner = true
|
||||
visible = false
|
||||
layout_mode = 1
|
||||
offset_left = 735.0
|
||||
offset_top = 280.0
|
||||
offset_top = 270.0
|
||||
offset_right = 735.0
|
||||
offset_bottom = 280.0
|
||||
offset_bottom = 270.0
|
||||
|
||||
[node name="ShortcutsPopup" parent="." unique_id=1513416242 instance=ExtResource("13_8eebo")]
|
||||
unique_name_in_owner = true
|
||||
visible = false
|
||||
layout_mode = 1
|
||||
offset_left = 610.0
|
||||
offset_top = 190.0
|
||||
offset_right = 1310.0
|
||||
offset_bottom = 890.0
|
||||
offset_top = 187.0
|
||||
offset_right = 610.0
|
||||
offset_bottom = 187.0
|
||||
|
||||
[node name="AboutPopup" parent="." unique_id=141261008 instance=ExtResource("16_about_window")]
|
||||
unique_name_in_owner = true
|
||||
|
||||
+21
-22
@@ -3,40 +3,39 @@
|
||||
[ext_resource type="Script" uid="uid://i35b3scbpwpu" path="res://popups/character_card.gd" id="1_pjt0s"]
|
||||
[ext_resource type="Texture2D" uid="uid://cv3hdd6bsruau" path="res://icon.svg" id="2_pjt0s"]
|
||||
|
||||
[node name="CharacterCard" type="Control"]
|
||||
layout_mode = 3
|
||||
anchors_preset = 0
|
||||
[node name="CharacterCard" type="Panel"]
|
||||
custom_minimum_size = Vector2(240, 150)
|
||||
mouse_default_cursor_shape = 2
|
||||
theme_type_variation = &"CharacterCard"
|
||||
script = ExtResource("1_pjt0s")
|
||||
|
||||
[node name="Panel" type="Panel" parent="."]
|
||||
layout_mode = 0
|
||||
offset_right = 240.0
|
||||
offset_bottom = 145.0
|
||||
theme_type_variation = &"CharacterCard"
|
||||
[node name="VBoxContainer" type="VBoxContainer" parent="."]
|
||||
layout_mode = 1
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
offset_left = 10.0
|
||||
offset_top = 10.0
|
||||
offset_right = -10.0
|
||||
offset_bottom = -6.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
mouse_filter = 2
|
||||
|
||||
[node name="MarginContainer" type="MarginContainer" parent="Panel"]
|
||||
layout_mode = 0
|
||||
offset_right = 240.0
|
||||
offset_bottom = 145.0
|
||||
theme_override_constants/margin_left = 15
|
||||
theme_override_constants/margin_top = 10
|
||||
theme_override_constants/margin_right = 15
|
||||
|
||||
[node name="VBoxContainer" type="VBoxContainer" parent="Panel/MarginContainer"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="CharacterTextureRect" type="TextureRect" parent="Panel/MarginContainer/VBoxContainer"]
|
||||
[node name="CharacterTextureRect" type="TextureRect" parent="VBoxContainer"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(200, 100)
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 4
|
||||
size_flags_vertical = 4
|
||||
mouse_filter = 2
|
||||
texture = ExtResource("2_pjt0s")
|
||||
expand_mode = 3
|
||||
expand_mode = 1
|
||||
stretch_mode = 5
|
||||
|
||||
[node name="CharacterName" type="Label" parent="Panel/MarginContainer/VBoxContainer"]
|
||||
[node name="CharacterName" type="Label" parent="VBoxContainer"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
text = "Aloy"
|
||||
horizontal_alignment = 1
|
||||
text_overrun_behavior = 3
|
||||
@@ -11,45 +11,42 @@ panel = NodePath("CharacterPanel")
|
||||
[node name="CharacterPanel" type="Panel" parent="."]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 0
|
||||
offset_right = 1290.0
|
||||
offset_bottom = 665.0
|
||||
offset_right = 1580.0
|
||||
offset_bottom = 720.0
|
||||
theme_type_variation = &"PopupBackground"
|
||||
|
||||
[node name="VBoxContainer" type="VBoxContainer" parent="CharacterPanel"]
|
||||
layout_mode = 0
|
||||
offset_right = 1290.0
|
||||
offset_bottom = 659.0
|
||||
size_flags_horizontal = 3
|
||||
|
||||
[node name="TitleLabel" type="Label" parent="CharacterPanel/VBoxContainer"]
|
||||
[node name="TitleLabel" type="Label" parent="CharacterPanel"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 0
|
||||
layout_mode = 0
|
||||
offset_right = 1580.0
|
||||
offset_bottom = 56.0
|
||||
theme_type_variation = &"Title"
|
||||
text = "Character Select"
|
||||
horizontal_alignment = 1
|
||||
vertical_alignment = 1
|
||||
|
||||
[node name="MarginContainer" type="MarginContainer" parent="CharacterPanel/VBoxContainer"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/margin_left = 20
|
||||
theme_override_constants/margin_top = 10
|
||||
theme_override_constants/margin_right = 20
|
||||
theme_override_constants/margin_bottom = 10
|
||||
|
||||
[node name="ScrollContainer" type="ScrollContainer" parent="CharacterPanel/VBoxContainer/MarginContainer"]
|
||||
custom_minimum_size = Vector2(0, 600)
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 3
|
||||
horizontal_scroll_mode = 0
|
||||
|
||||
[node name="CharacterGridContainer" type="GridContainer" parent="CharacterPanel/VBoxContainer/MarginContainer/ScrollContainer"]
|
||||
[node name="ScrollContainer" type="ScrollContainer" parent="CharacterPanel"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(1150, 600)
|
||||
layout_mode = 0
|
||||
offset_left = 24.0
|
||||
offset_top = 60.0
|
||||
offset_right = 1564.0
|
||||
offset_bottom = 704.0
|
||||
horizontal_scroll_mode = 0
|
||||
vertical_scroll_mode = 2
|
||||
|
||||
[node name="ScrollbarGap" type="MarginContainer" parent="CharacterPanel/ScrollContainer"]
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 3
|
||||
theme_override_constants/h_separation = 10
|
||||
theme_override_constants/v_separation = 10
|
||||
columns = 5
|
||||
size_flags_horizontal = 3
|
||||
theme_override_constants/margin_right = 24
|
||||
|
||||
[node name="CharacterGridContainer" type="GridContainer" parent="CharacterPanel/ScrollContainer/ScrollbarGap"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 0
|
||||
theme_override_constants/h_separation = 12
|
||||
theme_override_constants/v_separation = 12
|
||||
columns = 6
|
||||
|
||||
[node name="CloseTimer" type="Timer" parent="CharacterPanel"]
|
||||
unique_name_in_owner = true
|
||||
@@ -31,7 +31,19 @@ theme_type_variation = &"Title"
|
||||
text = "Log"
|
||||
horizontal_alignment = 1
|
||||
|
||||
[node name="EmptyLabel" type="Label" parent="LogPanel"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 0
|
||||
offset_left = 20.0
|
||||
offset_top = 80.0
|
||||
offset_right = 680.0
|
||||
offset_bottom = 110.0
|
||||
theme_type_variation = &"TextLarge"
|
||||
text = "Nothing has happened yet"
|
||||
horizontal_alignment = 1
|
||||
|
||||
[node name="LogScrollContainer" type="ScrollContainer" parent="LogPanel"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 0
|
||||
offset_left = 20.0
|
||||
offset_top = 56.0
|
||||
|
||||
+15
-5
@@ -17,16 +17,26 @@ panel = NodePath("QRPanel")
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 0
|
||||
offset_right = 450.0
|
||||
offset_bottom = 520.0
|
||||
offset_bottom = 540.0
|
||||
theme_type_variation = &"PopupBackground"
|
||||
|
||||
[node name="TitleLabel" type="Label" parent="QRPanel" unique_id=1021365844]
|
||||
layout_mode = 0
|
||||
offset_top = 8.0
|
||||
offset_right = 450.0
|
||||
offset_bottom = 56.0
|
||||
theme_type_variation = &"Title"
|
||||
text = "Scan to search on your phone"
|
||||
horizontal_alignment = 1
|
||||
vertical_alignment = 1
|
||||
|
||||
[node name="QRCodeRect" type="TextureRect" parent="QRPanel" unique_id=1054378438]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 0
|
||||
offset_left = 20.0
|
||||
offset_top = 20.0
|
||||
offset_top = 60.0
|
||||
offset_right = 430.0
|
||||
offset_bottom = 430.0
|
||||
offset_bottom = 470.0
|
||||
script = ExtResource("2_jpsxt")
|
||||
auto_update = true
|
||||
mode = 4
|
||||
@@ -47,9 +57,9 @@ metadata/_custom_type_script = "uid://dynulhnyq5hpq"
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 0
|
||||
offset_left = 20.0
|
||||
offset_top = 450.0
|
||||
offset_top = 482.0
|
||||
offset_right = 430.0
|
||||
offset_bottom = 490.0
|
||||
offset_bottom = 522.0
|
||||
text = "URL"
|
||||
horizontal_alignment = 1
|
||||
vertical_alignment = 1
|
||||
+17
-24
@@ -3,7 +3,6 @@
|
||||
[ext_resource type="Script" uid="uid://rolypd0cqd6h" path="res://popups/shortcuts_popup.gd" id="1_12pxi"]
|
||||
|
||||
[node name="ShortcutsPopup" type="Control" node_paths=PackedStringArray("panel")]
|
||||
custom_minimum_size = Vector2(700, 700)
|
||||
layout_mode = 3
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
@@ -15,35 +14,29 @@ panel = NodePath("ShortcutsPanel")
|
||||
|
||||
[node name="ShortcutsPanel" type="Panel" parent="."]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(700, 700)
|
||||
layout_mode = 0
|
||||
offset_right = 40.0
|
||||
offset_bottom = 40.0
|
||||
offset_right = 700.0
|
||||
offset_bottom = 706.0
|
||||
theme_type_variation = &"PopupBackground"
|
||||
|
||||
[node name="ScrollContainer" type="ScrollContainer" parent="ShortcutsPanel"]
|
||||
custom_minimum_size = Vector2(700, 700)
|
||||
[node name="TitleLabel" type="Label" parent="ShortcutsPanel"]
|
||||
layout_mode = 0
|
||||
offset_right = 40.0
|
||||
offset_bottom = 40.0
|
||||
offset_right = 700.0
|
||||
offset_bottom = 56.0
|
||||
theme_type_variation = &"Title"
|
||||
text = "Shortcuts"
|
||||
horizontal_alignment = 1
|
||||
vertical_alignment = 1
|
||||
|
||||
[node name="ScrollContainer" type="ScrollContainer" parent="ShortcutsPanel"]
|
||||
layout_mode = 0
|
||||
offset_left = 24.0
|
||||
offset_top = 60.0
|
||||
offset_right = 676.0
|
||||
offset_bottom = 690.0
|
||||
horizontal_scroll_mode = 0
|
||||
|
||||
[node name="shortcutsVBox" type="VBoxContainer" parent="ShortcutsPanel/ScrollContainer"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(700, 700)
|
||||
layout_mode = 2
|
||||
|
||||
[node name="HBoxContainer" type="HBoxContainer" parent="ShortcutsPanel/ScrollContainer/shortcutsVBox"]
|
||||
layout_mode = 2
|
||||
alignment = 1
|
||||
|
||||
[node name="Label" type="Label" parent="ShortcutsPanel/ScrollContainer/shortcutsVBox/HBoxContainer"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
text = "testar"
|
||||
horizontal_alignment = 1
|
||||
|
||||
[node name="Label2" type="Label" parent="ShortcutsPanel/ScrollContainer/shortcutsVBox/HBoxContainer"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
text = "testera 2"
|
||||
horizontal_alignment = 1
|
||||
+46
-31
@@ -1,48 +1,63 @@
|
||||
class_name CharacterCard
|
||||
extends Control
|
||||
extends Panel
|
||||
## One character in the character select popup: its picture and name. Clicking it picks
|
||||
## the character.
|
||||
|
||||
## Emitted with the character's picture when the card is clicked.
|
||||
|
||||
signal new_character_selected(img_texture: ImageTexture)
|
||||
|
||||
@onready var panel: Panel = $Panel
|
||||
@onready var character_image: TextureRect = %CharacterTextureRect
|
||||
@onready var character_name_label: Label = %CharacterName
|
||||
|
||||
|
||||
func _input(event: InputEvent) -> void:
|
||||
if visible && Settings.character_select_open:
|
||||
if event is InputEventMouseButton && event.is_pressed() \
|
||||
&& event.get_button_index() == MOUSE_BUTTON_LEFT:
|
||||
var local_event: InputEvent = make_input_local(event)
|
||||
if Rect2(Vector2(0, 0), panel.size).has_point(local_event.position):
|
||||
GameLog.debug(character_name_label.text)
|
||||
new_character_selected.emit(character_image.texture)
|
||||
func _ready() -> void:
|
||||
gui_input.connect(_on_gui_input)
|
||||
mouse_entered.connect(_set_hovered.bind(true))
|
||||
mouse_exited.connect(_set_hovered.bind(false))
|
||||
|
||||
|
||||
## Shows [param character_name] and fetches its picture from the server.
|
||||
func set_character_name(character_name: String) -> void:
|
||||
panel.size = Vector2(240, 145)
|
||||
character_name_label.text = character_name.split(".")[0]
|
||||
character_name_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
character_name_label.text = display_name(character_name)
|
||||
tooltip_text = character_name_label.text
|
||||
var image_fetched: Callable = func image_fetched(image: PackedByteArray) -> void:
|
||||
var img: Image = Image.new()
|
||||
var err: Error = img.load_png_from_buffer(image)
|
||||
if err != OK:
|
||||
GameLog.debug("Character image is not a PNG, trying JPG: ", error_string(err))
|
||||
var err2: Error = img.load_jpg_from_buffer(image)
|
||||
if err2 != OK:
|
||||
GameLog.warning("Could not load the image for ", character_name, ": ",
|
||||
error_string(err2))
|
||||
var new_image: ImageTexture = ImageTexture.new()
|
||||
new_image.set_image(img)
|
||||
character_image.custom_minimum_size = Vector2(200, 100)
|
||||
character_image.size = Vector2(200, 100)
|
||||
character_image.size_flags_horizontal = Control.SIZE_SHRINK_CENTER
|
||||
character_image.size_flags_vertical = Control.SIZE_SHRINK_CENTER
|
||||
character_image.expand_mode = TextureRect.EXPAND_FIT_WIDTH_PROPORTIONAL
|
||||
character_image.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
|
||||
character_image.texture = new_image
|
||||
var picture: Image = image_from_bytes(image)
|
||||
if picture == null:
|
||||
GameLog.warning("Could not load the picture of ", character_name)
|
||||
return
|
||||
character_image.texture = ImageTexture.create_from_image(picture)
|
||||
MusicServerClient.fetch_character_image(character_name, image_fetched)
|
||||
|
||||
|
||||
## The name to show for a picture's [param file_name]: without the file extension, and
|
||||
## with words that are written together split, e.g. "PaperMario.png" is "Paper Mario".
|
||||
static func display_name(file_name: String) -> String:
|
||||
var name_part: String = file_name.get_basename() if "." in file_name else file_name
|
||||
var words: RegEx = RegEx.create_from_string("([a-z])([A-Z])")
|
||||
return words.sub(name_part, "$1 $2", true)
|
||||
|
||||
|
||||
## Reads a PNG, JPG or WebP picture, telling them apart by their first bytes so that no
|
||||
## error is printed for trying the wrong format. Returns null if it is none of them.
|
||||
static func image_from_bytes(bytes: PackedByteArray) -> Image:
|
||||
var image: Image = Image.new()
|
||||
var error: Error = ERR_FILE_UNRECOGNIZED
|
||||
if bytes.size() >= 4 && bytes.slice(0, 4) == PackedByteArray([0x89, 0x50, 0x4E, 0x47]):
|
||||
error = image.load_png_from_buffer(bytes)
|
||||
elif bytes.size() >= 3 && bytes.slice(0, 3) == PackedByteArray([0xFF, 0xD8, 0xFF]):
|
||||
error = image.load_jpg_from_buffer(bytes)
|
||||
elif bytes.size() >= 12 && bytes.slice(8, 12).get_string_from_ascii() == "WEBP":
|
||||
error = image.load_webp_from_buffer(bytes)
|
||||
return image if error == OK else null
|
||||
|
||||
|
||||
func _set_hovered(hovered: bool) -> void:
|
||||
theme_type_variation = &"CharacterCardHover" if hovered else &"CharacterCard"
|
||||
|
||||
|
||||
func _on_gui_input(event: InputEvent) -> void:
|
||||
if event is InputEventMouseButton && event.pressed \
|
||||
&& event.button_index == MOUSE_BUTTON_LEFT && Settings.character_select_open:
|
||||
accept_event()
|
||||
GameLog.debug(character_name_label.text)
|
||||
new_character_selected.emit(character_image.texture)
|
||||
@@ -33,8 +33,6 @@ func load_characters() -> void:
|
||||
GameLog.debug("character_name ", character_name)
|
||||
var character_box: CharacterCard = character.instantiate()
|
||||
character_grid.add_child(character_box)
|
||||
character_box.custom_minimum_size = Vector2(235, 145)
|
||||
character_box.size = Vector2(235, 145)
|
||||
character_box.set_character_name(character_name)
|
||||
character_box.new_character_selected.connect(_on_character_selected)
|
||||
MusicServerClient.fetch_character_list(fetch_character_list)
|
||||
|
||||
+24
-14
@@ -1,28 +1,38 @@
|
||||
class_name LogPopup
|
||||
extends BasePopup
|
||||
## The game log: a row with the time for each thing that happens in the quiz, such as a
|
||||
## player getting a point. Opened with Alt+L.
|
||||
|
||||
var log_rows: Array
|
||||
## player getting a point. Opened with Alt+L. The newest row is at the bottom and is
|
||||
## scrolled into view.
|
||||
|
||||
@onready var log_panel: Panel = %LogPanel
|
||||
@onready var log_box: VBoxContainer = %LogVBoxContainer
|
||||
@onready var log_scroll: ScrollContainer = %LogScrollContainer
|
||||
@onready var empty_label: Label = %EmptyLabel
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
visibility_changed.connect(_on_visibility_changed)
|
||||
|
||||
|
||||
func add_log_row(text: String) -> void:
|
||||
var log_row: String = get_time() + text
|
||||
log_rows.append(log_row)
|
||||
log_box.add_child(make_log_label(log_row))
|
||||
|
||||
|
||||
func make_log_label(row: String) -> Label:
|
||||
var log_label: Label = Label.new()
|
||||
log_label.theme_type_variation = &"TextLarge"
|
||||
log_label.text = row
|
||||
log_label.autowrap_mode = TextServer.AUTOWRAP_WORD
|
||||
return log_label
|
||||
var row: int = log_box.get_child_count()
|
||||
log_box.add_child(ListRow.create(get_time() + text, row, &"TextLarge"))
|
||||
empty_label.visible = false
|
||||
_scroll_to_newest.call_deferred()
|
||||
|
||||
|
||||
func get_time() -> String:
|
||||
var time: Dictionary = Time.get_time_dict_from_system()
|
||||
return "%02d:%02d:%02d " % [time.hour, time.minute, time.second]
|
||||
|
||||
|
||||
func _on_visibility_changed() -> void:
|
||||
if visible:
|
||||
empty_label.visible = log_box.get_child_count() == 0
|
||||
_scroll_to_newest.call_deferred()
|
||||
|
||||
|
||||
# Deferred, as the list only knows its new height once the new row is laid out
|
||||
func _scroll_to_newest() -> void:
|
||||
await get_tree().process_frame
|
||||
log_scroll.scroll_vertical = int(log_scroll.get_v_scroll_bar().max_value)
|
||||
+10
-13
@@ -9,21 +9,18 @@ extends BasePopup
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
qr_code_rect.light_module_color = Color.WHITE
|
||||
qr_code_rect.dark_module_color = Color.BLACK
|
||||
qr_code_rect.quiet_zone_size = 4
|
||||
|
||||
# Generate QR code for the search URL
|
||||
var url: String = Settings.default_path + "/search"
|
||||
qr_code_rect.data = url
|
||||
url_label.text = qr_code_rect.data
|
||||
GameLog.debug("QR code generated for: ", url)
|
||||
_show_search_url()
|
||||
|
||||
|
||||
func open() -> void:
|
||||
super()
|
||||
# Regenerate QR code in case settings changed
|
||||
var url: String = Settings.default_path + "/search"
|
||||
qr_code_rect.data = url
|
||||
url_label.text = qr_code_rect.data
|
||||
# Another server may have been picked in the settings since last time
|
||||
_show_search_url()
|
||||
|
||||
|
||||
func _show_search_url() -> void:
|
||||
var url: String = Settings.default_path + "/search"
|
||||
if qr_code_rect.data != url:
|
||||
qr_code_rect.data = url
|
||||
GameLog.debug("QR code generated for: ", url)
|
||||
url_label.text = url
|
||||
+60
-46
@@ -1,65 +1,79 @@
|
||||
class_name ShortcutsPopup
|
||||
extends BasePopup
|
||||
## Lists the keyboard shortcuts. Opened with Alt+K or the Shortcuts button.
|
||||
## Lists the keyboard shortcuts, grouped by what they do. Opened with Alt+K or the
|
||||
## Shortcuts button.
|
||||
|
||||
var shortcuts_dic: Dictionary = {
|
||||
"Alt + S": "Search",
|
||||
"Alt + A": "Add Players",
|
||||
"Alt + Z": "Reset",
|
||||
"Alt + X": "Play/Pause",
|
||||
"Alt + C": "Next Song",
|
||||
"Alt + V": "Show Answer",
|
||||
"Alt + L": "Show Debug Log",
|
||||
"Alt + K": "Show shortcuts",
|
||||
"Alt + Enter": "Fullscreen",
|
||||
"Alt + UP": "Volume up",
|
||||
"Alt + DOWN": "Volume down",
|
||||
"Alt + LEFT": "Jump back in song",
|
||||
"Alt + Ctrl + LEFT": "Restart song",
|
||||
"Alt + RIGHT": "Jump forward in song",
|
||||
"Alt + 1 - 9": "Give player point",
|
||||
"Alt + Ctrl + 1 - 9": "Take point from player",
|
||||
}
|
||||
## Each group's heading and its shortcuts, as [code][keys, what they do][/code].
|
||||
## [code]tests/integration/test_shortcuts_popup.gd[/code] checks that every button
|
||||
## shortcut in the game is here with the right keys.
|
||||
const SHORTCUTS: Array = [
|
||||
["Players and points", [
|
||||
["Alt + 1–9", "Give a player a point"],
|
||||
["Alt + Ctrl + 1–9", "Take a point from a player"],
|
||||
["Alt + A", "Add players"],
|
||||
]],
|
||||
["Songs", [
|
||||
["Alt + C", "Next song"],
|
||||
["Alt + V", "Show answer"],
|
||||
["Alt + X", "Play or pause"],
|
||||
["Alt + Z", "Restart song"],
|
||||
["Alt + Ctrl + Left", "Restart song"],
|
||||
["Alt + Left", "Jump back 5 seconds"],
|
||||
["Alt + Right", "Jump forward 5 seconds"],
|
||||
["Alt + Up", "Volume up"],
|
||||
["Alt + Down", "Volume down"],
|
||||
]],
|
||||
["Windows", [
|
||||
["Alt + S", "Search"],
|
||||
["Alt + K", "Shortcuts"],
|
||||
["Alt + L", "Log"],
|
||||
["Alt + Enter", "Fullscreen"],
|
||||
]],
|
||||
]
|
||||
|
||||
## Space above each group but the first.
|
||||
const GROUP_GAP: Vector2 = Vector2(0, 8)
|
||||
|
||||
@onready var shortcuts_vbox: VBoxContainer = %shortcutsVBox
|
||||
@onready var shortcuts_panel: Panel = %ShortcutsPanel
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
shortcuts_dic.sort()
|
||||
Settings.delete_children(shortcuts_vbox)
|
||||
for group: Array in SHORTCUTS:
|
||||
if shortcuts_vbox.get_child_count() > 0:
|
||||
var gap: Control = Control.new()
|
||||
gap.custom_minimum_size = GROUP_GAP
|
||||
shortcuts_vbox.add_child(gap)
|
||||
var heading: Label = Label.new()
|
||||
heading.theme_type_variation = &"HighlightLarge"
|
||||
heading.text = group[0]
|
||||
shortcuts_vbox.add_child(heading)
|
||||
var row: int = 0
|
||||
for shortcut: Array in group[1]:
|
||||
shortcuts_vbox.add_child(_make_row(shortcut[0], shortcut[1], row))
|
||||
row += 1
|
||||
|
||||
var title: Label = Label.new()
|
||||
title.theme_type_variation = &"TextLarge"
|
||||
title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
title.text = "Shortcuts"
|
||||
shortcuts_vbox.add_child(title)
|
||||
|
||||
var count: int = 0
|
||||
for shortcut: String in shortcuts_dic.keys():
|
||||
## Every key combination in the list, e.g. "Alt + C".
|
||||
static func listed_keys() -> Array[String]:
|
||||
var keys: Array[String] = []
|
||||
for group: Array in SHORTCUTS:
|
||||
for shortcut: Array in group[1]:
|
||||
keys.append(shortcut[0])
|
||||
return keys
|
||||
|
||||
|
||||
func _make_row(keys: String, action: String, row: int) -> PanelContainer:
|
||||
var panel: PanelContainer = PanelContainer.new()
|
||||
if row % 2 == 0:
|
||||
panel.theme_type_variation = &"ShortcutRowDark"
|
||||
var hbox: HBoxContainer = HBoxContainer.new()
|
||||
hbox.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||
|
||||
for text: String in [keys, action]:
|
||||
var label: Label = Label.new()
|
||||
label.theme_type_variation = &"TextLarge"
|
||||
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
label.text = shortcut
|
||||
label.autowrap_mode = TextServer.AUTOWRAP_WORD
|
||||
label.text = text
|
||||
label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
|
||||
var label2: Label = Label.new()
|
||||
label2.theme_type_variation = &"TextLarge"
|
||||
label2.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
label2.text = shortcuts_dic[shortcut]
|
||||
label2.autowrap_mode = TextServer.AUTOWRAP_WORD
|
||||
label2.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
|
||||
if count % 2 == 0:
|
||||
panel.theme_type_variation = &"ShortcutRowDark"
|
||||
|
||||
hbox.add_child(label)
|
||||
hbox.add_child(label2)
|
||||
panel.add_child(hbox)
|
||||
shortcuts_vbox.add_child(panel)
|
||||
count += 1
|
||||
return panel
|
||||
+4
-4
@@ -102,10 +102,10 @@ offset_bottom = 220.0
|
||||
unique_name_in_owner = true
|
||||
visible = false
|
||||
layout_mode = 1
|
||||
offset_left = 315.0
|
||||
offset_top = 208.0
|
||||
offset_right = 1467.0
|
||||
offset_bottom = 848.0
|
||||
offset_left = 170.0
|
||||
offset_top = 180.0
|
||||
offset_right = 1750.0
|
||||
offset_bottom = 900.0
|
||||
|
||||
[node name="WinnerPopup" parent="." unique_id=999999999 instance=ExtResource("17_winner_window")]
|
||||
unique_name_in_owner = true
|
||||
|
||||
+4148
-5147
File diff suppressed because it is too large.
Load diff
@@ -382,8 +382,8 @@ func _freeze_music_player() -> void:
|
||||
## The log starts each row with the time of day; use a fixed time instead.
|
||||
func _fix_log_times() -> void:
|
||||
var log_popup: BasePopup = _node("LogPopup")
|
||||
for row: Label in log_popup.log_box.get_children():
|
||||
row.text = "12:00:00 " + row.text.substr(9)
|
||||
for row: ListRow in log_popup.log_box.get_children():
|
||||
row.label.text = "12:00:00 " + row.label.text.substr(9)
|
||||
|
||||
|
||||
func _open_popup(popup_name: String) -> Node:
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
extends MainWindowTestSuite
|
||||
## The character select popup: picking a character by clicking its card, scrolling when
|
||||
## there are more characters than fit, and reading the pictures and names.
|
||||
|
||||
## More characters than fit without scrolling. The number changes on the server over
|
||||
## time, so nothing here depends on it.
|
||||
const CHARACTER_COUNT: int = 27
|
||||
|
||||
var _popup: CharacterSelectPopup
|
||||
var _crono: PlayerObject
|
||||
var _picked: Array[Texture2D] = []
|
||||
|
||||
|
||||
func _before_window_starts() -> void:
|
||||
var names: Array[String] = []
|
||||
var png: PackedByteArray = _picture().save_png_to_buffer()
|
||||
for i: int in CHARACTER_COUNT:
|
||||
names.append("Character%d.png" % i)
|
||||
_server.respond_bytes("/character?name=" + names[i].uri_encode(), png)
|
||||
_server.respond_json("/character/list", names)
|
||||
|
||||
|
||||
func before_test() -> void:
|
||||
super()
|
||||
_picked.clear()
|
||||
_popup = _quiz.get_node("CharacterSelectPopup")
|
||||
_popup.character_selected.connect(func(texture: Texture2D) -> void: _picked.append(texture))
|
||||
# Opened from the player's row, like clicking the player's picture
|
||||
_crono = _add_player("Crono")
|
||||
_player_row(_crono).change_character()
|
||||
await _runner.simulate_frames(2)
|
||||
|
||||
|
||||
func _picture() -> Image:
|
||||
var image: Image = Image.create(20, 10, false, Image.FORMAT_RGB8)
|
||||
image.fill(Color.ORANGE)
|
||||
return image
|
||||
|
||||
|
||||
func _cards() -> Array[Node]:
|
||||
return _popup.character_grid.get_children()
|
||||
|
||||
|
||||
func test_one_card_per_character_in_six_columns() -> void:
|
||||
assert_int(_cards().size()).is_equal(CHARACTER_COUNT)
|
||||
assert_int(_popup.character_grid.columns).is_equal(6)
|
||||
assert_object((_cards()[0] as CharacterCard).character_image.texture).is_not_null()
|
||||
|
||||
|
||||
func test_more_characters_than_fit_can_be_scrolled() -> void:
|
||||
var scroll: ScrollContainer = _popup.find_child("ScrollContainer")
|
||||
|
||||
assert_float(_popup.character_grid.size.y).is_greater(scroll.size.y)
|
||||
assert_bool(scroll.get_v_scroll_bar().visible).is_true()
|
||||
|
||||
|
||||
func test_cards_keep_away_from_the_scrollbar() -> void:
|
||||
var scroll: ScrollContainer = _popup.find_child("ScrollContainer")
|
||||
var scrollbar: Rect2 = scroll.get_v_scroll_bar().get_global_rect()
|
||||
var last_column: Rect2 = (_cards()[5] as Control).get_global_rect()
|
||||
|
||||
assert_float(scrollbar.position.x - last_column.end.x).is_greater_equal(20.0)
|
||||
|
||||
|
||||
func test_clicking_a_card_picks_that_character() -> void:
|
||||
var card: CharacterCard = _cards()[2]
|
||||
|
||||
await _click_at(card.get_global_rect().get_center())
|
||||
|
||||
assert_array(_picked).contains_exactly([card.character_image.texture])
|
||||
assert_object(_crono.character).is_same(card.character_image.texture)
|
||||
assert_bool(_popup.visible).is_false()
|
||||
|
||||
|
||||
func test_click_where_a_scrolled_away_card_is_picks_nothing() -> void:
|
||||
# The last row is below the visible part of the list, under the bottom of the panel
|
||||
var hidden_card: CharacterCard = _cards().back()
|
||||
var where: Vector2 = hidden_card.get_global_rect().get_center()
|
||||
assert_bool(_popup.panel.get_global_rect().has_point(where)).is_false()
|
||||
|
||||
await _click_at(where)
|
||||
|
||||
assert_array(_picked).is_empty()
|
||||
assert_bool(_popup.visible).is_false()
|
||||
|
||||
|
||||
func test_hovering_a_card_highlights_it() -> void:
|
||||
var card: CharacterCard = _cards()[0]
|
||||
|
||||
card.mouse_entered.emit()
|
||||
assert_str(String(card.theme_type_variation)).is_equal("CharacterCardHover")
|
||||
card.mouse_exited.emit()
|
||||
assert_str(String(card.theme_type_variation)).is_equal("CharacterCard")
|
||||
|
||||
|
||||
func test_names_are_shown_without_extension_and_split_into_words() -> void:
|
||||
assert_str(CharacterCard.display_name("PaperMario.png")).is_equal("Paper Mario")
|
||||
assert_str(CharacterCard.display_name("Mizuti.jpg")).is_equal("Mizuti")
|
||||
assert_str(CharacterCard.display_name("Kingdom Hearts.png")).is_equal("Kingdom Hearts")
|
||||
assert_str((_cards()[3] as CharacterCard).character_name_label.text).is_equal("Character3")
|
||||
|
||||
|
||||
func test_png_jpg_and_webp_pictures_are_read() -> void:
|
||||
var picture: Image = _picture()
|
||||
|
||||
for bytes: PackedByteArray in [picture.save_png_to_buffer(),
|
||||
picture.save_jpg_to_buffer(), picture.save_webp_to_buffer()]:
|
||||
var image: Image = CharacterCard.image_from_bytes(bytes)
|
||||
assert_object(image).is_not_null()
|
||||
assert_vector(Vector2(image.get_size())).is_equal(Vector2(20, 10))
|
||||
|
||||
|
||||
func test_something_that_is_not_a_picture_gives_null() -> void:
|
||||
assert_object(CharacterCard.image_from_bytes("not a picture".to_utf8_buffer())).is_null()
|
||||
assert_object(CharacterCard.image_from_bytes(PackedByteArray())).is_null()
|
||||
@@ -0,0 +1 @@
|
||||
uid://bivqa3ioaqra
|
||||
@@ -0,0 +1,71 @@
|
||||
extends MainWindowTestSuite
|
||||
## The Log popup (rows, empty state, scrolling to the newest row) and the QR popup.
|
||||
|
||||
var _log: LogPopup
|
||||
var _qr: QRPopup
|
||||
|
||||
|
||||
func before_test() -> void:
|
||||
super()
|
||||
_log = _window.get_node("LogPopup")
|
||||
_qr = _window.get_node("QRPopup")
|
||||
|
||||
|
||||
func test_empty_log_says_so() -> void:
|
||||
_log.open()
|
||||
|
||||
assert_bool(_log.empty_label.visible).is_true()
|
||||
|
||||
|
||||
func test_log_rows_alternate_and_hide_the_empty_text() -> void:
|
||||
_log.add_log_row("Crono got a point")
|
||||
_log.add_log_row("Marle got a point")
|
||||
_log.open()
|
||||
|
||||
var rows: Array[Node] = _log.log_box.get_children()
|
||||
assert_str((rows[0] as ListRow).text).ends_with("Crono got a point")
|
||||
assert_str(String((rows[0] as ListRow).theme_type_variation)).is_equal("ListRowEven")
|
||||
assert_str(String((rows[1] as ListRow).theme_type_variation)).is_equal("ListRowOdd")
|
||||
assert_bool(_log.empty_label.visible).is_false()
|
||||
|
||||
|
||||
func test_log_shows_the_newest_row() -> void:
|
||||
_log.open()
|
||||
for i: int in 40:
|
||||
_log.add_log_row("Event %d" % i)
|
||||
await _runner.simulate_frames(4)
|
||||
|
||||
var scroll: ScrollContainer = _log.log_scroll
|
||||
var newest: Rect2 = (_log.log_box.get_children().back() as Control).get_global_rect()
|
||||
assert_bool(scroll.get_global_rect().encloses(newest)).is_true()
|
||||
|
||||
|
||||
func test_log_scrolls_to_the_newest_row_when_opened() -> void:
|
||||
for i: int in 40:
|
||||
_log.add_log_row("Event %d" % i)
|
||||
|
||||
_log.open()
|
||||
await _runner.simulate_frames(4)
|
||||
|
||||
var newest: Rect2 = (_log.log_box.get_children().back() as Control).get_global_rect()
|
||||
assert_bool(_log.log_scroll.get_global_rect().encloses(newest)).is_true()
|
||||
|
||||
|
||||
func test_qr_popup_has_a_title_and_the_search_address() -> void:
|
||||
_qr.open()
|
||||
|
||||
assert_str((_qr.qr_panel.get_node("TitleLabel") as Label).text) \
|
||||
.is_equal("Scan to search on your phone")
|
||||
assert_str(_qr.url_label.text).is_equal(Settings.default_path + "/search")
|
||||
assert_str(_qr.qr_code_rect.data).is_equal(_qr.url_label.text)
|
||||
|
||||
|
||||
func test_qr_code_follows_the_server_picked_in_the_settings() -> void:
|
||||
_qr.open()
|
||||
_qr.close()
|
||||
Settings.default_path = "http://localhost:8080"
|
||||
|
||||
_qr.open()
|
||||
|
||||
assert_str(_qr.url_label.text).is_equal("http://localhost:8080/search")
|
||||
assert_str(_qr.qr_code_rect.data).is_equal("http://localhost:8080/search")
|
||||
@@ -0,0 +1 @@
|
||||
uid://cm030oben0fhb
|
||||
@@ -238,5 +238,5 @@ func test_log_adds_one_row_per_point_with_padded_time() -> void:
|
||||
|
||||
assert_int(log_box.get_child_count()).is_equal(2)
|
||||
var time_pattern: RegEx = RegEx.create_from_string("^\\d\\d:\\d\\d:\\d\\d Crono got a point$")
|
||||
assert_object(time_pattern.search((log_box.get_child(0) as Label).text)).is_not_null()
|
||||
assert_str((log_box.get_child(1) as Label).text).ends_with("Crono lost a point")
|
||||
assert_object(time_pattern.search((log_box.get_child(0) as ListRow).text)).is_not_null()
|
||||
assert_str((log_box.get_child(1) as ListRow).text).ends_with("Crono lost a point")
|
||||
@@ -0,0 +1,99 @@
|
||||
extends MainWindowTestSuite
|
||||
## The Shortcuts popup lists every shortcut in the game with the right keys, grouped by
|
||||
## what they do.
|
||||
|
||||
var _popup: ShortcutsPopup
|
||||
|
||||
|
||||
func before_test() -> void:
|
||||
super()
|
||||
_popup = _window.get_node("ShortcutsPopup")
|
||||
|
||||
|
||||
## How the list writes [param event]: "Alt + Ctrl + Left", with player numbers as 1–9.
|
||||
static func _key_text(event: InputEventKey) -> String:
|
||||
var parts: PackedStringArray = []
|
||||
if event.alt_pressed:
|
||||
parts.append("Alt")
|
||||
if event.ctrl_pressed:
|
||||
parts.append("Ctrl")
|
||||
var keycode: Key = event.keycode if event.keycode != KEY_NONE else event.physical_keycode
|
||||
var key: String = OS.get_keycode_string(keycode)
|
||||
parts.append("1–9" if key.is_valid_int() else key)
|
||||
return " + ".join(parts)
|
||||
|
||||
|
||||
## Every button in [param node] and below it that has a keyboard shortcut.
|
||||
func _buttons_with_shortcuts(node: Node) -> Array[BaseButton]:
|
||||
var buttons: Array[BaseButton] = []
|
||||
if node is BaseButton && node.shortcut != null:
|
||||
buttons.append(node)
|
||||
for child: Node in node.get_children():
|
||||
buttons.append_array(_buttons_with_shortcuts(child))
|
||||
return buttons
|
||||
|
||||
|
||||
func _description(keys: String) -> String:
|
||||
for group: Array in ShortcutsPopup.SHORTCUTS:
|
||||
for shortcut: Array in group[1]:
|
||||
if shortcut[0] == keys:
|
||||
return shortcut[1]
|
||||
return ""
|
||||
|
||||
|
||||
func test_every_button_shortcut_is_listed() -> void:
|
||||
# A player row brings the point keys
|
||||
_add_player("Crono")
|
||||
var buttons: Array[BaseButton] = _buttons_with_shortcuts(_window)
|
||||
assert_int(buttons.size()).is_greater(5)
|
||||
|
||||
for button: BaseButton in buttons:
|
||||
for event: InputEvent in button.shortcut.events:
|
||||
var keys: String = _key_text(event)
|
||||
assert_array(ShortcutsPopup.listed_keys()).override_failure_message(
|
||||
"%s (%s) is not in the list" % [keys, button.name]).contains([keys])
|
||||
|
||||
|
||||
func test_buttons_do_what_the_list_says() -> void:
|
||||
var expected: Dictionary = {
|
||||
"SongControls/NextButton": "Next song",
|
||||
"SongControls/ShowAnswerButton": "Show answer",
|
||||
"MusicPlayer/PlayerBar/MusicPlayerContainer/PlayTextureButton": "Play or pause",
|
||||
"MusicPlayer/PlayerBar/MusicPlayerContainer/RestartTextureButton": "Restart song",
|
||||
}
|
||||
for path: String in expected:
|
||||
var button: BaseButton = _quiz.get_node(path)
|
||||
assert_str(_description(_key_text(button.shortcut.events[0]))) \
|
||||
.override_failure_message(path).is_equal(expected[path])
|
||||
|
||||
|
||||
func test_keys_are_listed_once() -> void:
|
||||
var keys: Array[String] = ShortcutsPopup.listed_keys()
|
||||
for keys_text: String in keys:
|
||||
assert_int(keys.count(keys_text)).override_failure_message(keys_text).is_equal(1)
|
||||
|
||||
|
||||
func test_groups_have_headings_and_rows() -> void:
|
||||
_popup.open()
|
||||
await _runner.simulate_frames(1)
|
||||
|
||||
var headings: Array[String] = []
|
||||
var rows: int = 0
|
||||
for child: Node in _popup.shortcuts_vbox.get_children():
|
||||
if child is Label:
|
||||
headings.append(child.text)
|
||||
elif child is PanelContainer:
|
||||
rows += 1
|
||||
assert_array(headings).contains_exactly(["Players and points", "Songs", "Windows"])
|
||||
assert_int(rows).is_equal(ShortcutsPopup.listed_keys().size())
|
||||
|
||||
|
||||
func test_list_fits_without_scrolling_or_empty_space() -> void:
|
||||
_popup.open()
|
||||
await _runner.simulate_frames(2)
|
||||
|
||||
var scroll: ScrollContainer = _popup.shortcuts_vbox.get_parent()
|
||||
var content: float = _popup.shortcuts_vbox.get_combined_minimum_size().y
|
||||
assert_float(content).is_less_equal(scroll.size.y)
|
||||
assert_float(scroll.size.y - content).override_failure_message(
|
||||
"%d px empty below the list" % (scroll.size.y - content)).is_less_equal(30.0)
|
||||
@@ -0,0 +1 @@
|
||||
uid://u58i760ykxxp
|
||||
@@ -16,6 +16,7 @@ const STYLES: Dictionary = {
|
||||
"PopupBackground": "Panel",
|
||||
"DarkSpacer": "Panel",
|
||||
"CharacterCard": "Panel",
|
||||
"CharacterCardHover": "Panel",
|
||||
"MusicList": "PanelContainer",
|
||||
"SongRow": "PanelContainer",
|
||||
"ShortcutRowDark": "PanelContainer",
|
||||
|
||||
@@ -74,6 +74,14 @@ corner_detail = 5
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_g21io"]
|
||||
bg_color = Color(0.302, 0.302, 0.302, 1)
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_charhover"]
|
||||
bg_color = Color(0.36, 0.36, 0.36, 1)
|
||||
border_width_left = 2
|
||||
border_width_top = 2
|
||||
border_width_right = 2
|
||||
border_width_bottom = 2
|
||||
border_color = Color(1, 0.5, 0, 1)
|
||||
|
||||
[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_7ceur"]
|
||||
content_margin_left = 6.0
|
||||
content_margin_top = 4.0
|
||||
@@ -776,6 +784,8 @@ Button/styles/normal = SubResource("StyleBoxFlat_ydht1")
|
||||
Button/styles/pressed = SubResource("StyleBoxFlat_8gw6x")
|
||||
CharacterCard/base_type = &"Panel"
|
||||
CharacterCard/styles/panel = SubResource("StyleBoxFlat_g21io")
|
||||
CharacterCardHover/base_type = &"Panel"
|
||||
CharacterCardHover/styles/panel = SubResource("StyleBoxFlat_charhover")
|
||||
CheckButton/colors/button_checked_color = Color(1, 1, 1, 1)
|
||||
CheckButton/colors/button_unchecked_color = Color(1, 1, 1, 1)
|
||||
CheckButton/colors/font_color = Color(0.875, 0.875, 0.875, 1)
|
||||
|
||||
Reference in new issue
Block a user