Path: blob/master/editor/settings/action_map_editor.cpp
20844 views
/**************************************************************************/1/* action_map_editor.cpp */2/**************************************************************************/3/* This file is part of: */4/* GODOT ENGINE */5/* https://godotengine.org */6/**************************************************************************/7/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */8/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */9/* */10/* Permission is hereby granted, free of charge, to any person obtaining */11/* a copy of this software and associated documentation files (the */12/* "Software"), to deal in the Software without restriction, including */13/* without limitation the rights to use, copy, modify, merge, publish, */14/* distribute, sublicense, and/or sell copies of the Software, and to */15/* permit persons to whom the Software is furnished to do so, subject to */16/* the following conditions: */17/* */18/* The above copyright notice and this permission notice shall be */19/* included in all copies or substantial portions of the Software. */20/* */21/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */22/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */23/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */24/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */25/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */26/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */27/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */28/**************************************************************************/2930#include "action_map_editor.h"3132#include "editor/editor_string_names.h"33#include "editor/settings/editor_event_search_bar.h"34#include "editor/settings/editor_settings.h"35#include "editor/settings/event_listener_line_edit.h"36#include "editor/settings/input_event_configuration_dialog.h"37#include "editor/themes/editor_scale.h"38#include "scene/gui/check_button.h"39#include "scene/gui/margin_container.h"40#include "scene/gui/separator.h"41#include "scene/gui/tree.h"4243static bool _is_action_name_valid(const String &p_name) {44const char32_t *cstr = p_name.get_data();45for (int i = 0; cstr[i]; i++) {46if (cstr[i] == '/' || cstr[i] == ':' || cstr[i] == '"' ||47cstr[i] == '=' || cstr[i] == '\\' || cstr[i] < 32) {48return false;49}50}51return true;52}5354void ActionMapEditor::_event_config_confirmed() {55Ref<InputEvent> ev = event_config_dialog->get_event();5657Dictionary new_action = current_action.duplicate();58Array events = new_action["events"].duplicate();5960if (current_action_event_index == -1) {61// Add new event62events.push_back(ev);63} else {64// Edit existing event65events[current_action_event_index] = ev;66}6768new_action["events"] = events;69emit_signal(SNAME("action_edited"), current_action_name, new_action);70}7172void ActionMapEditor::_add_action_pressed() {73_add_action(add_edit->get_text());74}7576String ActionMapEditor::_check_new_action_name(const String &p_name) {77if (p_name.is_empty() || !_is_action_name_valid(p_name)) {78return TTR("Invalid action name. It cannot be empty nor contain '/', ':', '=', '\\' or '\"'");79}8081if (_has_action(p_name)) {82return vformat(TTR("An action with the name '%s' already exists."), p_name);83}8485return "";86}8788void ActionMapEditor::_add_edit_text_changed(const String &p_name) {89const String error = _check_new_action_name(p_name);90add_button->set_tooltip_text(error);91add_button->set_disabled(!error.is_empty());92}9394bool ActionMapEditor::_has_action(const String &p_name) const {95for (const ActionInfo &action_info : actions_cache) {96if (p_name == action_info.name) {97return true;98}99}100return false;101}102103void ActionMapEditor::_add_action(const String &p_name) {104String error = _check_new_action_name(p_name);105if (!error.is_empty()) {106show_message(error);107return;108}109110add_edit->clear();111emit_signal(SNAME("action_added"), p_name);112}113114void ActionMapEditor::_action_edited() {115TreeItem *ti = action_tree->get_edited();116if (!ti) {117return;118}119120if (action_tree->get_selected_column() == 0) {121// Name Edited122String new_name = ti->get_text(0);123String old_name = ti->get_meta("__name");124125if (new_name == old_name) {126return;127}128129if (new_name.is_empty() || !_is_action_name_valid(new_name)) {130ti->set_text(0, old_name);131show_message(TTR("Invalid action name. It cannot be empty nor contain '/', ':', '=', '\\' or '\"'"));132return;133}134135if (_has_action(new_name)) {136ti->set_text(0, old_name);137show_message(vformat(TTR("An action with the name '%s' already exists."), new_name));138return;139}140141emit_signal(SNAME("action_renamed"), old_name, new_name);142} else if (action_tree->get_selected_column() == 1) {143// Deadzone Edited144String name = ti->get_meta("__name");145Dictionary old_action = ti->get_meta("__action");146Dictionary new_action = old_action.duplicate();147new_action["deadzone"] = ti->get_range(1);148149// Call deferred so that input can finish propagating through tree, allowing re-making of tree to occur.150call_deferred(SNAME("emit_signal"), "action_edited", name, new_action);151}152}153154void ActionMapEditor::_tree_button_pressed(Object *p_item, int p_column, int p_id, MouseButton p_button) {155if (p_button != MouseButton::LEFT) {156return;157}158159ItemButton option = (ItemButton)p_id;160161TreeItem *item = Object::cast_to<TreeItem>(p_item);162if (!item) {163return;164}165166switch (option) {167case ActionMapEditor::BUTTON_ADD_EVENT: {168current_action = item->get_meta("__action");169current_action_name = item->get_meta("__name");170current_action_event_index = -1;171172event_config_dialog->popup_and_configure(Ref<InputEvent>(), current_action_name);173} break;174case ActionMapEditor::BUTTON_EDIT_EVENT: {175// Action and Action name is located on the parent of the event.176current_action = item->get_parent()->get_meta("__action");177current_action_name = item->get_parent()->get_meta("__name");178179current_action_event_index = item->get_meta("__index");180181Ref<InputEvent> ie = item->get_meta("__event");182if (ie.is_valid()) {183event_config_dialog->popup_and_configure(ie, current_action_name);184}185} break;186case ActionMapEditor::BUTTON_REMOVE_ACTION: {187// Send removed action name188String name = item->get_meta("__name");189emit_signal(SNAME("action_removed"), name);190} break;191case ActionMapEditor::BUTTON_REMOVE_EVENT: {192// Remove event and send updated action193Dictionary action = item->get_parent()->get_meta("__action").duplicate();194String action_name = item->get_parent()->get_meta("__name");195196int event_index = item->get_meta("__index");197198Array events = action["events"].duplicate();199events.remove_at(event_index);200action["events"] = events;201202emit_signal(SNAME("action_edited"), action_name, action);203} break;204case ActionMapEditor::BUTTON_REVERT_ACTION: {205ERR_FAIL_COND_MSG(!item->has_meta("__action_initial"), "Tree Item for action which can be reverted is expected to have meta value with initial value of action.");206207Dictionary action = item->get_meta("__action_initial").duplicate();208String action_name = item->get_meta("__name");209210emit_signal(SNAME("action_edited"), action_name, action);211} break;212default:213break;214}215}216217void ActionMapEditor::_tree_item_activated() {218TreeItem *item = action_tree->get_selected();219220if (!item || !item->has_meta("__event")) {221return;222}223224_tree_button_pressed(item, 2, BUTTON_EDIT_EVENT, MouseButton::LEFT);225}226227void ActionMapEditor::_set_show_builtin_actions(bool p_show) {228show_builtin_actions = p_show;229EditorSettings::get_singleton()->set_project_metadata("project_settings", "show_builtin_actions", show_builtin_actions);230231// Prevent unnecessary updates of action list when cache is empty.232if (!actions_cache.is_empty()) {233update_action_list();234}235}236237void ActionMapEditor::_on_search_bar_value_changed() {238if (action_list_search_bar->is_searching()) {239show_builtin_actions_checkbutton->set_pressed_no_signal(true);240show_builtin_actions_checkbutton->set_disabled(true);241show_builtin_actions_checkbutton->set_tooltip_text(TTRC("Built-in actions are always shown when searching."));242} else {243show_builtin_actions_checkbutton->set_pressed_no_signal(show_builtin_actions);244show_builtin_actions_checkbutton->set_disabled(false);245show_builtin_actions_checkbutton->set_tooltip_text(String());246}247update_action_list();248}249250Variant ActionMapEditor::get_drag_data_fw(const Point2 &p_point, Control *p_from) {251TreeItem *selected = action_tree->get_selected();252if (!selected) {253return Variant();254}255256String name = selected->get_text(0);257Label *label = memnew(Label(name));258label->set_theme_type_variation("HeaderSmall");259label->set_modulate(Color(1, 1, 1, 1.0f));260label->set_auto_translate_mode(AUTO_TRANSLATE_MODE_DISABLED);261action_tree->set_drag_preview(label);262263get_viewport()->gui_set_drag_description(vformat(RTR("Action %s"), name));264265Dictionary drag_data;266267if (selected->has_meta("__action")) {268drag_data["input_type"] = "action";269}270271if (selected->has_meta("__event")) {272drag_data["input_type"] = "event";273}274275drag_data["source"] = selected->get_instance_id();276277action_tree->set_drop_mode_flags(Tree::DROP_MODE_INBETWEEN);278279return drag_data;280}281282bool ActionMapEditor::can_drop_data_fw(const Point2 &p_point, const Variant &p_data, Control *p_from) const {283Dictionary d = p_data;284if (!d.has("input_type")) {285return false;286}287288TreeItem *source = Object::cast_to<TreeItem>(ObjectDB::get_instance(d["source"].operator ObjectID()));289TreeItem *selected = action_tree->get_selected();290291TreeItem *item = (p_point == Vector2(Math::INF, Math::INF)) ? selected : action_tree->get_item_at_position(p_point);292if (!selected || !item || item == source) {293return false;294}295296// Don't allow moving an action in-between events.297if (d["input_type"] == "action" && item->has_meta("__event")) {298return false;299}300301// Don't allow moving an event to a different action.302if (d["input_type"] == "event" && item->get_parent() != selected->get_parent()) {303return false;304}305306return true;307}308309void ActionMapEditor::drop_data_fw(const Point2 &p_point, const Variant &p_data, Control *p_from) {310if (!can_drop_data_fw(p_point, p_data, p_from)) {311return;312}313314TreeItem *selected = action_tree->get_selected();315TreeItem *target = (p_point == Vector2(Math::INF, Math::INF)) ? selected : action_tree->get_item_at_position(p_point);316if (!target) {317return;318}319320bool drop_above = ((p_point == Vector2(Math::INF, Math::INF)) ? action_tree->get_drop_section_at_position(action_tree->get_item_rect(target).position) : action_tree->get_drop_section_at_position(p_point)) == -1;321322Dictionary d = p_data;323if (d["input_type"] == "action") {324// Change action order.325String relative_to = target->get_meta("__name");326String action_name = selected->get_meta("__name");327emit_signal(SNAME("action_reordered"), action_name, relative_to, drop_above);328329} else if (d["input_type"] == "event") {330// Change event order331int current_index = selected->get_meta("__index");332int target_index = target->get_meta("__index");333334// Construct new events array.335Dictionary new_action = selected->get_parent()->get_meta("__action");336337Array events = new_action["events"];338Array new_events;339340// The following method was used to perform the array changes since `remove` followed by `insert` was not working properly at time of writing.341// Loop thought existing events342for (int i = 0; i < events.size(); i++) {343// If you come across the current index, just skip it, as it has been moved.344if (i == current_index) {345continue;346} else if (i == target_index) {347// We are at the target index. If drop above, add selected event there first, then target, so moved event goes on top.348if (drop_above) {349new_events.push_back(events[current_index]);350new_events.push_back(events[target_index]);351} else {352new_events.push_back(events[target_index]);353new_events.push_back(events[current_index]);354}355} else {356new_events.push_back(events[i]);357}358}359360new_action["events"] = new_events;361emit_signal(SNAME("action_edited"), selected->get_parent()->get_meta("__name"), new_action);362}363}364365void ActionMapEditor::_notification(int p_what) {366switch (p_what) {367case NOTIFICATION_TRANSLATION_CHANGED: {368if (!actions_cache.is_empty()) {369update_action_list();370}371if (!add_button->get_tooltip_text().is_empty()) {372_add_edit_text_changed(add_edit->get_text());373}374} break;375376case NOTIFICATION_THEME_CHANGED: {377add_button->set_button_icon(get_editor_theme_icon(SNAME("Add")));378if (!actions_cache.is_empty()) {379update_action_list();380}381} break;382}383}384385void ActionMapEditor::_bind_methods() {386ADD_SIGNAL(MethodInfo("action_added", PropertyInfo(Variant::STRING, "name")));387ADD_SIGNAL(MethodInfo("action_edited", PropertyInfo(Variant::STRING, "name"), PropertyInfo(Variant::DICTIONARY, "new_action")));388ADD_SIGNAL(MethodInfo("action_removed", PropertyInfo(Variant::STRING, "name")));389ADD_SIGNAL(MethodInfo("action_renamed", PropertyInfo(Variant::STRING, "old_name"), PropertyInfo(Variant::STRING, "new_name")));390ADD_SIGNAL(MethodInfo("action_reordered", PropertyInfo(Variant::STRING, "action_name"), PropertyInfo(Variant::STRING, "relative_to"), PropertyInfo(Variant::BOOL, "before")));391}392393LineEdit *ActionMapEditor::get_search_box() const {394return action_list_search_bar->get_name_search_box();395}396397LineEdit *ActionMapEditor::get_path_box() const {398return add_edit;399}400401InputEventConfigurationDialog *ActionMapEditor::get_configuration_dialog() {402return event_config_dialog;403}404405bool ActionMapEditor::_should_display_action(const String &p_name, const Array &p_events) const {406const Ref<InputEvent> search_ev = action_list_search_bar->get_event();407bool event_match = true;408if (search_ev.is_valid()) {409event_match = false;410for (int i = 0; i < p_events.size(); ++i) {411const Ref<InputEvent> ev = p_events[i];412if (ev.is_valid() && ev->is_match(search_ev, true)) {413event_match = true;414}415}416}417418return event_match && action_list_search_bar->get_name().is_subsequence_ofn(p_name);419}420421void ActionMapEditor::update_action_list(const Vector<ActionInfo> &p_action_infos) {422if (!p_action_infos.is_empty()) {423actions_cache = p_action_infos;424}425426Pair<String, int> selected_item;427TreeItem *ti = action_tree->get_selected();428if (ti) {429selected_item.first = ti->get_text(0);430selected_item.second = action_tree->get_selected_column();431}432433HashSet<String> collapsed_actions;434TreeItem *root = action_tree->get_root();435if (root) {436for (TreeItem *child = root->get_first_child(); child; child = child->get_next()) {437if (child->is_collapsed()) {438collapsed_actions.insert(child->get_meta("__name"));439}440}441}442443action_tree->clear();444root = action_tree->create_item();445446for (const ActionInfo &action_info : actions_cache) {447const Array events = action_info.action["events"];448if (!_should_display_action(action_info.name, events)) {449continue;450}451452if (!action_info.editable && !action_list_search_bar->is_searching() && !show_builtin_actions) {453continue;454}455456const Variant deadzone = action_info.action["deadzone"];457458// Update Tree...459460TreeItem *action_item = action_tree->create_item(root);461ERR_FAIL_NULL(action_item);462action_item->set_meta("__action", action_info.action);463action_item->set_meta("__name", action_info.name);464action_item->set_collapsed(collapsed_actions.has(action_info.name));465466// First Column - Action Name467action_item->set_auto_translate_mode(0, AUTO_TRANSLATE_MODE_DISABLED);468action_item->set_cell_mode(0, TreeItem::CELL_MODE_STRING);469action_item->set_text(0, action_info.name);470action_item->set_editable(0, action_info.editable);471action_item->set_icon(0, action_info.icon);472473// Second Column - Deadzone474action_item->set_editable(1, true);475action_item->set_cell_mode(1, TreeItem::CELL_MODE_RANGE);476action_item->set_range_config(1, 0.0, 1.0, 0.01);477action_item->set_range(1, deadzone);478479// Third column - buttons480if (action_info.has_initial) {481bool deadzone_eq = action_info.action_initial["deadzone"] == action_info.action["deadzone"];482bool events_eq = Shortcut::is_event_array_equal(action_info.action_initial["events"], action_info.action["events"]);483bool action_eq = deadzone_eq && events_eq;484action_item->set_meta("__action_initial", action_info.action_initial);485action_item->add_button(2, get_editor_theme_icon(SNAME("ReloadSmall")), BUTTON_REVERT_ACTION, action_eq, action_eq ? TTRC("Cannot Revert - Action is same as initial") : TTRC("Revert Action"));486}487action_item->add_button(2, get_editor_theme_icon(SNAME("Add")), BUTTON_ADD_EVENT, false, TTRC("Add Event"));488action_item->add_button(2, get_editor_theme_icon(SNAME("Remove")), BUTTON_REMOVE_ACTION, !action_info.editable, action_info.editable ? TTRC("Remove Action") : TTRC("Cannot Remove Action"));489490action_item->set_custom_bg_color(0, get_theme_color(SNAME("prop_subsection"), EditorStringName(Editor)));491action_item->set_custom_bg_color(1, get_theme_color(SNAME("prop_subsection"), EditorStringName(Editor)));492action_item->set_custom_stylebox(0, get_theme_stylebox(SNAME("prop_subsection_stylebox_left"), EditorStringName(Editor)));493action_item->set_custom_stylebox(1, get_theme_stylebox(SNAME("prop_subsection_stylebox_right"), EditorStringName(Editor)));494495if (selected_item.first == action_info.name) {496action_item->select(selected_item.second);497}498499for (int evnt_idx = 0; evnt_idx < events.size(); evnt_idx++) {500Ref<InputEvent> event = events[evnt_idx];501if (event.is_null()) {502continue;503}504505TreeItem *event_item = action_tree->create_item(action_item);506507// First Column - Text508event_item->set_auto_translate_mode(0, AUTO_TRANSLATE_MODE_DISABLED);509event_item->set_text(0, EventListenerLineEdit::get_event_text(event, true));510event_item->set_meta("__event", event);511event_item->set_meta("__index", evnt_idx);512513// First Column - Icon514Ref<InputEventKey> k = event;515if (k.is_valid()) {516if (k->get_physical_keycode() == Key::NONE && k->get_keycode() == Key::NONE && k->get_key_label() != Key::NONE) {517event_item->set_icon(0, get_editor_theme_icon(SNAME("KeyboardLabel")));518} else if (k->get_keycode() != Key::NONE) {519event_item->set_icon(0, get_editor_theme_icon(SNAME("Keyboard")));520} else if (k->get_physical_keycode() != Key::NONE) {521event_item->set_icon(0, get_editor_theme_icon(SNAME("KeyboardPhysical")));522} else {523event_item->set_icon(0, get_editor_theme_icon(SNAME("KeyboardError")));524}525}526527Ref<InputEventMouseButton> mb = event;528if (mb.is_valid()) {529event_item->set_icon(0, get_editor_theme_icon(SNAME("Mouse")));530}531532Ref<InputEventJoypadButton> jb = event;533if (jb.is_valid()) {534event_item->set_icon(0, get_editor_theme_icon(SNAME("JoyButton")));535}536537Ref<InputEventJoypadMotion> jm = event;538if (jm.is_valid()) {539event_item->set_icon(0, get_editor_theme_icon(SNAME("JoyAxis")));540}541542// Third Column - Buttons543event_item->add_button(2, get_editor_theme_icon(SNAME("Edit")), BUTTON_EDIT_EVENT, false, TTRC("Edit Event"), TTRC("Edit Event"));544event_item->add_button(2, get_editor_theme_icon(SNAME("Remove")), BUTTON_REMOVE_EVENT, false, TTRC("Remove Event"), TTRC("Remove Event"));545event_item->set_button_color(2, 0, Color(1, 1, 1, 0.75));546event_item->set_button_color(2, 1, Color(1, 1, 1, 0.75));547548if (selected_item.first == event_item->get_text(0)) {549event_item->select(selected_item.second);550}551}552}553}554555void ActionMapEditor::show_message(const String &p_message) {556message->set_text(p_message);557message->popup_centered();558}559560ActionMapEditor::ActionMapEditor() {561// Main Vbox Container562VBoxContainer *main_vbox = memnew(VBoxContainer);563main_vbox->set_anchors_and_offsets_preset(PRESET_FULL_RECT);564add_child(main_vbox);565566action_list_search_bar = memnew(EditorEventSearchBar);567action_list_search_bar->connect(SceneStringName(value_changed), callable_mp(this, &ActionMapEditor::_on_search_bar_value_changed));568main_vbox->add_child(action_list_search_bar);569570// Adding Action line edit + button571add_hbox = memnew(HBoxContainer);572add_hbox->set_h_size_flags(Control::SIZE_EXPAND_FILL);573574add_edit = memnew(LineEdit);575add_edit->set_h_size_flags(Control::SIZE_EXPAND_FILL);576add_edit->set_placeholder(TTRC("Add New Action"));577add_edit->set_accessibility_name(TTRC("Add New Action"));578add_edit->set_clear_button_enabled(true);579add_edit->set_keep_editing_on_text_submit(true);580add_edit->connect(SceneStringName(text_changed), callable_mp(this, &ActionMapEditor::_add_edit_text_changed));581add_edit->connect(SceneStringName(text_submitted), callable_mp(this, &ActionMapEditor::_add_action));582add_hbox->add_child(add_edit);583584add_button = memnew(Button);585add_button->set_text(TTRC("Add"));586add_button->connect(SceneStringName(pressed), callable_mp(this, &ActionMapEditor::_add_action_pressed));587add_hbox->add_child(add_button);588// Disable the button and set its tooltip.589_add_edit_text_changed(add_edit->get_text());590591add_hbox->add_child(memnew(VSeparator));592593show_builtin_actions_checkbutton = memnew(CheckButton);594show_builtin_actions_checkbutton->set_text(TTRC("Show Built-in Actions"));595show_builtin_actions_checkbutton->connect(SceneStringName(toggled), callable_mp(this, &ActionMapEditor::_set_show_builtin_actions));596add_hbox->add_child(show_builtin_actions_checkbutton);597598show_builtin_actions = EditorSettings::get_singleton()->get_project_metadata("project_settings", "show_builtin_actions", false);599show_builtin_actions_checkbutton->set_pressed_no_signal(show_builtin_actions);600601main_vbox->add_child(add_hbox);602603MarginContainer *mc = memnew(MarginContainer);604mc->set_v_size_flags(Control::SIZE_EXPAND_FILL);605mc->set_theme_type_variation("NoBorderHorizontalBottom");606main_vbox->add_child(mc);607608// Action Editor Tree609action_tree = memnew(Tree);610action_tree->set_accessibility_name(TTRC("Action Map"));611action_tree->set_theme_type_variation("TreeTable");612action_tree->set_columns(3);613action_tree->set_hide_root(true);614action_tree->set_column_titles_visible(true);615action_tree->set_column_title(0, TTRC("Action"));616action_tree->set_column_clip_content(0, true);617action_tree->set_column_title(1, TTRC("Deadzone"));618action_tree->set_column_expand(1, false);619action_tree->set_column_custom_minimum_width(1, 80 * EDSCALE);620action_tree->set_column_expand(2, false);621action_tree->set_column_custom_minimum_width(2, 50 * EDSCALE);622action_tree->connect("item_edited", callable_mp(this, &ActionMapEditor::_action_edited), CONNECT_DEFERRED);623action_tree->connect("item_activated", callable_mp(this, &ActionMapEditor::_tree_item_activated));624action_tree->connect("button_clicked", callable_mp(this, &ActionMapEditor::_tree_button_pressed));625mc->add_child(action_tree);626627SET_DRAG_FORWARDING_GCD(action_tree, ActionMapEditor);628629// Adding event dialog630event_config_dialog = memnew(InputEventConfigurationDialog);631event_config_dialog->connect(SceneStringName(confirmed), callable_mp(this, &ActionMapEditor::_event_config_confirmed));632add_child(event_config_dialog);633634message = memnew(AcceptDialog);635add_child(message);636}637638639