401 lines
12 KiB
GDScript3
401 lines
12 KiB
GDScript3
|
|
class_name KeyboardMouseMappingPanel
|
||
|
|
extends Control
|
||
|
|
|
||
|
|
signal closed
|
||
|
|
|
||
|
|
const KeyboardMouseMappingManagerType = preload(
|
||
|
|
"res://settings/keyboard_mouse_mapping_manager.gd"
|
||
|
|
)
|
||
|
|
const UtilityPageStyleType = preload("res://ui/utility_page_style.gd")
|
||
|
|
|
||
|
|
var _mapping_manager: KeyboardMouseMappingManagerType
|
||
|
|
var _binding_buttons: Dictionary = {}
|
||
|
|
var _progress_label: Label
|
||
|
|
var _mapping_scroll: ScrollContainer
|
||
|
|
var _reset_button: Button
|
||
|
|
var _close_button: Button
|
||
|
|
var _capturing_role: StringName = &""
|
||
|
|
|
||
|
|
|
||
|
|
func _ready() -> void:
|
||
|
|
set_process_input(true)
|
||
|
|
_build_interface()
|
||
|
|
hide()
|
||
|
|
|
||
|
|
|
||
|
|
func setup(mapping_manager: KeyboardMouseMappingManagerType) -> void:
|
||
|
|
_mapping_manager = mapping_manager
|
||
|
|
if not _mapping_manager.mapping_changed.is_connected(_refresh_bindings):
|
||
|
|
_mapping_manager.mapping_changed.connect(_refresh_bindings)
|
||
|
|
_refresh_bindings()
|
||
|
|
|
||
|
|
|
||
|
|
func open_panel() -> void:
|
||
|
|
if _mapping_manager == null:
|
||
|
|
return
|
||
|
|
_cancel_capture()
|
||
|
|
show()
|
||
|
|
_refresh_bindings()
|
||
|
|
var first_button := _binding_buttons.get(
|
||
|
|
KeyboardMouseMappingManagerType.ROLE_ORDER.front()
|
||
|
|
) as Button
|
||
|
|
if first_button != null:
|
||
|
|
first_button.grab_focus()
|
||
|
|
|
||
|
|
|
||
|
|
func is_open() -> bool:
|
||
|
|
return visible
|
||
|
|
|
||
|
|
|
||
|
|
func is_capturing() -> bool:
|
||
|
|
return visible and not _capturing_role.is_empty()
|
||
|
|
|
||
|
|
|
||
|
|
func request_back() -> void:
|
||
|
|
if not visible:
|
||
|
|
return
|
||
|
|
if not _capturing_role.is_empty():
|
||
|
|
_cancel_capture()
|
||
|
|
_progress_label.text = "keyboard binding cancelled"
|
||
|
|
return
|
||
|
|
close_panel()
|
||
|
|
|
||
|
|
|
||
|
|
func close_panel() -> void:
|
||
|
|
_cancel_capture()
|
||
|
|
hide()
|
||
|
|
closed.emit()
|
||
|
|
|
||
|
|
|
||
|
|
func _input(event: InputEvent) -> void:
|
||
|
|
if not visible or _mapping_manager == null or _capturing_role.is_empty():
|
||
|
|
return
|
||
|
|
var key_event := event as InputEventKey
|
||
|
|
if (
|
||
|
|
key_event != null
|
||
|
|
and key_event.pressed
|
||
|
|
and not key_event.echo
|
||
|
|
and key_event.keycode == KEY_ESCAPE
|
||
|
|
):
|
||
|
|
get_viewport().set_input_as_handled()
|
||
|
|
_cancel_capture()
|
||
|
|
_progress_label.text = "keyboard binding cancelled"
|
||
|
|
return
|
||
|
|
var binding: Dictionary = _mapping_manager.binding_from_event(event)
|
||
|
|
if binding.is_empty():
|
||
|
|
return
|
||
|
|
get_viewport().set_input_as_handled()
|
||
|
|
var role: StringName = _capturing_role
|
||
|
|
var saved: bool = _mapping_manager.set_binding(role, binding)
|
||
|
|
_cancel_capture()
|
||
|
|
_progress_label.text = (
|
||
|
|
"updated " + _mapping_manager.get_role_label(role)
|
||
|
|
if saved else "could not save that keyboard binding"
|
||
|
|
)
|
||
|
|
_refresh_bindings()
|
||
|
|
|
||
|
|
|
||
|
|
func _build_interface() -> void:
|
||
|
|
set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||
|
|
mouse_filter = Control.MOUSE_FILTER_STOP
|
||
|
|
z_index = 100
|
||
|
|
UtilityPageStyleType.apply_page(self)
|
||
|
|
|
||
|
|
var frame := PanelContainer.new()
|
||
|
|
frame.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||
|
|
frame.offset_left = 18.0
|
||
|
|
frame.offset_top = 18.0
|
||
|
|
frame.offset_right = -18.0
|
||
|
|
frame.offset_bottom = -18.0
|
||
|
|
frame.add_theme_stylebox_override(
|
||
|
|
"panel",
|
||
|
|
UtilityPageStyleType.rounded_style(
|
||
|
|
UtilityPageStyleType.OCEAN_PANEL_DEEP,
|
||
|
|
20,
|
||
|
|
),
|
||
|
|
)
|
||
|
|
add_child(frame)
|
||
|
|
|
||
|
|
var outer_margin := MarginContainer.new()
|
||
|
|
outer_margin.add_theme_constant_override("margin_left", 24)
|
||
|
|
outer_margin.add_theme_constant_override("margin_top", 20)
|
||
|
|
outer_margin.add_theme_constant_override("margin_right", 24)
|
||
|
|
outer_margin.add_theme_constant_override("margin_bottom", 20)
|
||
|
|
frame.add_child(outer_margin)
|
||
|
|
|
||
|
|
var page := VBoxContainer.new()
|
||
|
|
page.add_theme_constant_override("separation", 12)
|
||
|
|
outer_margin.add_child(page)
|
||
|
|
|
||
|
|
var heading_row := HBoxContainer.new()
|
||
|
|
heading_row.add_theme_constant_override("separation", 12)
|
||
|
|
page.add_child(heading_row)
|
||
|
|
|
||
|
|
var title := Label.new()
|
||
|
|
title.text = "keyboard binds"
|
||
|
|
title.add_theme_font_size_override("font_size", 28)
|
||
|
|
title.add_theme_color_override(
|
||
|
|
"font_color", UtilityPageStyleType.OCEAN_TEXT_PRIMARY
|
||
|
|
)
|
||
|
|
title.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||
|
|
heading_row.add_child(title)
|
||
|
|
|
||
|
|
var device_label := Label.new()
|
||
|
|
device_label.text = "mouse + keyboard"
|
||
|
|
device_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
|
||
|
|
device_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||
|
|
device_label.add_theme_font_size_override("font_size", 15)
|
||
|
|
device_label.add_theme_color_override(
|
||
|
|
"font_color", UtilityPageStyleType.OCEAN_TEXT_SECONDARY
|
||
|
|
)
|
||
|
|
heading_row.add_child(device_label)
|
||
|
|
|
||
|
|
var instruction_label := Label.new()
|
||
|
|
instruction_label.text = (
|
||
|
|
"select a row, then press a keyboard key or mouse button. "
|
||
|
|
+ "duplicate bindings are marked in red."
|
||
|
|
)
|
||
|
|
instruction_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||
|
|
instruction_label.add_theme_font_size_override("font_size", 15)
|
||
|
|
instruction_label.add_theme_color_override(
|
||
|
|
"font_color", UtilityPageStyleType.OCEAN_TEXT_SECONDARY
|
||
|
|
)
|
||
|
|
page.add_child(instruction_label)
|
||
|
|
|
||
|
|
_progress_label = Label.new()
|
||
|
|
_progress_label.custom_minimum_size.y = 34.0
|
||
|
|
_progress_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||
|
|
_progress_label.add_theme_font_size_override("font_size", 18)
|
||
|
|
_progress_label.add_theme_color_override(
|
||
|
|
"font_color", UtilityPageStyleType.OCEAN_TEXT_PRIMARY
|
||
|
|
)
|
||
|
|
page.add_child(_progress_label)
|
||
|
|
|
||
|
|
_mapping_scroll = ScrollContainer.new()
|
||
|
|
_mapping_scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||
|
|
_mapping_scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
|
||
|
|
_mapping_scroll.follow_focus = true
|
||
|
|
page.add_child(_mapping_scroll)
|
||
|
|
|
||
|
|
var role_grid := GridContainer.new()
|
||
|
|
role_grid.columns = 2
|
||
|
|
role_grid.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||
|
|
role_grid.add_theme_constant_override("h_separation", 12)
|
||
|
|
role_grid.add_theme_constant_override("v_separation", 7)
|
||
|
|
_mapping_scroll.add_child(role_grid)
|
||
|
|
|
||
|
|
for role: StringName in KeyboardMouseMappingManagerType.ROLE_ORDER:
|
||
|
|
var role_label := Label.new()
|
||
|
|
role_label.text = KeyboardMouseMappingManagerType.ROLE_LABELS.get(
|
||
|
|
role,
|
||
|
|
str(role),
|
||
|
|
)
|
||
|
|
role_label.custom_minimum_size = Vector2(330.0, 42.0)
|
||
|
|
role_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||
|
|
role_label.add_theme_font_size_override("font_size", 16)
|
||
|
|
role_label.add_theme_color_override(
|
||
|
|
"font_color", UtilityPageStyleType.OCEAN_TEXT_PRIMARY
|
||
|
|
)
|
||
|
|
role_grid.add_child(role_label)
|
||
|
|
|
||
|
|
var binding_button := Button.new()
|
||
|
|
binding_button.custom_minimum_size = Vector2(210.0, 42.0)
|
||
|
|
binding_button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||
|
|
binding_button.focus_mode = Control.FOCUS_ALL
|
||
|
|
binding_button.tooltip_text = (
|
||
|
|
"change " + _mapping_manager_label(role)
|
||
|
|
)
|
||
|
|
UtilityPageStyleType.apply_compact_ocean_button(binding_button)
|
||
|
|
binding_button.pressed.connect(_begin_capture.bind(role))
|
||
|
|
role_grid.add_child(binding_button)
|
||
|
|
_binding_buttons[role] = binding_button
|
||
|
|
|
||
|
|
var footer := HBoxContainer.new()
|
||
|
|
footer.alignment = BoxContainer.ALIGNMENT_END
|
||
|
|
footer.add_theme_constant_override("separation", 10)
|
||
|
|
page.add_child(footer)
|
||
|
|
|
||
|
|
_reset_button = Button.new()
|
||
|
|
_reset_button.text = "restore defaults"
|
||
|
|
_reset_button.tooltip_text = "restore the project keyboard defaults"
|
||
|
|
UtilityPageStyleType.apply_ocean_button(_reset_button)
|
||
|
|
_reset_button.pressed.connect(_reset_mapping)
|
||
|
|
footer.add_child(_reset_button)
|
||
|
|
|
||
|
|
_close_button = Button.new()
|
||
|
|
_close_button.text = "done"
|
||
|
|
UtilityPageStyleType.apply_ocean_button(_close_button)
|
||
|
|
_close_button.pressed.connect(close_panel)
|
||
|
|
footer.add_child(_close_button)
|
||
|
|
_configure_mapping_navigation()
|
||
|
|
|
||
|
|
|
||
|
|
func _configure_mapping_navigation() -> void:
|
||
|
|
var buttons: Array[Button] = []
|
||
|
|
for role: StringName in KeyboardMouseMappingManagerType.ROLE_ORDER:
|
||
|
|
var binding_button := _binding_buttons.get(role) as Button
|
||
|
|
if binding_button != null:
|
||
|
|
buttons.append(binding_button)
|
||
|
|
if buttons.is_empty():
|
||
|
|
return
|
||
|
|
for index: int in buttons.size():
|
||
|
|
var button: Button = buttons[index]
|
||
|
|
var previous: Control = buttons[index - 1] if index > 0 else button
|
||
|
|
var next: Control = (
|
||
|
|
buttons[index + 1]
|
||
|
|
if index < buttons.size() - 1
|
||
|
|
else _close_button
|
||
|
|
)
|
||
|
|
_set_focus_neighbors(button, button, button, previous, next)
|
||
|
|
button.focus_previous = button.get_path_to(previous)
|
||
|
|
button.focus_next = button.get_path_to(next)
|
||
|
|
var last_button: Button = buttons.back()
|
||
|
|
_set_focus_neighbors(
|
||
|
|
_reset_button,
|
||
|
|
_reset_button,
|
||
|
|
_close_button,
|
||
|
|
last_button,
|
||
|
|
_reset_button,
|
||
|
|
)
|
||
|
|
_set_focus_neighbors(
|
||
|
|
_close_button,
|
||
|
|
_reset_button,
|
||
|
|
_close_button,
|
||
|
|
last_button,
|
||
|
|
_close_button,
|
||
|
|
)
|
||
|
|
_close_button.focus_previous = _close_button.get_path_to(last_button)
|
||
|
|
_close_button.focus_next = _close_button.get_path_to(_reset_button)
|
||
|
|
|
||
|
|
|
||
|
|
func _set_focus_neighbors(
|
||
|
|
control: Control,
|
||
|
|
left: Control,
|
||
|
|
right: Control,
|
||
|
|
top: Control,
|
||
|
|
bottom: Control,
|
||
|
|
) -> void:
|
||
|
|
control.focus_neighbor_left = control.get_path_to(left)
|
||
|
|
control.focus_neighbor_right = control.get_path_to(right)
|
||
|
|
control.focus_neighbor_top = control.get_path_to(top)
|
||
|
|
control.focus_neighbor_bottom = control.get_path_to(bottom)
|
||
|
|
|
||
|
|
|
||
|
|
func _mapping_manager_label(role: StringName) -> String:
|
||
|
|
if _mapping_manager == null:
|
||
|
|
return str(
|
||
|
|
KeyboardMouseMappingManagerType.ROLE_LABELS.get(role, str(role))
|
||
|
|
)
|
||
|
|
return _mapping_manager.get_role_label(role)
|
||
|
|
|
||
|
|
|
||
|
|
func _begin_capture(role: StringName) -> void:
|
||
|
|
if _mapping_manager == null:
|
||
|
|
return
|
||
|
|
_capturing_role = role
|
||
|
|
_set_action_buttons_disabled(true)
|
||
|
|
_progress_label.text = (
|
||
|
|
"press a key or mouse button for "
|
||
|
|
+ _mapping_manager.get_role_label(role)
|
||
|
|
+ " • escape cancels"
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
func _cancel_capture() -> void:
|
||
|
|
_capturing_role = &""
|
||
|
|
_set_action_buttons_disabled(false)
|
||
|
|
if _progress_label != null:
|
||
|
|
_progress_label.text = ""
|
||
|
|
|
||
|
|
|
||
|
|
func _set_action_buttons_disabled(disabled: bool) -> void:
|
||
|
|
if _reset_button != null:
|
||
|
|
_reset_button.disabled = disabled
|
||
|
|
if _close_button != null:
|
||
|
|
_close_button.disabled = disabled
|
||
|
|
for button_value: Variant in _binding_buttons.values():
|
||
|
|
var button := button_value as Button
|
||
|
|
if button != null:
|
||
|
|
button.disabled = disabled
|
||
|
|
|
||
|
|
|
||
|
|
func _reset_mapping() -> void:
|
||
|
|
if _mapping_manager == null:
|
||
|
|
return
|
||
|
|
var reset: bool = _mapping_manager.reset_mapping()
|
||
|
|
_progress_label.text = (
|
||
|
|
"restored the project keyboard defaults"
|
||
|
|
if reset else "could not restore keyboard defaults"
|
||
|
|
)
|
||
|
|
_refresh_bindings()
|
||
|
|
|
||
|
|
|
||
|
|
func _refresh_bindings() -> void:
|
||
|
|
if _mapping_manager == null or _reset_button == null:
|
||
|
|
return
|
||
|
|
var bindings: Dictionary = _mapping_manager.get_active_bindings()
|
||
|
|
for role: StringName in KeyboardMouseMappingManagerType.ROLE_ORDER:
|
||
|
|
var button := _binding_buttons.get(role) as Button
|
||
|
|
if button == null:
|
||
|
|
continue
|
||
|
|
var raw_binding: Variant = bindings.get(str(role), {})
|
||
|
|
button.text = (
|
||
|
|
_mapping_manager.binding_label(raw_binding as Dictionary)
|
||
|
|
if typeof(raw_binding) == TYPE_DICTIONARY else "unmapped"
|
||
|
|
)
|
||
|
|
var conflict_role: StringName = _find_binding_conflict(
|
||
|
|
role,
|
||
|
|
raw_binding as Dictionary,
|
||
|
|
bindings,
|
||
|
|
)
|
||
|
|
_apply_binding_button_style(button, not conflict_role.is_empty())
|
||
|
|
button.tooltip_text = (
|
||
|
|
"warning: also assigned to "
|
||
|
|
+ _mapping_manager.get_role_label(conflict_role)
|
||
|
|
if not conflict_role.is_empty()
|
||
|
|
else "change " + _mapping_manager.get_role_label(role)
|
||
|
|
)
|
||
|
|
_reset_button.disabled = not _mapping_manager.has_custom_mapping()
|
||
|
|
|
||
|
|
|
||
|
|
func _find_binding_conflict(
|
||
|
|
role: StringName,
|
||
|
|
binding: Dictionary,
|
||
|
|
bindings: Dictionary,
|
||
|
|
) -> StringName:
|
||
|
|
if binding.is_empty():
|
||
|
|
return &""
|
||
|
|
for other_role: StringName in KeyboardMouseMappingManagerType.ROLE_ORDER:
|
||
|
|
if other_role == role:
|
||
|
|
continue
|
||
|
|
var raw_other: Variant = bindings.get(str(other_role), {})
|
||
|
|
if typeof(raw_other) != TYPE_DICTIONARY:
|
||
|
|
continue
|
||
|
|
if KeyboardMouseMappingManagerType.bindings_conflict(
|
||
|
|
binding,
|
||
|
|
raw_other as Dictionary,
|
||
|
|
):
|
||
|
|
return other_role
|
||
|
|
return &""
|
||
|
|
|
||
|
|
|
||
|
|
func _apply_binding_button_style(button: Button, has_conflict: bool) -> void:
|
||
|
|
UtilityPageStyleType.apply_compact_ocean_button(button)
|
||
|
|
if not has_conflict:
|
||
|
|
return
|
||
|
|
var colors: Dictionary = {
|
||
|
|
&"normal": UtilityPageStyleType.OCEAN_DANGER,
|
||
|
|
&"hover": UtilityPageStyleType.OCEAN_DANGER.lightened(0.12),
|
||
|
|
&"pressed": UtilityPageStyleType.OCEAN_DANGER.darkened(0.12),
|
||
|
|
&"focus": UtilityPageStyleType.OCEAN_DANGER.lightened(0.12),
|
||
|
|
}
|
||
|
|
for state: StringName in colors:
|
||
|
|
var style := UtilityPageStyleType.ocean_button_style(
|
||
|
|
colors[state] as Color
|
||
|
|
)
|
||
|
|
style.content_margin_left = 10.0
|
||
|
|
style.content_margin_right = 10.0
|
||
|
|
style.content_margin_top = 4.0
|
||
|
|
style.content_margin_bottom = 4.0
|
||
|
|
button.add_theme_stylebox_override(state, style)
|