diff --git a/Mods/Keybinds/Keybinds.gd b/Mods/Keybinds/Keybinds.gd new file mode 100644 index 00000000..3ecfbd2a --- /dev/null +++ b/Mods/Keybinds/Keybinds.gd @@ -0,0 +1,258 @@ +extends Mod_Base +class_name Keybinds + +@export var panel : Control = null + +# List of actions and keys, that will be imported to the input map +# [ +# { +# "key" : number_for_key_representation +# "action_name" : "action_name_here" +# } +# ] +var key_actions : Array = [] +var settings_ui : KeybindSettingUI +var KEYBIND_PREFIX : String = "kb_" + +func _ready() -> void: + # Alert all mods that rely on keybinds of the prefix we are using. + var global_dict = get_global_mod_data("Keybinds") + if not global_dict.has("prefix"): + global_dict["prefix"] = KEYBIND_PREFIX + else: + KEYBIND_PREFIX = global_dict["prefix"] + + # Not essential, but allows us to reference it in other mods quickly. + send_global_mod_message("Keybinds", global_dict) + + # Remove the temp panel and attach the children to the + # settings window! This way we aren't messing around with + # tracked settings, and instead are simply loading our own UI. + remove_child(panel) + + var new_keybind_button : Button = Button.new() + new_keybind_button.text = "New Keybind" + get_settings_window().add_child(new_keybind_button) + new_keybind_button.pressed.connect(_new_keybind) + + var load_default_button : Button = Button.new() + load_default_button.text = "Reset Keybinds" + get_settings_window().add_child(load_default_button) + load_default_button.pressed.connect(_load_default) + + var first_child = panel.get_child(0) + settings_ui = first_child + panel.remove_child(settings_ui) + get_settings_window().add_child(settings_ui) + settings_ui.on_change_item.connect(on_ui_change_item) + settings_ui.size_flags_vertical = Control.SIZE_EXPAND_FILL + + # Always make sure we remove previous actions in case there's a double-up. + _remove_actions() + +func _new_keybind() -> void: + settings_ui.add_key_action(null) + +func on_ui_change_item(action : int, item : Dictionary, old_item : Dictionary): + if action == ChangeAction.ACTION_NAME or action == ChangeAction.KEY_BIND: + # Updates should be fine as there is a reference to the item. + # We only have to check if there isn't an item, then add an item. + if action == ChangeAction.ACTION_NAME \ + and item["action_name"] != "" \ + and _get_key_action_by_item(item) == null: + key_actions.append(item) + _create_action(item) + else: + _update_action(item, old_item) + if action == ChangeAction.DELETE: + # Remove the existing item from our key_actions. + _remove_key_action_by_item(item) + + # Adjust our input map. + var action_event_count = len(InputMap.action_get_events(KEYBIND_PREFIX + item["action_name"])) + if action_event_count <= 1: + # Delete the entire action. + print_log("Deleting the entire action for %s" % item["action_name"]) + InputMap.erase_action(KEYBIND_PREFIX + item["action_name"]) + else: + # Delete just our event. + var key_event = _create_key_event(item) + if key_event != null: + print_log("Deleting just our event for %s" % item["action_name"]) + InputMap.action_erase_event(KEYBIND_PREFIX + item["action_name"], key_event) + + save_settings() + +func _create_key_event(item : Dictionary) -> InputEventKey: + var key : int = item["key"] + var alt_pressed : bool = item.get("modifier_alt", false) + var ctrl_pressed : bool = item.get("modifier_ctrl", false) + var meta_pressed : bool = item.get("modifier_meta", false) + var shift_pressed : bool = item.get("modifier_shift", false) + if key == -1: + return null + var new_key_event = InputEventKey.new() + new_key_event.physical_keycode = key + new_key_event.alt_pressed = alt_pressed + new_key_event.ctrl_pressed = ctrl_pressed + new_key_event.meta_pressed = meta_pressed + new_key_event.shift_pressed = shift_pressed + return new_key_event + +func _create_action(item : Dictionary) -> void: + var action = item["action_name"] + + # Do not create empty. + if action == "": + return + + print_log("Creating new action and associated event %s" % action) + var key_event = _create_key_event(item) + + if not InputMap.has_action(KEYBIND_PREFIX + action): + InputMap.add_action(KEYBIND_PREFIX + action) + + # Always add the event to the action, if there is no action it was made. + # existing events for the action have been cleared. + if key_event != null: + InputMap.action_add_event(KEYBIND_PREFIX + action, key_event) + +func _update_action(new_item : Dictionary, old_item : Dictionary) -> void: + var new_action = new_item["action_name"] + var old_action = old_item["action_name"] + var new_key = new_item["key"] + var old_key = old_item["key"] + + if new_action != old_action: + print_log("New item action name (%s) is different from the old (%s)." % + [new_action, old_action]) + var existing_events = InputMap.action_get_events(KEYBIND_PREFIX + old_action) + if old_action != "" and len(existing_events) < 1: + InputMap.erase_action(KEYBIND_PREFIX + old_action) + _create_action(new_item) + elif new_key != old_key \ + or new_item["modifier_alt"] != old_item["modifier_alt"] \ + or new_item["modifier_ctrl"] != old_item["modifier_ctrl"] \ + or new_item["modifier_shift"] != old_item["modifier_shift"] \ + or new_item["modifier_meta"] != old_item["modifier_meta"]: + print_log("New item key is not the same as the old key.") + if old_key != -1: + print_log("Old item key is not unassigned. Removing the old item key event.") + var old_key_event = _create_key_event(old_item) + if new_action == "": + # We need to cycle through all actions and events + # and find where the key is used and remove it. + for action_name in InputMap.get_actions(): + if not action_name.begins_with(KEYBIND_PREFIX): + continue + if not InputMap.event_is_action(old_key_event, action_name, true): + continue + print("Removing orphan event from %s action" % action_name) + InputMap.action_erase_event(action_name, old_key_event) + else: + InputMap.action_erase_event(KEYBIND_PREFIX + new_action, old_key_event) + + print_log("Adding event for the action name with the correct key.") + + # We only want to create a new key event if it wasn't reset. + var new_key_event = _create_key_event(new_item) + if new_key_event != null and new_action != "": + InputMap.action_add_event(KEYBIND_PREFIX + new_action, new_key_event) + +func _input(event : InputEvent) -> void: + for action in InputMap.get_actions(): + if event.is_action_pressed(action, false, true): + send_global_mod_message("KeybindsActionPressed", + { + "action": action.replace(KEYBIND_PREFIX, ""), + "event": event + }) + # This is one way to do actions. The other is with global mod data. + if InputMap.has_action(KEYBIND_PREFIX + "ping") \ + and event.is_action_pressed(KEYBIND_PREFIX + "ping", false, true): + print_log("Input map pong!") + set_status("Pong!") + + # The alternative approach is commented below. + #func _handle_global_mod_message(key : String, values : Dictionary): + #if key == "KeybindsActionPressed" and values["action"] == "ping": + #print_log("Global mod message pong!") + #set_status("Pong!") + +func _get_key_action_by_item(item : Dictionary): + for i in key_actions: + if i["key"] == item["key"] and i["action_name"] == item["action_name"]: + return i + return null + +func _update_key_action_by_item(item : Dictionary, new_item : Dictionary) -> bool: + for i in key_actions: + if i["key"] == item["key"] and i["action_name"] == item["action_name"]: + i["key"] = new_item["key"] + i["action_name"] = new_item["action_name"] + return true + return false + +func _remove_key_action_by_item(item : Dictionary) -> bool: + var index = key_actions.find(item) + if index >= 0: + key_actions.remove_at(index) + return true + return false + +func _add_actions() -> void: + for item in key_actions: + _create_action(item) + +func _create_initial_actions() -> void: + key_actions = [ + { + "key": KEY_P, + "action_name": "ping", + "modifier_alt": false, + "modifier_ctrl": false, + "modifier_meta": false, + "modifier_shift": false + }, + { + "key": KEY_Y, + "action_name": "ping", + "modifier_alt": false, + "modifier_ctrl": true, + "modifier_meta": false, + "modifier_shift": false + }, + ] + +func _remove_actions() -> void: + for action_name in InputMap.get_actions(): + # ONLY remove our actions we make. + if not action_name.begins_with(KEYBIND_PREFIX): + continue + InputMap.erase_action(action_name) + +func _load_default() -> void: + _remove_actions() + _create_initial_actions() + _add_actions() + settings_ui.reset() + settings_ui.set_initial_key_actions(key_actions) + +func save_before(_settings_current : Dictionary): + _settings_current["keybinds_key_actions"] = key_actions + print_log("Saved key actions") + +func load_after(_settings_old : Dictionary, _settings_new : Dictionary): + if _settings_new.has("keybinds_key_actions") and len(_settings_new["keybinds_key_actions"]) > 0: + key_actions = _settings_new["keybinds_key_actions"] + print_log("Loaded key actions.") + + # Try set initial, it will return if initial has already been set. + if len(key_actions) <= 0: + return + print_log("Attempting to load...") + if not settings_ui.has_set_initial: + print("Initial adding of actions.") + _add_actions() + print_log("Setting up UI...") + settings_ui.set_initial_key_actions(key_actions) diff --git a/Mods/Keybinds/Keybinds.gd.uid b/Mods/Keybinds/Keybinds.gd.uid new file mode 100644 index 00000000..14bd02cb --- /dev/null +++ b/Mods/Keybinds/Keybinds.gd.uid @@ -0,0 +1 @@ +uid://c47cbmmvkrwvl diff --git a/Mods/Keybinds/Keybinds.tscn b/Mods/Keybinds/Keybinds.tscn new file mode 100644 index 00000000..9f5e33a4 --- /dev/null +++ b/Mods/Keybinds/Keybinds.tscn @@ -0,0 +1,33 @@ +[gd_scene load_steps=4 format=3 uid="uid://cvokpd5h375w1"] + +[ext_resource type="Script" uid="uid://c47cbmmvkrwvl" path="res://Mods/Keybinds/Keybinds.gd" id="1_8jfd0"] +[ext_resource type="Script" uid="uid://bqviy8gst7123" path="res://Mods/Keybinds/Resources/SettingsUI.gd" id="2_hfm7f"] +[ext_resource type="PackedScene" uid="uid://d3tfxmryd30cl" path="res://Mods/Keybinds/Resources/ActionUI.tscn" id="3_gqp3c"] + +[node name="Keybinds" type="Node3D" node_paths=PackedStringArray("panel")] +script = ExtResource("1_8jfd0") +panel = NodePath("TempPanel") + +[node name="TempPanel" type="Control" parent="."] +visible = false +layout_mode = 3 +anchors_preset = 0 +offset_right = 40.0 +offset_bottom = 40.0 + +[node name="SettingsUIContainer" type="ScrollContainer" parent="TempPanel" node_paths=PackedStringArray("attached")] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +offset_right = 70.0 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("2_hfm7f") +base = ExtResource("3_gqp3c") +attached = NodePath("FlowContainer") + +[node name="FlowContainer" type="FlowContainer" parent="TempPanel/SettingsUIContainer"] +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 diff --git a/Mods/Keybinds/Resources/ActionUI.gd b/Mods/Keybinds/Resources/ActionUI.gd new file mode 100644 index 00000000..eefd7e21 --- /dev/null +++ b/Mods/Keybinds/Resources/ActionUI.gd @@ -0,0 +1,91 @@ +extends Control +class_name ActionUI + +var ui_item : Dictionary = {} +var old_item : Dictionary = {} +var currently_setting : bool = false + +signal on_change_item(action : int, item : Dictionary, old_item : Dictionary) + +## Load the item to the UI elements. +## Keybind does not have to be set, but must be -1 if not set. +func set_item(item : Dictionary) -> void: + # Bind to the UI item. + ui_item = item + + var action : String = item["action_name"] + if item["key"] != -1: + _set_key_bind_display(item) + + %ActionNameTxt.text = action + emit_signal("on_change_item", ChangeAction.INITIAL, item, {}) + +func _set_key_bind_display(item : Dictionary): + var key_code : Key = item["key"] + var alt_pressed = item.get("modifier_alt", false) + var ctrl_pressed = item.get("modifier_ctrl", false) + var meta_pressed = item.get("modifier_meta", false) + var shift_pressed = item.get("modifier_shift", false) + + var input_event = InputEventKey.new() + input_event.physical_keycode = key_code + input_event.alt_pressed = alt_pressed + input_event.ctrl_pressed = ctrl_pressed + input_event.meta_pressed = meta_pressed + input_event.shift_pressed = shift_pressed + + var display_bind = OS.get_keycode_string( + DisplayServer.keyboard_get_label_from_physical( + input_event.get_physical_keycode_with_modifiers())) + + %KeybindDisplay.text = display_bind + +## Set the UI item to be blank. +func blank_item() -> void: + ui_item = { + "key": -1, + "modifier_alt": false, + "modifier_ctrl": false, + "modifier_meta": false, + "modifier_shift": false, + "action_name": "" + } + +func _on_action_name_txt_text_changed(new_text : String) -> void: + old_item = ui_item.duplicate() + ui_item["action_name"] = new_text + emit_signal("on_change_item", ChangeAction.ACTION_NAME, ui_item, old_item) + +func _on_delete_btn_pressed() -> void: + emit_signal("on_change_item", ChangeAction.DELETE, ui_item, {}) + queue_free() + +func _on_reset_btn_pressed() -> void: + old_item = ui_item.duplicate() + ui_item["key"] = -1 + emit_signal("on_change_item", ChangeAction.KEY_BIND, ui_item, old_item) + %KeybindDisplay.text = "Empty" + +func _input(event : InputEvent): + if currently_setting and event is InputEventKey: + var key_event : InputEventKey = event + if key_event.physical_keycode == KEY_CTRL \ + or key_event.physical_keycode == KEY_ALT \ + or key_event.physical_keycode == KEY_SHIFT \ + or key_event.physical_keycode == KEY_META: + # Default, probably only has modifier applied. + return + currently_setting = false + old_item = ui_item.duplicate() + ui_item["key"] = event.physical_keycode + ui_item["modifier_alt"] = event.alt_pressed + ui_item["modifier_ctrl"] = event.ctrl_pressed + ui_item["modifier_meta"] = event.meta_pressed + ui_item["modifier_shift"] = event.shift_pressed + %SetBtn.text = "Set" + emit_signal("on_change_item", ChangeAction.KEY_BIND, ui_item, old_item) + _set_key_bind_display(ui_item) + +func _on_set_btn_pressed() -> void: + currently_setting = true + %SetBtn.text = "Press Keys" diff --git a/Mods/Keybinds/Resources/ActionUI.gd.uid b/Mods/Keybinds/Resources/ActionUI.gd.uid new file mode 100644 index 00000000..154c25dd --- /dev/null +++ b/Mods/Keybinds/Resources/ActionUI.gd.uid @@ -0,0 +1 @@ +uid://dsy8dnqv77moj diff --git a/Mods/Keybinds/Resources/ActionUI.tscn b/Mods/Keybinds/Resources/ActionUI.tscn new file mode 100644 index 00000000..b3dbd052 --- /dev/null +++ b/Mods/Keybinds/Resources/ActionUI.tscn @@ -0,0 +1,82 @@ +[gd_scene load_steps=2 format=3 uid="uid://d3tfxmryd30cl"] + +[ext_resource type="Script" uid="uid://dsy8dnqv77moj" path="res://Mods/Keybinds/Resources/ActionUI.gd" id="1_kpyqj"] + +[node name="ActionUi" type="Control"] +custom_minimum_size = Vector2(230, 165) +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +script = ExtResource("1_kpyqj") + +[node name="Panel" type="Panel" parent="."] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 + +[node name="VBoxContainer" type="VBoxContainer" parent="Panel"] +layout_mode = 0 +offset_left = 6.0 +offset_top = 4.0 +offset_right = 225.0 +offset_bottom = 159.0 + +[node name="VBox" type="VBoxContainer" parent="Panel/VBoxContainer"] +layout_mode = 2 +alignment = 1 + +[node name="Label" type="Label" parent="Panel/VBoxContainer/VBox"] +layout_mode = 2 +text = "Action Name" + +[node name="ActionNameTxt" type="LineEdit" parent="Panel/VBoxContainer/VBox"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="VBox2" type="VBoxContainer" parent="Panel/VBoxContainer"] +layout_mode = 2 +alignment = 1 + +[node name="Label" type="Label" parent="Panel/VBoxContainer/VBox2"] +layout_mode = 2 +text = "Keybind" + +[node name="KeybindDisplay" type="LineEdit" parent="Panel/VBoxContainer/VBox2"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +focus_mode = 0 +text = "Empty" +alignment = 1 +editable = false + +[node name="HBoxContainer3" type="HBoxContainer" parent="Panel/VBoxContainer"] +layout_mode = 2 +alignment = 1 + +[node name="SetBtn" type="Button" parent="Panel/VBoxContainer/HBoxContainer3"] +unique_name_in_owner = true +layout_mode = 2 +text = "Set" + +[node name="ResetBtn" type="Button" parent="Panel/VBoxContainer/HBoxContainer3"] +layout_mode = 2 +text = "Reset" + +[node name="DeleteBtn" type="Button" parent="Panel/VBoxContainer/HBoxContainer3"] +layout_mode = 2 +text = "Delete" + +[connection signal="text_changed" from="Panel/VBoxContainer/VBox/ActionNameTxt" to="." method="_on_action_name_txt_text_changed"] +[connection signal="pressed" from="Panel/VBoxContainer/HBoxContainer3/SetBtn" to="." method="_on_set_btn_pressed"] +[connection signal="pressed" from="Panel/VBoxContainer/HBoxContainer3/ResetBtn" to="." method="_on_reset_btn_pressed"] +[connection signal="pressed" from="Panel/VBoxContainer/HBoxContainer3/DeleteBtn" to="." method="_on_delete_btn_pressed"] diff --git a/Mods/Keybinds/Resources/ChangeAction.gd b/Mods/Keybinds/Resources/ChangeAction.gd new file mode 100644 index 00000000..c5ac70ed --- /dev/null +++ b/Mods/Keybinds/Resources/ChangeAction.gd @@ -0,0 +1,7 @@ +class_name ChangeAction +enum { + DELETE = 1, + INITIAL = 2, + ACTION_NAME = 3, + KEY_BIND = 4 +} diff --git a/Mods/Keybinds/Resources/ChangeAction.gd.uid b/Mods/Keybinds/Resources/ChangeAction.gd.uid new file mode 100644 index 00000000..c8b0c03c --- /dev/null +++ b/Mods/Keybinds/Resources/ChangeAction.gd.uid @@ -0,0 +1 @@ +uid://ba335sykgfxm diff --git a/Mods/Keybinds/Resources/SettingsUI.gd b/Mods/Keybinds/Resources/SettingsUI.gd new file mode 100644 index 00000000..ba7bd248 --- /dev/null +++ b/Mods/Keybinds/Resources/SettingsUI.gd @@ -0,0 +1,41 @@ +extends Node +class_name KeybindSettingUI + +@export var base : PackedScene +@export var attached : Control +@export var has_set_initial : bool = false + +signal on_change_item(action : int, item : Dictionary, old_item : Dictionary) + +## Resets initial state and removes all UI items. +func reset() -> void: + has_set_initial = false + for child in attached.get_children(): + attached.remove_child(child) + +## ONLY used for the first initial ready setup of key actions. +func set_initial_key_actions(new_key_actions : Array) -> void: + if has_set_initial: + return + + _build_ui(new_key_actions) + has_set_initial = true + +func _build_ui(key_actions : Array): + for item in key_actions: + add_key_action(item) + +## Create an action UI element for a particular item. +## If the item is null, it will create a blank item. +func add_key_action(item): + var new_item : ActionUI = base.instantiate() + new_item.on_change_item.connect(_on_change_item) + if item != null: + new_item.set_item(item) + else: + new_item.blank_item() + new_item.visible = true + attached.add_child(new_item) + +func _on_change_item(action : int, item : Dictionary, old_item : Dictionary): + emit_signal("on_change_item", action, item, old_item) diff --git a/Mods/Keybinds/Resources/SettingsUI.gd.uid b/Mods/Keybinds/Resources/SettingsUI.gd.uid new file mode 100644 index 00000000..341bb0a9 --- /dev/null +++ b/Mods/Keybinds/Resources/SettingsUI.gd.uid @@ -0,0 +1 @@ +uid://bqviy8gst7123 diff --git a/Mods/Keybinds/description.txt b/Mods/Keybinds/description.txt new file mode 100644 index 00000000..659f734e --- /dev/null +++ b/Mods/Keybinds/description.txt @@ -0,0 +1 @@ +Customise keybinds for various actions across SnekStudio. diff --git a/Mods/ModelToggle/ModelToggle.gd b/Mods/ModelToggle/ModelToggle.gd index 48a3bcd3..c47ddcbe 100644 --- a/Mods/ModelToggle/ModelToggle.gd +++ b/Mods/ModelToggle/ModelToggle.gd @@ -4,6 +4,8 @@ var model_name : String = "" var redeem_name : String = "" var start_hidden : bool = true var time_until_undo : float = 0.0 +var keybind_action_name : String = "" +var keybind_prefix : String = "" var currently_toggled : bool = false var current_time_until_undo : float = 0.0 @@ -42,6 +44,22 @@ func handle_channel_point_redeem(_redeemer_username, _redeemer_display_name, _re _update_model() +func _handle_global_mod_message(key : String, values : Dictionary): + if key == "KeybindsActionPressed" and values["action"] == keybind_action_name: + print_log("Toggling via keybind.") + currently_toggled = !currently_toggled + _update_model() + elif key == "Keybinds": + keybind_prefix = values["prefix"] + +func check_configuration() -> PackedStringArray: + var errors : PackedStringArray = [] + var global_dict = get_global_mod_data("Keybinds") + if global_dict.get("prefix", "") == "" and keybind_action_name != "": + errors.append("No Keybinds mod installed. Keybind toggle for model will not work.") + + return errors + func _process(delta): if current_time_until_undo > 0.0: @@ -60,6 +78,7 @@ func load_after(_settings_old : Dictionary, _settings_new : Dictionary): func _ready(): add_tracked_setting("start_hidden", "Start hidden") add_tracked_setting("model_name", "Model name") + add_tracked_setting("keybind_action_name", "Toggle keybind action name") add_tracked_setting("redeem_name", "Redeem name", {"is_redeem" : true}) add_tracked_setting("time_until_undo", "Time until reset", {"min": 0.0, "max":24*60}) update_settings_ui()