fix: harden controller navigation and text entry
This commit is contained in:
parent
c4e49cdc52
commit
8ea877ecf4
28 changed files with 1320 additions and 96 deletions
|
|
@ -15,6 +15,7 @@ var _depth_scale: float = 1.0
|
|||
var _batch_selected: bool = false
|
||||
var _focused_catch: bool = false
|
||||
var _hovered: bool = false
|
||||
var _controller_focus_presentation_active: bool = false
|
||||
var _quality_color := Color.WHITE
|
||||
|
||||
|
||||
|
|
@ -96,6 +97,11 @@ func set_item_state(
|
|||
_refresh_style()
|
||||
|
||||
|
||||
func set_controller_focus_presentation_active(active: bool) -> void:
|
||||
_controller_focus_presentation_active = active
|
||||
_refresh_style()
|
||||
|
||||
|
||||
func advance_presentation(delta: float, elapsed: float) -> void:
|
||||
var response: float = 1.0 - exp(-9.0 * delta)
|
||||
_neutral_position = _neutral_position.lerp(_target_position, response)
|
||||
|
|
@ -107,7 +113,7 @@ func advance_presentation(delta: float, elapsed: float) -> void:
|
|||
interaction_lift += 3.0
|
||||
if _focused_catch:
|
||||
interaction_lift += 1.5
|
||||
elif _hovered:
|
||||
elif _hovered and not _controller_focus_presentation_active:
|
||||
interaction_lift += 0.8
|
||||
_visual_root.position = Vector2(0.0, bob - interaction_lift)
|
||||
_visual_root.rotation = tilt
|
||||
|
|
@ -116,7 +122,7 @@ func advance_presentation(delta: float, elapsed: float) -> void:
|
|||
emphasis += 0.085
|
||||
if _focused_catch:
|
||||
emphasis += 0.035
|
||||
elif _hovered:
|
||||
elif _hovered and not _controller_focus_presentation_active:
|
||||
emphasis += 0.045
|
||||
_visual_root.scale = Vector2.ONE * _depth_scale * emphasis
|
||||
|
||||
|
|
@ -150,7 +156,7 @@ func _refresh_style() -> void:
|
|||
)
|
||||
var circle_visible: bool = (
|
||||
_batch_selected
|
||||
or _hovered
|
||||
or (_hovered and not _controller_focus_presentation_active)
|
||||
or _focused_catch
|
||||
)
|
||||
var base_style: StyleBox = idle
|
||||
|
|
|
|||
|
|
@ -24,6 +24,20 @@ const FOCUS_COLOR_REPLACEMENTS: Dictionary[StringName, StringName] = {
|
|||
&"font_selected_color": &"font_color",
|
||||
&"font_hovered_selected_color": &"font_hovered_color",
|
||||
}
|
||||
const HOVER_STYLE_REPLACEMENTS: Dictionary[StringName, StringName] = {
|
||||
&"hover": &"normal",
|
||||
&"hover_pressed": &"pressed",
|
||||
&"hovered": &"",
|
||||
&"hovered_selected": &"",
|
||||
}
|
||||
const HOVER_COLOR_REPLACEMENTS: Dictionary[StringName, StringName] = {
|
||||
&"font_hover_color": &"font_color",
|
||||
&"icon_hover_color": &"icon_normal_color",
|
||||
&"font_hover_pressed_color": &"font_pressed_color",
|
||||
&"icon_hover_pressed_color": &"icon_pressed_color",
|
||||
&"font_hovered_color": &"font_color",
|
||||
&"font_hovered_selected_color": &"font_color",
|
||||
}
|
||||
|
||||
var _controller_active: bool = false
|
||||
var _focused_control: Control
|
||||
|
|
@ -32,6 +46,9 @@ var _popup_scroll_offset: float = 0.0
|
|||
var _suppressed_control: Control
|
||||
var _original_style_overrides: Dictionary[StringName, Dictionary] = {}
|
||||
var _original_color_overrides: Dictionary[StringName, Dictionary] = {}
|
||||
var _suppressed_hover_control: Control
|
||||
var _original_hover_style_overrides: Dictionary[StringName, Dictionary] = {}
|
||||
var _original_hover_color_overrides: Dictionary[StringName, Dictionary] = {}
|
||||
var _original_popup_hover_style: Dictionary = {}
|
||||
var _original_popup_hover_color: Dictionary = {}
|
||||
var _focus_layer: CanvasLayer
|
||||
|
|
@ -47,6 +64,7 @@ func _ready() -> void:
|
|||
|
||||
func _exit_tree() -> void:
|
||||
_clear_focus_presentation()
|
||||
_restore_native_hover_highlight()
|
||||
|
||||
|
||||
func _input(event: InputEvent) -> void:
|
||||
|
|
@ -69,6 +87,7 @@ func _input(event: InputEvent) -> void:
|
|||
|
||||
|
||||
func _process(_delta: float) -> void:
|
||||
_update_hover_suppression()
|
||||
if _controller_active:
|
||||
var focused_popup: PopupMenu = _active_focused_popup(get_viewport())
|
||||
if focused_popup != null:
|
||||
|
|
@ -111,6 +130,7 @@ func _set_controller_active(active: bool) -> void:
|
|||
if _controller_active == active:
|
||||
return
|
||||
_controller_active = active
|
||||
_update_hover_suppression()
|
||||
if _controller_active:
|
||||
var focused_popup: PopupMenu = _active_focused_popup(get_viewport())
|
||||
if focused_popup != null:
|
||||
|
|
@ -440,6 +460,113 @@ func _suppress_native_focus_highlight(control: Control) -> void:
|
|||
)
|
||||
|
||||
|
||||
func _update_hover_suppression() -> void:
|
||||
if not _controller_active:
|
||||
_restore_native_hover_highlight()
|
||||
return
|
||||
var hovered: Control = _hover_presentation_control(
|
||||
_hovered_control_in_viewport(get_viewport())
|
||||
)
|
||||
if hovered == _suppressed_hover_control:
|
||||
return
|
||||
_restore_native_hover_highlight()
|
||||
if hovered != null:
|
||||
_suppress_native_hover_highlight(hovered)
|
||||
|
||||
|
||||
func _hovered_control_in_viewport(viewport: Viewport) -> Control:
|
||||
var embedded_windows: Array[Window] = viewport.get_embedded_subwindows()
|
||||
for index: int in range(embedded_windows.size() - 1, -1, -1):
|
||||
var window: Window = embedded_windows[index]
|
||||
if not window.visible:
|
||||
continue
|
||||
var nested_hovered: Control = _hovered_control_in_viewport(window)
|
||||
if nested_hovered != null:
|
||||
return nested_hovered
|
||||
return viewport.gui_get_hovered_control()
|
||||
|
||||
|
||||
func _hover_presentation_control(control: Control) -> Control:
|
||||
var candidate: Control = control
|
||||
while candidate != null:
|
||||
if (
|
||||
candidate is BaseButton
|
||||
or candidate is ItemList
|
||||
or candidate is Tree
|
||||
or candidate.focus_mode != Control.FOCUS_NONE
|
||||
):
|
||||
return candidate
|
||||
candidate = candidate.get_parent() as Control
|
||||
return null
|
||||
|
||||
|
||||
func _suppress_native_hover_highlight(control: Control) -> void:
|
||||
_suppressed_hover_control = control
|
||||
_original_hover_style_overrides.clear()
|
||||
_original_hover_color_overrides.clear()
|
||||
if control.has_method(&"set_controller_focus_presentation_active"):
|
||||
control.call(&"set_controller_focus_presentation_active", true)
|
||||
for hover_name: StringName in HOVER_STYLE_REPLACEMENTS:
|
||||
var replacement_name: StringName = HOVER_STYLE_REPLACEMENTS[hover_name]
|
||||
_original_hover_style_overrides[hover_name] = {
|
||||
"had_override": control.has_theme_stylebox_override(hover_name),
|
||||
"value": control.get_theme_stylebox(hover_name),
|
||||
}
|
||||
var replacement: StyleBox = (
|
||||
StyleBoxEmpty.new()
|
||||
if replacement_name.is_empty()
|
||||
else control.get_theme_stylebox(replacement_name)
|
||||
)
|
||||
control.add_theme_stylebox_override(hover_name, replacement)
|
||||
for hover_name: StringName in HOVER_COLOR_REPLACEMENTS:
|
||||
var replacement_name: StringName = HOVER_COLOR_REPLACEMENTS[hover_name]
|
||||
if not control.has_theme_color(replacement_name):
|
||||
continue
|
||||
_original_hover_color_overrides[hover_name] = {
|
||||
"had_override": control.has_theme_color_override(hover_name),
|
||||
"value": control.get_theme_color(hover_name),
|
||||
}
|
||||
control.add_theme_color_override(
|
||||
hover_name,
|
||||
control.get_theme_color(replacement_name),
|
||||
)
|
||||
|
||||
|
||||
func _restore_native_hover_highlight() -> void:
|
||||
if not is_instance_valid(_suppressed_hover_control):
|
||||
_suppressed_hover_control = null
|
||||
_original_hover_style_overrides.clear()
|
||||
_original_hover_color_overrides.clear()
|
||||
return
|
||||
for hover_name: StringName in _original_hover_style_overrides:
|
||||
var state: Dictionary = _original_hover_style_overrides[hover_name]
|
||||
if bool(state.get("had_override", false)):
|
||||
_suppressed_hover_control.add_theme_stylebox_override(
|
||||
hover_name,
|
||||
state.get("value") as StyleBox,
|
||||
)
|
||||
else:
|
||||
_suppressed_hover_control.remove_theme_stylebox_override(hover_name)
|
||||
for hover_name: StringName in _original_hover_color_overrides:
|
||||
var state: Dictionary = _original_hover_color_overrides[hover_name]
|
||||
if bool(state.get("had_override", false)):
|
||||
_suppressed_hover_control.add_theme_color_override(
|
||||
hover_name,
|
||||
state.get("value") as Color,
|
||||
)
|
||||
else:
|
||||
_suppressed_hover_control.remove_theme_color_override(hover_name)
|
||||
if _suppressed_hover_control.has_method(
|
||||
&"set_controller_focus_presentation_active"
|
||||
):
|
||||
_suppressed_hover_control.call(
|
||||
&"set_controller_focus_presentation_active", false
|
||||
)
|
||||
_suppressed_hover_control = null
|
||||
_original_hover_style_overrides.clear()
|
||||
_original_hover_color_overrides.clear()
|
||||
|
||||
|
||||
func _suppress_popup_focus_highlight(popup: PopupMenu) -> void:
|
||||
_original_popup_hover_style = {
|
||||
"had_override": popup.has_theme_stylebox_override(&"hover"),
|
||||
|
|
|
|||
|
|
@ -67,6 +67,12 @@ func select_only(catch_id: StringName) -> void:
|
|||
_anchor_id = catch_id
|
||||
|
||||
|
||||
func focus_only(catch_id: StringName) -> void:
|
||||
if catch_id.is_empty() or not _visible_ids.has(catch_id):
|
||||
return
|
||||
_focused_id = catch_id
|
||||
|
||||
|
||||
func clear() -> void:
|
||||
_selected_ids.clear()
|
||||
_focused_id = StringName()
|
||||
|
|
|
|||
|
|
@ -93,6 +93,11 @@ enum ShopSection {
|
|||
SELL_FISH,
|
||||
}
|
||||
|
||||
enum ControllerZone {
|
||||
TABS,
|
||||
CONTENT,
|
||||
}
|
||||
|
||||
const SHOP_SECTION_LABELS: Array[String] = [
|
||||
"Upgrades",
|
||||
"Bait and Lures",
|
||||
|
|
@ -175,6 +180,7 @@ var _cooler_modal_open: bool = false
|
|||
var _shop_section: ShopSection = ShopSection.UPGRADES
|
||||
var _shop_tabs: Array[OrganizerTab] = []
|
||||
var _controller_mapping_manager: ControllerMappingManagerType
|
||||
var _controller_zone: ControllerZone = ControllerZone.TABS
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
|
|
@ -204,6 +210,40 @@ func _input(event: InputEvent) -> void:
|
|||
var button_event := event as InputEventJoypadButton
|
||||
if button_event == null:
|
||||
return
|
||||
var uses_accept: bool = (
|
||||
_controller_mapping_manager.event_matches_role(
|
||||
event,
|
||||
ControllerMappingManagerType.ROLE_A,
|
||||
)
|
||||
if _controller_mapping_manager != null
|
||||
else button_event.button_index == JOY_BUTTON_A
|
||||
)
|
||||
var uses_cancel: bool = (
|
||||
_controller_mapping_manager.event_matches_role(
|
||||
event,
|
||||
ControllerMappingManagerType.ROLE_B,
|
||||
)
|
||||
if _controller_mapping_manager != null
|
||||
else button_event.button_index == JOY_BUTTON_B
|
||||
)
|
||||
if uses_cancel:
|
||||
get_viewport().set_input_as_handled()
|
||||
if not button_event.pressed:
|
||||
return
|
||||
if _controller_zone == ControllerZone.CONTENT:
|
||||
_enter_shop_tabs_zone()
|
||||
else:
|
||||
close_shop()
|
||||
return
|
||||
if uses_accept:
|
||||
get_viewport().set_input_as_handled()
|
||||
if not button_event.pressed or _transaction_in_progress:
|
||||
return
|
||||
if _controller_zone == ControllerZone.TABS:
|
||||
_enter_shop_content_zone()
|
||||
else:
|
||||
_activate_focused_shop_control()
|
||||
return
|
||||
var uses_left_bumper: bool = (
|
||||
_controller_mapping_manager.event_uses_role(
|
||||
event,
|
||||
|
|
@ -226,12 +266,69 @@ func _input(event: InputEvent) -> void:
|
|||
if not button_event.pressed or _transaction_in_progress:
|
||||
return
|
||||
var direction: int = -1 if uses_left_bumper else 1
|
||||
var current_section: int = _focused_shop_tab_index()
|
||||
if current_section < 0:
|
||||
current_section = int(_shop_section)
|
||||
var next_section: int = wrapi(
|
||||
int(_shop_section) + direction,
|
||||
current_section + direction,
|
||||
0,
|
||||
ShopSection.size(),
|
||||
)
|
||||
_select_shop_section(next_section, true)
|
||||
if _controller_zone == ControllerZone.TABS:
|
||||
_focus_shop_tab(next_section)
|
||||
else:
|
||||
_select_shop_section(next_section, true)
|
||||
|
||||
|
||||
func _enter_shop_tabs_zone() -> void:
|
||||
_controller_zone = ControllerZone.TABS
|
||||
_apply_shop_controller_zone_focus_modes()
|
||||
_configure_controller_focus()
|
||||
_focus_shop_tab(int(_shop_section))
|
||||
|
||||
|
||||
func _enter_shop_content_zone() -> void:
|
||||
var section_index: int = _focused_shop_tab_index()
|
||||
if section_index < 0:
|
||||
section_index = int(_shop_section)
|
||||
_controller_zone = ControllerZone.CONTENT
|
||||
_select_shop_section(section_index, true)
|
||||
|
||||
|
||||
func _activate_focused_shop_control() -> bool:
|
||||
var focused := get_viewport().gui_get_focus_owner() as BaseButton
|
||||
if (
|
||||
focused == null
|
||||
or focused.disabled
|
||||
or _is_shop_tab(focused)
|
||||
or not focused.is_visible_in_tree()
|
||||
):
|
||||
return false
|
||||
focused.pressed.emit()
|
||||
return true
|
||||
|
||||
|
||||
func _focused_shop_tab_index() -> int:
|
||||
var focused: Control = get_viewport().gui_get_focus_owner()
|
||||
for tab_index: int in _shop_tabs.size():
|
||||
if _shop_tabs[tab_index] == focused:
|
||||
return tab_index
|
||||
return -1
|
||||
|
||||
|
||||
func _is_shop_tab(control: Control) -> bool:
|
||||
for tab: OrganizerTab in _shop_tabs:
|
||||
if tab == control:
|
||||
return true
|
||||
return false
|
||||
|
||||
|
||||
func _focus_shop_tab(section_index: int) -> void:
|
||||
if section_index < 0 or section_index >= _shop_tabs.size():
|
||||
return
|
||||
var tab: OrganizerTab = _shop_tabs[section_index]
|
||||
if tab.focus_mode != Control.FOCUS_NONE and not tab.disabled:
|
||||
tab.call_deferred("grab_focus")
|
||||
|
||||
|
||||
func _request_shop_cooler() -> bool:
|
||||
|
|
@ -249,10 +346,15 @@ func _focus_shop_section() -> void:
|
|||
_set_feedback("")
|
||||
_configure_controller_focus()
|
||||
if _shop_section == ShopSection.UPGRADES:
|
||||
_reel_purchase.grab_focus()
|
||||
return
|
||||
for upgrade_button: Button in [
|
||||
_reel_purchase,
|
||||
_barrier_purchase,
|
||||
_cooler_purchase,
|
||||
]:
|
||||
if not upgrade_button.disabled:
|
||||
upgrade_button.grab_focus()
|
||||
return
|
||||
if _shop_section == ShopSection.SELL_FISH:
|
||||
_shop_tabs[int(ShopSection.SELL_FISH)].grab_focus()
|
||||
return
|
||||
for child: Node in _supplies_list.find_children(
|
||||
"*", "Button", true, false
|
||||
|
|
@ -261,9 +363,9 @@ func _focus_shop_section() -> void:
|
|||
if stock_button != null and not stock_button.disabled:
|
||||
stock_button.grab_focus()
|
||||
return
|
||||
var section_index: int = int(_shop_section)
|
||||
if section_index >= 0 and section_index < _shop_tabs.size():
|
||||
_shop_tabs[section_index].grab_focus()
|
||||
var close_button := %CloseButton as Button
|
||||
if close_button.focus_mode != Control.FOCUS_NONE:
|
||||
close_button.grab_focus()
|
||||
|
||||
|
||||
func _build_shop_tabs() -> void:
|
||||
|
|
@ -293,6 +395,8 @@ func _build_shop_tabs() -> void:
|
|||
func _select_shop_section(section_index: int, focus_content: bool) -> void:
|
||||
if _closing:
|
||||
return
|
||||
if focus_content:
|
||||
_controller_zone = ControllerZone.CONTENT
|
||||
if section_index == int(_shop_section):
|
||||
if section_index != ShopSection.SELL_FISH:
|
||||
var showing_upgrades := section_index == ShopSection.UPGRADES
|
||||
|
|
@ -325,11 +429,36 @@ func _select_shop_section(section_index: int, focus_content: bool) -> void:
|
|||
func _configure_controller_focus() -> void:
|
||||
if not visible or _cooler_page_active:
|
||||
return
|
||||
_apply_shop_controller_zone_focus_modes()
|
||||
var candidates: Array[Control] = []
|
||||
_collect_controller_focusables(self, candidates)
|
||||
ControllerFocusNavigationType.configure_spatial_neighbors(candidates)
|
||||
|
||||
|
||||
func _apply_shop_controller_zone_focus_modes() -> void:
|
||||
var tabs_active: bool = _controller_zone == ControllerZone.TABS
|
||||
for tab: OrganizerTab in _shop_tabs:
|
||||
tab.focus_mode = (
|
||||
Control.FOCUS_ALL if tabs_active else Control.FOCUS_NONE
|
||||
)
|
||||
var content_focus_mode := (
|
||||
Control.FOCUS_NONE if tabs_active else Control.FOCUS_ALL
|
||||
)
|
||||
for content_root: Control in [_upgrades_content, _supplies_content]:
|
||||
_set_descendant_button_focus_mode(content_root, content_focus_mode)
|
||||
(%CloseButton as Button).focus_mode = content_focus_mode
|
||||
|
||||
|
||||
func _set_descendant_button_focus_mode(
|
||||
root_control: Control,
|
||||
focus_mode: Control.FocusMode,
|
||||
) -> void:
|
||||
for child: Node in root_control.find_children("*", "BaseButton", true, false):
|
||||
var button := child as BaseButton
|
||||
if button != null:
|
||||
button.focus_mode = focus_mode
|
||||
|
||||
|
||||
func _collect_controller_focusables(
|
||||
root: Node,
|
||||
output: Array[Control],
|
||||
|
|
@ -488,10 +617,11 @@ func open_shop() -> bool:
|
|||
deactivate_shop_cooler_page()
|
||||
show()
|
||||
_shop_tab_bar.show()
|
||||
_controller_zone = ControllerZone.TABS
|
||||
_select_shop_section(ShopSection.UPGRADES, false)
|
||||
_refresh_all()
|
||||
call_deferred("_configure_controller_focus")
|
||||
_reel_purchase.grab_focus()
|
||||
_focus_shop_tab(int(ShopSection.UPGRADES))
|
||||
menu_visibility_changed.emit(true)
|
||||
return true
|
||||
|
||||
|
|
@ -751,6 +881,7 @@ func _refresh_supplies() -> void:
|
|||
_supplies_list.remove_child(child)
|
||||
child.queue_free()
|
||||
if _shop_section in [ShopSection.UPGRADES, ShopSection.SELL_FISH]:
|
||||
call_deferred("_configure_controller_focus")
|
||||
return
|
||||
_stock_title.text = SHOP_SECTION_LABELS[int(_shop_section)]
|
||||
var stock_item_ids: Array[StringName] = (
|
||||
|
|
@ -934,6 +1065,7 @@ func _refresh_supplies() -> void:
|
|||
var canvas_grid := _add_stock_icon_grid()
|
||||
for product_id: StringName in ArtShopStockType.GRID_PRODUCTS:
|
||||
_add_art_upgrade_button(product_id, canvas_grid)
|
||||
call_deferred("_configure_controller_focus")
|
||||
|
||||
|
||||
func _item_belongs_in_current_section(item: ItemDataType) -> bool:
|
||||
|
|
|
|||
|
|
@ -456,6 +456,10 @@ func setup(
|
|||
|
||||
|
||||
func _input(event: InputEvent) -> void:
|
||||
# The on-screen keyboard owns controller input while it is open. Its
|
||||
# overlay is processed before the UI beneath it and consumes the event.
|
||||
if is_controller_text_entry_open():
|
||||
return
|
||||
if _handle_virtual_mouse_input(event):
|
||||
get_viewport().set_input_as_handled()
|
||||
return
|
||||
|
|
@ -476,6 +480,8 @@ func _input(event: InputEvent) -> void:
|
|||
):
|
||||
return
|
||||
if event.is_action_pressed("character_call") and _can_use_character_call():
|
||||
if _character_call_yields_to_world_interaction(event):
|
||||
return
|
||||
_network_chat_service.send_local_character_call(
|
||||
_network_profile.call_id
|
||||
)
|
||||
|
|
@ -554,6 +560,19 @@ func _can_use_character_call() -> bool:
|
|||
)
|
||||
|
||||
|
||||
func _character_call_yields_to_world_interaction(
|
||||
event: InputEvent,
|
||||
) -> bool:
|
||||
return (
|
||||
event.is_action_pressed("interact")
|
||||
and _shop_interaction != null
|
||||
and _shop_interaction.is_local_player_in_range()
|
||||
and _fishing_spot != null
|
||||
and _fishing_spot.can_open_fishing_shop()
|
||||
and not _fishing_shop.visible
|
||||
)
|
||||
|
||||
|
||||
func _on_character_call_received(
|
||||
peer_id: int,
|
||||
call_id: String,
|
||||
|
|
|
|||
|
|
@ -358,6 +358,7 @@ func _build_interface() -> void:
|
|||
_catalog_scroll.horizontal_scroll_mode = (
|
||||
ScrollContainer.SCROLL_MODE_DISABLED
|
||||
)
|
||||
_catalog_scroll.follow_focus = true
|
||||
_catalog_scroll.scroll_vertical_custom_step = CATALOG_ROW_STEP
|
||||
catalog_scroll_margin.add_child(_catalog_scroll)
|
||||
_catalog_grid = GridContainer.new()
|
||||
|
|
|
|||
|
|
@ -4,6 +4,9 @@ extends Control
|
|||
const ControllerFocusNavigationType = preload(
|
||||
"res://ui/controller_focus_navigation.gd"
|
||||
)
|
||||
const ControllerMappingManagerType = preload(
|
||||
"res://settings/controller_mapping_manager.gd"
|
||||
)
|
||||
const CHECK_ICON: Texture2D = preload(
|
||||
"res://ui/icons/pictograms/check_mark_dark.png"
|
||||
)
|
||||
|
|
@ -12,6 +15,8 @@ const CHARACTER_FONT_SIZE: int = 34
|
|||
const CANONICAL_WINDOW_SIZE: Vector2 = Vector2(1280.0, 720.0)
|
||||
const TRIGGER_PRESS_THRESHOLD: float = 0.55
|
||||
const TRIGGER_RELEASE_THRESHOLD: float = 0.25
|
||||
const CARET_BLINK_INTERVAL: float = 0.5
|
||||
const CARET_GLYPH: String = "▌"
|
||||
|
||||
signal text_submitted(value: String)
|
||||
|
||||
|
|
@ -22,10 +27,14 @@ enum Page {
|
|||
}
|
||||
|
||||
var _enabled: bool = false
|
||||
var _controller_mapping_manager: ControllerMappingManagerType
|
||||
var _page: Page = Page.LOWER
|
||||
var _target: Control
|
||||
var _target_virtual_keyboard_enabled: bool = true
|
||||
var _buffer: String = ""
|
||||
var _buffer_caret: int = 0
|
||||
var _caret_blink_elapsed: float = 0.0
|
||||
var _caret_visible: bool = true
|
||||
var _preview: Label
|
||||
var _page_buttons: Array[Button] = []
|
||||
var _keys_host: VBoxContainer
|
||||
|
|
@ -52,6 +61,21 @@ func _ready() -> void:
|
|||
_build_interface()
|
||||
hide()
|
||||
set_process_input(true)
|
||||
set_process(true)
|
||||
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
if not visible:
|
||||
return
|
||||
_caret_blink_elapsed += delta
|
||||
if _caret_blink_elapsed < CARET_BLINK_INTERVAL:
|
||||
return
|
||||
_caret_blink_elapsed = fmod(
|
||||
_caret_blink_elapsed,
|
||||
CARET_BLINK_INTERVAL,
|
||||
)
|
||||
_caret_visible = not _caret_visible
|
||||
_refresh_preview()
|
||||
|
||||
|
||||
func set_enabled(enabled: bool) -> void:
|
||||
|
|
@ -68,6 +92,12 @@ func is_open() -> bool:
|
|||
return visible
|
||||
|
||||
|
||||
func setup_controller_mapping(
|
||||
mapping_manager: ControllerMappingManagerType,
|
||||
) -> void:
|
||||
_controller_mapping_manager = mapping_manager
|
||||
|
||||
|
||||
func request_for_focused_control() -> bool:
|
||||
return request_for_control(get_viewport().gui_get_focus_owner())
|
||||
|
||||
|
|
@ -88,23 +118,51 @@ func _input(event: InputEvent) -> void:
|
|||
return
|
||||
if visible:
|
||||
var joy_motion := event as InputEventJoypadMotion
|
||||
if joy_motion != null and _handle_trigger_shortcut(joy_motion):
|
||||
if joy_motion != null:
|
||||
if not _handle_trigger_shortcut(joy_motion):
|
||||
_move_key_focus(_controller_direction(event))
|
||||
get_viewport().set_input_as_handled()
|
||||
return
|
||||
var joy_button := event as InputEventJoypadButton
|
||||
if joy_button != null and joy_button.pressed:
|
||||
if joy_button.button_index == JOY_BUTTON_X:
|
||||
_backspace()
|
||||
get_viewport().set_input_as_handled()
|
||||
return
|
||||
if joy_button.button_index == JOY_BUTTON_LEFT_SHOULDER:
|
||||
_close_keyboard(false)
|
||||
get_viewport().set_input_as_handled()
|
||||
return
|
||||
if joy_button.button_index == JOY_BUTTON_RIGHT_SHOULDER:
|
||||
_set_page(wrapi(int(_page) + 1, 0, Page.size()))
|
||||
get_viewport().set_input_as_handled()
|
||||
return
|
||||
if joy_button != null:
|
||||
if joy_button.pressed:
|
||||
# Resolve the automapper's logical roles before raw button numbers.
|
||||
# Handheld mappings do not necessarily report their labeled A/B/X
|
||||
# buttons using Godot's matching physical button constants.
|
||||
if _event_matches_role(
|
||||
event,
|
||||
ControllerMappingManagerType.ROLE_X,
|
||||
JOY_BUTTON_X,
|
||||
):
|
||||
_backspace()
|
||||
elif _event_matches_role(
|
||||
event,
|
||||
ControllerMappingManagerType.ROLE_LB,
|
||||
JOY_BUTTON_LEFT_SHOULDER,
|
||||
):
|
||||
_set_page(wrapi(int(_page) - 1, 0, Page.size()))
|
||||
elif _event_matches_role(
|
||||
event,
|
||||
ControllerMappingManagerType.ROLE_RB,
|
||||
JOY_BUTTON_RIGHT_SHOULDER,
|
||||
):
|
||||
_set_page(wrapi(int(_page) + 1, 0, Page.size()))
|
||||
elif _event_matches_role(
|
||||
event,
|
||||
ControllerMappingManagerType.ROLE_A,
|
||||
JOY_BUTTON_A,
|
||||
):
|
||||
_activate_focused_key()
|
||||
elif _event_matches_role(
|
||||
event,
|
||||
ControllerMappingManagerType.ROLE_B,
|
||||
JOY_BUTTON_B,
|
||||
):
|
||||
_close_keyboard(true)
|
||||
else:
|
||||
_move_key_focus(_controller_direction(event))
|
||||
get_viewport().set_input_as_handled()
|
||||
return
|
||||
if event.is_action_pressed(&"ui_cancel"):
|
||||
_close_keyboard(true)
|
||||
get_viewport().set_input_as_handled()
|
||||
|
|
@ -114,8 +172,15 @@ func _input(event: InputEvent) -> void:
|
|||
joy_event == null
|
||||
or not joy_event.pressed
|
||||
or (
|
||||
joy_event.button_index != JOY_BUTTON_A
|
||||
and not event.is_action_pressed(&"ui_accept")
|
||||
not _event_matches_role(
|
||||
event,
|
||||
ControllerMappingManagerType.ROLE_A,
|
||||
JOY_BUTTON_A,
|
||||
)
|
||||
and (
|
||||
_controller_mapping_manager != null
|
||||
or not event.is_action_pressed(&"ui_accept")
|
||||
)
|
||||
)
|
||||
):
|
||||
return
|
||||
|
|
@ -123,6 +188,17 @@ func _input(event: InputEvent) -> void:
|
|||
get_viewport().set_input_as_handled()
|
||||
|
||||
|
||||
func _event_matches_role(
|
||||
event: InputEvent,
|
||||
role: StringName,
|
||||
fallback_button: JoyButton,
|
||||
) -> bool:
|
||||
if _controller_mapping_manager != null:
|
||||
return _controller_mapping_manager.event_matches_role(event, role)
|
||||
var button := event as InputEventJoypadButton
|
||||
return button != null and button.button_index == fallback_button
|
||||
|
||||
|
||||
func _is_available_for_controller() -> bool:
|
||||
return should_enable_for_controller(
|
||||
_enabled,
|
||||
|
|
@ -164,9 +240,12 @@ func _open_for(control: Control) -> void:
|
|||
)
|
||||
_target.set("virtual_keyboard_enabled", false)
|
||||
_buffer = str(_target.get("text"))
|
||||
_buffer_caret = _buffer.length()
|
||||
_set_target_caret(_buffer_caret)
|
||||
_page = Page.LOWER
|
||||
_attach_to_target_window(control)
|
||||
show()
|
||||
_reset_caret_blink()
|
||||
_refresh_preview()
|
||||
_rebuild_keys()
|
||||
|
||||
|
|
@ -179,6 +258,8 @@ func _close_keyboard(restore_focus: bool) -> void:
|
|||
_target_virtual_keyboard_enabled
|
||||
)
|
||||
hide()
|
||||
_left_trigger_pressed = false
|
||||
_right_trigger_pressed = false
|
||||
get_viewport().gui_release_focus()
|
||||
_restore_portable_host()
|
||||
_target = null
|
||||
|
|
@ -283,15 +364,13 @@ func _move_caret(direction: int) -> void:
|
|||
if not is_instance_valid(_target):
|
||||
_close_keyboard(false)
|
||||
return
|
||||
var caret: int = clampi(
|
||||
_buffer_caret = clampi(
|
||||
_get_caret_column() + direction,
|
||||
0,
|
||||
_buffer.length(),
|
||||
)
|
||||
if _target is LineEdit:
|
||||
(_target as LineEdit).caret_column = caret
|
||||
elif _target is TextEdit:
|
||||
(_target as TextEdit).set_caret_column(caret)
|
||||
_set_target_caret(_buffer_caret)
|
||||
_reset_caret_blink()
|
||||
_refresh_preview()
|
||||
|
||||
|
||||
|
|
@ -320,28 +399,44 @@ func _handle_trigger_shortcut(event: InputEventJoypadMotion) -> bool:
|
|||
|
||||
|
||||
func _get_caret_column() -> int:
|
||||
if _target is LineEdit:
|
||||
return clampi(
|
||||
(_target as LineEdit).caret_column,
|
||||
0,
|
||||
_buffer.length()
|
||||
)
|
||||
return _buffer.length()
|
||||
return clampi(_buffer_caret, 0, _buffer.length())
|
||||
|
||||
|
||||
func _set_target_text(caret: int) -> void:
|
||||
_buffer_caret = clampi(caret, 0, _buffer.length())
|
||||
if _target is LineEdit:
|
||||
var line_edit := _target as LineEdit
|
||||
line_edit.text = _buffer
|
||||
line_edit.caret_column = caret
|
||||
line_edit.text_changed.emit(_buffer)
|
||||
elif _target is TextEdit:
|
||||
var text_edit := _target as TextEdit
|
||||
text_edit.text = _buffer
|
||||
text_edit.text_changed.emit()
|
||||
_set_target_caret(_buffer_caret)
|
||||
_reset_caret_blink()
|
||||
_refresh_preview()
|
||||
|
||||
|
||||
func _set_target_caret(caret: int) -> void:
|
||||
if _target is LineEdit:
|
||||
(_target as LineEdit).caret_column = caret
|
||||
elif _target is TextEdit:
|
||||
var text_edit := _target as TextEdit
|
||||
var text_before_caret: String = _buffer.substr(0, caret)
|
||||
var caret_line: int = text_before_caret.count("\n")
|
||||
var last_newline: int = text_before_caret.rfind("\n")
|
||||
var caret_column: int = (
|
||||
caret if last_newline < 0 else caret - last_newline - 1
|
||||
)
|
||||
text_edit.set_caret_line(caret_line)
|
||||
text_edit.set_caret_column(caret_column)
|
||||
|
||||
|
||||
func _reset_caret_blink() -> void:
|
||||
_caret_blink_elapsed = 0.0
|
||||
_caret_visible = true
|
||||
|
||||
|
||||
func _refresh_preview() -> void:
|
||||
if _preview == null:
|
||||
return
|
||||
|
|
@ -349,7 +444,58 @@ func _refresh_preview() -> void:
|
|||
if _target is LineEdit and (_target as LineEdit).secret:
|
||||
displayed_text = "*".repeat(_buffer.length())
|
||||
var caret: int = clampi(_get_caret_column(), 0, displayed_text.length())
|
||||
_preview.text = displayed_text.insert(caret, "|")
|
||||
var caret_glyph: String = CARET_GLYPH if _caret_visible else ""
|
||||
_preview.text = displayed_text.insert(caret, caret_glyph)
|
||||
|
||||
|
||||
func _controller_direction(event: InputEvent) -> Vector2:
|
||||
if event.is_action_pressed(&"ui_up"):
|
||||
return Vector2.UP
|
||||
if event.is_action_pressed(&"ui_down"):
|
||||
return Vector2.DOWN
|
||||
if event.is_action_pressed(&"ui_left"):
|
||||
return Vector2.LEFT
|
||||
if event.is_action_pressed(&"ui_right"):
|
||||
return Vector2.RIGHT
|
||||
return Vector2.ZERO
|
||||
|
||||
|
||||
func _move_key_focus(direction: Vector2) -> void:
|
||||
if direction == Vector2.ZERO:
|
||||
return
|
||||
var focused: Control = get_viewport().gui_get_focus_owner()
|
||||
if focused == null or not is_ancestor_of(focused):
|
||||
_focus_first_key()
|
||||
return
|
||||
var neighbor_path := NodePath()
|
||||
if direction == Vector2.UP:
|
||||
neighbor_path = focused.focus_neighbor_top
|
||||
elif direction == Vector2.DOWN:
|
||||
neighbor_path = focused.focus_neighbor_bottom
|
||||
elif direction == Vector2.LEFT:
|
||||
neighbor_path = focused.focus_neighbor_left
|
||||
elif direction == Vector2.RIGHT:
|
||||
neighbor_path = focused.focus_neighbor_right
|
||||
if neighbor_path.is_empty():
|
||||
return
|
||||
var neighbor := focused.get_node_or_null(neighbor_path) as Control
|
||||
if ControllerFocusNavigationType.is_focusable(neighbor):
|
||||
neighbor.grab_focus()
|
||||
|
||||
|
||||
func _activate_focused_key() -> void:
|
||||
var focused: Control = get_viewport().gui_get_focus_owner()
|
||||
if focused == null or not is_ancestor_of(focused):
|
||||
_focus_first_key()
|
||||
focused = get_viewport().gui_get_focus_owner()
|
||||
var button := focused as BaseButton
|
||||
if button != null and not button.disabled:
|
||||
button.pressed.emit()
|
||||
|
||||
|
||||
func _focus_first_key() -> void:
|
||||
if not _key_buttons.is_empty():
|
||||
_key_buttons[0].grab_focus()
|
||||
|
||||
|
||||
func _set_page(page_index: int) -> void:
|
||||
|
|
|
|||
|
|
@ -219,6 +219,8 @@ func consume_escape() -> bool:
|
|||
func handle_controller_input(event: InputEvent) -> bool:
|
||||
if not _profile_active or not _profile_interactive:
|
||||
return false
|
||||
if event is InputEventJoypadButton or event is InputEventJoypadMotion:
|
||||
_ensure_controller_zone_focus()
|
||||
var cancel_pressed := _event_matches_controller_press(
|
||||
event,
|
||||
&"ui_cancel",
|
||||
|
|
@ -395,23 +397,7 @@ func _apply_controller_zone_focus() -> void:
|
|||
func _focus_controller_zone() -> void:
|
||||
if not _profile_interactive:
|
||||
return
|
||||
var controls: Array[Control] = []
|
||||
match _controller_zone:
|
||||
ControllerZone.ACCOUNT:
|
||||
controls = _account_controller_controls()
|
||||
ControllerZone.CATEGORIES:
|
||||
controls = _controls_under(_category_list)
|
||||
ControllerZone.OPTIONS:
|
||||
var groups: Array = _controller_option_groups()
|
||||
if not groups.is_empty():
|
||||
for item: Variant in groups[clampi(
|
||||
_controller_option_depth, 0, groups.size() - 1
|
||||
)]:
|
||||
var option_control := item as Control
|
||||
if option_control != null:
|
||||
controls.append(option_control)
|
||||
ControllerZone.COLOR_PICKER:
|
||||
controls = _color_picker_controller_controls()
|
||||
var controls: Array[Control] = _active_controller_zone_controls()
|
||||
for control: Control in controls:
|
||||
var button := control as BaseButton
|
||||
if (
|
||||
|
|
@ -429,6 +415,55 @@ func _focus_controller_zone() -> void:
|
|||
return
|
||||
|
||||
|
||||
func _ensure_controller_zone_focus() -> void:
|
||||
var controls: Array[Control] = _active_controller_zone_controls()
|
||||
if controls.is_empty():
|
||||
if _controller_zone == ControllerZone.OPTIONS:
|
||||
_controller_zone = ControllerZone.CATEGORIES
|
||||
_controller_option_depth = 0
|
||||
controls = _active_controller_zone_controls()
|
||||
if controls.is_empty():
|
||||
return
|
||||
var focused: Control = get_viewport().gui_get_focus_owner()
|
||||
if (
|
||||
focused != null
|
||||
and focused in controls
|
||||
and focused.is_visible_in_tree()
|
||||
and focused.focus_mode != Control.FOCUS_NONE
|
||||
):
|
||||
return
|
||||
_apply_controller_zone_focus()
|
||||
_focus_controller_zone()
|
||||
|
||||
|
||||
func _active_controller_zone_controls() -> Array[Control]:
|
||||
if (
|
||||
_discard_confirmation != null
|
||||
and _discard_confirmation.visible
|
||||
):
|
||||
return [_confirmation_confirm, _keep_editing_button]
|
||||
match _controller_zone:
|
||||
ControllerZone.ACCOUNT:
|
||||
return _account_controller_controls()
|
||||
ControllerZone.CATEGORIES:
|
||||
return _controls_under(_category_list)
|
||||
ControllerZone.OPTIONS:
|
||||
var groups: Array = _controller_option_groups()
|
||||
if groups.is_empty():
|
||||
return []
|
||||
var controls: Array[Control] = []
|
||||
for item: Variant in groups[clampi(
|
||||
_controller_option_depth, 0, groups.size() - 1
|
||||
)]:
|
||||
var option_control := item as Control
|
||||
if option_control != null:
|
||||
controls.append(option_control)
|
||||
return controls
|
||||
ControllerZone.COLOR_PICKER:
|
||||
return _color_picker_controller_controls()
|
||||
return []
|
||||
|
||||
|
||||
func _account_controller_controls() -> Array[Control]:
|
||||
var controls: Array[Control] = []
|
||||
for control: Control in [
|
||||
|
|
@ -451,21 +486,23 @@ func _controller_option_groups() -> Array:
|
|||
if _option_list == null:
|
||||
return groups
|
||||
if _category_id != "fur_pattern":
|
||||
groups.append(_controls_under(_option_list))
|
||||
var option_controls: Array[Control] = _controls_under(_option_list)
|
||||
if not option_controls.is_empty():
|
||||
groups.append(option_controls)
|
||||
return groups
|
||||
var section_tabs := _option_list.find_child(
|
||||
"FurSectionTabs", true, false
|
||||
) as Node
|
||||
groups.append(_controls_under(section_tabs))
|
||||
_append_controller_group(groups, _controls_under(section_tabs))
|
||||
if _active_fur_section == FUR_SECTION_PATTERNS:
|
||||
groups.append(_controls_under(_option_list.find_child(
|
||||
_append_controller_group(groups, _controls_under(_option_list.find_child(
|
||||
"FurPatternPartTabs", true, false
|
||||
)))
|
||||
groups.append(_controls_under(_option_list.find_child(
|
||||
_append_controller_group(groups, _controls_under(_option_list.find_child(
|
||||
"FurPatternGrid", true, false
|
||||
)))
|
||||
else:
|
||||
groups.append(_controls_under(_option_list.find_child(
|
||||
_append_controller_group(groups, _controls_under(_option_list.find_child(
|
||||
"FurColorChannelGrid", true, false
|
||||
)))
|
||||
var palette_controls: Array[Control] = _controls_under(
|
||||
|
|
@ -476,10 +513,15 @@ func _controller_option_groups() -> Array:
|
|||
) as Control
|
||||
if custom_picker != null:
|
||||
palette_controls.append(custom_picker)
|
||||
groups.append(palette_controls)
|
||||
_append_controller_group(groups, palette_controls)
|
||||
return groups
|
||||
|
||||
|
||||
func _append_controller_group(groups: Array, controls: Array[Control]) -> void:
|
||||
if not controls.is_empty():
|
||||
groups.append(controls)
|
||||
|
||||
|
||||
func _controls_under(root: Node) -> Array[Control]:
|
||||
var controls: Array[Control] = []
|
||||
if root == null:
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ const ControllerFocusNavigationType = preload(
|
|||
@export var focus_paths: Array[NodePath] = []
|
||||
@export var initial_focus_path: NodePath
|
||||
@export var back_focus_path: NodePath
|
||||
@export var back_entry_path: NodePath
|
||||
@export var maximum_layout_size: Vector2 = Vector2(720.0, 520.0)
|
||||
@export var compact_maximum_layout_size: Vector2 = Vector2.ZERO
|
||||
@export var compact_width_threshold: float = 680.0
|
||||
|
|
@ -146,6 +147,23 @@ func focus_back() -> void:
|
|||
back.grab_focus()
|
||||
|
||||
|
||||
func set_controller_focus_scope(controls: Array[Control]) -> void:
|
||||
for focus_control: Control in _focus_controls:
|
||||
focus_control.focus_mode = Control.FOCUS_NONE
|
||||
for control: Control in controls:
|
||||
if control != null and control.is_visible_in_tree():
|
||||
control.focus_mode = Control.FOCUS_ALL
|
||||
ControllerFocusNavigationType.configure_spatial_neighbors(controls)
|
||||
|
||||
|
||||
func restore_controller_focus_scope() -> void:
|
||||
if not visible or _is_transitioning:
|
||||
return
|
||||
for focus_control: Control in _focus_controls:
|
||||
focus_control.focus_mode = Control.FOCUS_ALL
|
||||
_configure_focus_navigation()
|
||||
|
||||
|
||||
func get_page_id() -> StringName:
|
||||
return page_id
|
||||
|
||||
|
|
@ -173,13 +191,13 @@ func _update_layout() -> void:
|
|||
_cluster.position = _resting_cluster_position
|
||||
_cluster.size = field_size
|
||||
_cluster.apply_layout(field_size, compact)
|
||||
ControllerFocusNavigationType.configure_spatial_neighbors(_focus_controls)
|
||||
_configure_focus_navigation()
|
||||
|
||||
|
||||
func _configure_focus_order() -> void:
|
||||
for bubble: BubbleButton in _bubbles:
|
||||
bubble.focus_mode = Control.FOCUS_NONE
|
||||
ControllerFocusNavigationType.configure_spatial_neighbors(_focus_controls)
|
||||
_configure_focus_navigation()
|
||||
|
||||
|
||||
func _set_interactive(interactive: bool) -> void:
|
||||
|
|
@ -203,16 +221,26 @@ func _set_interactive(interactive: bool) -> void:
|
|||
if interactive:
|
||||
call_deferred("_refresh_focus_navigation")
|
||||
else:
|
||||
ControllerFocusNavigationType.configure_spatial_neighbors(
|
||||
_focus_controls
|
||||
)
|
||||
_configure_focus_navigation()
|
||||
|
||||
|
||||
func _refresh_focus_navigation() -> void:
|
||||
if visible and not _is_transitioning:
|
||||
ControllerFocusNavigationType.configure_spatial_neighbors(
|
||||
_focus_controls
|
||||
)
|
||||
_configure_focus_navigation()
|
||||
|
||||
|
||||
func _configure_focus_navigation() -> void:
|
||||
ControllerFocusNavigationType.configure_spatial_neighbors(_focus_controls)
|
||||
if back_entry_path.is_empty() or back_focus_path.is_empty():
|
||||
return
|
||||
var entry := get_node_or_null(back_entry_path) as Control
|
||||
var back := get_node_or_null(back_focus_path) as Control
|
||||
if (
|
||||
ControllerFocusNavigationType.is_focusable(entry)
|
||||
and ControllerFocusNavigationType.is_focusable(back)
|
||||
):
|
||||
entry.focus_neighbor_bottom = entry.get_path_to(back)
|
||||
back.focus_neighbor_top = back.get_path_to(entry)
|
||||
|
||||
|
||||
func _finish_transition_out(generation: int, completed: Callable) -> void:
|
||||
|
|
|
|||
|
|
@ -131,6 +131,7 @@ var _pending_identity_operation := ""
|
|||
var _pending_identity_type := ""
|
||||
var _pending_identity_path := ""
|
||||
var _pending_import_data: Dictionary = {}
|
||||
var _active_adjustment_group: Dictionary = {}
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
|
|
@ -293,6 +294,56 @@ func setup_data_and_identity(
|
|||
_refresh_data_page()
|
||||
|
||||
|
||||
func _input(event: InputEvent) -> void:
|
||||
if not visible or is_input_mapping_capturing():
|
||||
return
|
||||
var mouse_button := event as InputEventMouseButton
|
||||
if (
|
||||
mouse_button != null
|
||||
and mouse_button.pressed
|
||||
and not _active_adjustment_group.is_empty()
|
||||
):
|
||||
_exit_adjustment_zone(false)
|
||||
return
|
||||
var button := event as InputEventJoypadButton
|
||||
if button == null or not button.pressed:
|
||||
return
|
||||
if (
|
||||
not _active_adjustment_group.is_empty()
|
||||
and _event_matches_controller_role(
|
||||
event,
|
||||
ControllerMappingManagerType.ROLE_B,
|
||||
&"ui_cancel",
|
||||
JOY_BUTTON_B,
|
||||
)
|
||||
):
|
||||
_exit_adjustment_zone(true)
|
||||
get_viewport().set_input_as_handled()
|
||||
return
|
||||
if not _event_matches_controller_role(
|
||||
event,
|
||||
ControllerMappingManagerType.ROLE_A,
|
||||
&"ui_accept",
|
||||
JOY_BUTTON_A,
|
||||
):
|
||||
return
|
||||
if _active_adjustment_group.is_empty():
|
||||
var group: Dictionary = _adjustment_group_for_control(
|
||||
get_viewport().gui_get_focus_owner()
|
||||
)
|
||||
if group.is_empty():
|
||||
return
|
||||
_enter_adjustment_zone(group)
|
||||
get_viewport().set_input_as_handled()
|
||||
return
|
||||
var focused := get_viewport().gui_get_focus_owner() as BaseButton
|
||||
var decrease := _active_adjustment_group.get("decrease") as BaseButton
|
||||
var increase := _active_adjustment_group.get("increase") as BaseButton
|
||||
if focused == decrease or focused == increase:
|
||||
focused.pressed.emit()
|
||||
get_viewport().set_input_as_handled()
|
||||
|
||||
|
||||
func open_panel(
|
||||
settings_manager: SettingsManagerType,
|
||||
presentation_mode: PresentationMode = PresentationMode.GAMEPLAY_MODAL,
|
||||
|
|
@ -370,6 +421,9 @@ func _finish_panel_close(applied_result: bool) -> void:
|
|||
|
||||
|
||||
func handle_back() -> void:
|
||||
if not _active_adjustment_group.is_empty():
|
||||
_exit_adjustment_zone(true)
|
||||
return
|
||||
if (
|
||||
_keyboard_mouse_mapping_panel != null
|
||||
and _keyboard_mouse_mapping_panel.is_open()
|
||||
|
|
@ -440,9 +494,105 @@ func _push_page(page_id: StringName) -> void:
|
|||
or get_active_page_id() == page_id
|
||||
):
|
||||
return
|
||||
_exit_adjustment_zone(false)
|
||||
_start_page_transition(page_id, true)
|
||||
|
||||
|
||||
func _adjustment_group_for_control(control: Control) -> Dictionary:
|
||||
for group: Dictionary in _adjustment_groups():
|
||||
if group.get("parent") as Control == control:
|
||||
return group
|
||||
return {}
|
||||
|
||||
|
||||
func _adjustment_groups() -> Array[Dictionary]:
|
||||
return [
|
||||
{
|
||||
"page": _controls_page,
|
||||
"parent": _mouse_value,
|
||||
"decrease": %MouseDecrease,
|
||||
"increase": %MouseIncrease,
|
||||
},
|
||||
{
|
||||
"page": _controls_page,
|
||||
"parent": _controller_value,
|
||||
"decrease": %ControllerDecrease,
|
||||
"increase": %ControllerIncrease,
|
||||
},
|
||||
{
|
||||
"page": _accessibility_page,
|
||||
"parent": _auto_click_interval,
|
||||
"decrease": %IntervalDecrease,
|
||||
"increase": %IntervalIncrease,
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
func _enter_adjustment_zone(group: Dictionary) -> void:
|
||||
var page := group.get("page") as SettingsBubblePage
|
||||
var parent := group.get("parent") as Control
|
||||
var decrease := group.get("decrease") as Control
|
||||
var increase := group.get("increase") as Control
|
||||
if page == null or parent == null or decrease == null or increase == null:
|
||||
return
|
||||
_active_adjustment_group = group
|
||||
var controls: Array[Control] = [decrease, parent, increase]
|
||||
page.set_controller_focus_scope(controls)
|
||||
_set_adjustment_neighbors(decrease, parent, increase)
|
||||
parent.grab_focus()
|
||||
|
||||
|
||||
func _exit_adjustment_zone(restore_parent_focus: bool) -> void:
|
||||
if _active_adjustment_group.is_empty():
|
||||
return
|
||||
var page := _active_adjustment_group.get("page") as SettingsBubblePage
|
||||
var parent := _active_adjustment_group.get("parent") as Control
|
||||
var decrease := _active_adjustment_group.get("decrease") as Control
|
||||
var increase := _active_adjustment_group.get("increase") as Control
|
||||
_active_adjustment_group.clear()
|
||||
for control: Control in [decrease, increase]:
|
||||
if control != null:
|
||||
control.focus_mode = Control.FOCUS_NONE
|
||||
if page != null:
|
||||
page.restore_controller_focus_scope()
|
||||
if restore_parent_focus and parent != null:
|
||||
parent.call_deferred("grab_focus")
|
||||
|
||||
|
||||
func _set_adjustment_neighbors(
|
||||
decrease: Control,
|
||||
parent: Control,
|
||||
increase: Control,
|
||||
) -> void:
|
||||
decrease.focus_neighbor_left = decrease.get_path_to(decrease)
|
||||
decrease.focus_neighbor_right = decrease.get_path_to(parent)
|
||||
parent.focus_neighbor_left = parent.get_path_to(decrease)
|
||||
parent.focus_neighbor_right = parent.get_path_to(increase)
|
||||
increase.focus_neighbor_left = increase.get_path_to(parent)
|
||||
increase.focus_neighbor_right = increase.get_path_to(increase)
|
||||
for control: Control in [decrease, parent, increase]:
|
||||
control.focus_neighbor_top = control.get_path_to(control)
|
||||
control.focus_neighbor_bottom = control.get_path_to(control)
|
||||
|
||||
|
||||
func _event_matches_controller_role(
|
||||
event: InputEvent,
|
||||
role: StringName,
|
||||
action: StringName,
|
||||
fallback_button: JoyButton,
|
||||
) -> bool:
|
||||
if _controller_mapping_manager != null:
|
||||
return _controller_mapping_manager.event_matches_role(event, role)
|
||||
var button := event as InputEventJoypadButton
|
||||
return (
|
||||
button != null
|
||||
and (
|
||||
button.button_index == fallback_button
|
||||
or event.is_action_pressed(action)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
func _start_page_transition(page_id: StringName, push_page: bool) -> void:
|
||||
var outgoing_page: SettingsBubblePage = _get_active_page()
|
||||
var incoming_page: SettingsBubblePage = _pages.get(page_id)
|
||||
|
|
|
|||
|
|
@ -425,6 +425,7 @@ bubble_paths = Array[NodePath]([NodePath("BubbleCluster/SoundBackButton")])
|
|||
focus_paths = Array[NodePath]([NodePath("Paper/Content/VolumeGrid/MasterVolumeSlider"), NodePath("Paper/Content/VolumeGrid/MusicVolumeSlider"), NodePath("Paper/Content/VolumeGrid/EffectsVolumeSlider"), NodePath("Paper/Content/VolumeGrid/EnvironmentVolumeSlider"), NodePath("BubbleCluster/SoundBackButton")])
|
||||
initial_focus_path = NodePath("Paper/Content/VolumeGrid/MasterVolumeSlider")
|
||||
back_focus_path = NodePath("BubbleCluster/SoundBackButton")
|
||||
back_entry_path = NodePath("Paper/Content/VolumeGrid/EnvironmentVolumeSlider")
|
||||
compact_maximum_layout_size = Vector2(544, 400)
|
||||
|
||||
[node name="BubbleCluster" type="Control" parent="SoundPage"]
|
||||
|
|
@ -601,7 +602,7 @@ grow_vertical = 2
|
|||
script = ExtResource("3_page")
|
||||
page_id = &"controls"
|
||||
bubble_paths = Array[NodePath]([NodePath("BubbleCluster/MouseDecrease"), NodePath("BubbleCluster/MouseValue"), NodePath("BubbleCluster/MouseIncrease"), NodePath("BubbleCluster/ControllerDecrease"), NodePath("BubbleCluster/ControllerValue"), NodePath("BubbleCluster/ControllerIncrease"), NodePath("BubbleCluster/InvertYToggle"), NodePath("BubbleCluster/OnScreenKeyboardToggle"), NodePath("BubbleCluster/ControllerMapping"), NodePath("BubbleCluster/KeyboardMapping"), NodePath("BubbleCluster/ControlsBackButton")])
|
||||
focus_paths = Array[NodePath]([NodePath("BubbleCluster/MouseDecrease"), NodePath("BubbleCluster/MouseValue"), NodePath("BubbleCluster/MouseIncrease"), NodePath("BubbleCluster/ControllerDecrease"), NodePath("BubbleCluster/ControllerValue"), NodePath("BubbleCluster/ControllerIncrease"), NodePath("BubbleCluster/InvertYToggle"), NodePath("BubbleCluster/OnScreenKeyboardToggle"), NodePath("BubbleCluster/ControllerMapping"), NodePath("BubbleCluster/KeyboardMapping"), NodePath("BubbleCluster/ControlsBackButton")])
|
||||
focus_paths = Array[NodePath]([NodePath("BubbleCluster/MouseValue"), NodePath("BubbleCluster/ControllerValue"), NodePath("BubbleCluster/InvertYToggle"), NodePath("BubbleCluster/OnScreenKeyboardToggle"), NodePath("BubbleCluster/ControllerMapping"), NodePath("BubbleCluster/KeyboardMapping"), NodePath("BubbleCluster/ControlsBackButton")])
|
||||
initial_focus_path = NodePath("BubbleCluster/MouseValue")
|
||||
back_focus_path = NodePath("BubbleCluster/ControlsBackButton")
|
||||
|
||||
|
|
@ -758,7 +759,7 @@ grow_vertical = 2
|
|||
script = ExtResource("3_page")
|
||||
page_id = &"accessibility"
|
||||
bubble_paths = Array[NodePath]([NodePath("BubbleCluster/AutoClickToggle"), NodePath("BubbleCluster/IntervalDecrease"), NodePath("BubbleCluster/AutoClickIntervalValue"), NodePath("BubbleCluster/IntervalIncrease"), NodePath("BubbleCluster/IntervalHelp"), NodePath("BubbleCluster/AccessibilityBackButton")])
|
||||
focus_paths = Array[NodePath]([NodePath("BubbleCluster/AutoClickToggle"), NodePath("BubbleCluster/IntervalDecrease"), NodePath("BubbleCluster/AutoClickIntervalValue"), NodePath("BubbleCluster/IntervalIncrease"), NodePath("BubbleCluster/AccessibilityBackButton")])
|
||||
focus_paths = Array[NodePath]([NodePath("BubbleCluster/AutoClickToggle"), NodePath("BubbleCluster/AutoClickIntervalValue"), NodePath("BubbleCluster/AccessibilityBackButton")])
|
||||
initial_focus_path = NodePath("BubbleCluster/AutoClickToggle")
|
||||
back_focus_path = NodePath("BubbleCluster/AccessibilityBackButton")
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,9 @@ const ControllerFocusPresentationType = preload(
|
|||
const ControllerFocusRecoveryType = preload(
|
||||
"res://ui/controller_focus_recovery.gd"
|
||||
)
|
||||
const ControllerMappingManagerType = preload(
|
||||
"res://settings/controller_mapping_manager.gd"
|
||||
)
|
||||
const OnScreenKeyboardType = preload("res://ui/on_screen_keyboard.gd")
|
||||
|
||||
signal effective_pixel_size_changed(
|
||||
|
|
@ -60,6 +63,12 @@ func set_on_screen_keyboard_enabled(enabled: bool) -> void:
|
|||
_on_screen_keyboard.set_enabled(enabled)
|
||||
|
||||
|
||||
func setup_controller_mapping(
|
||||
mapping_manager: ControllerMappingManagerType,
|
||||
) -> void:
|
||||
_on_screen_keyboard.setup_controller_mapping(mapping_manager)
|
||||
|
||||
|
||||
func set_pixel_size(pixel_size: int) -> void:
|
||||
_requested_pixel_size = clampi(
|
||||
pixel_size,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue