Split the main window into TopBar and Quiz scenes
The quiz (song, playlist, players, music player and the popups opened from it) moves into Quiz.tscn and the top panel into TopBar.tscn. The main window keeps only the top bar, the quiz and the popups opened from the top bar, so other views can be added next to the quiz later. Every node keeps its place on screen. Screenshots of every UI state before and after are pixel-identical, and a layout test checks each node against a baseline taken before the split.
This commit is contained in:
1 parent
75c59eb223
commit
3c7b4e5c9d
27 files changed
+3432
-980
No files matched your search
@@ -0,0 +1,246 @@
|
||||
class_name UiTour
|
||||
extends RefCounted
|
||||
## Walks the main window through a fixed set of states against a fake MusicServer:
|
||||
## startup, players added, a song playing, every popup open and a sync.
|
||||
##
|
||||
## Used for screenshots ([code]tools/take_screenshots.gd[/code]) and for the layout
|
||||
## test, so both look at exactly the same states. Nodes are looked up by name in the
|
||||
## main window and in its Quiz and TopBar scenes, so the tour works no matter which of
|
||||
## them a node lives in.
|
||||
|
||||
const SOUNDTRACKS: Array[String] = [
|
||||
"Chrono Trigger", "Final Fantasy VI", "Secret of Mana", "Terranigma",
|
||||
"Earthbound", "Super Metroid", "The Legend of Zelda: A Link to the Past",
|
||||
"Donkey Kong Country 2", "Lufia II", "Illusion of Gaia",
|
||||
]
|
||||
const SONG_INFO: Dictionary = {
|
||||
"SongNo": 0, "Soundtrack": "Chrono Trigger", "Song": "Corridors of Time",
|
||||
}
|
||||
const SYNC_PROGRESS: Dictionary = {"progress": 40, "time_spent": "00:00:02"}
|
||||
const SYNC_RESULT: Dictionary = {
|
||||
"soundtracks_added": ["Terranigma", "Lufia II"],
|
||||
"soundtracks_re_added": [],
|
||||
"soundtracks_changed_title": {"FF6": "Final Fantasy VI"},
|
||||
"soundtracks_changed_content": ["Chrono Trigger"],
|
||||
"soundtracks_removed": ["Old Soundtrack"],
|
||||
"catched_errors": [],
|
||||
"total_time": "00:00:03",
|
||||
}
|
||||
|
||||
## Popups in the order the tour opens them.
|
||||
const POPUPS: Array[String] = [
|
||||
"Search", "SyncWindow", "SettingsWindow", "QRWindow", "NewCharacterSelect",
|
||||
"ShortcutsWindow", "AboutWindow", "WinnerWindow", "Log", "StatisticsPopupPanel",
|
||||
]
|
||||
|
||||
var _main: Control
|
||||
|
||||
|
||||
func _init(main: Control) -> void:
|
||||
_main = main
|
||||
|
||||
|
||||
## Returns a fake server that answers every request the main window makes on the tour.
|
||||
## Call [method FakeHttpTransport.install] before the main window is created.
|
||||
static func make_server() -> FakeHttpTransport:
|
||||
var server: FakeHttpTransport = FakeHttpTransport.new()
|
||||
var mp3: PackedByteArray = FileAccess.get_file_as_bytes("res://tests/data/short.mp3")
|
||||
server.respond_json("/music/all", SOUNDTRACKS)
|
||||
server.respond_json("/music/all/order", SOUNDTRACKS)
|
||||
server.respond_json("/music/list", [])
|
||||
server.respond_json("/characters", [])
|
||||
server.respond_bytes("/music/rand", mp3)
|
||||
server.respond_bytes("/music/addQue", PackedByteArray())
|
||||
server.respond_json("/music/info", SONG_INFO)
|
||||
server.respond_bytes("/music/reset", PackedByteArray())
|
||||
server.respond_bytes("/music/played?song=0", PackedByteArray())
|
||||
server.respond_bytes("/sync", PackedByteArray())
|
||||
server.respond_json("/sync/progress", SYNC_PROGRESS)
|
||||
return server
|
||||
|
||||
|
||||
## Puts Settings and Playlist in the state the tour expects. Call it before the main
|
||||
## window is created.
|
||||
static func prepare_autoloads() -> void:
|
||||
Playlist.reset_playlist()
|
||||
Playlist.first_song_played = false
|
||||
Playlist.is_song_downloading = false
|
||||
Settings.player_array.clear()
|
||||
Settings.edit_players = false
|
||||
Settings.currently_syncing = false
|
||||
Settings.character_select_open = false
|
||||
Settings.add_to_stats = true
|
||||
Settings.hide_next_track = true
|
||||
Settings.use_low_played_mode = false
|
||||
Settings.winning_score = 20
|
||||
# The inspiration list scrolls by itself, which would make every picture different
|
||||
Settings.inspiration_list_speed = 0
|
||||
|
||||
|
||||
## Runs the tour. [param on_state] is called (and awaited) with the name of each state
|
||||
## and the popup that is open in it, or [code]null[/code] when no popup is open.
|
||||
func run(on_state: Callable) -> void:
|
||||
# The debug build lowers the winning score on startup; keep it out of reach
|
||||
Settings.winning_score = 20
|
||||
await _settle()
|
||||
await on_state.call("startup", null)
|
||||
|
||||
_call("add_players")
|
||||
await _settle()
|
||||
await on_state.call("add_player_row", null)
|
||||
|
||||
_add_player("Crono")
|
||||
_add_player("Marle")
|
||||
_call("add_players")
|
||||
await _settle()
|
||||
await on_state.call("players_added", null)
|
||||
|
||||
_call("play_next_song")
|
||||
await _settle()
|
||||
_freeze_music_player()
|
||||
_player_row(0).add_point()
|
||||
_player_row(0).add_point()
|
||||
_player_row(1).add_point()
|
||||
_call("show_answer_pressed")
|
||||
await _settle()
|
||||
_freeze_music_player()
|
||||
_fix_log_times()
|
||||
await _settle()
|
||||
await on_state.call("playing", null)
|
||||
|
||||
for popup_name: String in POPUPS:
|
||||
var popup: Node = await _open_popup(popup_name)
|
||||
await _settle()
|
||||
await on_state.call("popup_" + popup_name, popup)
|
||||
_close_popup(popup)
|
||||
await _settle()
|
||||
|
||||
var sync_window: PopupWindow = _node("SyncWindow")
|
||||
Settings.currently_syncing = false
|
||||
sync_window.has_synced = false
|
||||
sync_window.start_sync()
|
||||
sync_window.sync_timer.stop()
|
||||
sync_window._on_sync_timeout()
|
||||
await _settle()
|
||||
await on_state.call("sync_in_progress", sync_window)
|
||||
sync_window.close()
|
||||
await _settle()
|
||||
await on_state.call("sync_buttons_locked", null)
|
||||
|
||||
_server().respond_json("/sync/progress", SYNC_RESULT)
|
||||
sync_window._on_sync_timeout()
|
||||
sync_window.open()
|
||||
await _settle()
|
||||
await on_state.call("sync_result", sync_window)
|
||||
sync_window.close()
|
||||
await _settle()
|
||||
|
||||
|
||||
## Returns where every Control and Window in [param subtree] is on screen, keyed by its
|
||||
## path from [param main]: [code]{"rect": [x, y, width, height], "visible": bool}[/code].
|
||||
## Nodes added at runtime without a name are keyed by their index, e.g. [code]#2[/code],
|
||||
## so the keys are the same on every run.
|
||||
static func dump_layout(main: Node, subtree: Node) -> Dictionary:
|
||||
var layout: Dictionary = {}
|
||||
var path: String = String(main.get_path_to(subtree))
|
||||
_dump_node(subtree, path, layout)
|
||||
return layout
|
||||
|
||||
|
||||
static func _dump_node(node: Node, path: String, layout: Dictionary) -> void:
|
||||
if node is Control:
|
||||
var rect: Rect2 = (node as Control).get_global_rect()
|
||||
layout[path] = {"rect": _round_rect(rect), "visible": node.is_visible_in_tree()}
|
||||
elif node is Window:
|
||||
var window: Window = node
|
||||
layout[path] = {
|
||||
"rect": _round_rect(Rect2(window.position, window.size)),
|
||||
"visible": window.visible,
|
||||
}
|
||||
for child: Node in node.get_children():
|
||||
var key: String = "#%d" % child.get_index() if "@" in child.name \
|
||||
else String(child.name)
|
||||
_dump_node(child, key if path == "." else path + "/" + key, layout)
|
||||
|
||||
|
||||
static func _round_rect(rect: Rect2) -> Array[float]:
|
||||
return [snappedf(rect.position.x, 0.01), snappedf(rect.position.y, 0.01),
|
||||
snappedf(rect.size.x, 0.01), snappedf(rect.size.y, 0.01)]
|
||||
|
||||
|
||||
## Finds a node of the main window by [param node_name], whether it is a direct child
|
||||
## of the main window or of its Quiz or TopBar scene.
|
||||
func _node(node_name: String) -> Node:
|
||||
for parent: Node in [_main, _main.get_node_or_null("Quiz"),
|
||||
_main.get_node_or_null("TopBar")]:
|
||||
if parent != null && parent.has_node(node_name):
|
||||
return parent.get_node(node_name)
|
||||
push_error("UiTour: no node named " + node_name)
|
||||
return null
|
||||
|
||||
|
||||
## Calls [param method] on the main window, or on its Quiz scene if that has it.
|
||||
func _call(method: String, args: Array = []) -> Variant:
|
||||
var quiz: Node = _main.get_node_or_null("Quiz")
|
||||
var target: Node = quiz if quiz != null && quiz.has_method(method) else _main
|
||||
return target.callv(method, args)
|
||||
|
||||
|
||||
func _server() -> FakeHttpTransport:
|
||||
return MusicServerClient.transport
|
||||
|
||||
|
||||
func _settle() -> void:
|
||||
for i: int in 4:
|
||||
await _main.get_tree().process_frame
|
||||
|
||||
|
||||
func _add_player(player_name: String) -> void:
|
||||
(_node("Players").get_node("VBoxContainer/AddPlayerContainer/PlayerNameField") \
|
||||
as TextEdit).text = player_name
|
||||
_call("add_player")
|
||||
|
||||
|
||||
func _player_row(index: int) -> Player:
|
||||
return _node("Players").get_node(
|
||||
"VBoxContainer/ScrollContainer/PlayersListVBoxContainer").get_child(index)
|
||||
|
||||
|
||||
## Stops the song so the time and progress bar look the same on every run.
|
||||
func _freeze_music_player() -> void:
|
||||
var music_player: MusicPlayer = _node("MusicPlayer")
|
||||
music_player.audio_player.stop()
|
||||
music_player.sound_player.stop()
|
||||
music_player.progress_slider.value = 0
|
||||
music_player.music_time_label.text = "00:00 / 00:01"
|
||||
|
||||
|
||||
## The log starts each row with the time of day; use a fixed time instead.
|
||||
func _fix_log_times() -> void:
|
||||
var log_window: PopupWindow = _node("Log")
|
||||
for row: Label in log_window.log_box.get_children():
|
||||
row.text = "12:00:00 " + row.text.substr(9)
|
||||
|
||||
|
||||
func _open_popup(popup_name: String) -> Node:
|
||||
var popup: Node = _node(popup_name)
|
||||
match popup_name:
|
||||
"SyncWindow":
|
||||
# Opening the window starts a sync; show the empty window instead
|
||||
popup.open()
|
||||
"NewCharacterSelect":
|
||||
popup.show_grid("Crono")
|
||||
"WinnerWindow":
|
||||
popup.show_winner(Settings.player_array[0])
|
||||
"StatisticsPopupPanel":
|
||||
_call("get_statistics")
|
||||
_:
|
||||
popup.open()
|
||||
return popup
|
||||
|
||||
|
||||
func _close_popup(popup: Node) -> void:
|
||||
if popup is PopupWindow:
|
||||
popup.close()
|
||||
else:
|
||||
popup.visible = false
|
||||
Reference in new issue
Block a user