Path: blob/master/editor/settings/action_map_editor.cpp
9902 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/separator.h"40#include "scene/gui/tree.h"4142static bool _is_action_name_valid(const String &p_name) {43const char32_t *cstr = p_name.get_data();44for (int i = 0; cstr[i]; i++) {45if (cstr[i] == '/' || cstr[i] == ':' || cstr[i] == '"' ||46cstr[i] == '=' || cstr[i] == '\\' || cstr[i] < 32) {47return false;48}49}50return true;51}5253void ActionMapEditor::_event_config_confirmed() {54Ref<InputEvent> ev = event_config_dialog->get_event();5556Dictionary new_action = current_action.duplicate();57Array events = new_action["events"].duplicate();5859if (current_action_event_index == -1) {60// Add new event61events.push_back(ev);62} else {63// Edit existing event64events[current_action_event_index] = ev;65}6667new_action["events"] = events;68emit_signal(SNAME("action_edited"), current_action_name, new_action);69}7071void ActionMapEditor::_add_action_pressed() {72_add_action(add_edit->get_text());73}7475String ActionMapEditor::_check_new_action_name(const String &p_name) {76if (p_name.is_empty() || !_is_action_name_valid(p_name)) {77return TTR("Invalid action name. It cannot be empty nor contain '/', ':', '=', '\\' or '\"'");78}7980if (_has_action(p_name)) {81return vformat(TTR("An action with the name '%s' already exists."), p_name);82}8384return "";85}8687void ActionMapEditor::_add_edit_text_changed(const String &p_name) {88const String error = _check_new_action_name(p_name);89add_button->set_tooltip_text(error);90add_button->set_disabled(!error.is_empty());91}9293bool ActionMapEditor::_has_action(const String &p_name) const {94for (const ActionInfo &action_info : actions_cache) {95if (p_name == action_info.name) {96return true;97}98}99return false;100}101102void ActionMapEditor::_add_action(const String &p_name) {103String error = _check_new_action_name(p_name);104if (!error.is_empty()) {105show_message(error);106return;107}108109add_edit->clear();110emit_signal(SNAME("action_added"), p_name);111}112113void ActionMapEditor::_action_edited() {114TreeItem *ti = action_tree->get_edited();115if (!ti) {116return;117}118119if (action_tree->get_selected_column() == 0) {120// Name Edited121String new_name = ti->get_text(0);122String old_name = ti->get_meta("__name");123124if (new_name == old_name) {125return;126}127128if (new_name.is_empty() || !_is_action_name_valid(new_name)) {129ti->set_text(0, old_name);130show_message(TTR("Invalid action name. It cannot be empty nor contain '/', ':', '=', '\\' or '\"'"));131return;132}133134if (_has_action(new_name)) {135ti->set_text(0, old_name);136show_message(vformat(TTR("An action with the name '%s' already exists."), new_name));137return;138}139140emit_signal(SNAME("action_renamed"), old_name, new_name);141} else if (action_tree->get_selected_column() == 1) {142// Deadzone Edited143String name = ti->get_meta("__name");144Dictionary old_action = ti->get_meta("__action");145Dictionary new_action = old_action.duplicate();146new_action["deadzone"] = ti->get_range(1);147148// Call deferred so that input can finish propagating through tree, allowing re-making of tree to occur.149call_deferred(SNAME("emit_signal"), "action_edited", name, new_action);150}151}152153void ActionMapEditor::_tree_button_pressed(Object *p_item, int p_column, int p_id, MouseButton p_button) {154if (p_button != MouseButton::LEFT) {155return;156}157158ItemButton option = (ItemButton)p_id;159160TreeItem *item = Object::cast_to<TreeItem>(p_item);161if (!item) {162return;163}164165switch (option) {166case ActionMapEditor::BUTTON_ADD_EVENT: {167current_action = item->get_meta("__action");168current_action_name = item->get_meta("__name");169current_action_event_index = -1;170171event_config_dialog->popup_and_configure(Ref<InputEvent>(), current_action_name);172} break;173case ActionMapEditor::BUTTON_EDIT_EVENT: {174// Action and Action name is located on the parent of the event.175current_action = item->get_parent()->get_meta("__action");176current_action_name = item->get_parent()->get_meta("__name");177178current_action_event_index = item->get_meta("__index");179180Ref<InputEvent> ie = item->get_meta("__event");181if (ie.is_valid()) {182event_config_dialog->popup_and_configure(ie, current_action_name);183}184} break;185case ActionMapEditor::BUTTON_REMOVE_ACTION: {186// Send removed action name187String name = item->get_meta("__name");188emit_signal(SNAME("action_removed"), name);189} break;190case ActionMapEditor::BUTTON_REMOVE_EVENT: {191// Remove event and send updated action192Dictionary action = item->get_parent()->get_meta("__action").duplicate();193String action_name = item->get_parent()->get_meta("__name");194195int event_index = item->get_meta("__index");196197Array events = action["events"].duplicate();198events.remove_at(event_index);199action["events"] = events;200201emit_signal(SNAME("action_edited"), action_name, action);202} break;203case ActionMapEditor::BUTTON_REVERT_ACTION: {204ERR_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.");205206Dictionary action = item->get_meta("__action_initial").duplicate();207String action_name = item->get_meta("__name");208209emit_signal(SNAME("action_edited"), action_name, action);210} break;211default:212break;213}214}215216void ActionMapEditor::_tree_item_activated() {217TreeItem *item = action_tree->get_selected();218219if (!item || !item->has_meta("__event")) {220return;221}222223_tree_button_pressed(item, 2, BUTTON_EDIT_EVENT, MouseButton::LEFT);224}225226void ActionMapEditor::_set_show_builtin_actions(bool p_show) {227show_builtin_actions = p_show;228EditorSettings::get_singleton()->set_project_metadata("project_settings", "show_builtin_actions", show_builtin_actions);229230// Prevent unnecessary updates of action list when cache is empty.231if (!actions_cache.is_empty()) {232update_action_list();233}234}235236void ActionMapEditor::_on_search_bar_value_changed() {237if (action_list_search_bar->is_searching()) {238show_builtin_actions_checkbutton->set_pressed_no_signal(true);239show_builtin_actions_checkbutton->set_disabled(true);240show_builtin_actions_checkbutton->set_tooltip_text(TTRC("Built-in actions are always shown when searching."));241} else {242show_builtin_actions_checkbutton->set_pressed_no_signal(show_builtin_actions);243show_builtin_actions_checkbutton->set_disabled(false);244show_builtin_actions_checkbutton->set_tooltip_text(String());245}246update_action_list();247}248249Variant ActionMapEditor::get_drag_data_fw(const Point2 &p_point, Control *p_from) {250TreeItem *selected = action_tree->get_selected();251if (!selected) {252return Variant();253}254255String name = selected->get_text(0);256Label *label = memnew(Label(name));257label->set_theme_type_variation("HeaderSmall");258label->set_modulate(Color(1, 1, 1, 1.0f));259label->set_auto_translate_mode(AUTO_TRANSLATE_MODE_DISABLED);260action_tree->set_drag_preview(label);261262get_viewport()->gui_set_drag_description(vformat(RTR("Action %s"), name));263264Dictionary drag_data;265266if (selected->has_meta("__action")) {267drag_data["input_type"] = "action";268}269270if (selected->has_meta("__event")) {271drag_data["input_type"] = "event";272}273274drag_data["source"] = selected->get_instance_id();275276action_tree->set_drop_mode_flags(Tree::DROP_MODE_INBETWEEN);277278return drag_data;279}280281bool ActionMapEditor::can_drop_data_fw(const Point2 &p_point, const Variant &p_data, Control *p_from) const {282Dictionary d = p_data;283if (!d.has("input_type")) {284return false;285}286287TreeItem *source = Object::cast_to<TreeItem>(ObjectDB::get_instance(d["source"].operator ObjectID()));288TreeItem *selected = action_tree->get_selected();289290TreeItem *item = (p_point == Vector2(Math::INF, Math::INF)) ? selected : action_tree->get_item_at_position(p_point);291if (!selected || !item || item == source) {292return false;293}294295// Don't allow moving an action in-between events.296if (d["input_type"] == "action" && item->has_meta("__event")) {297return false;298}299300// Don't allow moving an event to a different action.301if (d["input_type"] == "event" && item->get_parent() != selected->get_parent()) {302return false;303}304305return true;306}307308void ActionMapEditor::drop_data_fw(const Point2 &p_point, const Variant &p_data, Control *p_from) {309if (!can_drop_data_fw(p_point, p_data, p_from)) {310return;311}312313TreeItem *selected = action_tree->get_selected();314TreeItem *target = (p_point == Vector2(Math::INF, Math::INF)) ? selected : action_tree->get_item_at_position(p_point);315if (!target) {316return;317}318319bool 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;320321Dictionary d = p_data;322if (d["input_type"] == "action") {323// Change action order.324String relative_to = target->get_meta("__name");325String action_name = selected->get_meta("__name");326emit_signal(SNAME("action_reordered"), action_name, relative_to, drop_above);327328} else if (d["input_type"] == "event") {329// Change event order330int current_index = selected->get_meta("__index");331int target_index = target->get_meta("__index");332333// Construct new events array.334Dictionary new_action = selected->get_parent()->get_meta("__action");335336Array events = new_action["events"];337Array new_events;338339// The following method was used to perform the array changes since `remove` followed by `insert` was not working properly at time of writing.340// Loop thought existing events341for (int i = 0; i < events.size(); i++) {342// If you come across the current index, just skip it, as it has been moved.343if (i == current_index) {344continue;345} else if (i == target_index) {346// We are at the target index. If drop above, add selected event there first, then target, so moved event goes on top.347if (drop_above) {348new_events.push_back(events[current_index]);349new_events.push_back(events[target_index]);350} else {351new_events.push_back(events[target_index]);352new_events.push_back(events[current_index]);353}354} else {355new_events.push_back(events[i]);356}357}358359new_action["events"] = new_events;360emit_signal(SNAME("action_edited"), selected->get_parent()->get_meta("__name"), new_action);361}362}363364void ActionMapEditor::_notification(int p_what) {365switch (p_what) {366case NOTIFICATION_TRANSLATION_CHANGED: {367if (!actions_cache.is_empty()) {368update_action_list();369}370if (!add_button->get_tooltip_text().is_empty()) {371_add_edit_text_changed(add_edit->get_text());372}373} break;374375case NOTIFICATION_THEME_CHANGED: {376add_button->set_button_icon(get_editor_theme_icon(SNAME("Add")));377if (!actions_cache.is_empty()) {378update_action_list();379}380} break;381}382}383384void ActionMapEditor::_bind_methods() {385ADD_SIGNAL(MethodInfo("action_added", PropertyInfo(Variant::STRING, "name")));386ADD_SIGNAL(MethodInfo("action_edited", PropertyInfo(Variant::STRING, "name"), PropertyInfo(Variant::DICTIONARY, "new_action")));387ADD_SIGNAL(MethodInfo("action_removed", PropertyInfo(Variant::STRING, "name")));388ADD_SIGNAL(MethodInfo("action_renamed", PropertyInfo(Variant::STRING, "old_name"), PropertyInfo(Variant::STRING, "new_name")));389ADD_SIGNAL(MethodInfo("action_reordered", PropertyInfo(Variant::STRING, "action_name"), PropertyInfo(Variant::STRING, "relative_to"), PropertyInfo(Variant::BOOL, "before")));390}391392LineEdit *ActionMapEditor::get_search_box() const {393return action_list_search_bar->get_name_search_box();394}395396LineEdit *ActionMapEditor::get_path_box() const {397return add_edit;398}399400InputEventConfigurationDialog *ActionMapEditor::get_configuration_dialog() {401return event_config_dialog;402}403404bool ActionMapEditor::_should_display_action(const String &p_name, const Array &p_events) const {405const Ref<InputEvent> search_ev = action_list_search_bar->get_event();406bool event_match = true;407if (search_ev.is_valid()) {408event_match = false;409for (int i = 0; i < p_events.size(); ++i) {410const Ref<InputEvent> ev = p_events[i];411if (ev.is_valid() && ev->is_match(search_ev, true)) {412event_match = true;413}414}415}416417return event_match && action_list_search_bar->get_name().is_subsequence_ofn(p_name);418}419420void ActionMapEditor::update_action_list(const Vector<ActionInfo> &p_action_infos) {421if (!p_action_infos.is_empty()) {422actions_cache = p_action_infos;423}424425HashSet<String> collapsed_actions;426TreeItem *root = action_tree->get_root();427if (root) {428for (TreeItem *child = root->get_first_child(); child; child = child->get_next()) {429if (child->is_collapsed()) {430collapsed_actions.insert(child->get_meta("__name"));431}432}433}434435action_tree->clear();436root = action_tree->create_item();437438for (const ActionInfo &action_info : actions_cache) {439const Array events = action_info.action["events"];440if (!_should_display_action(action_info.name, events)) {441continue;442}443444if (!action_info.editable && !action_list_search_bar->is_searching() && !show_builtin_actions) {445continue;446}447448const Variant deadzone = action_info.action["deadzone"];449450// Update Tree...451452TreeItem *action_item = action_tree->create_item(root);453ERR_FAIL_NULL(action_item);454action_item->set_meta("__action", action_info.action);455action_item->set_meta("__name", action_info.name);456action_item->set_collapsed(collapsed_actions.has(action_info.name));457458// First Column - Action Name459action_item->set_auto_translate_mode(0, AUTO_TRANSLATE_MODE_DISABLED);460action_item->set_text(0, action_info.name);461action_item->set_editable(0, action_info.editable);462action_item->set_icon(0, action_info.icon);463464// Second Column - Deadzone465action_item->set_editable(1, true);466action_item->set_cell_mode(1, TreeItem::CELL_MODE_RANGE);467action_item->set_range_config(1, 0.0, 1.0, 0.01);468action_item->set_range(1, deadzone);469470// Third column - buttons471if (action_info.has_initial) {472bool deadzone_eq = action_info.action_initial["deadzone"] == action_info.action["deadzone"];473bool events_eq = Shortcut::is_event_array_equal(action_info.action_initial["events"], action_info.action["events"]);474bool action_eq = deadzone_eq && events_eq;475action_item->set_meta("__action_initial", action_info.action_initial);476action_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"));477}478action_item->add_button(2, get_editor_theme_icon(SNAME("Add")), BUTTON_ADD_EVENT, false, TTRC("Add Event"));479action_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"));480481action_item->set_custom_bg_color(0, get_theme_color(SNAME("prop_subsection"), EditorStringName(Editor)));482action_item->set_custom_bg_color(1, get_theme_color(SNAME("prop_subsection"), EditorStringName(Editor)));483484for (int evnt_idx = 0; evnt_idx < events.size(); evnt_idx++) {485Ref<InputEvent> event = events[evnt_idx];486if (event.is_null()) {487continue;488}489490TreeItem *event_item = action_tree->create_item(action_item);491492// First Column - Text493event_item->set_auto_translate_mode(0, AUTO_TRANSLATE_MODE_DISABLED);494event_item->set_text(0, EventListenerLineEdit::get_event_text(event, true));495event_item->set_meta("__event", event);496event_item->set_meta("__index", evnt_idx);497498// First Column - Icon499Ref<InputEventKey> k = event;500if (k.is_valid()) {501if (k->get_physical_keycode() == Key::NONE && k->get_keycode() == Key::NONE && k->get_key_label() != Key::NONE) {502event_item->set_icon(0, get_editor_theme_icon(SNAME("KeyboardLabel")));503} else if (k->get_keycode() != Key::NONE) {504event_item->set_icon(0, get_editor_theme_icon(SNAME("Keyboard")));505} else if (k->get_physical_keycode() != Key::NONE) {506event_item->set_icon(0, get_editor_theme_icon(SNAME("KeyboardPhysical")));507} else {508event_item->set_icon(0, get_editor_theme_icon(SNAME("KeyboardError")));509}510}511512Ref<InputEventMouseButton> mb = event;513if (mb.is_valid()) {514event_item->set_icon(0, get_editor_theme_icon(SNAME("Mouse")));515}516517Ref<InputEventJoypadButton> jb = event;518if (jb.is_valid()) {519event_item->set_icon(0, get_editor_theme_icon(SNAME("JoyButton")));520}521522Ref<InputEventJoypadMotion> jm = event;523if (jm.is_valid()) {524event_item->set_icon(0, get_editor_theme_icon(SNAME("JoyAxis")));525}526527// Third Column - Buttons528event_item->add_button(2, get_editor_theme_icon(SNAME("Edit")), BUTTON_EDIT_EVENT, false, TTRC("Edit Event"), TTRC("Edit Event"));529event_item->add_button(2, get_editor_theme_icon(SNAME("Remove")), BUTTON_REMOVE_EVENT, false, TTRC("Remove Event"), TTRC("Remove Event"));530event_item->set_button_color(2, 0, Color(1, 1, 1, 0.75));531event_item->set_button_color(2, 1, Color(1, 1, 1, 0.75));532}533}534}535536void ActionMapEditor::show_message(const String &p_message) {537message->set_text(p_message);538message->popup_centered();539}540541ActionMapEditor::ActionMapEditor() {542// Main Vbox Container543VBoxContainer *main_vbox = memnew(VBoxContainer);544main_vbox->set_anchors_and_offsets_preset(PRESET_FULL_RECT);545add_child(main_vbox);546547action_list_search_bar = memnew(EditorEventSearchBar);548action_list_search_bar->connect(SceneStringName(value_changed), callable_mp(this, &ActionMapEditor::_on_search_bar_value_changed));549main_vbox->add_child(action_list_search_bar);550551// Adding Action line edit + button552add_hbox = memnew(HBoxContainer);553add_hbox->set_h_size_flags(Control::SIZE_EXPAND_FILL);554555add_edit = memnew(LineEdit);556add_edit->set_h_size_flags(Control::SIZE_EXPAND_FILL);557add_edit->set_placeholder(TTRC("Add New Action"));558add_edit->set_accessibility_name(TTRC("Add New Action"));559add_edit->set_clear_button_enabled(true);560add_edit->set_keep_editing_on_text_submit(true);561add_edit->connect(SceneStringName(text_changed), callable_mp(this, &ActionMapEditor::_add_edit_text_changed));562add_edit->connect(SceneStringName(text_submitted), callable_mp(this, &ActionMapEditor::_add_action));563add_hbox->add_child(add_edit);564565add_button = memnew(Button);566add_button->set_text(TTRC("Add"));567add_button->connect(SceneStringName(pressed), callable_mp(this, &ActionMapEditor::_add_action_pressed));568add_hbox->add_child(add_button);569// Disable the button and set its tooltip.570_add_edit_text_changed(add_edit->get_text());571572add_hbox->add_child(memnew(VSeparator));573574show_builtin_actions_checkbutton = memnew(CheckButton);575show_builtin_actions_checkbutton->set_text(TTRC("Show Built-in Actions"));576show_builtin_actions_checkbutton->connect(SceneStringName(toggled), callable_mp(this, &ActionMapEditor::_set_show_builtin_actions));577add_hbox->add_child(show_builtin_actions_checkbutton);578579show_builtin_actions = EditorSettings::get_singleton()->get_project_metadata("project_settings", "show_builtin_actions", false);580show_builtin_actions_checkbutton->set_pressed_no_signal(show_builtin_actions);581582main_vbox->add_child(add_hbox);583584// Action Editor Tree585action_tree = memnew(Tree);586action_tree->set_v_size_flags(Control::SIZE_EXPAND_FILL);587action_tree->set_accessibility_name(TTRC("Action Map"));588action_tree->set_columns(3);589action_tree->set_hide_root(true);590action_tree->set_column_titles_visible(true);591action_tree->set_column_title(0, TTRC("Action"));592action_tree->set_column_clip_content(0, true);593action_tree->set_column_title(1, TTRC("Deadzone"));594action_tree->set_column_expand(1, false);595action_tree->set_column_custom_minimum_width(1, 80 * EDSCALE);596action_tree->set_column_expand(2, false);597action_tree->set_column_custom_minimum_width(2, 50 * EDSCALE);598action_tree->connect("item_edited", callable_mp(this, &ActionMapEditor::_action_edited), CONNECT_DEFERRED);599action_tree->connect("item_activated", callable_mp(this, &ActionMapEditor::_tree_item_activated));600action_tree->connect("button_clicked", callable_mp(this, &ActionMapEditor::_tree_button_pressed));601main_vbox->add_child(action_tree);602603SET_DRAG_FORWARDING_GCD(action_tree, ActionMapEditor);604605// Adding event dialog606event_config_dialog = memnew(InputEventConfigurationDialog);607event_config_dialog->connect(SceneStringName(confirmed), callable_mp(this, &ActionMapEditor::_event_config_confirmed));608add_child(event_config_dialog);609610message = memnew(AcceptDialog);611add_child(message);612}613614615