Compare commits
6
Commits
222d231b1b
...
4a934840f6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4a934840f6 | ||
|
|
78e1facaf0 | ||
|
|
84e137e16a | ||
|
|
a4e6da4c28 | ||
|
|
cd4331881c | ||
|
|
06f457b027 |
No files matched your search
@@ -0,0 +1,69 @@
|
||||
class_name ButtonBounce
|
||||
extends RefCounted
|
||||
## Small animations that make buttons feel alive: a button grows a little under the mouse,
|
||||
## shrinks while it is held down, and can pop when its icon changes.
|
||||
##
|
||||
## The button is scaled around its middle, so its place in a container does not change.
|
||||
|
||||
## How big a button is under the mouse.
|
||||
const HOVER_SCALE: Vector2 = Vector2(1.08, 1.08)
|
||||
## How big a button is while it is held down.
|
||||
const PRESSED_SCALE: Vector2 = Vector2(0.92, 0.92)
|
||||
## How small a button starts when it pops.
|
||||
const POP_SCALE: Vector2 = Vector2(0.8, 0.8)
|
||||
## How long growing or shrinking takes, in seconds.
|
||||
const DURATION: float = 0.08
|
||||
## How long a pop takes, in seconds.
|
||||
const POP_DURATION: float = 0.18
|
||||
|
||||
const _TWEEN_META: StringName = &"button_bounce_tween"
|
||||
const _PIVOT_META: StringName = &"button_bounce_pivot"
|
||||
|
||||
|
||||
## Makes [param button] grow under the mouse and shrink while held down.
|
||||
static func add_to(button: BaseButton) -> void:
|
||||
_keep_pivot_centred(button)
|
||||
button.mouse_entered.connect(func() -> void:
|
||||
if !button.disabled:
|
||||
_scale_to(button, HOVER_SCALE))
|
||||
button.mouse_exited.connect(func() -> void: _scale_to(button, Vector2.ONE))
|
||||
button.button_down.connect(func() -> void: _scale_to(button, PRESSED_SCALE))
|
||||
button.button_up.connect(func() -> void: _scale_to(button, resting_scale(button)))
|
||||
|
||||
|
||||
## The size [param button] goes back to when it is not held down: bigger under the mouse.
|
||||
static func resting_scale(button: BaseButton) -> Vector2:
|
||||
return HOVER_SCALE if button.is_hovered() && !button.disabled else Vector2.ONE
|
||||
|
||||
|
||||
## Shrinks [param button] and lets it spring back, e.g. when its icon changes.
|
||||
static func pop(button: BaseButton) -> void:
|
||||
_keep_pivot_centred(button)
|
||||
button.scale = POP_SCALE
|
||||
var tween: Tween = _new_tween(button)
|
||||
tween.set_trans(Tween.TRANS_BACK).set_ease(Tween.EASE_OUT)
|
||||
tween.tween_property(button, ^"scale", resting_scale(button), POP_DURATION)
|
||||
|
||||
|
||||
static func _scale_to(button: BaseButton, target: Vector2) -> void:
|
||||
var tween: Tween = _new_tween(button)
|
||||
tween.set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_OUT)
|
||||
tween.tween_property(button, ^"scale", target, DURATION)
|
||||
|
||||
|
||||
# A new tween for [param button], stopping the one before so they never fight
|
||||
static func _new_tween(button: BaseButton) -> Tween:
|
||||
if button.has_meta(_TWEEN_META):
|
||||
var running: Tween = button.get_meta(_TWEEN_META)
|
||||
if running.is_valid():
|
||||
running.kill()
|
||||
var tween: Tween = button.create_tween()
|
||||
button.set_meta(_TWEEN_META, tween)
|
||||
return tween
|
||||
|
||||
|
||||
static func _keep_pivot_centred(button: BaseButton) -> void:
|
||||
button.pivot_offset = button.size / 2
|
||||
if !button.has_meta(_PIVOT_META):
|
||||
button.set_meta(_PIVOT_META, true)
|
||||
button.resized.connect(func() -> void: button.pivot_offset = button.size / 2)
|
||||
@@ -0,0 +1 @@
|
||||
uid://c4vekt3m8q1p7
|
||||
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="416" height="416" viewBox="48 48 416 416">
|
||||
<!-- The pause shape of pause_icon_light.svg in one colour -->
|
||||
<path fill="#f9f9f9" d="M224,435.8V76.1c0-6.7-5.4-12.1-12.2-12.1h-71.6c-6.8,0-12.2,5.4-12.2,12.1v359.7c0,6.7,5.4,12.2,12.2,12.2h71.6 C218.6,448,224,442.6,224,435.8z"/>
|
||||
<path fill="#f9f9f9" d="M371.8,64h-71.6c-6.7,0-12.2,5.4-12.2,12.1v359.7c0,6.7,5.4,12.2,12.2,12.2h71.6c6.7,0,12.2-5.4,12.2-12.2V76.1 C384,69.4,378.6,64,371.8,64z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 502 B |
@@ -0,0 +1,43 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://djdsgbhw8gpj"
|
||||
path="res://.godot/imported/pause_button_icon.svg-cf86876de39506a769c5610b329fdb75.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://icons/pause_button_icon.svg"
|
||||
dest_files=["res://.godot/imported/pause_button_icon.svg-cf86876de39506a769c5610b329fdb75.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
svg/scale=1.0
|
||||
editor/scale_with_editor_scale=false
|
||||
editor/convert_colors_with_editor_theme=false
|
||||
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="416" height="416" viewBox="64 48 416 416">
|
||||
<!-- The play shape of play_icon_light.svg in one colour, shifted right so it looks centred -->
|
||||
<path fill="#f9f9f9" transform="translate(16 0)" d="M405.2,232.9L126.8,67.2c-3.4-2-6.9-3.2-10.9-3.2c-10.9,0-19.8,9-19.8,20H96v344h0.1c0,11,8.9,20,19.8,20 c4.1,0,7.5-1.4,11.2-3.4l278.1-165.5c6.6-5.5,10.8-13.8,10.8-23.1C416,246.7,411.8,238.5,405.2,232.9z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 454 B |
@@ -0,0 +1,43 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://dkweun1f426gl"
|
||||
path="res://.godot/imported/play_button_icon.svg-76b19c7150b176f215ad401b4dd9488d.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://icons/play_button_icon.svg"
|
||||
dest_files=["res://.godot/imported/play_button_icon.svg-76b19c7150b176f215ad401b4dd9488d.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
svg/scale=1.0
|
||||
editor/scale_with_editor_scale=false
|
||||
editor/convert_colors_with_editor_theme=false
|
||||
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 28 28">
|
||||
<!-- reload_light_icon.svg mirrored, so the arrow turns back like a restart -->
|
||||
<path fill="#f9f9f9" transform="translate(28 0) scale(-1 1)" d="M 12.273759,27.821814 C 8.301606,27.305128 4.6414458,25.025364 2.4172439,21.682595 -0.6740156,17.036719 -0.6987372,11.050866 2.3541959,6.4167276 6.4831292,0.1492961 14.7875,-1.7884148 21.095349,2.0437368 l 0.883303,0.5366251 0.09577,-0.6963111 c 0.07593,-0.5520493 0.198918,-0.7994615 0.593645,-1.1941885 0.682393,-0.6823936 1.29683,-0.7876518 2.135162,-0.3657721 1.11162,0.5594083 1.142977,0.6968977 1.099029,4.8188242 C 25.866598,8.48735 25.848168,8.714727 25.583585,9.075118 24.993194,9.879284 24.711316,9.931761 20.980939,9.931993 17.03905,9.932238 16.871859,9.891713 16.324094,8.803226 15.94065,8.041272 16.00249,7.4396205 16.528498,6.8144942 17.041282,6.2050862 17.302265,6.1106614 18.732442,6.0170974 l 1.29765,-0.084894 -0.711421,-0.4557502 C 16.407302,3.611373 12.42501,3.4754273 9.250891,5.1327637 6.9185825,6.3505563 5.186242,8.47902 4.3514391,11.152542 c -0.4596099,1.471937 -0.4596099,4.222979 0,5.694916 1.0532717,3.373185 3.4279179,5.747831 6.8011029,6.801103 0.874696,0.273122 1.291973,0.316826 2.983051,0.31243 1.791555,-0.0047 2.062404,-0.03855 3.050848,-0.381822 2.095349,-0.727675 4.004905,-2.225187 5.276499,-4.137935 0.712133,-1.071201 1.028577,-1.77101 1.500599,-3.318525 0.234713,-0.769505 0.466198,-1.242244 0.733848,-1.49867 0.768275,-0.736054 1.953395,-0.694987 2.67526,0.0927 0.56162,0.612834 0.678351,1.318164 0.398766,2.409492 -0.7664,2.991551 -3.210883,6.444937 -5.916304,8.35812 -2.688606,1.901292 -6.251776,2.77056 -9.58135,2.337459 z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
@@ -0,0 +1,43 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://4t7vk634k0v4"
|
||||
path="res://.godot/imported/restart_light_icon.svg-3822a30546bff23ba1adb74c1723ba80.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://icons/restart_light_icon.svg"
|
||||
dest_files=["res://.godot/imported/restart_light_icon.svg-3822a30546bff23ba1adb74c1723ba80.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
svg/scale=1.0
|
||||
editor/scale_with_editor_scale=false
|
||||
editor/convert_colors_with_editor_theme=false
|
||||
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
||||
<path fill="#f9f9f9" d="M3 9.5v5c0 .55.45 1 1 1h3.2l4.1 3.6c.65.57 1.7.11 1.7-.76V5.66c0-.87-1.05-1.33-1.7-.76L7.2 8.5H4c-.55 0-1 .45-1 1z"/>
|
||||
<path fill="none" stroke="#f9f9f9" stroke-width="2" stroke-linecap="round" d="M16 8.5a5 5 0 0 1 0 7M18.8 5.8a9 9 0 0 1 0 12.4"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 366 B |
@@ -0,0 +1,43 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://d0uyxivsv58uh"
|
||||
path="res://.godot/imported/volume_light_icon.svg-a1b08f489924a01123c9c4f7fa5e16a0.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://icons/volume_light_icon.svg"
|
||||
dest_files=["res://.godot/imported/volume_light_icon.svg-a1b08f489924a01123c9c4f7fa5e16a0.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
svg/scale=1.0
|
||||
editor/scale_with_editor_scale=false
|
||||
editor/convert_colors_with_editor_theme=false
|
||||
+52
-22
@@ -1,9 +1,10 @@
|
||||
[gd_scene load_steps=10 format=3 uid="uid://ds15cgsf8vpvc"]
|
||||
[gd_scene format=3 uid="uid://ds15cgsf8vpvc"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://d1v0rquma6nt8" path="res://player/music_player.gd" id="1_t24ra"]
|
||||
[ext_resource type="AudioStream" uid="uid://n2g8jddr85h2" path="res://sounds/01. Opening.mp3" id="2_xti80"]
|
||||
[ext_resource type="Texture2D" uid="uid://comxqfiykp54f" path="res://icons/play_icon_light.svg" id="3_6g308"]
|
||||
[ext_resource type="Texture2D" uid="uid://ccb6rvbldlgdg" path="res://icons/reload_light_icon.svg" id="4_jleuo"]
|
||||
[ext_resource type="Texture2D" path="res://icons/play_button_icon.svg" id="3_6g308"]
|
||||
[ext_resource type="Texture2D" path="res://icons/restart_light_icon.svg" id="4_jleuo"]
|
||||
[ext_resource type="Texture2D" path="res://icons/volume_light_icon.svg" id="6_volume"]
|
||||
[ext_resource type="PackedScene" uid="uid://w400rnew7453" path="res://player/VolumeSlider.tscn" id="5_iifuj"]
|
||||
|
||||
[sub_resource type="InputEventKey" id="InputEventKey_r4qo3"]
|
||||
@@ -29,7 +30,7 @@ events = [SubResource("InputEventKey_2nvce")]
|
||||
[node name="MusicPlayer" type="Control"]
|
||||
layout_mode = 3
|
||||
offset_right = 1792.0
|
||||
offset_bottom = 71.0
|
||||
offset_bottom = 42.0
|
||||
mouse_filter = 2
|
||||
script = ExtResource("1_t24ra")
|
||||
|
||||
@@ -46,52 +47,81 @@ stream = ExtResource("2_xti80")
|
||||
bus = &"music"
|
||||
|
||||
[node name="PlayerBar" type="PanelContainer" parent="."]
|
||||
layout_mode = 0
|
||||
offset_left = 1.0
|
||||
offset_right = 1792.0
|
||||
offset_bottom = 32.0
|
||||
layout_mode = 1
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
mouse_filter = 2
|
||||
theme_type_variation = &"PlayerBar"
|
||||
|
||||
[node name="MusicPlayerContainer" type="HBoxContainer" parent="PlayerBar"]
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 4
|
||||
mouse_filter = 2
|
||||
theme_override_constants/separation = 10
|
||||
|
||||
[node name="PlayTextureButton" type="TextureButton" parent="PlayerBar/MusicPlayerContainer"]
|
||||
[node name="PlayButton" type="Button" parent="PlayerBar/MusicPlayerContainer"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(32, 32)
|
||||
custom_minimum_size = Vector2(34, 34)
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 4
|
||||
tooltip_text = "Play or pause (Alt+X)"
|
||||
focus_mode = 0
|
||||
theme_type_variation = &"PlayerButtonPrimary"
|
||||
action_mode = 0
|
||||
shortcut = SubResource("Shortcut_ipcfh")
|
||||
texture_normal = ExtResource("3_6g308")
|
||||
ignore_texture_size = true
|
||||
stretch_mode = 0
|
||||
shortcut_feedback = false
|
||||
shortcut_in_tooltip = false
|
||||
icon = ExtResource("3_6g308")
|
||||
icon_alignment = 1
|
||||
expand_icon = true
|
||||
|
||||
[node name="RestartTextureButton" type="TextureButton" parent="PlayerBar/MusicPlayerContainer"]
|
||||
[node name="RestartButton" type="Button" parent="PlayerBar/MusicPlayerContainer"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(32, 32)
|
||||
custom_minimum_size = Vector2(34, 34)
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 4
|
||||
tooltip_text = "Restart the song (Alt+Z)"
|
||||
focus_mode = 0
|
||||
theme_type_variation = &"PlayerButton"
|
||||
action_mode = 0
|
||||
shortcut = SubResource("Shortcut_i7swq")
|
||||
texture_normal = ExtResource("4_jleuo")
|
||||
ignore_texture_size = true
|
||||
stretch_mode = 0
|
||||
flip_h = true
|
||||
shortcut_feedback = false
|
||||
shortcut_in_tooltip = false
|
||||
icon = ExtResource("4_jleuo")
|
||||
icon_alignment = 1
|
||||
expand_icon = true
|
||||
|
||||
[node name="MusicPlayerSlider" type="HSlider" parent="PlayerBar/MusicPlayerContainer"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
size_flags_vertical = 4
|
||||
theme_type_variation = &"ProgressSlider"
|
||||
scrollable = false
|
||||
|
||||
[node name="MusicTimeLabel" type="Label" parent="PlayerBar/MusicPlayerContainer"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(120, 0)
|
||||
layout_mode = 2
|
||||
text = "0:00 / 0:00"
|
||||
theme_type_variation = &"PlayerTime"
|
||||
text = "00:00 / 00:00"
|
||||
horizontal_alignment = 1
|
||||
|
||||
[node name="VolumeIcon" type="TextureRect" parent="PlayerBar/MusicPlayerContainer"]
|
||||
custom_minimum_size = Vector2(22, 22)
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 4
|
||||
mouse_filter = 2
|
||||
texture = ExtResource("6_volume")
|
||||
expand_mode = 1
|
||||
stretch_mode = 5
|
||||
|
||||
[node name="VolumeSlider" parent="PlayerBar/MusicPlayerContainer" instance=ExtResource("5_iifuj")]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(100, 0)
|
||||
custom_minimum_size = Vector2(110, 0)
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 4
|
||||
tooltip_text = "Volume"
|
||||
theme_type_variation = &"VolumeBar"
|
||||
+29
-11
@@ -2,19 +2,22 @@ class_name MusicPlayer
|
||||
extends Control
|
||||
## The bar that plays the songs, with play, restart, progress and volume. Whether a song
|
||||
## repeats is a quiz setting ([code]Settings.auto_repeat_song[/code]).
|
||||
##
|
||||
## The buttons grow a little under the mouse and the play button pops when it switches
|
||||
## between play and pause, see [ButtonBounce].
|
||||
|
||||
signal update_song_list
|
||||
signal play_next_song
|
||||
|
||||
var play_icon: Texture = preload("res://icons/play_icon_light.svg")
|
||||
var pause_icon: Texture = preload("res://icons/pause_icon_light.svg")
|
||||
var play_icon: Texture = preload("res://icons/play_button_icon.svg")
|
||||
var pause_icon: Texture = preload("res://icons/pause_button_icon.svg")
|
||||
var is_changing: bool = false
|
||||
var playback_position: float
|
||||
var stream: AudioStream
|
||||
var song_finished: bool = false
|
||||
|
||||
@onready var play_button: TextureButton = %PlayTextureButton
|
||||
@onready var restart_button: TextureButton = %RestartTextureButton
|
||||
@onready var play_button: Button = %PlayButton
|
||||
@onready var restart_button: Button = %RestartButton
|
||||
@onready var audio_player: AudioStreamPlayer = $AudioStreamPlayer
|
||||
@onready var sound_player: AudioStreamPlayer = $AudioSoundPlayer
|
||||
@onready var sound_effect_player: AudioStreamPlayer = $AudioSoundEffectPlayer
|
||||
@@ -26,6 +29,8 @@ var song_finished: bool = false
|
||||
func _ready() -> void:
|
||||
play_button.pressed.connect(play_or_pause)
|
||||
restart_button.pressed.connect(restart)
|
||||
ButtonBounce.add_to(play_button)
|
||||
ButtonBounce.add_to(restart_button)
|
||||
progress_slider.drag_started.connect(_on_drag_started)
|
||||
progress_slider.drag_ended.connect(_on_drag_ended)
|
||||
|
||||
@@ -43,6 +48,19 @@ func _process(_delta: float) -> void:
|
||||
music_time_label.text = format_text(progress_slider.value, 0.0)
|
||||
|
||||
|
||||
## Whether the play button shows pause, because a song is playing.
|
||||
func shows_playing() -> bool:
|
||||
return play_button.icon == pause_icon
|
||||
|
||||
|
||||
# Shows pause on the play button while a song plays, and play otherwise
|
||||
func _show_playing(playing: bool) -> void:
|
||||
var icon: Texture = pause_icon if playing else play_icon
|
||||
if play_button.icon != icon:
|
||||
play_button.icon = icon
|
||||
ButtonBounce.pop(play_button)
|
||||
|
||||
|
||||
func format_time(time: float) -> String:
|
||||
var total_seconds: int = roundi(time)
|
||||
@warning_ignore("integer_division")
|
||||
@@ -64,7 +82,7 @@ func play_or_pause() -> void:
|
||||
if song_finished:
|
||||
restart()
|
||||
elif audio_player.stream_paused:
|
||||
play_button.texture_normal = pause_icon
|
||||
_show_playing(true)
|
||||
audio_player.stream_paused = false
|
||||
song_finished = false
|
||||
audio_player.seek(playback_position)
|
||||
@@ -81,7 +99,7 @@ func play_or_pause() -> void:
|
||||
func pause() -> void:
|
||||
audio_player.stream_paused = true
|
||||
playback_position = audio_player.get_playback_position()
|
||||
play_button.texture_normal = play_icon
|
||||
_show_playing(false)
|
||||
|
||||
|
||||
func restart() -> void:
|
||||
@@ -91,12 +109,12 @@ func restart() -> void:
|
||||
progress_slider.value = 0
|
||||
playback_position = audio_player.get_playback_position()
|
||||
audio_player.seek(playback_position)
|
||||
play_button.texture_normal = pause_icon
|
||||
_show_playing(true)
|
||||
audio_player.play()
|
||||
|
||||
|
||||
func _on_finished() -> void:
|
||||
play_button.texture_normal = play_icon
|
||||
_show_playing(false)
|
||||
song_finished = true
|
||||
if !Settings.stop_after_current:
|
||||
play_next_song.emit()
|
||||
@@ -150,7 +168,7 @@ func play_sound(sound_name: AudioStream) -> void:
|
||||
audio_player.stop()
|
||||
sound_player.play()
|
||||
song_finished = true
|
||||
play_button.texture_normal = play_icon
|
||||
_show_playing(false)
|
||||
progress_slider.value = 0
|
||||
|
||||
|
||||
@@ -159,7 +177,7 @@ func play_song(song: AudioStream) -> void:
|
||||
audio_player.play()
|
||||
sound_player.stop()
|
||||
song_finished = false
|
||||
play_button.texture_normal = pause_icon
|
||||
_show_playing(true)
|
||||
stream = audio_player.stream
|
||||
progress_slider.value = 0
|
||||
progress_slider.max_value = round(stream.get_length())
|
||||
@@ -179,7 +197,7 @@ func play_song_object(song_object_no: int) -> void:
|
||||
sound_player.stop()
|
||||
audio_player.play()
|
||||
song_finished = false
|
||||
play_button.texture_normal = pause_icon
|
||||
_show_playing(true)
|
||||
stream = audio_player.stream
|
||||
progress_slider.value = 0
|
||||
progress_slider.max_value = round(stream.get_length())
|
||||
|
||||
@@ -158,6 +158,21 @@ focus_mode = 0
|
||||
action_mode = 0
|
||||
text = "Count plays in statistics"
|
||||
|
||||
[node name="ConfigFolderRow" type="HBoxContainer" parent="SettingsPanel/VBoxContainer"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="Title" type="Label" parent="SettingsPanel/VBoxContainer/ConfigFolderRow"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
theme_type_variation = &"SettingsRowLabel"
|
||||
text = "Settings and cached files"
|
||||
|
||||
[node name="OpenConfigFolderButton" type="Button" parent="SettingsPanel/VBoxContainer/ConfigFolderRow"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
focus_mode = 0
|
||||
text = "Open folder"
|
||||
|
||||
[node name="FolderDialog" type="FileDialog" parent="."]
|
||||
unique_name_in_owner = true
|
||||
title = "Choose a music folder"
|
||||
|
||||
@@ -2,7 +2,8 @@ class_name SettingsPopup
|
||||
extends BasePopup
|
||||
## The application settings: which MusicServer or music folder to play from, fullscreen,
|
||||
## how many songs to download ahead, whether to prefer less played soundtracks and whether
|
||||
## plays count in the statistics. Opened from the Settings button in the top bar.
|
||||
## plays count in the statistics, and a button that opens the folder the settings are
|
||||
## kept in. Opened from the Settings button in the top bar.
|
||||
##
|
||||
## A music folder (see [MusicFolder]) is used instead of the server until Use server is
|
||||
## pressed or the game is closed. The settings that need the server are off meanwhile.
|
||||
@@ -32,6 +33,11 @@ const PADDING: Vector2 = Vector2(20, 12)
|
||||
@onready var play_order_row: HBoxContainer = %PlayOrderRow
|
||||
@onready var play_order_button: OptionButton = %PlayOrderButton
|
||||
@onready var folder_dialog: FileDialog = %FolderDialog
|
||||
@onready var open_config_folder_button: Button = %OpenConfigFolderButton
|
||||
|
||||
## Shows a folder in the system's file manager. The tests replace it, so no file manager
|
||||
## opens while they run.
|
||||
var show_folder: Callable = _show_in_file_manager
|
||||
|
||||
# Where the panel's middle is, so it stays centred when rows are shown or hidden
|
||||
var _panel_middle_y: float = 0.0
|
||||
@@ -48,6 +54,8 @@ func _ready() -> void:
|
||||
choose_folder_button.pressed.connect(folder_dialog.popup_centered)
|
||||
folder_dialog.dir_selected.connect(use_music_folder)
|
||||
use_server_button.pressed.connect(use_server)
|
||||
open_config_folder_button.pressed.connect(open_config_folder)
|
||||
open_config_folder_button.tooltip_text = Settings.config_dir()
|
||||
play_order_button.item_selected.connect(_on_play_order_selected)
|
||||
lower_cache_button.pressed.connect(lower_cache)
|
||||
increase_cache_button.pressed.connect(increase_cache)
|
||||
@@ -88,6 +96,14 @@ func use_server() -> void:
|
||||
server_changed.emit()
|
||||
|
||||
|
||||
## Shows the folder with the config file, the saved settings and the cached characters
|
||||
## and songs in the system's file manager.
|
||||
func open_config_folder() -> void:
|
||||
var dir: String = Settings.config_dir()
|
||||
DirAccess.make_dir_recursive_absolute(dir)
|
||||
show_folder.call(dir)
|
||||
|
||||
|
||||
func lower_cache() -> void:
|
||||
Playlist.number_of_tracks_to_preload = maxi(Playlist.number_of_tracks_to_preload - 1, 1)
|
||||
cache_label.text = str(Playlist.number_of_tracks_to_preload)
|
||||
@@ -160,5 +176,14 @@ func _fit_to_rows() -> void:
|
||||
rows.size = Vector2(settings_panel.size.x - PADDING.x * 2, height - PADDING.y * 2)
|
||||
|
||||
|
||||
func _show_in_file_manager(dir: String) -> void:
|
||||
var error: Error = OS.shell_show_in_file_manager(dir)
|
||||
if error != OK:
|
||||
# Not every platform has a file manager to show a folder in; open it instead
|
||||
error = OS.shell_open(dir)
|
||||
if error != OK:
|
||||
GameLog.warning("Could not open %s: %s" % [dir, error_string(error)])
|
||||
|
||||
|
||||
func _update_fullscreen_button() -> void:
|
||||
fullscreen_button.set_pressed_no_signal(Settings.fullscreen)
|
||||
@@ -13,6 +13,7 @@ script = ExtResource("part_script")
|
||||
[node name="ScrollContainer" type="ScrollContainer" parent="." unique_id=707750459]
|
||||
layout_mode = 2
|
||||
follow_focus = true
|
||||
horizontal_scroll_mode = 0
|
||||
script = ExtResource("7_dj026")
|
||||
|
||||
[node name="MusicList" type="VBoxContainer" parent="ScrollContainer" unique_id=373398568]
|
||||
@@ -20,4 +21,4 @@ unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
size_flags_vertical = 3
|
||||
theme_override_constants/separation = 10
|
||||
theme_override_constants/separation = 4
|
||||
+2
-2
@@ -48,9 +48,9 @@ offset_bottom = 384.0
|
||||
[node name="MusicPlayer" parent="." unique_id=1990459832 instance=ExtResource("5_emn36")]
|
||||
layout_mode = 0
|
||||
offset_left = 64.0
|
||||
offset_top = 992.0
|
||||
offset_top = 984.0
|
||||
offset_right = 1856.0
|
||||
offset_bottom = 1063.0
|
||||
offset_bottom = 1026.0
|
||||
|
||||
[node name="PlayersPanel" parent="." unique_id=1708375945 instance=ExtResource("scene_PlayersPanel")]
|
||||
layout_mode = 0
|
||||
|
||||
+9
-43
@@ -1,63 +1,29 @@
|
||||
[gd_scene format=3 uid="uid://1jbtmhbqolqe"]
|
||||
|
||||
[ext_resource type="Texture2D" uid="uid://c8r7ykphss4c2" path="res://characters/Link.png" id="1_ll28s"]
|
||||
[ext_resource type="Script" uid="uid://cjo13wixip8mq" path="res://quiz/song_row.gd" id="1_lspki"]
|
||||
|
||||
[node name="SongRow" type="PanelContainer"]
|
||||
custom_minimum_size = Vector2(1000, 0)
|
||||
offset_right = 1088.0
|
||||
offset_bottom = 59.0
|
||||
theme_type_variation = &"SongRow"
|
||||
script = ExtResource("1_lspki")
|
||||
|
||||
[node name="HBoxContainer" type="HBoxContainer" parent="."]
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 0
|
||||
mouse_filter = 2
|
||||
theme_override_constants/separation = 12
|
||||
|
||||
[node name="SongListLabel" type="Label" parent="HBoxContainer"]
|
||||
unique_name_in_owner = true
|
||||
texture_filter = 2
|
||||
custom_minimum_size = Vector2(865, 28)
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
mouse_filter = 1
|
||||
theme_type_variation = &"SongRowLabel"
|
||||
text = "Final Fantasy Dissidia 012 Duodecim (Sista Fantasin Meningsskiljaktigheter 012 Tolv) - 'Crystal Cave -arrange-'' from FINAL FANTASY III"
|
||||
autowrap_mode = 2
|
||||
vertical_alignment = 1
|
||||
autowrap_mode = 3
|
||||
|
||||
[node name="Panel" type="Panel" parent="HBoxContainer"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
size_flags_vertical = 0
|
||||
|
||||
[node name="SongListPlayers" type="HBoxContainer" parent="HBoxContainer"]
|
||||
[node name="SongListPlayers" type="VBoxContainer" parent="HBoxContainer"]
|
||||
unique_name_in_owner = true
|
||||
visible = false
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 0
|
||||
|
||||
[node name="TextureRect" type="TextureRect" parent="HBoxContainer/SongListPlayers"]
|
||||
custom_minimum_size = Vector2(50, 25)
|
||||
layout_mode = 2
|
||||
texture = ExtResource("1_ll28s")
|
||||
expand_mode = 1
|
||||
stretch_mode = 5
|
||||
|
||||
[node name="TextureRect2" type="TextureRect" parent="HBoxContainer/SongListPlayers"]
|
||||
custom_minimum_size = Vector2(50, 25)
|
||||
layout_mode = 2
|
||||
texture = ExtResource("1_ll28s")
|
||||
expand_mode = 1
|
||||
stretch_mode = 6
|
||||
|
||||
[node name="TextureRect3" type="TextureRect" parent="HBoxContainer/SongListPlayers"]
|
||||
custom_minimum_size = Vector2(50, 25)
|
||||
layout_mode = 2
|
||||
texture = ExtResource("1_ll28s")
|
||||
expand_mode = 1
|
||||
|
||||
[node name="TextureRect4" type="TextureRect" parent="HBoxContainer/SongListPlayers"]
|
||||
custom_minimum_size = Vector2(50, 25)
|
||||
layout_mode = 2
|
||||
texture = ExtResource("1_ll28s")
|
||||
expand_mode = 1
|
||||
stretch_mode = 6
|
||||
size_flags_vertical = 4
|
||||
mouse_filter = 2
|
||||
theme_type_variation = &"SongRowPictureLines"
|
||||
@@ -14,9 +14,12 @@ var _song_list_object: PackedScene = preload("res://quiz/SongRow.tscn")
|
||||
## Shows the songs in the playlist again, so it shows their latest state.
|
||||
func refresh() -> void:
|
||||
Settings.delete_children(_rows)
|
||||
for s: SongObject in Playlist.get_playlist():
|
||||
var songs: Array[SongObject] = Playlist.get_playlist()
|
||||
for i: int in songs.size():
|
||||
var s: SongObject = songs[i]
|
||||
var new_song_list_object: SongRow = _song_list_object.instantiate()
|
||||
new_song_list_object.song_object = s
|
||||
new_song_list_object.index = i
|
||||
new_song_list_object.gui_input.connect(_on_row_input.bind(s.song_number))
|
||||
_rows.add_child(new_song_list_object)
|
||||
|
||||
|
||||
+80
-12
@@ -1,37 +1,105 @@
|
||||
class_name SongRow
|
||||
extends PanelContainer
|
||||
## One song in the music list: its number, soundtrack and title, and a marker for
|
||||
## One song in the music list: its number, soundtrack and title, and the character of
|
||||
## each player who got a point for it.
|
||||
##
|
||||
## The pictures are on the right and the text takes the room they leave, wrapping only
|
||||
## when it does not fit on one line. With many pictures they go on more lines, as even as
|
||||
## possible and at most [constant MAX_PICTURES_PER_LINE] on each, so the text keeps most
|
||||
## of the row. Every line ends at the row's right edge. The row is as tall as the taller
|
||||
## of the text and the pictures.
|
||||
|
||||
## The most pictures on one line before they go on another.
|
||||
const MAX_PICTURES_PER_LINE: int = 4
|
||||
## The size of each picture. Characters are twice as wide as they are tall.
|
||||
const PICTURE_SIZE: Vector2 = Vector2(56, 28)
|
||||
|
||||
var song_object: SongObject
|
||||
## The row's place in the list, for the alternating background.
|
||||
var index: int = 0
|
||||
|
||||
var _hovered: bool = false
|
||||
|
||||
@onready var song_list_label: Label = %SongListLabel
|
||||
@onready var song_list_players: HBoxContainer = %SongListPlayers
|
||||
## The lines of pictures, top to bottom, each an HBoxContainer.
|
||||
@onready var song_list_players: VBoxContainer = %SongListPlayers
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
mouse_entered.connect(_set_hovered.bind(true))
|
||||
mouse_exited.connect(_set_hovered.bind(false))
|
||||
set_label()
|
||||
update_players()
|
||||
|
||||
|
||||
## The pictures shown, line by line.
|
||||
func pictures() -> Array[TextureRect]:
|
||||
var shown: Array[TextureRect] = []
|
||||
for line: Node in song_list_players.get_children():
|
||||
shown.append_array(line.get_children())
|
||||
return shown
|
||||
|
||||
|
||||
## The number of pictures on a full line for [param pictures] pictures: as few lines as
|
||||
## [constant MAX_PICTURES_PER_LINE] allows, with the pictures spread evenly over them,
|
||||
## e.g. 3 and 2 for five pictures instead of 4 and 1.
|
||||
static func picture_columns(pictures: int) -> int:
|
||||
if pictures <= 0:
|
||||
return 1
|
||||
var lines: int = ceili(float(pictures) / MAX_PICTURES_PER_LINE)
|
||||
return ceili(float(pictures) / lines)
|
||||
|
||||
|
||||
func set_label() -> void:
|
||||
if song_object != null:
|
||||
var size_to_change: int = song_object.players_given_point.size() * 50
|
||||
song_list_label.custom_minimum_size = Vector2(1000 - size_to_change, 28)
|
||||
if song_object.is_playing:
|
||||
song_list_label.theme_type_variation = &"SongRowLabelPlaying"
|
||||
song_list_label.theme_type_variation = &"SongRowLabelPlaying" \
|
||||
if song_object.is_playing else &"SongRowLabel"
|
||||
song_list_label.text = song_object.get_song_info()
|
||||
_update_style()
|
||||
|
||||
|
||||
func update_players() -> void:
|
||||
Settings.delete_children(song_list_players)
|
||||
var scorers: Array[PlayerObject] = []
|
||||
if song_object != null:
|
||||
# Show players in the same order as the player list
|
||||
for player: PlayerObject in Settings.player_array:
|
||||
if song_object.players_given_point.has(player.id):
|
||||
var texture: TextureRect = TextureRect.new()
|
||||
texture.texture = player.character
|
||||
texture.expand_mode = TextureRect.EXPAND_FIT_WIDTH_PROPORTIONAL
|
||||
texture.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
|
||||
texture.custom_minimum_size = Vector2(50, 25)
|
||||
song_list_players.add_child(texture)
|
||||
scorers.append(player)
|
||||
var per_line: int = picture_columns(scorers.size())
|
||||
var line: HBoxContainer = null
|
||||
for i: int in scorers.size():
|
||||
if i % per_line == 0:
|
||||
line = HBoxContainer.new()
|
||||
line.theme_type_variation = &"SongRowPictureLine"
|
||||
line.alignment = BoxContainer.ALIGNMENT_END
|
||||
line.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
song_list_players.add_child(line)
|
||||
line.add_child(_make_picture(scorers[i]))
|
||||
song_list_players.visible = !scorers.is_empty()
|
||||
|
||||
|
||||
func _make_picture(player: PlayerObject) -> TextureRect:
|
||||
var picture: TextureRect = TextureRect.new()
|
||||
picture.texture = player.character
|
||||
picture.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
|
||||
picture.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
|
||||
picture.custom_minimum_size = PICTURE_SIZE
|
||||
picture.tooltip_text = player.player_name
|
||||
# Clicks on a picture still click the row
|
||||
picture.mouse_filter = Control.MOUSE_FILTER_PASS
|
||||
return picture
|
||||
|
||||
|
||||
func _set_hovered(hovered: bool) -> void:
|
||||
_hovered = hovered
|
||||
_update_style()
|
||||
|
||||
|
||||
func _update_style() -> void:
|
||||
if _hovered:
|
||||
theme_type_variation = &"SongRowHover"
|
||||
elif song_object != null && song_object.is_playing:
|
||||
theme_type_variation = &"SongRowPlaying"
|
||||
else:
|
||||
theme_type_variation = &"SongRowOdd" if index % 2 != 0 else &"SongRow"
|
||||
@@ -0,0 +1,68 @@
|
||||
[gd_scene format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://statistics/song_stats_list.gd" id="list_script"]
|
||||
|
||||
[node name="SongStatsList" type="Control"]
|
||||
layout_mode = 3
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
mouse_filter = 2
|
||||
script = ExtResource("list_script")
|
||||
|
||||
[node name="Layout" type="HBoxContainer" parent="."]
|
||||
layout_mode = 1
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
mouse_filter = 2
|
||||
|
||||
[node name="Columns" type="VBoxContainer" parent="Layout"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
mouse_filter = 2
|
||||
|
||||
[node name="Header" type="PanelContainer" parent="Layout/Columns"]
|
||||
layout_mode = 2
|
||||
mouse_filter = 2
|
||||
theme_type_variation = &"SongStatsHeader"
|
||||
|
||||
[node name="Titles" type="HBoxContainer" parent="Layout/Columns/Header"]
|
||||
layout_mode = 2
|
||||
mouse_filter = 2
|
||||
theme_override_constants/separation = 16
|
||||
|
||||
[node name="SongTitle" type="Label" parent="Layout/Columns/Header/Titles"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
size_flags_stretch_ratio = 3.0
|
||||
theme_type_variation = &"HighlightMedium"
|
||||
text = "Song"
|
||||
|
||||
[node name="SoundtrackTitle" type="Label" parent="Layout/Columns/Header/Titles"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
size_flags_stretch_ratio = 2.0
|
||||
theme_type_variation = &"HighlightMedium"
|
||||
text = "Soundtrack"
|
||||
|
||||
[node name="PlayedTitle" type="Label" parent="Layout/Columns/Header/Titles"]
|
||||
custom_minimum_size = Vector2(100, 0)
|
||||
layout_mode = 2
|
||||
theme_type_variation = &"HighlightMedium"
|
||||
text = "Played"
|
||||
horizontal_alignment = 2
|
||||
|
||||
[node name="RowsArea" type="Control" parent="Layout/Columns"]
|
||||
unique_name_in_owner = true
|
||||
clip_contents = true
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 3
|
||||
|
||||
[node name="ScrollBar" type="VScrollBar" parent="Layout"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
@@ -0,0 +1,50 @@
|
||||
[gd_scene format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://statistics/song_stats_row.gd" id="row_script"]
|
||||
|
||||
[node name="SongStatsRow" type="PanelContainer"]
|
||||
mouse_filter = 1
|
||||
theme_type_variation = &"ListRowEven"
|
||||
script = ExtResource("row_script")
|
||||
|
||||
[node name="Columns" type="HBoxContainer" parent="."]
|
||||
layout_mode = 2
|
||||
mouse_filter = 2
|
||||
theme_override_constants/separation = 16
|
||||
|
||||
[node name="SongLabel" type="RichTextLabel" parent="Columns"]
|
||||
unique_name_in_owner = true
|
||||
clip_contents = true
|
||||
custom_minimum_size = Vector2(0, 28)
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
size_flags_stretch_ratio = 3.0
|
||||
mouse_filter = 2
|
||||
theme_type_variation = &"SongStatsText"
|
||||
bbcode_enabled = true
|
||||
scroll_active = false
|
||||
autowrap_mode = 0
|
||||
vertical_alignment = 1
|
||||
|
||||
[node name="SoundtrackLabel" type="RichTextLabel" parent="Columns"]
|
||||
unique_name_in_owner = true
|
||||
clip_contents = true
|
||||
custom_minimum_size = Vector2(0, 28)
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
size_flags_stretch_ratio = 2.0
|
||||
mouse_filter = 2
|
||||
theme_type_variation = &"SongStatsSecondary"
|
||||
bbcode_enabled = true
|
||||
scroll_active = false
|
||||
autowrap_mode = 0
|
||||
vertical_alignment = 1
|
||||
|
||||
[node name="PlayedLabel" type="Label" parent="Columns"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(100, 0)
|
||||
layout_mode = 2
|
||||
mouse_filter = 2
|
||||
theme_type_variation = &"TextMedium"
|
||||
horizontal_alignment = 2
|
||||
vertical_alignment = 1
|
||||
@@ -1,6 +1,9 @@
|
||||
[gd_scene format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://statistics/statistics_view.gd" id="view_script"]
|
||||
[ext_resource type="PackedScene" path="res://statistics/SongStatsList.tscn" id="song_list_scene"]
|
||||
|
||||
[sub_resource type="ButtonGroup" id="ButtonGroup_mode"]
|
||||
|
||||
[node name="StatisticsView" type="Panel" unique_id=1212000001]
|
||||
anchors_preset = 15
|
||||
@@ -23,6 +26,30 @@ offset_bottom = 56.0
|
||||
grow_horizontal = 2
|
||||
theme_override_constants/separation = 12
|
||||
|
||||
[node name="SoundtracksModeButton" type="Button" parent="Toolbar"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(120, 0)
|
||||
layout_mode = 2
|
||||
focus_mode = 0
|
||||
theme_type_variation = &"TabButton"
|
||||
toggle_mode = true
|
||||
button_pressed = true
|
||||
button_group = SubResource("ButtonGroup_mode")
|
||||
text = "Soundtracks"
|
||||
|
||||
[node name="SongsModeButton" type="Button" parent="Toolbar"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(120, 0)
|
||||
layout_mode = 2
|
||||
focus_mode = 0
|
||||
theme_type_variation = &"TabButton"
|
||||
toggle_mode = true
|
||||
button_group = SubResource("ButtonGroup_mode")
|
||||
text = "Songs"
|
||||
|
||||
[node name="ModeSeparator" type="VSeparator" parent="Toolbar"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="SearchField" type="LineEdit" parent="Toolbar" unique_id=1212000003]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(480, 0)
|
||||
@@ -100,6 +127,20 @@ size_flags_horizontal = 0
|
||||
theme_type_variation = &"StatisticsGrid"
|
||||
columns = 6
|
||||
|
||||
[node name="SongList" parent="." instance=ExtResource("song_list_scene")]
|
||||
unique_name_in_owner = true
|
||||
visible = false
|
||||
layout_mode = 1
|
||||
offset_left = 32.0
|
||||
offset_top = 72.0
|
||||
offset_right = -32.0
|
||||
offset_bottom = -16.0
|
||||
|
||||
[node name="SearchDelay" type="Timer" parent="."]
|
||||
unique_name_in_owner = true
|
||||
wait_time = 0.15
|
||||
one_shot = true
|
||||
|
||||
[node name="StatusLabel" type="Label" parent="." unique_id=1212000012]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 1
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
class_name SongStatsList
|
||||
extends Control
|
||||
## A scrollable list of songs with their soundtrack and how often they were played, under
|
||||
## a header naming the columns. Shown in the statistics view instead of the soundtrack
|
||||
## cards when Songs is chosen.
|
||||
##
|
||||
## The server has tens of thousands of songs, so only the rows on screen exist. They are
|
||||
## reused for other songs as the list scrolls.
|
||||
|
||||
## Emitted when a song is clicked.
|
||||
signal song_pressed(song: SongInfoForStats)
|
||||
|
||||
const ROW_SCENE: PackedScene = preload("res://statistics/SongStatsRow.tscn")
|
||||
## How many rows one turn of the mouse wheel scrolls.
|
||||
const ROWS_PER_WHEEL_STEP: int = 3
|
||||
|
||||
## The songs in the list, in the order shown.
|
||||
var songs: Array[SongInfoForStats] = []
|
||||
|
||||
# The rows that fit on screen, top to bottom
|
||||
var _rows: Array[SongStatsRow] = []
|
||||
var _row_height: float = 0.0
|
||||
var _search_text: String = ""
|
||||
|
||||
@onready var rows_area: Control = %RowsArea
|
||||
@onready var scroll_bar: VScrollBar = %ScrollBar
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
var measured: SongStatsRow = ROW_SCENE.instantiate()
|
||||
add_child(measured)
|
||||
_row_height = measured.get_combined_minimum_size().y
|
||||
measured.free()
|
||||
rows_area.resized.connect(_fit_rows)
|
||||
scroll_bar.value_changed.connect(func(_value: float) -> void: _show_rows())
|
||||
rows_area.gui_input.connect(_on_gui_input)
|
||||
_fit_rows()
|
||||
|
||||
|
||||
## Shows [param new_songs] from the top, with the letters [param search_text] matched
|
||||
## highlighted.
|
||||
func show_songs(new_songs: Array[SongInfoForStats], search_text: String = "") -> void:
|
||||
songs = new_songs
|
||||
_search_text = search_text
|
||||
_update_scroll_bar()
|
||||
scroll_bar.value = 0
|
||||
_show_rows()
|
||||
|
||||
|
||||
## The rows showing a song now, top to bottom.
|
||||
func shown_rows() -> Array[SongStatsRow]:
|
||||
var shown: Array[SongStatsRow] = []
|
||||
for row: SongStatsRow in _rows:
|
||||
if row.visible:
|
||||
shown.append(row)
|
||||
return shown
|
||||
|
||||
|
||||
## Scrolls so song number [param index] is at the top, as far as the list goes.
|
||||
func scroll_to(index: int) -> void:
|
||||
scroll_bar.value = index * _row_height
|
||||
|
||||
|
||||
# Makes as many rows as fit on screen, plus one for the row partly scrolled out
|
||||
func _fit_rows() -> void:
|
||||
var needed: int = ceili(rows_area.size.y / _row_height) + 1
|
||||
while _rows.size() < needed:
|
||||
var row: SongStatsRow = ROW_SCENE.instantiate()
|
||||
rows_area.add_child(row)
|
||||
row.pressed.connect(song_pressed.emit)
|
||||
_rows.append(row)
|
||||
while _rows.size() > needed:
|
||||
_rows.pop_back().free()
|
||||
for row: SongStatsRow in _rows:
|
||||
row.size = Vector2(rows_area.size.x, _row_height)
|
||||
_update_scroll_bar()
|
||||
_show_rows()
|
||||
|
||||
|
||||
func _update_scroll_bar() -> void:
|
||||
scroll_bar.max_value = songs.size() * _row_height
|
||||
scroll_bar.page = rows_area.size.y
|
||||
scroll_bar.step = 0
|
||||
scroll_bar.visible = scroll_bar.max_value > scroll_bar.page
|
||||
|
||||
|
||||
func _show_rows() -> void:
|
||||
var first: int = floori(scroll_bar.value / _row_height)
|
||||
var offset: float = scroll_bar.value - first * _row_height
|
||||
for i: int in _rows.size():
|
||||
var row: SongStatsRow = _rows[i]
|
||||
var index: int = first + i
|
||||
row.visible = index < songs.size()
|
||||
if row.visible:
|
||||
row.position = Vector2(0, i * _row_height - offset)
|
||||
row.show_song(songs[index], index, _search_text)
|
||||
|
||||
|
||||
func _on_gui_input(event: InputEvent) -> void:
|
||||
if !(event is InputEventMouseButton) || !event.pressed:
|
||||
return
|
||||
var step: float = _row_height * ROWS_PER_WHEEL_STEP
|
||||
match event.button_index:
|
||||
MOUSE_BUTTON_WHEEL_UP:
|
||||
scroll_bar.value -= step * event.factor if event.factor > 0 else step
|
||||
MOUSE_BUTTON_WHEEL_DOWN:
|
||||
scroll_bar.value += step * event.factor if event.factor > 0 else step
|
||||
_:
|
||||
return
|
||||
rows_area.accept_event()
|
||||
@@ -0,0 +1 @@
|
||||
uid://tfgi4wql3cwa
|
||||
@@ -0,0 +1,66 @@
|
||||
class_name SongStatsRow
|
||||
extends PanelContainer
|
||||
## One song in the statistics' list of songs: its name, its soundtrack and how often it
|
||||
## was played, with the letters a search matched highlighted. Clicking it asks for the
|
||||
## song's soundtrack.
|
||||
##
|
||||
## Rows are reused for other songs as the list scrolls, see [SongStatsList].
|
||||
|
||||
## Emitted when the row is clicked.
|
||||
signal pressed(song: SongInfoForStats)
|
||||
|
||||
## The song the row shows.
|
||||
var song: SongInfoForStats
|
||||
|
||||
# The place in the list of the song shown, for the alternating background
|
||||
var _index: int = 0
|
||||
var _hovered: bool = false
|
||||
|
||||
@onready var song_label: RichTextLabel = %SongLabel
|
||||
@onready var soundtrack_label: RichTextLabel = %SoundtrackLabel
|
||||
@onready var played_label: Label = %PlayedLabel
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
mouse_entered.connect(_set_hovered.bind(true))
|
||||
mouse_exited.connect(_set_hovered.bind(false))
|
||||
gui_input.connect(_on_gui_input)
|
||||
|
||||
|
||||
## Shows [param data], song number [param index] in the list, with the letters
|
||||
## [param search_text] matched highlighted.
|
||||
func show_song(data: SongInfoForStats, index: int, search_text: String) -> void:
|
||||
song = data
|
||||
_index = index
|
||||
_update_style()
|
||||
var highlight: Color = get_theme_color(&"font_color", &"HighlightLarge")
|
||||
_set_text(song_label, SearchPopup.highlighted_name(data.song_name, search_text, highlight))
|
||||
_set_text(soundtrack_label,
|
||||
SearchPopup.highlighted_name(data.soundtrack_name, search_text, highlight))
|
||||
played_label.text = str(data.times_played)
|
||||
tooltip_text = "%s\n%s" % [data.song_name, data.soundtrack_name]
|
||||
|
||||
|
||||
# Setting the same text again would make the label lay it out again for nothing
|
||||
static func _set_text(label: RichTextLabel, text: String) -> void:
|
||||
if label.text != text:
|
||||
label.text = text
|
||||
|
||||
|
||||
func _set_hovered(hovered: bool) -> void:
|
||||
_hovered = hovered
|
||||
_update_style()
|
||||
|
||||
|
||||
func _update_style() -> void:
|
||||
if _hovered:
|
||||
theme_type_variation = &"ListRowHover"
|
||||
else:
|
||||
theme_type_variation = &"ListRowOdd" if _index % 2 != 0 else &"ListRowEven"
|
||||
|
||||
|
||||
func _on_gui_input(event: InputEvent) -> void:
|
||||
if event is InputEventMouseButton && event.pressed \
|
||||
&& event.button_index == MOUSE_BUTTON_LEFT:
|
||||
accept_event()
|
||||
pressed.emit(song)
|
||||
@@ -0,0 +1 @@
|
||||
uid://dibwxcgne3p8c
|
||||
@@ -1,12 +1,15 @@
|
||||
class_name SoundtrackCard
|
||||
extends PanelContainer
|
||||
## One soundtrack in the statistics view: its name, how often and when it was last
|
||||
## played, and its most and least played songs. Clicking it asks for all its songs.
|
||||
## played, and its most and least played songs. Songs never played are not listed, and
|
||||
## the least played are only shown when there are enough played songs to not repeat
|
||||
## any. Clicking it asks for all its songs.
|
||||
|
||||
## Emitted when the card is clicked.
|
||||
signal pressed(soundtrack: SoundtrackWithSongs)
|
||||
|
||||
## How many of the most and least played songs the card lists.
|
||||
## How many of the most and least played songs the card lists. With fewer than twice
|
||||
## as many played songs, all of them are listed as most played.
|
||||
const SONGS_SHOWN: int = 3
|
||||
|
||||
## The soundtrack the card shows.
|
||||
@@ -36,13 +39,17 @@ func show_soundtrack(data: SoundtrackWithSongs) -> void:
|
||||
last_played_label.text = last_played_text(data.soundtrack_last_played)
|
||||
last_played_label.visible = !data.soundtrack_last_played.is_empty()
|
||||
|
||||
var most_played: Array[SongInfoForStats] = songs_by_plays(data.songs).slice(0, SONGS_SHOWN)
|
||||
# Without the songs already shown as most played
|
||||
var least_played: Array[SongInfoForStats] = songs_by_plays(data.songs, true).filter(
|
||||
func(song: SongInfoForStats) -> bool: return !most_played.has(song)
|
||||
).slice(0, SONGS_SHOWN)
|
||||
var most_played: Array[SongInfoForStats] = []
|
||||
var least_played: Array[SongInfoForStats] = []
|
||||
var played: Array[SongInfoForStats] = played_songs(data.songs)
|
||||
if played.size() >= SONGS_SHOWN * 2:
|
||||
most_played = played.slice(0, SONGS_SHOWN)
|
||||
least_played = songs_by_plays(played, true).slice(0, SONGS_SHOWN)
|
||||
else:
|
||||
# Too few to split without listing a song twice, so all of them are most played
|
||||
most_played = played
|
||||
|
||||
most_played_header.text = "Most played" if !data.songs.is_empty() else "No songs"
|
||||
most_played_header.text = song_list_header(data.songs.size(), played.size())
|
||||
_fill_song_list(most_played_list, most_played)
|
||||
least_played_header.visible = !least_played.is_empty()
|
||||
_fill_song_list(least_played_list, least_played)
|
||||
@@ -63,6 +70,25 @@ static func last_played_text(last_played: String) -> String:
|
||||
return "Last played " + last_played.substr(0, 10)
|
||||
|
||||
|
||||
## The songs of [param songs] that have been played, most played first. Songs never
|
||||
## played are left out: the card only lists them in the popup with every song.
|
||||
static func played_songs(songs: Array[SongInfoForStats]) -> Array[SongInfoForStats]:
|
||||
var played: Array[SongInfoForStats] = []
|
||||
played.assign(songs_by_plays(songs).filter(
|
||||
func(song: SongInfoForStats) -> bool: return song.times_played > 0))
|
||||
return played
|
||||
|
||||
|
||||
## The header above the most played songs, for a soundtrack with [param song_count]
|
||||
## songs of which [param played_count] have been played.
|
||||
static func song_list_header(song_count: int, played_count: int) -> String:
|
||||
if song_count == 0:
|
||||
return "No songs"
|
||||
if played_count == 0:
|
||||
return "No songs played yet"
|
||||
return "Most played"
|
||||
|
||||
|
||||
## One line of a song list: how often the song was played, then its name.
|
||||
static func song_text(song: SongInfoForStats) -> String:
|
||||
return "%d × %s" % [song.times_played, song.song_name]
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
class_name StatisticsView
|
||||
extends Panel
|
||||
## Shows every soundtrack on the server as a card with its play statistics. The cards
|
||||
## can be searched, filtered on whether they have been played, and sorted.
|
||||
## Shows every soundtrack on the server as a card with its play statistics, or every
|
||||
## played song in a list with its soundtrack. Both can be searched and sorted, and the
|
||||
## cards filtered on whether the soundtrack has been played.
|
||||
##
|
||||
## Shown instead of the quiz when the Statistics tab is chosen.
|
||||
|
||||
@@ -11,29 +12,50 @@ signal soundtrack_selected(soundtrack: SoundtrackWithSongs)
|
||||
## The orders the cards can be sorted in, in the order of the sort menu.
|
||||
enum SortOrder { A_TO_Z, Z_TO_A, MOST_PLAYED, LEAST_PLAYED }
|
||||
|
||||
## Which soundtracks are shown, in the order of the show menu.
|
||||
## Which soundtracks or songs are shown, in the order of the show menu.
|
||||
enum ShowFilter { ALL, PLAYED, NEVER_PLAYED }
|
||||
|
||||
## Whether soundtracks or songs are shown.
|
||||
enum Mode { SOUNDTRACKS, SONGS }
|
||||
|
||||
const CARD_SCENE: PackedScene = preload("res://statistics/SoundtrackCard.tscn")
|
||||
|
||||
## The cards in the order they are shown, hidden ones included.
|
||||
var cards: Array[SoundtrackCard] = []
|
||||
## Whether soundtracks or songs are shown. Change it with [method set_mode].
|
||||
var mode: Mode = Mode.SOUNDTRACKS
|
||||
|
||||
var _search: SoundtrackSearch = SoundtrackSearch.new()
|
||||
var _loading: bool = false
|
||||
# The soundtracks last loaded, by name
|
||||
var _soundtracks: Dictionary[String, SoundtrackWithSongs] = {}
|
||||
# Every song sorted A to Z, made when songs are first shown after loading
|
||||
var _songs_a_to_z: Array[SongInfoForStats] = []
|
||||
var _song_search: SoundtrackSearch = SoundtrackSearch.new()
|
||||
var _songs_ready: bool = false
|
||||
|
||||
@onready var search_field: LineEdit = %SearchField
|
||||
@onready var clear_button: Button = %ClearButton
|
||||
@onready var show_label: Label = $Toolbar/ShowLabel
|
||||
@onready var show_option: OptionButton = %ShowOption
|
||||
@onready var sort_option: OptionButton = %SortOption
|
||||
@onready var count_label: Label = %CountLabel
|
||||
@onready var status_label: Label = %StatusLabel
|
||||
@onready var retry_button: Button = %RetryButton
|
||||
@onready var card_grid: GridContainer = %CardGrid
|
||||
@onready var card_scroll: ScrollContainer = $ScrollContainer
|
||||
@onready var song_list: SongStatsList = %SongList
|
||||
@onready var soundtracks_mode_button: Button = %SoundtracksModeButton
|
||||
@onready var songs_mode_button: Button = %SongsModeButton
|
||||
@onready var search_delay: Timer = %SearchDelay
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
search_field.text_changed.connect(func(_text: String) -> void: apply_filter())
|
||||
search_field.text_changed.connect(_on_search_text_changed)
|
||||
search_delay.timeout.connect(apply_filter)
|
||||
soundtracks_mode_button.pressed.connect(set_mode.bind(Mode.SOUNDTRACKS))
|
||||
songs_mode_button.pressed.connect(set_mode.bind(Mode.SONGS))
|
||||
song_list.song_pressed.connect(_on_song_pressed)
|
||||
clear_button.pressed.connect(clear_search)
|
||||
show_option.item_selected.connect(func(_index: int) -> void: apply_filter())
|
||||
sort_option.item_selected.connect(func(_index: int) -> void: apply_sort())
|
||||
@@ -53,14 +75,34 @@ func refresh() -> void:
|
||||
MusicServerClient.fetch_soundtrack_statistics(_on_loaded, _on_load_failed)
|
||||
|
||||
|
||||
## Shows every card again, keeping the show filter and the sort order.
|
||||
## Shows soundtracks or songs, keeping the search, the show filter and the sort order.
|
||||
func set_mode(new_mode: Mode) -> void:
|
||||
mode = new_mode
|
||||
soundtracks_mode_button.set_pressed_no_signal(mode == Mode.SOUNDTRACKS)
|
||||
songs_mode_button.set_pressed_no_signal(mode == Mode.SONGS)
|
||||
card_scroll.visible = mode == Mode.SOUNDTRACKS
|
||||
song_list.visible = mode == Mode.SONGS
|
||||
# The song list only has played songs, so there is nothing to filter
|
||||
show_label.visible = mode == Mode.SOUNDTRACKS
|
||||
show_option.visible = mode == Mode.SOUNDTRACKS
|
||||
search_field.placeholder_text = "Search soundtracks" if mode == Mode.SOUNDTRACKS \
|
||||
else "Search songs and soundtracks"
|
||||
apply_sort()
|
||||
if mode == Mode.SOUNDTRACKS:
|
||||
apply_filter()
|
||||
|
||||
|
||||
## Shows everything again, keeping the show filter and the sort order.
|
||||
func clear_search() -> void:
|
||||
search_field.text = ""
|
||||
apply_filter()
|
||||
|
||||
|
||||
## Puts the cards in the order chosen in the sort menu.
|
||||
## Puts the cards or songs in the order chosen in the sort menu.
|
||||
func apply_sort() -> void:
|
||||
if mode == Mode.SONGS:
|
||||
apply_filter()
|
||||
return
|
||||
var order: SortOrder = sort_option.selected as SortOrder
|
||||
cards.sort_custom(func(a: SoundtrackCard, b: SoundtrackCard) -> bool:
|
||||
return _comes_before(a.soundtrack, b.soundtrack, order))
|
||||
@@ -68,8 +110,12 @@ func apply_sort() -> void:
|
||||
card_grid.move_child(cards[i], i)
|
||||
|
||||
|
||||
## Shows only the cards that match the search text and the show menu.
|
||||
## Shows only the cards or songs that match the search text and the show menu.
|
||||
func apply_filter() -> void:
|
||||
search_delay.stop()
|
||||
if mode == Mode.SONGS:
|
||||
_show_songs()
|
||||
return
|
||||
var search_text: String = search_field.text.strip_edges()
|
||||
var matching: Dictionary = {}
|
||||
for soundtrack_name: String in _search.find_matches(search_text) \
|
||||
@@ -100,14 +146,108 @@ func _on_loaded(soundtracks: Array[SoundtrackWithSongs]) -> void:
|
||||
while cards.size() > soundtracks.size():
|
||||
cards.pop_back().free()
|
||||
var names: Array[String] = []
|
||||
_soundtracks.clear()
|
||||
for i: int in soundtracks.size():
|
||||
cards[i].show_soundtrack(soundtracks[i])
|
||||
names.append(soundtracks[i].soundtrack_name)
|
||||
_soundtracks[soundtracks[i].soundtrack_name] = soundtracks[i]
|
||||
_search.set_names(names)
|
||||
_songs_ready = false
|
||||
apply_sort()
|
||||
if mode == Mode.SOUNDTRACKS:
|
||||
apply_filter()
|
||||
|
||||
|
||||
# Typing in the song list waits for a pause, since searching every song takes a moment
|
||||
func _on_search_text_changed(_text: String) -> void:
|
||||
if mode == Mode.SONGS:
|
||||
search_delay.start()
|
||||
else:
|
||||
apply_filter()
|
||||
|
||||
|
||||
func _on_song_pressed(song: SongInfoForStats) -> void:
|
||||
if _soundtracks.has(song.soundtrack_name):
|
||||
soundtrack_selected.emit(_soundtracks[song.soundtrack_name])
|
||||
|
||||
|
||||
# Shows the songs matching the search text, in the sort menu's order
|
||||
func _show_songs() -> void:
|
||||
_prepare_songs()
|
||||
var search_text: String = search_field.text.strip_edges()
|
||||
var song_names: Dictionary = {}
|
||||
var soundtrack_names: Dictionary = {}
|
||||
if !search_text.is_empty():
|
||||
for song_name: String in _song_search.find_matches(search_text):
|
||||
song_names[song_name] = true
|
||||
for soundtrack_name: String in _search.find_matches(search_text):
|
||||
soundtrack_names[soundtrack_name] = true
|
||||
var shown: Array[SongInfoForStats] = []
|
||||
for song: SongInfoForStats in songs_in_order(_songs_a_to_z,
|
||||
sort_option.selected as SortOrder):
|
||||
if search_text.is_empty() || song_names.has(song.song_name) \
|
||||
|| soundtrack_names.has(song.soundtrack_name):
|
||||
shown.append(song)
|
||||
song_list.show_songs(shown, search_text)
|
||||
if shown.size() == _songs_a_to_z.size():
|
||||
count_label.text = "%d played songs" % shown.size()
|
||||
else:
|
||||
count_label.text = "%d of %d played songs" % [shown.size(), _songs_a_to_z.size()]
|
||||
|
||||
|
||||
# Lists every played song once after loading, sorted A to Z, which the other orders
|
||||
# start from. Songs never played are left out: there are many and they say nothing.
|
||||
func _prepare_songs() -> void:
|
||||
if _songs_ready:
|
||||
return
|
||||
_songs_ready = true
|
||||
_songs_a_to_z.clear()
|
||||
var names: Dictionary = {}
|
||||
for soundtrack: SoundtrackWithSongs in _soundtracks.values():
|
||||
for song: SongInfoForStats in soundtrack.songs:
|
||||
if song.times_played == 0:
|
||||
continue
|
||||
# The song list finds a song's soundtrack by its name
|
||||
song.soundtrack_name = soundtrack.soundtrack_name
|
||||
_songs_a_to_z.append(song)
|
||||
names[song.song_name] = true
|
||||
_songs_a_to_z.sort_custom(func(a: SongInfoForStats, b: SongInfoForStats) -> bool:
|
||||
var by_name: int = a.song_name.naturalnocasecmp_to(b.song_name)
|
||||
if by_name != 0:
|
||||
return by_name < 0
|
||||
return a.soundtrack_name.naturalnocasecmp_to(b.soundtrack_name) < 0)
|
||||
_song_search.set_names(names.keys())
|
||||
|
||||
|
||||
## [param a_to_z], songs sorted A to Z, in [param order]. Songs played as often stay in
|
||||
## A to Z order.
|
||||
static func songs_in_order(
|
||||
a_to_z: Array[SongInfoForStats], order: SortOrder) -> Array[SongInfoForStats]:
|
||||
match order:
|
||||
SortOrder.Z_TO_A:
|
||||
var z_to_a: Array[SongInfoForStats] = a_to_z.duplicate()
|
||||
z_to_a.reverse()
|
||||
return z_to_a
|
||||
SortOrder.MOST_PLAYED, SortOrder.LEAST_PLAYED:
|
||||
# Grouped by plays instead of sorted, which keeps the A to Z order within a
|
||||
# group and is quick for tens of thousands of songs
|
||||
var by_plays: Dictionary[int, Array] = {}
|
||||
for song: SongInfoForStats in a_to_z:
|
||||
if !by_plays.has(song.times_played):
|
||||
by_plays[song.times_played] = []
|
||||
by_plays[song.times_played].append(song)
|
||||
var plays: Array[int] = []
|
||||
plays.assign(by_plays.keys())
|
||||
plays.sort()
|
||||
if order == SortOrder.MOST_PLAYED:
|
||||
plays.reverse()
|
||||
var sorted: Array[SongInfoForStats] = []
|
||||
for times_played: int in plays:
|
||||
sorted.append_array(by_plays[times_played])
|
||||
return sorted
|
||||
return a_to_z
|
||||
|
||||
|
||||
func _on_load_failed() -> void:
|
||||
_loading = false
|
||||
if cards.is_empty():
|
||||
|
||||
+9828
-2165
File diff suppressed because it is too large.
Load diff
@@ -267,6 +267,17 @@ func run(on_state: Callable) -> void:
|
||||
statistics_view.apply_filter()
|
||||
await _settle()
|
||||
await on_state.call("statistics_search", statistics_view)
|
||||
# Chrono Trigger has never been played, so the song list searches for another
|
||||
statistics_view.search_field.text = "zelda"
|
||||
statistics_view.set_mode(StatisticsView.Mode.SONGS)
|
||||
await _settle()
|
||||
await on_state.call("statistics_songs_search", statistics_view)
|
||||
statistics_view.clear_search()
|
||||
await _settle()
|
||||
await on_state.call("statistics_songs", statistics_view)
|
||||
statistics_view.search_field.text = "chrono"
|
||||
statistics_view.set_mode(StatisticsView.Mode.SOUNDTRACKS)
|
||||
await _settle()
|
||||
var stats_popup: SoundtrackStatsPopup = _node("SoundtrackStatsPopup")
|
||||
for card: SoundtrackCard in statistics_view.cards:
|
||||
if card.visible:
|
||||
@@ -435,6 +446,11 @@ func _freeze_music_player() -> void:
|
||||
music_player.sound_player.stop()
|
||||
music_player.progress_slider.value = 0
|
||||
music_player.music_time_label.text = "00:00 / 00:01"
|
||||
# The buttons' animations would otherwise be caught halfway, see ButtonBounce
|
||||
for button: Button in [music_player.play_button, music_player.restart_button]:
|
||||
if button.has_meta(&"button_bounce_tween"):
|
||||
(button.get_meta(&"button_bounce_tween") as Tween).kill()
|
||||
button.scale = Vector2.ONE
|
||||
|
||||
|
||||
## The log starts each row with the time of day; use a fixed time instead.
|
||||
|
||||
@@ -26,3 +26,129 @@ func _click(control: Control) -> void:
|
||||
|
||||
func test_empty_space_lets_clicks_through() -> void:
|
||||
assert_int(_player.mouse_filter).is_equal(Control.MOUSE_FILTER_IGNORE)
|
||||
|
||||
|
||||
func _song() -> AudioStreamMP3:
|
||||
var song: AudioStreamMP3 = AudioStreamMP3.new()
|
||||
song.data = FileAccess.get_file_as_bytes("res://tests/data/short.mp3")
|
||||
return song
|
||||
|
||||
|
||||
func _buttons() -> Array[Button]:
|
||||
return [_player.play_button, _player.restart_button]
|
||||
|
||||
|
||||
# Waits until the buttons' animations have finished
|
||||
func _settle() -> void:
|
||||
await await_millis(int(maxf(ButtonBounce.DURATION, ButtonBounce.POP_DURATION) * 1000) + 80)
|
||||
|
||||
|
||||
# Icons
|
||||
|
||||
func test_icons_sit_inside_their_buttons_with_room_around_them() -> void:
|
||||
await _runner.simulate_frames(1)
|
||||
for button: Button in _buttons():
|
||||
var style: StyleBox = button.get_theme_stylebox(&"normal")
|
||||
for side: Side in [SIDE_LEFT, SIDE_TOP, SIDE_RIGHT, SIDE_BOTTOM]:
|
||||
assert_float(style.get_content_margin(side)).override_failure_message(button.name) \
|
||||
.is_greater_equal(8.0)
|
||||
assert_bool(button.expand_icon).override_failure_message(button.name).is_true()
|
||||
assert_int(button.icon_alignment).is_equal(HORIZONTAL_ALIGNMENT_CENTER)
|
||||
# A square button, so the round background is a circle around the icon
|
||||
assert_float(button.size.x).override_failure_message(button.name) \
|
||||
.is_equal(button.size.y)
|
||||
var icon_room: Vector2 = button.size - style.get_minimum_size()
|
||||
assert_float(icon_room.x).override_failure_message(button.name).is_greater(12.0)
|
||||
|
||||
|
||||
func test_buttons_are_centred_in_the_bar() -> void:
|
||||
await _runner.simulate_frames(1)
|
||||
var bar: Rect2 = _player.get_node("PlayerBar").get_global_rect()
|
||||
for button: Button in _buttons():
|
||||
var rect: Rect2 = button.get_global_rect()
|
||||
assert_bool(bar.encloses(rect)).override_failure_message(button.name).is_true()
|
||||
assert_float(rect.get_center().y).override_failure_message(button.name) \
|
||||
.is_equal_approx(bar.get_center().y, 1.0)
|
||||
|
||||
|
||||
func test_play_button_stands_out_from_restart() -> void:
|
||||
assert_str(_player.play_button.theme_type_variation).is_equal("PlayerButtonPrimary")
|
||||
assert_str(_player.restart_button.theme_type_variation).is_equal("PlayerButton")
|
||||
assert_str(_player.progress_slider.theme_type_variation).is_equal("ProgressSlider")
|
||||
assert_str(_player.volume_slider.theme_type_variation).is_equal("VolumeBar")
|
||||
assert_str(_player.music_time_label.theme_type_variation).is_equal("PlayerTime")
|
||||
|
||||
|
||||
func test_buttons_have_tooltips_with_their_keys() -> void:
|
||||
assert_str(_player.play_button.tooltip_text).contains("Alt+X")
|
||||
assert_str(_player.restart_button.tooltip_text).contains("Alt+Z")
|
||||
|
||||
|
||||
# Play and pause
|
||||
|
||||
func test_play_button_shows_play_until_a_song_plays() -> void:
|
||||
assert_bool(_player.shows_playing()).is_false()
|
||||
|
||||
_player.play_song(_song())
|
||||
|
||||
assert_bool(_player.shows_playing()).is_true()
|
||||
assert_object(_player.play_button.icon).is_same(_player.pause_icon)
|
||||
|
||||
|
||||
func test_pausing_shows_play_again() -> void:
|
||||
await _runner.simulate_frames(1)
|
||||
_player.play_song(_song())
|
||||
|
||||
await _click(_player.play_button)
|
||||
|
||||
assert_bool(_player.audio_player.stream_paused).is_true()
|
||||
assert_bool(_player.shows_playing()).is_false()
|
||||
|
||||
|
||||
func test_play_button_pops_when_it_changes() -> void:
|
||||
await _runner.simulate_frames(1)
|
||||
|
||||
_player.play_song(_song())
|
||||
|
||||
assert_vector(_player.play_button.scale).is_equal(ButtonBounce.POP_SCALE)
|
||||
await _settle()
|
||||
assert_vector(_player.play_button.scale).is_equal_approx(Vector2.ONE, Vector2(0.01, 0.01))
|
||||
|
||||
|
||||
func test_play_button_does_not_pop_when_it_stays_the_same() -> void:
|
||||
_player.play_song(_song())
|
||||
await _settle()
|
||||
|
||||
_player.play_song(_song())
|
||||
|
||||
assert_vector(_player.play_button.scale).is_equal_approx(Vector2.ONE, Vector2(0.01, 0.01))
|
||||
|
||||
|
||||
# Hover
|
||||
|
||||
func test_buttons_grow_under_the_mouse_and_shrink_back() -> void:
|
||||
await _runner.simulate_frames(1)
|
||||
for button: Button in _buttons():
|
||||
_runner.set_mouse_position(_player.get_viewport().get_final_transform()
|
||||
* button.get_global_rect().get_center())
|
||||
await _runner.simulate_frames(2)
|
||||
await _settle()
|
||||
assert_vector(button.scale).override_failure_message(button.name) \
|
||||
.is_equal_approx(ButtonBounce.HOVER_SCALE, Vector2(0.01, 0.01))
|
||||
|
||||
_runner.set_mouse_position(Vector2.ZERO)
|
||||
await _runner.simulate_frames(2)
|
||||
await _settle()
|
||||
assert_vector(button.scale).override_failure_message(button.name) \
|
||||
.is_equal_approx(Vector2.ONE, Vector2(0.01, 0.01))
|
||||
|
||||
|
||||
func test_growing_keeps_the_button_in_place() -> void:
|
||||
await _runner.simulate_frames(1)
|
||||
var before: Vector2 = _player.restart_button.get_global_rect().get_center()
|
||||
|
||||
_player.restart_button.mouse_entered.emit()
|
||||
await _settle()
|
||||
|
||||
assert_vector(_player.restart_button.get_global_rect().get_center()) \
|
||||
.is_equal_approx(before, Vector2(0.5, 0.5))
|
||||
@@ -124,6 +124,8 @@ func test_server_features_are_off_and_the_folder_is_shown() -> void:
|
||||
assert_str(_top_bar.offline_label.text).is_equal("Offline: Music")
|
||||
assert_bool(_settings.select_server_button.disabled).is_true()
|
||||
assert_bool(_settings.add_to_database_button.disabled).is_true()
|
||||
# The config folder holds the cached characters, which are still used offline
|
||||
assert_bool(_settings.open_config_folder_button.disabled).is_false()
|
||||
|
||||
|
||||
func test_picking_a_folder_on_the_statistics_tab_goes_to_the_quiz() -> void:
|
||||
|
||||
@@ -313,3 +313,15 @@ func test_alt_s_opens_search() -> void:
|
||||
await _press_keys([KEY_ALT, KEY_S])
|
||||
|
||||
assert_bool((_window.get_node("SearchPopup") as Control).visible).is_true()
|
||||
|
||||
|
||||
func test_music_player_fits_between_the_song_buttons_and_quiz_settings() -> void:
|
||||
var bar: Rect2 = _quiz.get_node("MusicPlayer/PlayerBar").get_global_rect()
|
||||
var above: Rect2 = _quiz.get_node("SongControls/NextButton").get_global_rect()
|
||||
var below: Rect2 = _quiz.get_node("QuizSettingsButton").get_global_rect()
|
||||
|
||||
assert_float(bar.position.y).is_greater_equal(above.end.y + 4)
|
||||
assert_float(bar.end.y).is_less_equal(below.position.y - 2)
|
||||
# As wide as the music list above it
|
||||
var list: Rect2 = _quiz.get_node("MusicListPanel").get_global_rect()
|
||||
assert_float(bar.position.x).is_equal(list.position.x)
|
||||
@@ -53,3 +53,16 @@ func test_popup_fits_its_rows_and_is_centred() -> void:
|
||||
# The rows' own height fits, so the last toggle is not squeezed or cut off
|
||||
assert_float(rows.get_combined_minimum_size().y).is_less_equal(rows.size.y)
|
||||
assert_vector(panel.get_center()).is_equal_approx(Vector2(960, 540), Vector2(1, 1))
|
||||
|
||||
|
||||
func test_open_folder_button_shows_the_config_folder() -> void:
|
||||
var shown: Array[String] = []
|
||||
_popup.show_folder = func(dir: String) -> void: shown.append(dir)
|
||||
_popup.open()
|
||||
await _runner.simulate_frames(1)
|
||||
|
||||
await _click_at(_popup.open_config_folder_button.get_global_rect().get_center())
|
||||
|
||||
assert_array(shown).contains_exactly([Settings.config_dir()])
|
||||
assert_bool(DirAccess.dir_exists_absolute(Settings.config_dir())).is_true()
|
||||
|
||||
@@ -58,8 +58,8 @@ 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",
|
||||
"MusicPlayer/PlayerBar/MusicPlayerContainer/PlayButton": "Play or pause",
|
||||
"MusicPlayer/PlayerBar/MusicPlayerContainer/RestartButton": "Restart song",
|
||||
}
|
||||
for path: String in expected:
|
||||
var button: BaseButton = _quiz.get_node(path)
|
||||
|
||||
@@ -0,0 +1,365 @@
|
||||
extends GdUnitTestSuite
|
||||
## How a song in the music list is laid out: the players' pictures on the right in even
|
||||
## lines of at most four, each ending at the row's right edge, the text in the room they leave, wrapping only when it does
|
||||
## not fit, and the row as tall as the taller of the two. Checked for every number of
|
||||
## players, with short, debug mode and very long song texts.
|
||||
|
||||
const SHORT: Array[String] = ["Chrono Trigger", "Corridors of Time"]
|
||||
const LONG: Array[String] = [
|
||||
"Final Fantasy Dissidia 012 Duodecim (Sista Fantasin Meningsskiljaktigheter 012 Tolv)",
|
||||
"'Crystal Cave -arrange-' from FINAL FANTASY III",
|
||||
]
|
||||
## The row's padding, from the SongRow styles in the theme.
|
||||
const PADDING: float = 8.0
|
||||
|
||||
var _snapshot: AutoloadSnapshot
|
||||
var _runner: GdUnitSceneRunner
|
||||
var _panel: MusicListPanel
|
||||
# The panel's width in the quiz, which songs must never make wider
|
||||
var _panel_width: float
|
||||
|
||||
|
||||
func before_test() -> void:
|
||||
_snapshot = AutoloadSnapshot.take([Settings, Playlist])
|
||||
Settings.is_debug = false
|
||||
Playlist.reset_playlist()
|
||||
Settings.player_array.clear()
|
||||
for i: int in Settings.MAX_NUMBER_OF_PLAYERS:
|
||||
Settings.player_array.append(PlayerObject.new("Player %d" % (i + 1)))
|
||||
_runner = scene_runner("res://quiz/MusicListPanel.tscn")
|
||||
_panel = _runner.scene()
|
||||
_panel_width = _panel.size.x
|
||||
|
||||
|
||||
func after_test() -> void:
|
||||
await await_idle_frame()
|
||||
_snapshot.restore()
|
||||
|
||||
|
||||
# Adds a played and answered song with points for the first [param players] players
|
||||
func _add_song(title: Array[String], players: int) -> SongObject:
|
||||
var song: SongObject = SongObject.new()
|
||||
song.song_number = Playlist.song_object_array.size()
|
||||
song.soundtrack_title = title[0]
|
||||
song.song_title = title[1]
|
||||
song.has_played = true
|
||||
song.is_answered = true
|
||||
for i: int in players:
|
||||
song.add_point(Settings.player_array[i].id)
|
||||
Playlist.add_to_playlist(song)
|
||||
return song
|
||||
|
||||
|
||||
func _show() -> void:
|
||||
_panel.refresh()
|
||||
await _runner.simulate_frames(3)
|
||||
|
||||
|
||||
func _rows() -> Array[SongRow]:
|
||||
var rows: Array[SongRow] = []
|
||||
rows.assign(_panel.get_node("%MusicList").get_children())
|
||||
return rows
|
||||
|
||||
|
||||
func _pictures(row: SongRow) -> Array[TextureRect]:
|
||||
return row.pictures()
|
||||
|
||||
|
||||
# How wide the row's text would be on one line
|
||||
func _text_width(row: SongRow) -> float:
|
||||
var label: Label = row.song_list_label
|
||||
return label.get_theme_font(&"font").get_string_size(label.text, HORIZONTAL_ALIGNMENT_LEFT,
|
||||
-1, label.get_theme_font_size(&"font_size")).x
|
||||
|
||||
|
||||
func _one_line_height() -> float:
|
||||
_add_song(SHORT, 0)
|
||||
await _show()
|
||||
var height: float = _rows()[0].size.y
|
||||
Playlist.reset_playlist()
|
||||
return height
|
||||
|
||||
|
||||
# Every number of players with every kind of text, in normal and debug mode
|
||||
func _each_layout(check: Callable) -> void:
|
||||
for debug: bool in [false, true]:
|
||||
Settings.is_debug = debug
|
||||
for title: Array[String] in [SHORT, LONG]:
|
||||
Playlist.reset_playlist()
|
||||
for players: int in Settings.MAX_NUMBER_OF_PLAYERS + 1:
|
||||
_add_song(title, players)
|
||||
await _show()
|
||||
assert_float(_panel.size.x).override_failure_message(
|
||||
"The songs made the list wider than its place in the quiz") \
|
||||
.is_equal(_panel_width)
|
||||
for row: SongRow in _rows():
|
||||
check.call(row, "%d players, %s text%s" % [_pictures(row).size(),
|
||||
"long" if title == LONG else "short", " (debug)" if debug else ""])
|
||||
|
||||
|
||||
# Columns
|
||||
|
||||
func test_pictures_are_spread_evenly_over_as_few_lines_as_possible() -> void:
|
||||
var columns: Array[int] = []
|
||||
for pictures: int in range(0, 9):
|
||||
columns.append(SongRow.picture_columns(pictures))
|
||||
|
||||
# 5 pictures are 3 + 2, not 4 + 1; 7 are 4 + 3
|
||||
assert_array(columns).contains_exactly([1, 1, 2, 3, 4, 3, 3, 4, 4])
|
||||
|
||||
|
||||
# Placement
|
||||
|
||||
func test_pictures_are_at_the_right_end_of_the_row() -> void:
|
||||
await _each_layout(func(row: SongRow, layout: String) -> void:
|
||||
if _pictures(row).is_empty():
|
||||
assert_bool(row.song_list_players.visible).override_failure_message(layout).is_false()
|
||||
return
|
||||
var pictures: Rect2 = row.song_list_players.get_global_rect()
|
||||
assert_float(pictures.end.x).override_failure_message(layout) \
|
||||
.is_equal_approx(row.get_global_rect().end.x - PADDING, 1.0))
|
||||
|
||||
|
||||
func test_every_line_of_pictures_ends_at_the_right_edge() -> void:
|
||||
await _each_layout(func(row: SongRow, layout: String) -> void:
|
||||
var right_edge: float = row.get_global_rect().end.x - PADDING
|
||||
for line: HBoxContainer in row.song_list_players.get_children():
|
||||
var last: TextureRect = line.get_child(line.get_child_count() - 1)
|
||||
assert_float(last.get_global_rect().end.x).override_failure_message(layout) \
|
||||
.is_equal_approx(right_edge, 1.0))
|
||||
|
||||
|
||||
func test_text_and_pictures_never_overlap_and_stay_inside_the_row() -> void:
|
||||
await _each_layout(func(row: SongRow, layout: String) -> void:
|
||||
var row_rect: Rect2 = row.get_global_rect().grow(0.5)
|
||||
var text: Rect2 = row.song_list_label.get_global_rect()
|
||||
assert_bool(row_rect.encloses(text)).override_failure_message(
|
||||
"Text outside the row: " + layout).is_true()
|
||||
for picture: TextureRect in _pictures(row):
|
||||
var rect: Rect2 = picture.get_global_rect()
|
||||
assert_bool(row_rect.encloses(rect)).override_failure_message(
|
||||
"Picture outside the row: " + layout).is_true()
|
||||
assert_float(rect.position.x).override_failure_message(
|
||||
"Picture over the text: " + layout).is_greater_equal(text.end.x))
|
||||
|
||||
|
||||
func test_text_gets_all_the_room_the_pictures_leave() -> void:
|
||||
await _each_layout(func(row: SongRow, layout: String) -> void:
|
||||
var text: Rect2 = row.song_list_label.get_global_rect()
|
||||
assert_float(text.position.x).override_failure_message(layout) \
|
||||
.is_equal_approx(row.get_global_rect().position.x + PADDING, 1.0)
|
||||
var right_end: float = row.get_global_rect().end.x - PADDING
|
||||
if row.song_list_players.visible:
|
||||
right_end = row.song_list_players.get_global_rect().position.x \
|
||||
- row.get_node("HBoxContainer").get_theme_constant(&"separation")
|
||||
assert_float(text.end.x).override_failure_message(layout) \
|
||||
.is_equal_approx(right_end, 1.0))
|
||||
|
||||
|
||||
func test_text_wraps_only_when_it_does_not_fit_on_one_line() -> void:
|
||||
await _each_layout(func(row: SongRow, layout: String) -> void:
|
||||
var lines: int = row.song_list_label.get_line_count()
|
||||
var room: float = row.song_list_label.size.x
|
||||
if _text_width(row) <= room - 1:
|
||||
assert_int(lines).override_failure_message("Wrapped though it fits: " + layout) \
|
||||
.is_equal(1)
|
||||
elif _text_width(row) > room + 1:
|
||||
assert_int(lines).override_failure_message("Not wrapped: " + layout) \
|
||||
.is_greater(1))
|
||||
|
||||
|
||||
func test_every_layout_holds_in_a_narrower_list() -> void:
|
||||
_panel.size.x = 640
|
||||
_panel_width = 640
|
||||
await _runner.simulate_frames(1)
|
||||
|
||||
await _each_layout(func(row: SongRow, layout: String) -> void:
|
||||
var text: Rect2 = row.song_list_label.get_global_rect()
|
||||
assert_bool(row.get_global_rect().grow(0.5).encloses(text)).override_failure_message(
|
||||
"Text outside the row: " + layout).is_true()
|
||||
if row.song_list_players.visible:
|
||||
assert_float(row.song_list_players.get_global_rect().position.x) \
|
||||
.override_failure_message("Pictures over the text: " + layout) \
|
||||
.is_greater_equal(text.end.x))
|
||||
|
||||
|
||||
func test_short_text_is_one_line_even_with_every_player() -> void:
|
||||
_add_song(SHORT, Settings.MAX_NUMBER_OF_PLAYERS)
|
||||
await _show()
|
||||
|
||||
assert_int(_rows()[0].song_list_label.get_line_count()).is_equal(1)
|
||||
|
||||
|
||||
func test_long_text_wraps_and_keeps_every_word() -> void:
|
||||
_add_song(LONG, 2)
|
||||
await _show()
|
||||
var label: Label = _rows()[0].song_list_label
|
||||
|
||||
assert_int(label.get_line_count()).is_greater(1)
|
||||
assert_str(label.text).ends_with("FINAL FANTASY III")
|
||||
assert_int(label.get_visible_line_count()).is_equal(label.get_line_count())
|
||||
|
||||
|
||||
# Height
|
||||
|
||||
func test_row_is_one_line_high_with_up_to_four_pictures() -> void:
|
||||
var one_line: float = await _one_line_height()
|
||||
for players: int in range(0, SongRow.MAX_PICTURES_PER_LINE + 1):
|
||||
_add_song(SHORT, players)
|
||||
await _show()
|
||||
|
||||
for row: SongRow in _rows():
|
||||
assert_float(row.size.y).override_failure_message(
|
||||
"%d pictures" % _pictures(row).size()).is_equal_approx(one_line, 0.5)
|
||||
|
||||
|
||||
func test_five_pictures_go_on_two_lines_of_three_and_two() -> void:
|
||||
_add_song(SHORT, 5)
|
||||
await _show()
|
||||
|
||||
assert_array(_lines_of_pictures(_rows()[0])).contains_exactly([3, 2])
|
||||
|
||||
|
||||
func test_seven_pictures_go_on_two_lines_of_four_and_three() -> void:
|
||||
_add_song(SHORT, 7)
|
||||
await _show()
|
||||
|
||||
assert_array(_lines_of_pictures(_rows()[0])).contains_exactly([4, 3])
|
||||
|
||||
|
||||
# How many pictures are on each line, top to bottom
|
||||
func _lines_of_pictures(row: SongRow) -> Array[int]:
|
||||
var by_top: Dictionary[float, int] = {}
|
||||
for picture: TextureRect in _pictures(row):
|
||||
var top: float = roundf(picture.global_position.y)
|
||||
by_top[top] = by_top.get(top, 0) + 1
|
||||
var tops: Array[float] = []
|
||||
tops.assign(by_top.keys())
|
||||
tops.sort()
|
||||
var lines: Array[int] = []
|
||||
for top: float in tops:
|
||||
lines.append(by_top[top])
|
||||
return lines
|
||||
|
||||
|
||||
func test_row_grows_to_fit_two_lines_of_pictures_and_centres_the_text() -> void:
|
||||
var one_line: float = await _one_line_height()
|
||||
_add_song(SHORT, 5)
|
||||
await _show()
|
||||
var row: SongRow = _rows()[0]
|
||||
var pictures: Rect2 = row.song_list_players.get_global_rect()
|
||||
|
||||
assert_float(row.size.y).is_greater(one_line)
|
||||
assert_float(pictures.size.y).is_equal_approx(
|
||||
SongRow.PICTURE_SIZE.y * 2 + row.song_list_players.get_theme_constant(
|
||||
&"separation"), 0.5)
|
||||
assert_bool(row.get_global_rect().encloses(pictures)).is_true()
|
||||
# The single line of text is in the middle, beside the pictures
|
||||
assert_int(row.song_list_label.vertical_alignment).is_equal(VERTICAL_ALIGNMENT_CENTER)
|
||||
assert_float(row.song_list_label.get_global_rect().get_center().y) \
|
||||
.is_equal_approx(pictures.get_center().y, 1.0)
|
||||
|
||||
|
||||
func test_pictures_are_centred_beside_text_on_many_lines() -> void:
|
||||
_add_song(LONG, 1)
|
||||
await _show()
|
||||
var row: SongRow = _rows()[0]
|
||||
var text: Rect2 = row.song_list_label.get_global_rect()
|
||||
var pictures: Rect2 = row.song_list_players.get_global_rect()
|
||||
|
||||
assert_int(row.song_list_label.get_line_count()).is_greater(1)
|
||||
assert_float(pictures.size.y).is_less(text.size.y)
|
||||
assert_float(pictures.get_center().y).is_equal_approx(text.get_center().y, 1.0)
|
||||
|
||||
|
||||
func test_every_picture_has_the_same_size() -> void:
|
||||
await _each_layout(func(row: SongRow, layout: String) -> void:
|
||||
for picture: TextureRect in _pictures(row):
|
||||
assert_vector(picture.size).override_failure_message(layout) \
|
||||
.is_equal(SongRow.PICTURE_SIZE))
|
||||
|
||||
|
||||
func test_rows_never_need_sideways_scrolling() -> void:
|
||||
Settings.is_debug = true
|
||||
for players: int in Settings.MAX_NUMBER_OF_PLAYERS + 1:
|
||||
_add_song(LONG, players)
|
||||
await _show()
|
||||
var scroll: ScrollContainer = _panel.get_node("ScrollContainer")
|
||||
|
||||
assert_bool(scroll.get_h_scroll_bar().visible).is_false()
|
||||
assert_float(_panel.size.x).is_equal(_panel_width)
|
||||
for row: SongRow in _rows():
|
||||
assert_float(row.get_global_rect().end.x) \
|
||||
.is_less_equal(scroll.get_global_rect().end.x + 0.5)
|
||||
|
||||
|
||||
# Pictures
|
||||
|
||||
func test_pictures_show_who_got_a_point_in_player_list_order() -> void:
|
||||
var song: SongObject = _add_song(SHORT, 0)
|
||||
# Points given in another order than the player list
|
||||
song.add_point(Settings.player_array[2].id)
|
||||
song.add_point(Settings.player_array[0].id)
|
||||
await _show()
|
||||
|
||||
var names: Array[String] = []
|
||||
for picture: TextureRect in _pictures(_rows()[0]):
|
||||
names.append(picture.tooltip_text)
|
||||
assert_array(names).contains_exactly(["Player 1", "Player 3"])
|
||||
assert_object(_pictures(_rows()[0])[0].texture).is_same(Settings.player_array[0].character)
|
||||
|
||||
|
||||
func test_pictures_are_redone_when_points_change() -> void:
|
||||
var song: SongObject = _add_song(SHORT, 4)
|
||||
await _show()
|
||||
song.add_point(Settings.player_array[4].id)
|
||||
|
||||
_rows()[0].update_players()
|
||||
await _runner.simulate_frames(2)
|
||||
|
||||
assert_array(_lines_of_pictures(_rows()[0])).contains_exactly([3, 2])
|
||||
|
||||
|
||||
func test_clicking_a_picture_clicks_the_song() -> void:
|
||||
_add_song(SHORT, 0)
|
||||
_add_song(SHORT, 3)
|
||||
await _show()
|
||||
var signals: GdUnitSignalAssert = assert_signal(monitor_signals(_panel))
|
||||
var picture: TextureRect = _pictures(_rows()[1])[1]
|
||||
|
||||
_runner.set_mouse_position(
|
||||
_panel.get_viewport().get_final_transform() * picture.get_global_rect().get_center())
|
||||
await _runner.simulate_mouse_button_pressed(MOUSE_BUTTON_LEFT)
|
||||
await _runner.await_input_processed()
|
||||
|
||||
await signals.is_emitted("song_clicked", [1])
|
||||
|
||||
|
||||
# Styles
|
||||
|
||||
func test_rows_alternate_and_the_playing_song_stands_out() -> void:
|
||||
for i: int in 4:
|
||||
_add_song(SHORT, 0)
|
||||
Playlist.song_object_array[2].is_playing = true
|
||||
await _show()
|
||||
|
||||
var styles: Array[StringName] = []
|
||||
for row: SongRow in _rows():
|
||||
styles.append(row.theme_type_variation)
|
||||
assert_array(styles).contains_exactly(
|
||||
[&"SongRow", &"SongRowOdd", &"SongRowPlaying", &"SongRowOdd"])
|
||||
assert_str(_rows()[2].song_list_label.theme_type_variation).is_equal("SongRowLabelPlaying")
|
||||
assert_str(_rows()[1].song_list_label.theme_type_variation).is_equal("SongRowLabel")
|
||||
|
||||
|
||||
func test_hovering_a_row_highlights_it_until_the_mouse_leaves() -> void:
|
||||
_add_song(SHORT, 0)
|
||||
_add_song(SHORT, 0)
|
||||
await _show()
|
||||
var row: SongRow = _rows()[1]
|
||||
|
||||
row.mouse_entered.emit()
|
||||
assert_str(row.theme_type_variation).is_equal("SongRowHover")
|
||||
|
||||
row.mouse_exited.emit()
|
||||
assert_str(row.theme_type_variation).is_equal("SongRowOdd")
|
||||
@@ -0,0 +1 @@
|
||||
uid://dnggvrk5thb2v
|
||||
@@ -1,6 +1,7 @@
|
||||
extends MainWindowTestSuite
|
||||
## The Statistics tab: switching between it and the quiz, the soundtrack cards, sorting,
|
||||
## searching, the show filter and the popup with all songs of a soundtrack.
|
||||
## The Statistics tab: switching between it and the quiz, the soundtrack cards, the list
|
||||
## of songs, sorting, searching, the show filter and the popup with all songs of a
|
||||
## soundtrack.
|
||||
|
||||
const STATISTICS: Array[Dictionary] = [
|
||||
{"soundtrack_uuid": "st-1", "soundtrack_name": "Chrono Trigger", "soundtrack_played": 5,
|
||||
@@ -12,6 +13,7 @@ const STATISTICS: Array[Dictionary] = [
|
||||
{"song_name": "To Far Away Times", "times_played": 0},
|
||||
{"song_name": "Schala's Theme", "times_played": 4},
|
||||
{"song_name": "Peaceful Days", "times_played": 1},
|
||||
{"song_name": "Zeal Palace", "times_played": 1},
|
||||
]},
|
||||
{"soundtrack_uuid": "st-2", "soundtrack_name": "Terranigma", "soundtrack_played": 0,
|
||||
"songs": [{"song_name": "Underworld", "times_played": 0}]},
|
||||
@@ -151,20 +153,48 @@ func test_card_shows_plays_last_played_and_most_and_least_played_songs() -> void
|
||||
assert_str(card.last_played_label.text).is_equal("Last played 2026-09-30")
|
||||
assert_array(_visible_texts(card.most_played_list)).contains_exactly(
|
||||
["4 × Schala's Theme", "3 × Frog's Theme", "2 × Battle with Magus"])
|
||||
# Songs never played are left out, so the least played are the least played of those
|
||||
assert_array(_visible_texts(card.least_played_list)).contains_exactly(
|
||||
["0 × To Far Away Times", "0 × Wind Scene", "1 × Corridors of Time"])
|
||||
["1 × Corridors of Time", "1 × Peaceful Days", "1 × Zeal Palace"])
|
||||
assert_bool(card.least_played_header.visible).is_true()
|
||||
|
||||
|
||||
func test_card_of_never_played_soundtrack() -> void:
|
||||
func test_card_of_never_played_soundtrack_lists_no_songs() -> void:
|
||||
await _open_statistics()
|
||||
var card: SoundtrackCard = _card("Terranigma")
|
||||
|
||||
assert_str(card.played_label.text).is_equal("Never played")
|
||||
assert_bool(card.last_played_label.visible).is_false()
|
||||
assert_array(_visible_texts(card.most_played_list)).contains_exactly(["0 × Underworld"])
|
||||
assert_str(card.most_played_header.text).is_equal("No songs played yet")
|
||||
assert_array(_visible_texts(card.most_played_list)).is_empty()
|
||||
assert_bool(card.least_played_header.visible).is_false()
|
||||
|
||||
|
||||
func test_card_with_one_played_song_lists_only_it() -> void:
|
||||
await _open_statistics()
|
||||
var card: SoundtrackCard = _card("Final Fantasy VI")
|
||||
|
||||
assert_str(card.most_played_header.text).is_equal("Most played")
|
||||
assert_array(_visible_texts(card.most_played_list)).contains_exactly(["1 × Dancing Mad"])
|
||||
assert_bool(card.least_played_header.visible).is_false()
|
||||
assert_array(_visible_texts(card.least_played_list)).is_empty()
|
||||
|
||||
|
||||
func test_card_with_fewer_than_six_played_songs_lists_them_all_as_most_played() -> void:
|
||||
var songs: Array[Dictionary] = [{"song_name": "Never", "times_played": 0}]
|
||||
for i: int in 5:
|
||||
songs.append({"song_name": "Song %d" % (i + 1), "times_played": i + 1})
|
||||
_server.respond_json("/statistics/soundtracks", [
|
||||
{"soundtrack_name": "Lufia II", "soundtrack_played": 15, "songs": songs}])
|
||||
await _open_statistics()
|
||||
var card: SoundtrackCard = _card("Lufia II")
|
||||
|
||||
assert_array(_visible_texts(card.most_played_list)).contains_exactly(
|
||||
["5 × Song 5", "4 × Song 4", "3 × Song 3", "2 × Song 2", "1 × Song 1"])
|
||||
assert_bool(card.least_played_header.visible).is_false()
|
||||
assert_array(_visible_texts(card.least_played_list)).is_empty()
|
||||
|
||||
|
||||
func test_card_without_songs() -> void:
|
||||
await _open_statistics()
|
||||
var card: SoundtrackCard = _card("earthbound")
|
||||
@@ -289,6 +319,193 @@ func test_filters_stay_when_statistics_are_loaded_again() -> void:
|
||||
assert_str(_view.cards[0].soundtrack.soundtrack_name).is_equal("Terranigma")
|
||||
|
||||
|
||||
# Songs
|
||||
|
||||
func _show_songs_mode() -> void:
|
||||
await _open_statistics()
|
||||
await _click_at(_center(_view.songs_mode_button))
|
||||
await _runner.simulate_frames(1)
|
||||
|
||||
|
||||
func _listed_songs() -> Array[String]:
|
||||
var listed: Array[String] = []
|
||||
for song: SongInfoForStats in _view.song_list.songs:
|
||||
listed.append(song.song_name)
|
||||
return listed
|
||||
|
||||
|
||||
func _search(text: String) -> void:
|
||||
_view.search_field.text = text
|
||||
_view.apply_filter()
|
||||
|
||||
|
||||
func test_songs_button_shows_every_played_song_a_to_z_instead_of_the_cards() -> void:
|
||||
await _show_songs_mode()
|
||||
|
||||
assert_bool(_view.song_list.visible).is_true()
|
||||
assert_bool(_view.card_scroll.visible).is_false()
|
||||
assert_bool(_view.songs_mode_button.button_pressed).is_true()
|
||||
assert_bool(_view.soundtracks_mode_button.button_pressed).is_false()
|
||||
# Songs never played are left out
|
||||
assert_array(_listed_songs()).contains_exactly([
|
||||
"Battle with Magus", "Corridors of Time", "Dancing Mad", "Frog's Theme",
|
||||
"Peaceful Days", "Schala's Theme", "Zeal Palace",
|
||||
])
|
||||
assert_str(_view.count_label.text).is_equal("7 played songs")
|
||||
assert_str(_view.search_field.placeholder_text).is_equal("Search songs and soundtracks")
|
||||
|
||||
|
||||
func test_song_rows_show_the_song_its_soundtrack_and_plays() -> void:
|
||||
await _show_songs_mode()
|
||||
|
||||
var row: SongStatsRow = _view.song_list.shown_rows()[0]
|
||||
assert_str(row.song_label.get_parsed_text()).is_equal("Battle with Magus")
|
||||
assert_str(row.soundtrack_label.get_parsed_text()).is_equal("Chrono Trigger")
|
||||
assert_str(row.played_label.text).is_equal("2")
|
||||
assert_int(_view.song_list.shown_rows().size()).is_equal(7)
|
||||
|
||||
|
||||
func test_soundtracks_button_shows_the_cards_again() -> void:
|
||||
await _show_songs_mode()
|
||||
|
||||
await _click_at(_center(_view.soundtracks_mode_button))
|
||||
|
||||
assert_bool(_view.card_scroll.visible).is_true()
|
||||
assert_bool(_view.song_list.visible).is_false()
|
||||
assert_str(_view.count_label.text).is_equal("4 soundtracks")
|
||||
assert_bool(_view.show_option.visible).is_true()
|
||||
|
||||
|
||||
func test_song_search_finds_song_names_and_soundtrack_names() -> void:
|
||||
await _show_songs_mode()
|
||||
|
||||
_search("magus")
|
||||
assert_array(_listed_songs()).contains_exactly(["Battle with Magus"])
|
||||
assert_str(_view.count_label.text).is_equal("1 of 7 played songs")
|
||||
|
||||
# Every song of a soundtrack whose name matches
|
||||
_search("final fantasy")
|
||||
assert_array(_listed_songs()).contains_exactly(["Dancing Mad"])
|
||||
|
||||
|
||||
func test_song_search_highlights_what_matched() -> void:
|
||||
await _show_songs_mode()
|
||||
|
||||
_search("magus")
|
||||
|
||||
var row: SongStatsRow = _view.song_list.shown_rows()[0]
|
||||
assert_str(row.song_label.text).contains("[color=")
|
||||
assert_str(row.song_label.get_parsed_text()).is_equal("Battle with Magus")
|
||||
assert_str(row.soundtrack_label.text).not_contains("[color=")
|
||||
|
||||
|
||||
func test_typing_a_song_search_waits_for_a_pause() -> void:
|
||||
await _show_songs_mode()
|
||||
|
||||
_view.search_field.text = "magus"
|
||||
_view.search_field.text_changed.emit("magus")
|
||||
assert_int(_view.song_list.songs.size()).is_equal(7)
|
||||
|
||||
await await_millis(300)
|
||||
assert_array(_listed_songs()).contains_exactly(["Battle with Magus"])
|
||||
|
||||
|
||||
func test_show_menu_is_hidden_for_songs() -> void:
|
||||
await _show_songs_mode()
|
||||
|
||||
assert_bool(_view.show_option.visible).is_false()
|
||||
assert_bool(_view.get_node("Toolbar/ShowLabel").visible).is_false()
|
||||
|
||||
|
||||
func test_song_sort_orders() -> void:
|
||||
await _show_songs_mode()
|
||||
var expected: Dictionary = {
|
||||
StatisticsView.SortOrder.MOST_PLAYED: ["Schala's Theme", "Frog's Theme",
|
||||
"Battle with Magus", "Corridors of Time", "Dancing Mad", "Peaceful Days",
|
||||
"Zeal Palace"],
|
||||
StatisticsView.SortOrder.LEAST_PLAYED: ["Corridors of Time", "Dancing Mad",
|
||||
"Peaceful Days", "Zeal Palace", "Battle with Magus", "Frog's Theme",
|
||||
"Schala's Theme"],
|
||||
StatisticsView.SortOrder.Z_TO_A: ["Zeal Palace", "Schala's Theme", "Peaceful Days",
|
||||
"Frog's Theme", "Dancing Mad", "Corridors of Time", "Battle with Magus"],
|
||||
}
|
||||
for order: StatisticsView.SortOrder in expected:
|
||||
_view.sort_option.select(order)
|
||||
_view.sort_option.item_selected.emit(order)
|
||||
|
||||
assert_array(_listed_songs()).contains_exactly(expected[order])
|
||||
|
||||
|
||||
func test_songs_with_the_same_name_are_sorted_by_soundtrack() -> void:
|
||||
_server.respond_json("/statistics/soundtracks", [
|
||||
{"soundtrack_name": "Lufia II",
|
||||
"songs": [{"song_name": "Main Theme", "times_played": 1}]},
|
||||
{"soundtrack_name": "Earthbound",
|
||||
"songs": [{"song_name": "Main Theme", "times_played": 1}]},
|
||||
])
|
||||
await _show_songs_mode()
|
||||
|
||||
var soundtracks: Array[String] = []
|
||||
for song: SongInfoForStats in _view.song_list.songs:
|
||||
soundtracks.append(song.soundtrack_name)
|
||||
assert_array(soundtracks).contains_exactly(["Earthbound", "Lufia II"])
|
||||
|
||||
|
||||
func test_clicking_a_song_shows_its_soundtrack() -> void:
|
||||
await _show_songs_mode()
|
||||
var popup: SoundtrackStatsPopup = _window.get_node("SoundtrackStatsPopup")
|
||||
_search("dancing")
|
||||
|
||||
await _click_at(_center(_view.song_list.shown_rows()[0]))
|
||||
|
||||
assert_bool(popup.visible).is_true()
|
||||
assert_str(popup.title_label.text).is_equal("Final Fantasy VI")
|
||||
|
||||
|
||||
func test_songs_stay_shown_when_statistics_are_loaded_again() -> void:
|
||||
await _show_songs_mode()
|
||||
_server.respond_json("/statistics/soundtracks", STATISTICS.slice(3, 4))
|
||||
|
||||
_view.refresh()
|
||||
|
||||
assert_bool(_view.song_list.visible).is_true()
|
||||
assert_array(_listed_songs()).contains_exactly(["Dancing Mad"])
|
||||
|
||||
|
||||
func test_only_the_rows_on_screen_are_made_for_many_songs() -> void:
|
||||
var songs: Array[Dictionary] = []
|
||||
for i: int in 5000:
|
||||
songs.append({"song_name": "Song %04d" % i, "times_played": i % 7 + 1})
|
||||
_server.respond_json("/statistics/soundtracks",
|
||||
[{"soundtrack_name": "Big", "songs": songs}])
|
||||
await _show_songs_mode()
|
||||
|
||||
var rows: int = _view.song_list.rows_area.get_child_count()
|
||||
assert_int(rows).is_less(60)
|
||||
assert_str(_view.count_label.text).is_equal("5000 played songs")
|
||||
|
||||
_view.song_list.scroll_to(2500)
|
||||
await _runner.simulate_frames(1)
|
||||
assert_str(_view.song_list.shown_rows()[0].song.song_name).is_equal("Song 2500")
|
||||
assert_int(_view.song_list.rows_area.get_child_count()).is_equal(rows)
|
||||
|
||||
|
||||
func test_mouse_wheel_scrolls_the_songs() -> void:
|
||||
var songs: Array[Dictionary] = []
|
||||
for i: int in 200:
|
||||
songs.append({"song_name": "Song %03d" % i, "times_played": 1})
|
||||
_server.respond_json("/statistics/soundtracks",
|
||||
[{"soundtrack_name": "Big", "songs": songs}])
|
||||
await _show_songs_mode()
|
||||
var transform: Transform2D = _window.get_viewport().get_final_transform()
|
||||
|
||||
_runner.set_mouse_position(transform * _center(_view.song_list.rows_area))
|
||||
await _runner.simulate_mouse_button_pressed(MOUSE_BUTTON_WHEEL_DOWN)
|
||||
await _runner.await_input_processed()
|
||||
|
||||
assert_str(_view.song_list.shown_rows()[0].song.song_name).is_equal("Song 003")
|
||||
|
||||
|
||||
# Popup
|
||||
|
||||
func test_clicking_a_card_shows_all_its_songs_most_played_first() -> void:
|
||||
@@ -300,13 +517,14 @@ func test_clicking_a_card_shows_all_its_songs_most_played_first() -> void:
|
||||
assert_bool(popup.visible).is_true()
|
||||
assert_str(popup.title_label.text).is_equal("Chrono Trigger")
|
||||
assert_str(popup.summary_label.text) \
|
||||
.is_equal("Played 5 times · Last played 2026-09-30 · 7 songs")
|
||||
.is_equal("Played 5 times · Last played 2026-09-30 · 8 songs")
|
||||
var rows: Array[String] = []
|
||||
for row: ListRow in popup.song_list.get_children():
|
||||
rows.append(row.text)
|
||||
assert_array(rows).contains_exactly([
|
||||
"4 × Schala's Theme", "3 × Frog's Theme", "2 × Battle with Magus",
|
||||
"1 × Corridors of Time", "1 × Peaceful Days", "0 × To Far Away Times", "0 × Wind Scene",
|
||||
"1 × Corridors of Time", "1 × Peaceful Days", "1 × Zeal Palace",
|
||||
"0 × To Far Away Times", "0 × Wind Scene",
|
||||
])
|
||||
|
||||
|
||||
|
||||
@@ -32,6 +32,17 @@ const STYLES: Dictionary = {
|
||||
"HighlightMedium": "Label",
|
||||
"SongRowLabel": "Label",
|
||||
"SongRowLabelPlaying": "Label",
|
||||
"SongRowOdd": "PanelContainer",
|
||||
"SongRowHover": "PanelContainer",
|
||||
"SongRowPlaying": "PanelContainer",
|
||||
"SongRowPictureLine": "HBoxContainer",
|
||||
"SongRowPictureLines": "VBoxContainer",
|
||||
"PlayerBar": "PanelContainer",
|
||||
"PlayerButton": "Button",
|
||||
"PlayerButtonPrimary": "Button",
|
||||
"PlayerTime": "Label",
|
||||
"ProgressSlider": "HSlider",
|
||||
"VolumeBar": "HSlider",
|
||||
"StatisticsCard": "PanelContainer",
|
||||
"StatisticsCardHover": "PanelContainer",
|
||||
"StatisticsGrid": "GridContainer",
|
||||
@@ -39,6 +50,10 @@ const STYLES: Dictionary = {
|
||||
"LargeButton": "Button",
|
||||
"SearchField": "LineEdit",
|
||||
"SearchResult": "RichTextLabel",
|
||||
"ListRowHover": "PanelContainer",
|
||||
"SongStatsHeader": "PanelContainer",
|
||||
"SongStatsText": "RichTextLabel",
|
||||
"SongStatsSecondary": "RichTextLabel",
|
||||
}
|
||||
|
||||
## Theme types that only scripts read from, so they are not styles a node can pick.
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
extends GdUnitTestSuite
|
||||
## ButtonBounce: buttons grow under the mouse, shrink while held down, pop when asked to,
|
||||
## and always around their middle.
|
||||
|
||||
var _button: Button
|
||||
|
||||
|
||||
func before_test() -> void:
|
||||
_button = auto_free(Button.new())
|
||||
_button.size = Vector2(40, 40)
|
||||
add_child(_button)
|
||||
ButtonBounce.add_to(_button)
|
||||
|
||||
|
||||
func _settle() -> void:
|
||||
await await_millis(int(maxf(ButtonBounce.DURATION, ButtonBounce.POP_DURATION) * 1000) + 80)
|
||||
|
||||
|
||||
func test_starts_at_its_normal_size_scaled_around_its_middle() -> void:
|
||||
assert_vector(_button.scale).is_equal(Vector2.ONE)
|
||||
assert_vector(_button.pivot_offset).is_equal(Vector2(20, 20))
|
||||
|
||||
|
||||
func test_pivot_follows_the_size() -> void:
|
||||
_button.size = Vector2(60, 30)
|
||||
|
||||
assert_vector(_button.pivot_offset).is_equal(Vector2(30, 15))
|
||||
|
||||
|
||||
func test_grows_when_the_mouse_enters_and_shrinks_back_when_it_leaves() -> void:
|
||||
_button.mouse_entered.emit()
|
||||
await _settle()
|
||||
assert_vector(_button.scale).is_equal_approx(ButtonBounce.HOVER_SCALE, Vector2(0.01, 0.01))
|
||||
|
||||
_button.mouse_exited.emit()
|
||||
await _settle()
|
||||
assert_vector(_button.scale).is_equal_approx(Vector2.ONE, Vector2(0.01, 0.01))
|
||||
|
||||
|
||||
func test_shrinks_while_held_down() -> void:
|
||||
_button.button_down.emit()
|
||||
await _settle()
|
||||
|
||||
assert_vector(_button.scale).is_equal_approx(ButtonBounce.PRESSED_SCALE, Vector2(0.01, 0.01))
|
||||
|
||||
|
||||
func test_goes_back_to_normal_when_let_go_away_from_the_mouse() -> void:
|
||||
_button.button_down.emit()
|
||||
await _settle()
|
||||
|
||||
_button.button_up.emit()
|
||||
await _settle()
|
||||
|
||||
assert_vector(_button.scale).is_equal_approx(Vector2.ONE, Vector2(0.01, 0.01))
|
||||
|
||||
|
||||
func test_disabled_button_does_not_grow() -> void:
|
||||
_button.disabled = true
|
||||
|
||||
_button.mouse_entered.emit()
|
||||
await _settle()
|
||||
|
||||
assert_vector(_button.scale).is_equal(Vector2.ONE)
|
||||
|
||||
|
||||
func test_pop_starts_small_and_springs_back() -> void:
|
||||
ButtonBounce.pop(_button)
|
||||
|
||||
assert_vector(_button.scale).is_equal(ButtonBounce.POP_SCALE)
|
||||
await _settle()
|
||||
assert_vector(_button.scale).is_equal_approx(Vector2.ONE, Vector2(0.01, 0.01))
|
||||
|
||||
|
||||
func test_a_new_animation_stops_the_one_before() -> void:
|
||||
_button.mouse_entered.emit()
|
||||
_button.button_down.emit()
|
||||
await _settle()
|
||||
|
||||
# Only the last one ran to the end
|
||||
assert_vector(_button.scale).is_equal_approx(ButtonBounce.PRESSED_SCALE, Vector2(0.01, 0.01))
|
||||
@@ -0,0 +1 @@
|
||||
uid://b8tyllgau53y6
|
||||
+11
-2
@@ -35,9 +35,12 @@ inspector or `theme_type_variation` in code:
|
||||
| `DarkSpacer` | Panel | The dark gaps in the player list and the add player row |
|
||||
| `CharacterCard` | Panel | A character in the character select popup |
|
||||
| `MusicList` | PanelContainer | The list of songs in the quiz |
|
||||
| `SongRow` | PanelContainer | One song in that list |
|
||||
| `SongRow`, `SongRowOdd` | PanelContainer | Alternating songs in that list |
|
||||
| `SongRowHover`, `SongRowPlaying` | PanelContainer | The song under the mouse, and the one that is playing |
|
||||
| `SongRowPictureLines`, `SongRowPictureLine` | VBoxContainer, HBoxContainer | The space between the lines of player pictures on a song, and between the pictures |
|
||||
| `ShortcutRowDark` | PanelContainer | Every other row in the shortcuts popup |
|
||||
| `ListRowEven`, `ListRowOdd` | PanelContainer | Alternating rows in the inspiration and sync lists |
|
||||
| `ListRowEven`, `ListRowOdd` | PanelContainer | Alternating rows in the inspiration, sync and statistics song lists |
|
||||
| `ListRowHover` | PanelContainer | A row in the statistics song list under the mouse |
|
||||
| `TitleLarge` | Label | The soundtrack that is playing (50) |
|
||||
| `TitleMedium` | Label | The song that is playing, and the winner (35) |
|
||||
| `Title` | Label | The window title and popup titles (25) |
|
||||
@@ -48,6 +51,12 @@ inspector or `theme_type_variation` in code:
|
||||
| `TabButton` | Button | The Quiz and Statistics tabs; the chosen one is orange |
|
||||
| `StatisticsCard`, `StatisticsCardHover` | PanelContainer | A soundtrack card in the statistics, and the one under the mouse |
|
||||
| `StatisticsGrid` | GridContainer | The grid of soundtrack cards, with the space between them |
|
||||
| `PlayerBar` | PanelContainer | The music player's rounded bar |
|
||||
| `PlayerButtonPrimary`, `PlayerButton` | Button | The round play button, and the restart button; the icon's colour on hover comes from here |
|
||||
| `ProgressSlider`, `VolumeBar` | HSlider | How far the song has played (orange), and the volume |
|
||||
| `PlayerTime` | Label | The time played and the song's length |
|
||||
| `SongStatsHeader` | PanelContainer | The column titles above the statistics song list, spaced like its rows |
|
||||
| `SongStatsText`, `SongStatsSecondary` | RichTextLabel | A song's name and its soundtrack (dimmer) in the statistics song list |
|
||||
|
||||
**Palette**: colours used from code.
|
||||
|
||||
|
||||
+272
-7
@@ -282,6 +282,23 @@ corner_radius_top_right = 4
|
||||
corner_radius_bottom_right = 4
|
||||
corner_radius_bottom_left = 4
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_listrowhover"]
|
||||
content_margin_left = 8.0
|
||||
content_margin_top = 2.0
|
||||
content_margin_right = 8.0
|
||||
content_margin_bottom = 2.0
|
||||
bg_color = Color(1, 1, 1, 0.2)
|
||||
corner_radius_top_left = 4
|
||||
corner_radius_top_right = 4
|
||||
corner_radius_bottom_right = 4
|
||||
corner_radius_bottom_left = 4
|
||||
|
||||
[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_songstatsheader"]
|
||||
content_margin_left = 8.0
|
||||
content_margin_top = 2.0
|
||||
content_margin_right = 8.0
|
||||
content_margin_bottom = 2.0
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_mxy8k"]
|
||||
content_margin_left = 8.0
|
||||
content_margin_top = 2.0
|
||||
@@ -294,7 +311,15 @@ corner_radius_bottom_right = 4
|
||||
corner_radius_bottom_left = 4
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_33f1l"]
|
||||
content_margin_left = 6.0
|
||||
content_margin_top = 6.0
|
||||
content_margin_right = 6.0
|
||||
content_margin_bottom = 6.0
|
||||
bg_color = Color(0.257813, 0.257813, 0.257813, 1)
|
||||
corner_radius_top_left = 6
|
||||
corner_radius_top_right = 6
|
||||
corner_radius_bottom_right = 6
|
||||
corner_radius_bottom_left = 6
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_wbnll"]
|
||||
content_margin_left = 8.0
|
||||
@@ -564,13 +589,50 @@ corner_radius_bottom_left = 3
|
||||
corner_detail = 5
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_oxgwg"]
|
||||
bg_color = Color(0.180392, 0.180392, 0.180392, 1)
|
||||
content_margin_left = 8.0
|
||||
content_margin_top = 4.0
|
||||
content_margin_right = 8.0
|
||||
content_margin_bottom = 4.0
|
||||
bg_color = Color(1, 1, 1, 0.05)
|
||||
corner_radius_top_left = 4
|
||||
corner_radius_top_right = 4
|
||||
corner_radius_bottom_right = 4
|
||||
corner_radius_bottom_left = 4
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_r1nnu"]
|
||||
bg_color = Color(0.180392, 0.180392, 0.180392, 1)
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_songrowodd"]
|
||||
content_margin_left = 8.0
|
||||
content_margin_top = 4.0
|
||||
content_margin_right = 8.0
|
||||
content_margin_bottom = 4.0
|
||||
bg_color = Color(1, 1, 1, 0.11)
|
||||
corner_radius_top_left = 4
|
||||
corner_radius_top_right = 4
|
||||
corner_radius_bottom_right = 4
|
||||
corner_radius_bottom_left = 4
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_lkqs8"]
|
||||
bg_color = Color(0.180392, 0.180392, 0.180392, 1)
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_songrowhover"]
|
||||
content_margin_left = 8.0
|
||||
content_margin_top = 4.0
|
||||
content_margin_right = 8.0
|
||||
content_margin_bottom = 4.0
|
||||
bg_color = Color(1, 1, 1, 0.2)
|
||||
corner_radius_top_left = 4
|
||||
corner_radius_top_right = 4
|
||||
corner_radius_bottom_right = 4
|
||||
corner_radius_bottom_left = 4
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_songrowplaying"]
|
||||
content_margin_left = 8.0
|
||||
content_margin_top = 4.0
|
||||
content_margin_right = 8.0
|
||||
content_margin_bottom = 4.0
|
||||
bg_color = Color(1, 0.5, 0, 0.12)
|
||||
border_width_left = 4
|
||||
border_color = Color(1, 0.5, 0, 1)
|
||||
corner_radius_top_left = 4
|
||||
corner_radius_top_right = 4
|
||||
corner_radius_bottom_right = 4
|
||||
corner_radius_bottom_left = 4
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_1tv07"]
|
||||
content_margin_left = 4.0
|
||||
@@ -757,6 +819,158 @@ corner_radius_top_right = 6
|
||||
corner_radius_bottom_right = 6
|
||||
corner_radius_bottom_left = 6
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_playerbar"]
|
||||
content_margin_left = 6.0
|
||||
content_margin_top = 4.0
|
||||
content_margin_right = 14.0
|
||||
content_margin_bottom = 4.0
|
||||
bg_color = Color(0.16, 0.16, 0.16, 1)
|
||||
corner_radius_top_left = 8
|
||||
corner_radius_top_right = 8
|
||||
corner_radius_bottom_right = 8
|
||||
corner_radius_bottom_left = 8
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_playerbutton"]
|
||||
content_margin_left = 8.0
|
||||
content_margin_top = 8.0
|
||||
content_margin_right = 8.0
|
||||
content_margin_bottom = 8.0
|
||||
bg_color = Color(1, 1, 1, 0)
|
||||
corner_radius_top_left = 17
|
||||
corner_radius_top_right = 17
|
||||
corner_radius_bottom_right = 17
|
||||
corner_radius_bottom_left = 17
|
||||
corner_detail = 12
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_playerbuttonhover"]
|
||||
content_margin_left = 8.0
|
||||
content_margin_top = 8.0
|
||||
content_margin_right = 8.0
|
||||
content_margin_bottom = 8.0
|
||||
bg_color = Color(1, 1, 1, 0.12)
|
||||
corner_radius_top_left = 17
|
||||
corner_radius_top_right = 17
|
||||
corner_radius_bottom_right = 17
|
||||
corner_radius_bottom_left = 17
|
||||
corner_detail = 12
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_playerbuttonpressed"]
|
||||
content_margin_left = 8.0
|
||||
content_margin_top = 8.0
|
||||
content_margin_right = 8.0
|
||||
content_margin_bottom = 8.0
|
||||
bg_color = Color(1, 1, 1, 0.2)
|
||||
corner_radius_top_left = 17
|
||||
corner_radius_top_right = 17
|
||||
corner_radius_bottom_right = 17
|
||||
corner_radius_bottom_left = 17
|
||||
corner_detail = 12
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_playerprimary"]
|
||||
content_margin_left = 9.0
|
||||
content_margin_top = 9.0
|
||||
content_margin_right = 9.0
|
||||
content_margin_bottom = 9.0
|
||||
bg_color = Color(1, 0.5, 0, 1)
|
||||
corner_radius_top_left = 17
|
||||
corner_radius_top_right = 17
|
||||
corner_radius_bottom_right = 17
|
||||
corner_radius_bottom_left = 17
|
||||
corner_detail = 12
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_playerprimaryhover"]
|
||||
content_margin_left = 9.0
|
||||
content_margin_top = 9.0
|
||||
content_margin_right = 9.0
|
||||
content_margin_bottom = 9.0
|
||||
bg_color = Color(1, 0.6, 0.18, 1)
|
||||
corner_radius_top_left = 17
|
||||
corner_radius_top_right = 17
|
||||
corner_radius_bottom_right = 17
|
||||
corner_radius_bottom_left = 17
|
||||
corner_detail = 12
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_playerprimarypressed"]
|
||||
content_margin_left = 9.0
|
||||
content_margin_top = 9.0
|
||||
content_margin_right = 9.0
|
||||
content_margin_bottom = 9.0
|
||||
bg_color = Color(0.82, 0.4, 0, 1)
|
||||
corner_radius_top_left = 17
|
||||
corner_radius_top_right = 17
|
||||
corner_radius_bottom_right = 17
|
||||
corner_radius_bottom_left = 17
|
||||
corner_detail = 12
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_playerprimarydisabled"]
|
||||
content_margin_left = 9.0
|
||||
content_margin_top = 9.0
|
||||
content_margin_right = 9.0
|
||||
content_margin_bottom = 9.0
|
||||
bg_color = Color(0.4, 0.4, 0.4, 1)
|
||||
corner_radius_top_left = 17
|
||||
corner_radius_top_right = 17
|
||||
corner_radius_bottom_right = 17
|
||||
corner_radius_bottom_left = 17
|
||||
corner_detail = 12
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_playertrack"]
|
||||
content_margin_left = 0.0
|
||||
content_margin_top = 3.0
|
||||
content_margin_right = 0.0
|
||||
content_margin_bottom = 3.0
|
||||
bg_color = Color(0, 0, 0, 0.35)
|
||||
corner_radius_top_left = 3
|
||||
corner_radius_top_right = 3
|
||||
corner_radius_bottom_right = 3
|
||||
corner_radius_bottom_left = 3
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_progressfill"]
|
||||
content_margin_left = 0.0
|
||||
content_margin_top = 3.0
|
||||
content_margin_right = 0.0
|
||||
content_margin_bottom = 3.0
|
||||
bg_color = Color(1, 0.5, 0, 1)
|
||||
corner_radius_top_left = 3
|
||||
corner_radius_top_right = 3
|
||||
corner_radius_bottom_right = 3
|
||||
corner_radius_bottom_left = 3
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_progressfillhover"]
|
||||
content_margin_left = 0.0
|
||||
content_margin_top = 3.0
|
||||
content_margin_right = 0.0
|
||||
content_margin_bottom = 3.0
|
||||
bg_color = Color(1, 0.62, 0.22, 1)
|
||||
corner_radius_top_left = 3
|
||||
corner_radius_top_right = 3
|
||||
corner_radius_bottom_right = 3
|
||||
corner_radius_bottom_left = 3
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_volumefill"]
|
||||
content_margin_left = 0.0
|
||||
content_margin_top = 3.0
|
||||
content_margin_right = 0.0
|
||||
content_margin_bottom = 3.0
|
||||
bg_color = Color(0.8, 0.8, 0.8, 1)
|
||||
corner_radius_top_left = 3
|
||||
corner_radius_top_right = 3
|
||||
corner_radius_bottom_right = 3
|
||||
corner_radius_bottom_left = 3
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_volumefillhover"]
|
||||
content_margin_left = 0.0
|
||||
content_margin_top = 3.0
|
||||
content_margin_right = 0.0
|
||||
content_margin_bottom = 3.0
|
||||
bg_color = Color(1, 1, 1, 1)
|
||||
corner_radius_top_left = 3
|
||||
corner_radius_top_right = 3
|
||||
corner_radius_bottom_right = 3
|
||||
corner_radius_bottom_left = 3
|
||||
|
||||
[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_playerfocus"]
|
||||
|
||||
[resource]
|
||||
default_font = ExtResource("1_default_font")
|
||||
Button/colors/font_color = Color(0.875, 0.875, 0.875, 1)
|
||||
@@ -844,6 +1058,8 @@ LargeButton/base_type = &"Button"
|
||||
LargeButton/font_sizes/font_size = 24
|
||||
ListRowEven/base_type = &"PanelContainer"
|
||||
ListRowEven/styles/panel = SubResource("StyleBoxFlat_u5sc5")
|
||||
ListRowHover/base_type = &"PanelContainer"
|
||||
ListRowHover/styles/panel = SubResource("StyleBoxFlat_listrowhover")
|
||||
ListRowOdd/base_type = &"PanelContainer"
|
||||
ListRowOdd/styles/panel = SubResource("StyleBoxFlat_mxy8k")
|
||||
MarginContainer/constants/margin_bottom = 0
|
||||
@@ -876,6 +1092,32 @@ OptionButton/styles/pressed_mirrored = SubResource("StyleBoxFlat_a0ksy")
|
||||
Palette/colors/backdrop = Color(0, 0, 0, 0.55)
|
||||
Panel/styles/panel = SubResource("StyleBoxFlat_hwwyi")
|
||||
PanelContainer/styles/panel = SubResource("StyleBoxFlat_wl3gy")
|
||||
PlayerBar/base_type = &"PanelContainer"
|
||||
PlayerBar/styles/panel = SubResource("StyleBoxFlat_playerbar")
|
||||
PlayerButton/base_type = &"Button"
|
||||
PlayerButton/colors/icon_hover_color = Color(1, 0.5, 0, 1)
|
||||
PlayerButton/colors/icon_hover_pressed_color = Color(1, 0.5, 0, 1)
|
||||
PlayerButton/colors/icon_normal_color = Color(0.88, 0.88, 0.88, 1)
|
||||
PlayerButton/colors/icon_pressed_color = Color(1, 0.6, 0.2, 1)
|
||||
PlayerButton/styles/focus = SubResource("StyleBoxEmpty_playerfocus")
|
||||
PlayerButton/styles/hover = SubResource("StyleBoxFlat_playerbuttonhover")
|
||||
PlayerButton/styles/hover_pressed = SubResource("StyleBoxFlat_playerbuttonpressed")
|
||||
PlayerButton/styles/normal = SubResource("StyleBoxFlat_playerbutton")
|
||||
PlayerButton/styles/pressed = SubResource("StyleBoxFlat_playerbuttonpressed")
|
||||
PlayerButtonPrimary/base_type = &"Button"
|
||||
PlayerButtonPrimary/colors/icon_hover_color = Color(1, 1, 1, 1)
|
||||
PlayerButtonPrimary/colors/icon_hover_pressed_color = Color(1, 1, 1, 1)
|
||||
PlayerButtonPrimary/colors/icon_normal_color = Color(1, 1, 1, 1)
|
||||
PlayerButtonPrimary/colors/icon_pressed_color = Color(1, 1, 1, 1)
|
||||
PlayerButtonPrimary/styles/disabled = SubResource("StyleBoxFlat_playerprimarydisabled")
|
||||
PlayerButtonPrimary/styles/focus = SubResource("StyleBoxEmpty_playerfocus")
|
||||
PlayerButtonPrimary/styles/hover = SubResource("StyleBoxFlat_playerprimaryhover")
|
||||
PlayerButtonPrimary/styles/hover_pressed = SubResource("StyleBoxFlat_playerprimarypressed")
|
||||
PlayerButtonPrimary/styles/normal = SubResource("StyleBoxFlat_playerprimary")
|
||||
PlayerButtonPrimary/styles/pressed = SubResource("StyleBoxFlat_playerprimarypressed")
|
||||
PlayerTime/base_type = &"Label"
|
||||
PlayerTime/colors/font_color = Color(0.82, 0.82, 0.82, 1)
|
||||
PlayerTime/font_sizes/font_size = 18
|
||||
PopupBackground/base_type = &"Panel"
|
||||
PopupBackground/styles/panel = SubResource("StyleBoxFlat_vputg")
|
||||
PopupMenu/colors/font_accelerator_color = Color(0.7, 0.7, 0.7, 0.8)
|
||||
@@ -926,6 +1168,10 @@ ScrollContainer/colors/scroll_hint_horizontal_color = Color(0, 0, 0, 1)
|
||||
ScrollContainer/colors/scroll_hint_vertical_color = Color(0, 0, 0, 1)
|
||||
ScrollContainer/styles/focus = SubResource("StyleBoxFlat_35dwi")
|
||||
ScrollContainer/styles/panel = SubResource("StyleBoxEmpty_r3gy8")
|
||||
ProgressSlider/base_type = &"HSlider"
|
||||
ProgressSlider/styles/grabber_area = SubResource("StyleBoxFlat_progressfill")
|
||||
ProgressSlider/styles/grabber_area_highlight = SubResource("StyleBoxFlat_progressfillhover")
|
||||
ProgressSlider/styles/slider = SubResource("StyleBoxFlat_playertrack")
|
||||
SearchField/base_type = &"LineEdit"
|
||||
SearchField/font_sizes/font_size = 28
|
||||
SearchResult/base_type = &"RichTextLabel"
|
||||
@@ -936,13 +1182,28 @@ ShortcutRowDark/base_type = &"PanelContainer"
|
||||
ShortcutRowDark/styles/panel = SubResource("StyleBoxFlat_anjdx")
|
||||
SongRow/base_type = &"PanelContainer"
|
||||
SongRow/styles/panel = SubResource("StyleBoxFlat_oxgwg")
|
||||
SongRowHover/base_type = &"PanelContainer"
|
||||
SongRowHover/styles/panel = SubResource("StyleBoxFlat_songrowhover")
|
||||
SongRowLabel/base_type = &"Label"
|
||||
SongRowLabel/font_sizes/font_size = 20
|
||||
SongRowLabel/styles/normal = SubResource("StyleBoxFlat_r1nnu")
|
||||
SongRowLabelPlaying/base_type = &"Label"
|
||||
SongRowLabelPlaying/colors/font_color = Color(1, 0.5, 0, 1)
|
||||
SongRowLabelPlaying/font_sizes/font_size = 20
|
||||
SongRowLabelPlaying/styles/normal = SubResource("StyleBoxFlat_lkqs8")
|
||||
SongRowOdd/base_type = &"PanelContainer"
|
||||
SongRowOdd/styles/panel = SubResource("StyleBoxFlat_songrowodd")
|
||||
SongRowPictureLine/base_type = &"HBoxContainer"
|
||||
SongRowPictureLine/constants/separation = 4
|
||||
SongRowPictureLines/base_type = &"VBoxContainer"
|
||||
SongRowPictureLines/constants/separation = 4
|
||||
SongRowPlaying/base_type = &"PanelContainer"
|
||||
SongRowPlaying/styles/panel = SubResource("StyleBoxFlat_songrowplaying")
|
||||
SongStatsHeader/base_type = &"PanelContainer"
|
||||
SongStatsHeader/styles/panel = SubResource("StyleBoxEmpty_songstatsheader")
|
||||
SongStatsSecondary/base_type = &"RichTextLabel"
|
||||
SongStatsSecondary/colors/default_color = Color(0.7, 0.7, 0.7, 1)
|
||||
SongStatsSecondary/font_sizes/normal_font_size = 18
|
||||
SongStatsText/base_type = &"RichTextLabel"
|
||||
SongStatsText/font_sizes/normal_font_size = 18
|
||||
StatisticsCard/base_type = &"PanelContainer"
|
||||
StatisticsCard/styles/panel = SubResource("StyleBoxFlat_statscard")
|
||||
StatisticsCardHover/base_type = &"PanelContainer"
|
||||
@@ -1000,5 +1261,9 @@ VScrollBar/styles/scroll = SubResource("StyleBoxFlat_6fedo")
|
||||
VScrollBar/styles/scroll_focus = SubResource("StyleBoxFlat_8sdcv")
|
||||
VSeparator/constants/separation = 4
|
||||
VSeparator/styles/separator = SubResource("StyleBoxLine_r3erx")
|
||||
VolumeBar/base_type = &"HSlider"
|
||||
VolumeBar/styles/grabber_area = SubResource("StyleBoxFlat_volumefill")
|
||||
VolumeBar/styles/grabber_area_highlight = SubResource("StyleBoxFlat_volumefillhover")
|
||||
VolumeBar/styles/slider = SubResource("StyleBoxFlat_playertrack")
|
||||
WindowBackground/base_type = &"Panel"
|
||||
WindowBackground/styles/panel = SubResource("StyleBoxFlat_2mdtn")
|
||||
Reference in new issue
Block a user