@tool ## Helper class for formatting GUIDE input for the UI. class_name GUIDEInputFormatter const IconMaker = preload("icon_maker/icon_maker.gd") const KeyRenderer = preload("renderers/keyboard/key_renderer.tscn") const MouseRenderer = preload("renderers/mouse/mouse_renderer.tscn") const TouchRenderer = preload("renderers/touch/touch_renderer.tscn") const JoyRenderer = preload("renderers/joy/joy_renderer.tscn") const XboxRenderer = preload("renderers/controllers/xbox/xbox_controller_renderer.tscn") const PlayStationRenderer = preload("renderers/controllers/playstation/playstation_controller_renderer.tscn") const SwitchRenderer = preload("renderers/controllers/switch/switch_controller_renderer.tscn") const ActionRenderer = preload("renderers/misc/action_renderer.tscn") const FallbackRenderer = preload("renderers/misc/fallback_renderer.tscn") const DefaultTextProvider = preload("text_providers/default_text_provider.gd") const XboxTextProvider = preload("text_providers/controllers/xbox/xbox_controller_text_provider.gd") const PlayStationTextProvider = preload("text_providers/controllers/playstation/playstation_controller_text_provider.gd") const SwitchTextProvider = preload("text_providers/controllers/switch/switch_controller_text_provider.gd") # These are shared across all instances static var _icon_maker:IconMaker static var _icon_renderers:Array[GUIDEIconRenderer] = [] static var _text_providers:Array[GUIDETextProvider] = [] static var _is_ready:bool = false ## Separator to separate mixed input. static var mixed_input_separator:String = ", " ## Separator to separate chorded input. static var chorded_input_separator:String = " + " ## Separator to separate combo input. static var combo_input_separator:String = " > " # These are per-instance var _action_resolver:Callable var _icon_size:int static func _ensure_readiness(): if _is_ready: return # reconnect to an icon maker that might be there var root = Engine.get_main_loop().root for child in root.get_children(): if child is IconMaker: _icon_maker = child if _icon_maker == null: _icon_maker = preload("icon_maker/icon_maker.tscn").instantiate() root.add_child.call_deferred(_icon_maker) add_icon_renderer(KeyRenderer.instantiate()) add_icon_renderer(MouseRenderer.instantiate()) add_icon_renderer(TouchRenderer.instantiate()) add_icon_renderer(ActionRenderer.instantiate()) add_icon_renderer(JoyRenderer.instantiate()) add_icon_renderer(XboxRenderer.instantiate()) add_icon_renderer(PlayStationRenderer.instantiate()) add_icon_renderer(SwitchRenderer.instantiate()) add_icon_renderer(FallbackRenderer.instantiate()) add_text_provider(DefaultTextProvider.new()) add_text_provider(XboxTextProvider.new()) add_text_provider(PlayStationTextProvider.new()) add_text_provider(SwitchTextProvider.new()) _is_ready = true ## This will clean up the rendering infrastructure used for generating ## icons. Note that in a normal game you will have no need to call this ## as the infrastructure is needed throughout the run of your game. ## It might be useful in tests though, to get rid of spurious warnings ## about orphaned nodes. static func cleanup(): _is_ready = false # free all the nodes to avoid memory leaks for renderer in _icon_renderers: renderer.queue_free() _icon_renderers.clear() _text_providers.clear() if is_instance_valid(_icon_maker): _icon_maker.queue_free() func _init(icon_size:int = 32, resolver:Callable = func(action) -> GUIDEActionMapping: return null ): _icon_size = icon_size _action_resolver = resolver ## Adds an icon renderer for rendering icons. static func add_icon_renderer(renderer:GUIDEIconRenderer) -> void: _icon_renderers.append(renderer) _icon_renderers.sort_custom(func(r1, r2): return r1.priority < r2.priority) ## Removes an icon renderer. static func remove_icon_renderer(renderer:GUIDEIconRenderer) -> void: _icon_renderers.erase(renderer) ## Adds a text provider for rendering text. static func add_text_provider(provider:GUIDETextProvider) -> void: _text_providers.append(provider) _text_providers.sort_custom(func(r1, r2): return r1.priority < r2.priority) ## Removes a text provider static func remove_text_provider(provider:GUIDETextProvider) -> void: _text_providers.erase(provider) ## Returns an input formatter that can format actions using the currently active inputs. static func for_active_contexts(icon_size:int = 32) -> GUIDEInputFormatter: var resolver = func(action:GUIDEAction) -> GUIDEActionMapping: for mapping in GUIDE._active_action_mappings: if mapping.action == action: return mapping return null return GUIDEInputFormatter.new(icon_size, resolver) ## Returns an input formatter that can format actions using the given context. static func for_context(context:GUIDEMappingContext, icon_size:int = 32) -> GUIDEInputFormatter: var resolver:Callable = func(action:GUIDEAction) -> GUIDEActionMapping: for mapping in context.mappings: if mapping.action == action: return mapping return null return GUIDEInputFormatter.new(icon_size, resolver) ## Formats the action input as richtext with icons suitable for a RichTextLabel. This function ## is async as icons may need to be rendered in the background which can take a few frames, so ## you will need to await on it. func action_as_richtext_async(action:GUIDEAction) -> String: return await _materialized_as_richtext_async(_materialize_action_input(action)) ## Formats the action input as plain text which can be used in any UI component. This is a bit ## more light-weight than formatting as icons and returns immediately. func action_as_text(action:GUIDEAction) -> String: return _materialized_as_text(_materialize_action_input(action)) ## Formats the input as richtext with icons suitable for a RichTextLabel. This function ## is async as icons may need to be rendered in the background which can take a few frames, so ## you will need to await on it. func input_as_richtext_async(input:GUIDEInput, materialize_actions:bool = true) -> String: return await _materialized_as_richtext_async(_materialize_input(input, materialize_actions)) ## Formats the input as plain text which can be used in any UI component. This is a bit ## more light-weight than formatting as icons and returns immediately. func input_as_text(input:GUIDEInput, materialize_actions:bool = true) -> String: return _materialized_as_text(_materialize_input(input, materialize_actions)) ## Renders materialized input as text. func _materialized_as_text(input:MaterializedInput) -> String: _ensure_readiness() if input is MaterializedSimpleInput: var text:String = "" for provider in _text_providers: if provider.supports(input.input): text = provider.get_text(input.input) # first provider wins break if text == "": pass ## push_warning("No formatter found for input ", input) return text var separator = _separator_for_input(input) if separator == "" or input.parts.is_empty(): return "" var parts:Array[String] = [] for part in input.parts: parts.append(_materialized_as_text(part)) return separator.join(parts) ## Renders materialized input as rich text. func _materialized_as_richtext_async(input:MaterializedInput) -> String: _ensure_readiness() if input is MaterializedSimpleInput: var icon:Texture2D = null for renderer in _icon_renderers: if renderer.supports(input.input): icon = await _icon_maker.make_icon(input.input, renderer, _icon_size) # first renderer wins break if icon == null: push_warning("No renderer found for input ", input) return "" return "[img]%s[/img]" % [icon.resource_path] var separator = _separator_for_input(input) if separator == "" or input.parts.is_empty(): return "" var parts:Array[String] = [] for part in input.parts: parts.append(await _materialized_as_richtext_async(part)) return separator.join(parts) func _separator_for_input(input:MaterializedInput) -> String: if input is MaterializedMixedInput: return mixed_input_separator elif input is MaterializedComboInput: return combo_input_separator elif input is MaterializedChordedInput: return chorded_input_separator push_error("Unknown materialized input type") return "" ## Materializes action input. func _materialize_action_input(action:GUIDEAction) -> MaterializedInput: var result := MaterializedMixedInput.new() if action == null: push_warning("Trying to get inputs for a null action.") return result # get the mapping for this action var mapping:GUIDEActionMapping = _action_resolver.call(action) # if we have no mapping, well that's it, return an empty mixed input if mapping == null: return result # collect input mappings for input_mapping in mapping.input_mappings: var chorded_actions:Array[MaterializedInput] = [] var combos:Array[MaterializedInput] = [] for trigger in input_mapping.triggers: # if we have a combo trigger, materialize its input. if trigger is GUIDETriggerCombo: var combo := MaterializedComboInput.new() for step:GUIDETriggerComboStep in trigger.steps: combo.parts.append(_materialize_action_input(step.action)) combos.append(combo) # if we have a chorded action, materialize its input if trigger is GUIDETriggerChordedAction: chorded_actions.append(_materialize_action_input(trigger.action)) if not chorded_actions.is_empty(): # if we have chorded action then the whole mapping is chorded. var chord := MaterializedChordedInput.new() for chorded_action in chorded_actions: chord.parts.append(chorded_action) for combo in combos: chord.parts.append(combo) if combos.is_empty(): if input_mapping.input != null: chord.parts.append(_materialize_input(input_mapping.input)) result.parts.append(chord) else: for combo in combos: result.parts.append(combo) if combos.is_empty(): if input_mapping.input != null: result.parts.append(_materialize_input(input_mapping.input)) return result ## Materializes direct input. func _materialize_input(input:GUIDEInput, materialize_actions:bool = true) -> MaterializedInput: if input == null: push_warning("Trying to materialize a null input.") return MaterializedMixedInput.new() # if its an action input, get its parts if input is GUIDEInputAction: if materialize_actions: return _materialize_action_input(input.action) else: return MaterializedSimpleInput.new(input) # if its a key input, split out the modifiers if input is GUIDEInputKey: var chord := MaterializedChordedInput.new() if input.control: var ctrl = GUIDEInputKey.new() ctrl.key = KEY_CTRL chord.parts.append(MaterializedSimpleInput.new(ctrl)) if input.alt: var alt = GUIDEInputKey.new() alt.key = KEY_ALT chord.parts.append(MaterializedSimpleInput.new(alt)) if input.shift: var shift = GUIDEInputKey.new() shift.key = KEY_SHIFT chord.parts.append(MaterializedSimpleInput.new(shift)) if input.meta: var meta = GUIDEInputKey.new() meta.key = KEY_META chord.parts.append(MaterializedSimpleInput.new(meta)) # got no modifiers? if chord.parts.is_empty(): return MaterializedSimpleInput.new(input) chord.parts.append(MaterializedSimpleInput.new(input)) return chord # everything else is just a simple input return MaterializedSimpleInput.new(input) class MaterializedInput: pass class MaterializedSimpleInput: extends MaterializedInput var input:GUIDEInput func _init(input:GUIDEInput): self.input = input class MaterializedMixedInput: extends MaterializedInput var parts:Array[MaterializedInput] = [] class MaterializedChordedInput: extends MaterializedInput var parts:Array[MaterializedInput] = [] class MaterializedComboInput: extends MaterializedInput var parts:Array[MaterializedInput] = [] ## Returns the name of the associated joystick/pad of the given input. ## If the input is no joy input or the device name cannot be determined ## returns an empty string. static func _joy_name_for_input(input:GUIDEInput) -> String: if not input is GUIDEInputJoyBase: return "" var joypads:Array[int] = Input.get_connected_joypads() var joy_index = input.joy_index if joy_index < 0: # pick the first one joy_index = 0 # We don't have such a controller, so bail out. if joypads.size() <= joy_index: return "" var id = joypads[joy_index] return Input.get_joy_name(id)