Polish title online and save slot interfaces

This commit is contained in:
Alexander Sellite 2026-08-23 23:04:15 -04:00
parent a3aea98982
commit 867fd431af
8 changed files with 463 additions and 86 deletions

View file

@ -30,8 +30,8 @@ enabled=PackedStringArray("res://addons/netfishing_shoreline_baker/plugin.cfg")
[gui]
theme/custom="res://ui/game_theme.tres"
timers/tooltip_delay_sec=0.0
theme/custom="res://ui/game_theme.tres"
[input]

View file

@ -130,7 +130,28 @@ func _validate_primary_menu_navigation() -> void:
and not (title.get_node("%DeleteSaveButton") as Control).visible,
"Legacy New/Delete title bubbles are still visible.",
)
_assert_directionally_reachable(title_controls.front(), title_controls)
_assert_neighbor(
title.get_node("%JoinGameButton") as Control,
&"focus_neighbor_right",
play_button,
)
_assert_neighbor(
title.get_node("%SettingsButton") as Control,
&"focus_neighbor_left",
play_button,
)
_assert_neighbor(
title.get_node("%CreditsButton") as Control,
&"focus_neighbor_right",
play_button,
)
_assert_neighbor(
title.get_node("%QuitButton") as Control,
&"focus_neighbor_left",
play_button,
)
for title_control: Control in title_controls:
_assert_directionally_reachable(title_control, title_controls)
title.queue_free()
await process_frame
@ -195,6 +216,13 @@ func _validate_save_slots_navigation() -> void:
page.call("_select_page", &"saves", false)
await process_frame
var import_button := page.get_node("%ImportSlotButton") as Button
var actions := page.get_node("%Actions") as VBoxContainer
var secondary_actions := page.get_node("%SecondaryActions") as GridContainer
var play_slot := page.get_node("%PlaySlotButton") as Button
var rename_slot := page.get_node("%RenameSlotButton") as Button
var duplicate_slot := page.get_node("%DuplicateSlotButton") as Button
var export_slot := page.get_node("%ExportSlotButton") as Button
var delete_slot := page.get_node("%DeleteSlotButton") as Button
_expect(
saves_tab.find_valid_focus_neighbor(SIDE_BOTTOM) == import_button,
"An empty Save Slots page does not lead from its tab to Import Save.",
@ -204,6 +232,29 @@ func _validate_save_slots_navigation() -> void:
== page.get_node("%BackButton"),
"An empty Save Slots page does not lead from Import Save to Back.",
)
_expect(
is_equal_approx(play_slot.size.x, actions.size.x),
"Save-slot Play does not span the full action width.",
)
_expect(
secondary_actions.columns == 2
and is_equal_approx(rename_slot.size.x, duplicate_slot.size.x)
and is_equal_approx(export_slot.size.x, delete_slot.size.x)
and is_equal_approx(rename_slot.position.y, duplicate_slot.position.y)
and is_equal_approx(export_slot.position.y, delete_slot.position.y)
and export_slot.position.y > rename_slot.position.y,
"Save-slot secondary actions are not arranged as a 2 by 2 grid.",
)
var play_style := play_slot.get_theme_stylebox("normal") as StyleBoxFlat
_expect(
play_style != null and play_style.bg_color == UtilityPageStyle.GREEN,
"Save-slot Play does not use the green primary-action style.",
)
_assert_neighbor(play_slot, &"focus_neighbor_bottom", rename_slot)
_assert_neighbor(rename_slot, &"focus_neighbor_right", duplicate_slot)
_assert_neighbor(rename_slot, &"focus_neighbor_bottom", export_slot)
_assert_neighbor(duplicate_slot, &"focus_neighbor_bottom", delete_slot)
_assert_neighbor(export_slot, &"focus_neighbor_right", delete_slot)
page.queue_free()
await process_frame
@ -222,6 +273,9 @@ func _validate_join_game_navigation() -> void:
var address := page.get_node("%Address") as LineEdit
var name_edit := page.get_node("%NameEdit") as LineEdit
var server_list := page.get_node("%ServerList") as ItemList
var presence := page.get_node("%PresenceButton") as Button
var room_open := page.get_node("%RoomOpenButton") as Button
var room_listing := page.get_node("%RoomListingButton") as Button
var refresh := page.get_node("%RefreshButton") as Button
var join := page.get_node("%JoinButton") as Button
var save := page.get_node("%SaveButton") as Button
@ -233,6 +287,7 @@ func _validate_join_game_navigation() -> void:
var direct_content := page.get_node("%DirectContent") as Control
var list_content := page.get_node("%ListContent") as Control
var content_panel := page.get_node("%ContentPanel") as PanelContainer
var actions := page.get_node("%Actions") as HBoxContainer
var modes: Array[Control] = [discover, friends, direct, saved, recent]
var tab_overlap: float = (
discover.get_global_rect().end.y
@ -265,9 +320,36 @@ func _validate_join_game_navigation() -> void:
_assert_neighbor(refresh, &"focus_neighbor_right", join)
_assert_neighbor(join, &"focus_neighbor_bottom", back)
_assert_neighbor(back, &"focus_neighbor_top", join)
_expect(
is_equal_approx(
join.get_global_rect().end.x,
actions.get_global_rect().end.x,
),
"Discover Refresh and Join are not right-justified.",
)
var discover_controls: Array[Control] = modes.duplicate()
discover_controls.append_array([server_list, refresh, join, back])
_assert_directionally_reachable(discover, discover_controls)
room_open.show()
room_listing.show()
_set_button_state(room_open, true)
_set_button_state(room_listing, true)
page.call("_configure_controller_navigation")
await process_frame
_assert_neighbor(room_open, &"focus_neighbor_right", room_listing)
_assert_neighbor(room_open, &"focus_neighbor_left", room_open)
_assert_neighbor(room_listing, &"focus_neighbor_left", room_open)
_assert_neighbor(room_listing, &"focus_neighbor_bottom", server_list)
var hosted_discover_controls: Array[Control] = modes.duplicate()
hosted_discover_controls.append_array([
room_open,
room_listing,
server_list,
refresh,
join,
back,
])
_assert_directionally_reachable(discover, hosted_discover_controls)
var rooms: Array[Dictionary] = [
{
"room_id": "controller-default-room",
@ -293,16 +375,36 @@ func _validate_join_game_navigation() -> void:
)
page.set("_mode", JoinGamePage.Mode.FRIENDS)
(page.get_node("%OnlineControls") as Control).hide()
presence.show()
page.call("_configure_controller_navigation")
await process_frame
_assert_neighbor(friends, &"focus_neighbor_bottom", server_list)
_assert_neighbor(server_list, &"focus_neighbor_top", friends)
_assert_neighbor(server_list, &"focus_neighbor_bottom", presence)
_assert_neighbor(presence, &"focus_neighbor_right", refresh)
_assert_neighbor(refresh, &"focus_neighbor_right", join)
_expect(
is_equal_approx(
presence.get_global_rect().position.x,
actions.get_global_rect().position.x,
),
"Friend presence is not left-justified in the action row.",
)
_expect(
is_equal_approx(
join.get_global_rect().end.x,
actions.get_global_rect().end.x,
),
"Friends Refresh and Join are not right-justified.",
)
var friend_controls: Array[Control] = modes.duplicate()
friend_controls.append_array([server_list, refresh, join, back])
friend_controls.append_array([server_list, presence, refresh, join, back])
_assert_directionally_reachable(friends, friend_controls)
list_content.hide()
direct_content.show()
presence.hide()
address.show()
address.editable = true
server_list.hide()

View file

@ -32,6 +32,10 @@ const DIRECT_WORKFLOW_HELP: String = (
@onready var _direct_content: Control = %DirectContent
@onready var _list_content: Control = %ListContent
@onready var _list_title: Label = %ListTitle
@onready var _online_controls: Control = %OnlineControls
@onready var _presence_button: Button = %PresenceButton
@onready var _room_open_button: Button = %RoomOpenButton
@onready var _room_listing_button: Button = %RoomListingButton
@onready var _details_panel: PanelContainer = %DetailsPanel
@onready var _address: LineEdit = %Address
@onready var _address_label: Label = %AddressLabel
@ -89,6 +93,9 @@ func _ready() -> void:
_direct_button.pressed.connect(_set_mode.bind(Mode.DIRECT))
_saved_button.pressed.connect(_set_mode.bind(Mode.SAVED))
_recent_button.pressed.connect(_set_mode.bind(Mode.RECENT))
_presence_button.pressed.connect(_on_presence_pressed)
_room_open_button.pressed.connect(_on_room_open_pressed)
_room_listing_button.pressed.connect(_on_room_listing_pressed)
_refresh_button.pressed.connect(_request_discovery_refresh)
_join_button.pressed.connect(_request_join)
_save_button.pressed.connect(_on_save_pressed)
@ -238,6 +245,10 @@ func setup(
_on_peer_count_changed
):
_network_session.peer_count_changed.connect(_on_peer_count_changed)
if not _network_session.host_openness_changed.is_connected(
_on_host_openness_changed
):
_network_session.host_openness_changed.connect(_on_host_openness_changed)
if (
_saved_servers != null
and not _saved_servers.data_changed.is_connected(_on_store_changed)
@ -278,6 +289,22 @@ func setup(
_discovery.social_status_changed.connect(
_on_social_status_changed
)
if not _discovery.host_settings_changed.is_connected(
_on_host_settings_changed
):
_discovery.host_settings_changed.connect(
_on_host_settings_changed
)
if not _discovery.host_status_changed.is_connected(
_on_host_status_changed
):
_discovery.host_status_changed.connect(_on_host_status_changed)
if not _discovery.presence_sharing_changed.is_connected(
_on_presence_sharing_changed
):
_discovery.presence_sharing_changed.connect(
_on_presence_sharing_changed
)
_friend_entries = _discovery.get_friend_presence()
_refresh()
@ -792,6 +819,7 @@ func _refresh() -> void:
_direct_content.visible = direct_content_visible
_list_content.visible = list_content_visible
_list_title.text = _current_list_title()
_refresh_online_controls(discovery_mode, friends_mode, connecting)
_address.visible = direct or _name_entry_active
_address_label.visible = _address.visible
_address_helper.visible = _address.visible
@ -916,6 +944,66 @@ func _current_list_title() -> String:
return "public rooms"
func _refresh_online_controls(
discovery_mode: bool,
friends_mode: bool,
connecting: bool,
) -> void:
_online_controls.visible = discovery_mode and not _name_entry_active
_presence_button.visible = friends_mode and not _name_entry_active
var presence_enabled: bool = (
_discovery != null and _discovery.is_presence_sharing()
)
_presence_button.text = (
"friends: online" if presence_enabled else "friends: offline"
)
_presence_button.disabled = (
connecting or _discovery == null or not _discovery.is_configured()
)
_presence_button.tooltip_text = (
"Stay visible to friends until you switch this off. "
+ "Your current room is shared only while it is publicly joinable."
if presence_enabled
else "Show as online to friends until you switch this off."
)
if not _online_controls.visible:
return
var local_host: bool = _network_session.is_host()
_room_open_button.visible = local_host
_room_listing_button.visible = local_host
if not local_host:
return
var room_open: bool = _network_session.is_open_host()
var room_listed: bool = (
_discovery != null and _discovery.is_discoverable()
)
_room_open_button.text = "room: open" if room_open else "room: closed"
_room_open_button.disabled = connecting
_room_open_button.tooltip_text = (
"Close this room to new connections."
if room_open
else "Open this room so other players can connect."
)
_room_listing_button.text = (
"listing: on" if room_listed else "listing: off"
)
_room_listing_button.disabled = (
connecting
or _discovery == null
or not _discovery.is_configured()
or not room_open
)
_room_listing_button.tooltip_text = (
"Remove this room from the public room browser."
if room_listed
else "List this open room in the public room browser."
if room_open and _discovery != null and _discovery.is_configured()
else "Open the room before enabling its public listing."
if _discovery != null and _discovery.is_configured()
else "Room discovery is not configured in this build."
)
func _configure_controller_navigation() -> void:
var mode_buttons: Array[Control] = [
_discover_button,
@ -924,12 +1012,24 @@ func _configure_controller_navigation() -> void:
_saved_button,
_recent_button,
]
var online_controls: Array[Control] = []
for control: Control in [
_room_open_button,
_room_listing_button,
]:
if _controller_focus_eligible(control):
online_controls.append(control)
var content_controls: Array[Control] = []
for control: Control in [_address, _name_edit, _server_list]:
for control: Control in [
_address,
_name_edit,
_server_list,
]:
if _controller_focus_eligible(control):
content_controls.append(control)
var action_controls: Array[Control] = []
for control: Control in [
_presence_button,
_refresh_button,
_join_button,
_save_button,
@ -942,12 +1042,16 @@ func _configure_controller_navigation() -> void:
action_controls.append(control)
var all_controls: Array[Control] = []
all_controls.append_array(mode_buttons)
all_controls.append_array(online_controls)
all_controls.append_array(content_controls)
all_controls.append_array(action_controls)
all_controls.append(_back_button)
for control: Control in all_controls:
control.focus_mode = Control.FOCUS_ALL
for control: Control in [
_presence_button,
_room_open_button,
_room_listing_button,
_address,
_name_edit,
_server_list,
@ -963,7 +1067,9 @@ func _configure_controller_navigation() -> void:
control.focus_mode = Control.FOCUS_NONE
_back_button.focus_mode = Control.FOCUS_ALL
var primary_content: Control = (
content_controls.front()
online_controls.front()
if not online_controls.is_empty()
else content_controls.front()
if not content_controls.is_empty()
else action_controls.front()
if not action_controls.is_empty()
@ -978,11 +1084,29 @@ func _configure_controller_navigation() -> void:
mode_button,
primary_content,
)
for index: int in online_controls.size():
var online_control: Control = online_controls[index]
var below: Control = (
content_controls.front()
if not content_controls.is_empty()
else action_controls.front()
if not action_controls.is_empty()
else _back_button
)
_set_controller_neighbors(
online_control,
online_controls[maxi(index - 1, 0)],
online_controls[mini(index + 1, online_controls.size() - 1)],
mode_buttons[int(_mode)],
below,
)
for index: int in content_controls.size():
var content: Control = content_controls[index]
var above: Control = (
content_controls[index - 1]
if index > 0
else online_controls.front()
if not online_controls.is_empty()
else mode_buttons[int(_mode)]
)
var below: Control = (
@ -1001,6 +1125,8 @@ func _configure_controller_navigation() -> void:
action_controls[mini(index + 1, action_controls.size() - 1)],
content_controls.back()
if not content_controls.is_empty()
else online_controls.back()
if not online_controls.is_empty()
else mode_buttons[int(_mode)],
_back_button,
)
@ -1009,6 +1135,8 @@ func _configure_controller_navigation() -> void:
if not action_controls.is_empty()
else content_controls.back()
if not content_controls.is_empty()
else online_controls.back()
if not online_controls.is_empty()
else mode_buttons[int(_mode)]
)
_set_controller_neighbors(
@ -1270,6 +1398,76 @@ func _on_social_status_changed(message: String, is_error: bool) -> void:
_set_status(message, is_error)
func _on_presence_pressed() -> void:
if _discovery == null:
_set_status("Friend presence is not available.", true)
return
var enabling: bool = not _discovery.is_presence_sharing()
if not _discovery.set_presence_sharing(enabling):
return
_set_status(
"You will remain online to friends until you switch this off."
if enabling
else "You now appear offline to friends."
)
_refresh()
func _on_room_open_pressed() -> void:
if _network_session == null or not _network_session.is_host():
return
var opening: bool = not _network_session.is_open_host()
if not _network_session.set_host_open(opening):
_set_status("The room could not be updated.", true)
return
_set_status(
"The room is open to new connections."
if opening
else "The room is closed to new connections."
)
_refresh()
func _on_room_listing_pressed() -> void:
if _discovery == null:
return
var enabling: bool = not _discovery.is_discoverable()
if not _discovery.set_discoverable(enabling):
return
_set_status(
"The room is being listed publicly."
if enabling
else "The room is no longer listed publicly."
)
_refresh()
func _on_host_openness_changed(_is_open: bool) -> void:
_refresh()
func _on_host_settings_changed(
_room_name: String,
_discoverable: bool,
) -> void:
_refresh()
func _on_host_status_changed(message: String, is_error: bool) -> void:
if (
_mode == Mode.DISCOVER
and is_visible_in_tree()
and _network_session != null
and _network_session.is_host()
):
_set_status(message, is_error)
_refresh()
func _on_presence_sharing_changed(_enabled: bool) -> void:
_refresh()
func _format_result_code(result_code: String) -> String:
match result_code.strip_edges().to_upper():
"SUCCESS":

View file

@ -138,9 +138,35 @@ layout_mode = 2
theme_override_font_sizes/font_size = 20
text = "public rooms"
[node name="OnlineControls" type="HBoxContainer" parent="MainPanel/OuterMargin/Layout/ContentPanel/ContentMargin/ContentLayout/PageStack/ListContent"]
unique_name_in_owner = true
custom_minimum_size = Vector2(0, 42)
layout_mode = 2
theme_override_constants/separation = 10
[node name="OnlineControlsSpacer" type="Control" parent="MainPanel/OuterMargin/Layout/ContentPanel/ContentMargin/ContentLayout/PageStack/ListContent/OnlineControls"]
layout_mode = 2
size_flags_horizontal = 3
[node name="RoomOpenButton" type="Button" parent="MainPanel/OuterMargin/Layout/ContentPanel/ContentMargin/ContentLayout/PageStack/ListContent/OnlineControls"]
unique_name_in_owner = true
visible = false
custom_minimum_size = Vector2(160, 42)
layout_mode = 2
focus_mode = 2
text = "room: closed"
[node name="RoomListingButton" type="Button" parent="MainPanel/OuterMargin/Layout/ContentPanel/ContentMargin/ContentLayout/PageStack/ListContent/OnlineControls"]
unique_name_in_owner = true
visible = false
custom_minimum_size = Vector2(180, 42)
layout_mode = 2
focus_mode = 2
text = "listing: off"
[node name="ServerList" type="ItemList" parent="MainPanel/OuterMargin/Layout/ContentPanel/ContentMargin/ContentLayout/PageStack/ListContent"]
unique_name_in_owner = true
custom_minimum_size = Vector2(0, 180)
custom_minimum_size = Vector2(0, 140)
layout_mode = 2
size_flags_vertical = 3
theme_override_font_sizes/font_size = 17
@ -235,7 +261,18 @@ unique_name_in_owner = true
custom_minimum_size = Vector2(0, 46)
layout_mode = 2
theme_override_constants/separation = 10
alignment = 1
[node name="PresenceButton" type="Button" parent="MainPanel/OuterMargin/Layout/ContentPanel/ContentMargin/ContentLayout/Actions"]
unique_name_in_owner = true
visible = false
custom_minimum_size = Vector2(210, 46)
layout_mode = 2
focus_mode = 2
text = "friends: offline"
[node name="ActionsSpacer" type="Control" parent="MainPanel/OuterMargin/Layout/ContentPanel/ContentMargin/ContentLayout/Actions"]
layout_mode = 2
size_flags_horizontal = 3
[node name="RefreshButton" type="Button" parent="MainPanel/OuterMargin/Layout/ContentPanel/ContentMargin/ContentLayout/Actions"]
unique_name_in_owner = true

View file

@ -44,6 +44,7 @@ var _online_state_label: Label
var _discoverable_toggle: Button
var _discoverable_state_label: Label
var _host_discovery_status: Label
var _reset_artwork_button: Button
var _current_tab := 0
var _active: bool = false
var _interactive: bool = false
@ -295,6 +296,9 @@ func _build_host_settings(root: VBoxContainer) -> void:
"font_color", UtilityPageStyle.OCEAN_TEXT_SECONDARY
)
access_row.add_child(_host_discovery_status)
_reset_artwork_button = Button.new()
_configure_session_artwork_reset_button(_reset_artwork_button)
access_row.add_child(_reset_artwork_button)
func _build_host_toggle(label_text: String, row: HBoxContainer) -> Button:
@ -389,7 +393,10 @@ func _refresh_host_settings() -> void:
return
var host_visible: bool = _service.is_local_host() and _current_tab == 0
_host_settings_panel.visible = host_visible
if not host_visible or _discovery == null:
if not host_visible:
return
_update_session_artwork_reset_button(_reset_artwork_button)
if _discovery == null:
return
if not _room_name_edit.has_focus():
_room_name_edit.text = _discovery.get_room_name()
@ -481,7 +488,9 @@ func _on_host_status_changed(message: String, is_error: bool) -> void:
"Open the game before enabling discovery.",
]
_host_discovery_status.text = "" if tooltip_only else message
_host_discovery_status.visible = not _host_discovery_status.text.is_empty()
# Keep this expanding label in the row even when there is no status text so
# the session reset action remains pinned to the far-right edge.
_host_discovery_status.visible = true
_host_discovery_status.add_theme_color_override(
"font_color",
UtilityPageStyle.OCEAN_DANGER
@ -491,7 +500,7 @@ func _on_host_status_changed(message: String, is_error: bool) -> void:
func _build_active_rows() -> void:
if _service.is_local_moderator():
if _service.is_local_moderator() and not _service.is_local_host():
_build_session_artwork_controls()
var entries := _service.get_entries()
if entries.is_empty():
@ -502,35 +511,29 @@ func _build_active_rows() -> void:
func _build_active_player_row(entry: PlayerListEntry) -> void:
var row := _make_active_player_row()
var identity_row := HBoxContainer.new()
identity_row.add_theme_constant_override("separation", 8)
row.add_child(identity_row)
var row := _make_row()
var identity := Label.new()
identity.clip_text = true
identity.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
identity.size_flags_horizontal = Control.SIZE_EXPAND_FILL
var markers: Array[String] = []
if entry.is_host:
markers.append("host")
identity.text = entry.display_name
var roles: Array[String] = ["host" if entry.is_host else "player"]
if entry.is_operator:
markers.append("operator")
roles.append("operator")
if entry.is_local_player:
markers.append("You")
identity.text = "%s%s · %s\n%s" % [
entry.display_name,
" [%s]" % ", ".join(markers) if not markers.is_empty() else "",
entry.compact_fingerprint,
roles.append("you")
_configure_identity_tooltip(
identity,
"identity fingerprint:\n%s\nrole: %s\nidentity status: %s" % [
NetworkIdentityCrypto.format_fingerprint(entry.full_fingerprint),
", ".join(roles),
entry.continuity_state,
]
identity.tooltip_text = "Full identity fingerprint:\n%s" % (
entry.full_fingerprint
],
)
identity.add_theme_color_override(
"font_color", UtilityPageStyle.OCEAN_TEXT_PRIMARY
)
identity_row.add_child(identity)
row.add_child(identity)
var ping := Label.new()
ping.custom_minimum_size.x = 72
ping.text = (
@ -542,7 +545,7 @@ func _build_active_player_row(entry: PlayerListEntry) -> void:
"font_color", UtilityPageStyle.OCEAN_TEXT_SECONDARY
)
ping.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
identity_row.add_child(ping)
row.add_child(ping)
var actions := HBoxContainer.new()
actions.alignment = BoxContainer.ALIGNMENT_END
@ -634,31 +637,38 @@ func _configure_moderation_button(
func _build_session_artwork_controls() -> void:
var counts: Vector2i = _service.get_session_artwork_counts()
var row := _make_row()
var label := Label.new()
label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
label.clip_text = true
label.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
label.text = "session artwork · %d layers · %d painted pixels" % [
counts.x, counts.y,
]
label.add_theme_color_override(
"font_color", UtilityPageStyle.OCEAN_TEXT_PRIMARY
)
row.add_child(label)
var spacer := Control.new()
spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL
spacer.mouse_filter = Control.MOUSE_FILTER_IGNORE
row.add_child(spacer)
var reset := Button.new()
reset.text = "reset paint"
reset.disabled = counts.x == 0
reset.tooltip_text = "Clears all shared artwork from this session."
reset.pressed.connect(func() -> void:
_configure_session_artwork_reset_button(reset)
_update_session_artwork_reset_button(reset)
row.add_child(reset)
func _configure_session_artwork_reset_button(button: Button) -> void:
button.text = "reset paint"
button.tooltip_text = "Clears all shared artwork from this session."
button.pressed.connect(func() -> void:
_confirm(
"Clear all shared paint from this session?\nThis cannot be undone.",
_service.reset_session_artwork,
)
)
UtilityPageStyle.apply_compact_ocean_button(reset)
row.add_child(reset)
UtilityPageStyle.apply_compact_ocean_button(button)
func _update_session_artwork_reset_button(button: Button) -> void:
if button == null or _service == null:
return
button.disabled = _service.get_session_artwork_counts().x == 0
func _configure_identity_tooltip(label: Label, text: String) -> void:
label.mouse_filter = Control.MOUSE_FILTER_STOP
label.tooltip_text = text
func _build_relationship_rows() -> void:
@ -673,12 +683,16 @@ func _build_relationship_rows() -> void:
label.clip_text = true
label.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
var fingerprint := str(record["fingerprint"])
label.text = "%s · %s %s" % [
label.text = "%s %s" % [
str(record.get("last_known_display_name", "Player")),
NetworkIdentityCrypto.compact_suffix(fingerprint),
"Blocked" if bool(record.get("blocked", false)) else "Muted",
]
label.tooltip_text = NetworkIdentityCrypto.format_fingerprint(fingerprint)
_configure_identity_tooltip(
label,
"identity fingerprint:\n%s" % (
NetworkIdentityCrypto.format_fingerprint(fingerprint)
),
)
label.add_theme_color_override(
"font_color", UtilityPageStyle.OCEAN_TEXT_PRIMARY
)
@ -724,16 +738,20 @@ func _build_friend_rows() -> void:
label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
label.clip_text = true
label.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
label.text = "%s · %s %s" % [
label.text = "%s %s" % [
display_name,
NetworkIdentityCrypto.compact_suffix(fingerprint),
"playing in %s" % str(room.get("room_name", "a public room"))
if not room.is_empty()
else "Online"
if bool(friend.get("online", false))
else "Offline",
]
label.tooltip_text = NetworkIdentityCrypto.format_fingerprint(fingerprint)
_configure_identity_tooltip(
label,
"identity fingerprint:\n%s" % (
NetworkIdentityCrypto.format_fingerprint(fingerprint)
),
)
label.add_theme_color_override(
"font_color", UtilityPageStyle.OCEAN_TEXT_PRIMARY
)
@ -811,12 +829,16 @@ func _build_ban_rows() -> void:
label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
label.clip_text = true
label.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
label.text = "%s · %s banned %s" % [
label.text = "%s banned %s" % [
str(record.get("last_known_display_name", "Player")),
NetworkIdentityCrypto.compact_suffix(fingerprint),
Time.get_date_string_from_unix_time(int(record.get("banned_unix", 0))),
]
label.tooltip_text = NetworkIdentityCrypto.format_fingerprint(fingerprint)
_configure_identity_tooltip(
label,
"identity fingerprint:\n%s" % (
NetworkIdentityCrypto.format_fingerprint(fingerprint)
),
)
label.add_theme_color_override(
"font_color", UtilityPageStyle.OCEAN_TEXT_PRIMARY
)
@ -841,20 +863,6 @@ func _make_row() -> HBoxContainer:
return row
func _make_active_player_row() -> VBoxContainer:
var panel := PanelContainer.new()
panel.clip_contents = true
panel.add_theme_stylebox_override(
"panel", UtilityPageStyle.row_style(false)
)
_list.add_child(panel)
var row := VBoxContainer.new()
row.custom_minimum_size.y = 88
row.add_theme_constant_override("separation", 4)
panel.add_child(row)
return row
func _add_empty(text: String) -> void:
var label := Label.new()
label.text = text

View file

@ -124,6 +124,12 @@ func _configure_style() -> void:
if node is OrganizerTab:
continue
UtilityPageStyle.apply_ocean_button(node as BaseButton)
_play_button.add_theme_stylebox_override(
"normal",
UtilityPageStyle.ocean_button_style(
UtilityPageStyle.GREEN,
),
)
_delete_button.add_theme_stylebox_override(
"normal",
UtilityPageStyle.ocean_button_style(
@ -639,11 +645,11 @@ func _configure_controller_focus() -> void:
_set_neighbors(button, button, _slot_name_edit, top, bottom)
_set_neighbors(_import_button, _import_button, _slot_name_edit, last, _back_button)
_set_neighbors(_slot_name_edit, first, _slot_name_edit, _saves_tab, _play_button)
_set_neighbors(_play_button, _import_button, _rename_button, _slot_name_edit, _duplicate_button)
_set_neighbors(_rename_button, _play_button, _rename_button, _slot_name_edit, _export_button)
_set_neighbors(_duplicate_button, _import_button, _export_button, _play_button, _delete_button)
_set_neighbors(_export_button, _duplicate_button, _export_button, _rename_button, _delete_button)
_set_neighbors(_delete_button, _import_button, _delete_button, _duplicate_button, _back_button)
_set_neighbors(_play_button, _import_button, _play_button, _slot_name_edit, _rename_button)
_set_neighbors(_rename_button, _import_button, _duplicate_button, _play_button, _export_button)
_set_neighbors(_duplicate_button, _rename_button, _duplicate_button, _play_button, _delete_button)
_set_neighbors(_export_button, _import_button, _delete_button, _rename_button, _back_button)
_set_neighbors(_delete_button, _export_button, _delete_button, _duplicate_button, _back_button)
_set_neighbors(_back_button, _back_button, _back_button, _import_button, _back_button)
return
_saves_tab.focus_neighbor_bottom = _saves_tab.get_path_to(_new_slot_name)

View file

@ -165,11 +165,10 @@ scroll_active = false
autowrap_mode = 2
vertical_alignment = 1
[node name="Actions" type="GridContainer" parent="MainPanel/OuterMargin/Layout/ContentPanel/ContentMargin/PageStack/SavesPage/DetailsColumn"]
[node name="Actions" type="VBoxContainer" parent="MainPanel/OuterMargin/Layout/ContentPanel/ContentMargin/PageStack/SavesPage/DetailsColumn"]
unique_name_in_owner = true
layout_mode = 2
theme_override_constants/h_separation = 10
theme_override_constants/v_separation = 10
columns = 2
theme_override_constants/separation = 10
[node name="PlaySlotButton" type="Button" parent="MainPanel/OuterMargin/Layout/ContentPanel/ContentMargin/PageStack/SavesPage/DetailsColumn/Actions"]
unique_name_in_owner = true
@ -179,7 +178,14 @@ size_flags_horizontal = 3
focus_mode = 2
text = "play"
[node name="RenameSlotButton" type="Button" parent="MainPanel/OuterMargin/Layout/ContentPanel/ContentMargin/PageStack/SavesPage/DetailsColumn/Actions"]
[node name="SecondaryActions" type="GridContainer" parent="MainPanel/OuterMargin/Layout/ContentPanel/ContentMargin/PageStack/SavesPage/DetailsColumn/Actions"]
unique_name_in_owner = true
layout_mode = 2
theme_override_constants/h_separation = 10
theme_override_constants/v_separation = 10
columns = 2
[node name="RenameSlotButton" type="Button" parent="MainPanel/OuterMargin/Layout/ContentPanel/ContentMargin/PageStack/SavesPage/DetailsColumn/Actions/SecondaryActions"]
unique_name_in_owner = true
custom_minimum_size = Vector2(0, 46)
layout_mode = 2
@ -187,7 +193,7 @@ size_flags_horizontal = 3
focus_mode = 2
text = "rename"
[node name="DuplicateSlotButton" type="Button" parent="MainPanel/OuterMargin/Layout/ContentPanel/ContentMargin/PageStack/SavesPage/DetailsColumn/Actions"]
[node name="DuplicateSlotButton" type="Button" parent="MainPanel/OuterMargin/Layout/ContentPanel/ContentMargin/PageStack/SavesPage/DetailsColumn/Actions/SecondaryActions"]
unique_name_in_owner = true
custom_minimum_size = Vector2(0, 46)
layout_mode = 2
@ -195,7 +201,7 @@ size_flags_horizontal = 3
focus_mode = 2
text = "duplicate"
[node name="ExportSlotButton" type="Button" parent="MainPanel/OuterMargin/Layout/ContentPanel/ContentMargin/PageStack/SavesPage/DetailsColumn/Actions"]
[node name="ExportSlotButton" type="Button" parent="MainPanel/OuterMargin/Layout/ContentPanel/ContentMargin/PageStack/SavesPage/DetailsColumn/Actions/SecondaryActions"]
unique_name_in_owner = true
custom_minimum_size = Vector2(0, 46)
layout_mode = 2
@ -203,7 +209,7 @@ size_flags_horizontal = 3
focus_mode = 2
text = "export"
[node name="DeleteSlotButton" type="Button" parent="MainPanel/OuterMargin/Layout/ContentPanel/ContentMargin/PageStack/SavesPage/DetailsColumn/Actions"]
[node name="DeleteSlotButton" type="Button" parent="MainPanel/OuterMargin/Layout/ContentPanel/ContentMargin/PageStack/SavesPage/DetailsColumn/Actions/SecondaryActions"]
unique_name_in_owner = true
custom_minimum_size = Vector2(0, 46)
layout_mode = 2

View file

@ -464,6 +464,7 @@ func _update_title_layout() -> void:
Vector2(field_width, field_height),
compact_layout
)
_configure_title_controller_navigation()
if not _title_settings_transition_active:
call_deferred("_capture_title_bubble_rest_position")
if _awaiting_start_input and not _title_entry_transition_active:
@ -1483,9 +1484,28 @@ func _set_title_bubbles_interactive(interactive: bool) -> void:
else Control.MOUSE_FILTER_IGNORE
)
if interactive:
_configure_title_controller_navigation()
func _configure_title_controller_navigation() -> void:
ControllerFocusNavigationType.configure_spatial_neighbors(
_get_title_buttons()
)
# The large Play bubble overlaps the inward edge of every surrounding
# bubble. Center-only spatial scoring otherwise links the four outside
# bubbles into a ring with no route back into Play.
_join_game_button.focus_neighbor_right = (
_join_game_button.get_path_to(_play_button)
)
_settings_button.focus_neighbor_left = (
_settings_button.get_path_to(_play_button)
)
_credits_button.focus_neighbor_right = (
_credits_button.get_path_to(_play_button)
)
_quit_button.focus_neighbor_left = (
_quit_button.get_path_to(_play_button)
)
func _capture_title_bubble_rest_position() -> void: