| 1 | /**************************************************************************/ |
| 2 | /* action_map_editor.cpp */ |
| 3 | /**************************************************************************/ |
| 4 | /* This file is part of: */ |
| 5 | /* GODOT ENGINE */ |
| 6 | /* https://godotengine.org */ |
| 7 | /**************************************************************************/ |
| 8 | /* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ |
| 9 | /* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ |
| 10 | /* */ |
| 11 | /* Permission is hereby granted, free of charge, to any person obtaining */ |
| 12 | /* a copy of this software and associated documentation files (the */ |
| 13 | /* "Software"), to deal in the Software without restriction, including */ |
| 14 | /* without limitation the rights to use, copy, modify, merge, publish, */ |
| 15 | /* distribute, sublicense, and/or sell copies of the Software, and to */ |
| 16 | /* permit persons to whom the Software is furnished to do so, subject to */ |
| 17 | /* the following conditions: */ |
| 18 | /* */ |
| 19 | /* The above copyright notice and this permission notice shall be */ |
| 20 | /* included in all copies or substantial portions of the Software. */ |
| 21 | /* */ |
| 22 | /* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ |
| 23 | /* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ |
| 24 | /* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ |
| 25 | /* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ |
| 26 | /* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ |
| 27 | /* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ |
| 28 | /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ |
| 29 | /**************************************************************************/ |
| 30 | |
| 31 | #include "editor/action_map_editor.h" |
| 32 | |
| 33 | #include "editor/editor_scale.h" |
| 34 | #include "editor/editor_settings.h" |
| 35 | #include "editor/editor_string_names.h" |
| 36 | #include "editor/event_listener_line_edit.h" |
| 37 | #include "editor/input_event_configuration_dialog.h" |
| 38 | #include "scene/gui/check_button.h" |
| 39 | #include "scene/gui/tree.h" |
| 40 | #include "scene/scene_string_names.h" |
| 41 | |
| 42 | static bool _is_action_name_valid(const String &p_name) { |
| 43 | const char32_t *cstr = p_name.get_data(); |
| 44 | for (int i = 0; cstr[i]; i++) { |
| 45 | if (cstr[i] == '/' || cstr[i] == ':' || cstr[i] == '"' || |
| 46 | cstr[i] == '=' || cstr[i] == '\\' || cstr[i] < 32) { |
| 47 | return false; |
| 48 | } |
| 49 | } |
| 50 | return true; |
| 51 | } |
| 52 | |
| 53 | void ActionMapEditor::_event_config_confirmed() { |
| 54 | Ref<InputEvent> ev = event_config_dialog->get_event(); |
| 55 | |
| 56 | Dictionary new_action = current_action.duplicate(); |
| 57 | Array events = new_action["events" ].duplicate(); |
| 58 | |
| 59 | if (current_action_event_index == -1) { |
| 60 | // Add new event |
| 61 | events.push_back(ev); |
| 62 | } else { |
| 63 | // Edit existing event |
| 64 | events[current_action_event_index] = ev; |
| 65 | } |
| 66 | |
| 67 | new_action["events" ] = events; |
| 68 | emit_signal(SNAME("action_edited" ), current_action_name, new_action); |
| 69 | } |
| 70 | |
| 71 | void ActionMapEditor::_add_action_pressed() { |
| 72 | _add_action(add_edit->get_text()); |
| 73 | } |
| 74 | |
| 75 | String ActionMapEditor::_check_new_action_name(const String &p_name) { |
| 76 | if (p_name.is_empty() || !_is_action_name_valid(p_name)) { |
| 77 | return TTR("Invalid action name. It cannot be empty nor contain '/', ':', '=', '\\' or '\"'" ); |
| 78 | } |
| 79 | |
| 80 | if (_has_action(p_name)) { |
| 81 | return vformat(TTR("An action with the name '%s' already exists." ), p_name); |
| 82 | } |
| 83 | |
| 84 | return "" ; |
| 85 | } |
| 86 | |
| 87 | void ActionMapEditor::_add_edit_text_changed(const String &p_name) { |
| 88 | String error = _check_new_action_name(p_name); |
| 89 | add_button->set_tooltip_text(error); |
| 90 | add_button->set_disabled(!error.is_empty()); |
| 91 | } |
| 92 | |
| 93 | bool ActionMapEditor::_has_action(const String &p_name) const { |
| 94 | for (const ActionInfo &action_info : actions_cache) { |
| 95 | if (p_name == action_info.name) { |
| 96 | return true; |
| 97 | } |
| 98 | } |
| 99 | return false; |
| 100 | } |
| 101 | |
| 102 | void ActionMapEditor::_add_action(const String &p_name) { |
| 103 | String error = _check_new_action_name(p_name); |
| 104 | if (!error.is_empty()) { |
| 105 | show_message(error); |
| 106 | return; |
| 107 | } |
| 108 | |
| 109 | add_edit->clear(); |
| 110 | emit_signal(SNAME("action_added" ), p_name); |
| 111 | } |
| 112 | |
| 113 | void ActionMapEditor::_action_edited() { |
| 114 | TreeItem *ti = action_tree->get_edited(); |
| 115 | if (!ti) { |
| 116 | return; |
| 117 | } |
| 118 | |
| 119 | if (action_tree->get_selected_column() == 0) { |
| 120 | // Name Edited |
| 121 | String new_name = ti->get_text(0); |
| 122 | String old_name = ti->get_meta("__name" ); |
| 123 | |
| 124 | if (new_name == old_name) { |
| 125 | return; |
| 126 | } |
| 127 | |
| 128 | if (new_name.is_empty() || !_is_action_name_valid(new_name)) { |
| 129 | ti->set_text(0, old_name); |
| 130 | show_message(TTR("Invalid action name. It cannot be empty nor contain '/', ':', '=', '\\' or '\"'" )); |
| 131 | return; |
| 132 | } |
| 133 | |
| 134 | if (_has_action(new_name)) { |
| 135 | ti->set_text(0, old_name); |
| 136 | show_message(vformat(TTR("An action with the name '%s' already exists." ), new_name)); |
| 137 | return; |
| 138 | } |
| 139 | |
| 140 | emit_signal(SNAME("action_renamed" ), old_name, new_name); |
| 141 | } else if (action_tree->get_selected_column() == 1) { |
| 142 | // Deadzone Edited |
| 143 | String name = ti->get_meta("__name" ); |
| 144 | Dictionary old_action = ti->get_meta("__action" ); |
| 145 | Dictionary new_action = old_action.duplicate(); |
| 146 | new_action["deadzone" ] = ti->get_range(1); |
| 147 | |
| 148 | // Call deferred so that input can finish propagating through tree, allowing re-making of tree to occur. |
| 149 | call_deferred(SNAME("emit_signal" ), "action_edited" , name, new_action); |
| 150 | } |
| 151 | } |
| 152 | |
| 153 | void ActionMapEditor::_tree_button_pressed(Object *p_item, int p_column, int p_id, MouseButton p_button) { |
| 154 | if (p_button != MouseButton::LEFT) { |
| 155 | return; |
| 156 | } |
| 157 | |
| 158 | ItemButton option = (ItemButton)p_id; |
| 159 | |
| 160 | TreeItem *item = Object::cast_to<TreeItem>(p_item); |
| 161 | if (!item) { |
| 162 | return; |
| 163 | } |
| 164 | |
| 165 | switch (option) { |
| 166 | case ActionMapEditor::BUTTON_ADD_EVENT: { |
| 167 | current_action = item->get_meta("__action" ); |
| 168 | current_action_name = item->get_meta("__name" ); |
| 169 | current_action_event_index = -1; |
| 170 | |
| 171 | event_config_dialog->popup_and_configure(); |
| 172 | } break; |
| 173 | case ActionMapEditor::BUTTON_EDIT_EVENT: { |
| 174 | // Action and Action name is located on the parent of the event. |
| 175 | current_action = item->get_parent()->get_meta("__action" ); |
| 176 | current_action_name = item->get_parent()->get_meta("__name" ); |
| 177 | |
| 178 | current_action_event_index = item->get_meta("__index" ); |
| 179 | |
| 180 | Ref<InputEvent> ie = item->get_meta("__event" ); |
| 181 | if (ie.is_valid()) { |
| 182 | event_config_dialog->popup_and_configure(ie); |
| 183 | } |
| 184 | } break; |
| 185 | case ActionMapEditor::BUTTON_REMOVE_ACTION: { |
| 186 | // Send removed action name |
| 187 | String name = item->get_meta("__name" ); |
| 188 | emit_signal(SNAME("action_removed" ), name); |
| 189 | } break; |
| 190 | case ActionMapEditor::BUTTON_REMOVE_EVENT: { |
| 191 | // Remove event and send updated action |
| 192 | Dictionary action = item->get_parent()->get_meta("__action" ).duplicate(); |
| 193 | String action_name = item->get_parent()->get_meta("__name" ); |
| 194 | |
| 195 | int event_index = item->get_meta("__index" ); |
| 196 | |
| 197 | Array events = action["events" ].duplicate(); |
| 198 | events.remove_at(event_index); |
| 199 | action["events" ] = events; |
| 200 | |
| 201 | emit_signal(SNAME("action_edited" ), action_name, action); |
| 202 | } break; |
| 203 | case ActionMapEditor::BUTTON_REVERT_ACTION: { |
| 204 | ERR_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." ); |
| 205 | |
| 206 | Dictionary action = item->get_meta("__action_initial" ).duplicate(); |
| 207 | String action_name = item->get_meta("__name" ); |
| 208 | |
| 209 | emit_signal(SNAME("action_edited" ), action_name, action); |
| 210 | } break; |
| 211 | default: |
| 212 | break; |
| 213 | } |
| 214 | } |
| 215 | |
| 216 | void ActionMapEditor::_tree_item_activated() { |
| 217 | TreeItem *item = action_tree->get_selected(); |
| 218 | |
| 219 | if (!item || !item->has_meta("__event" )) { |
| 220 | return; |
| 221 | } |
| 222 | |
| 223 | _tree_button_pressed(item, 2, BUTTON_EDIT_EVENT, MouseButton::LEFT); |
| 224 | } |
| 225 | |
| 226 | void ActionMapEditor::set_show_builtin_actions(bool p_show) { |
| 227 | show_builtin_actions = p_show; |
| 228 | show_builtin_actions_checkbutton->set_pressed(p_show); |
| 229 | EditorSettings::get_singleton()->set_project_metadata("project_settings" , "show_builtin_actions" , show_builtin_actions); |
| 230 | |
| 231 | // Prevent unnecessary updates of action list when cache is empty. |
| 232 | if (!actions_cache.is_empty()) { |
| 233 | update_action_list(); |
| 234 | } |
| 235 | } |
| 236 | |
| 237 | void ActionMapEditor::_search_term_updated(const String &) { |
| 238 | update_action_list(); |
| 239 | } |
| 240 | |
| 241 | void ActionMapEditor::_search_by_event(const Ref<InputEvent> &p_event) { |
| 242 | if (p_event.is_null() || (p_event->is_pressed() && !p_event->is_echo())) { |
| 243 | update_action_list(); |
| 244 | } |
| 245 | } |
| 246 | |
| 247 | Variant ActionMapEditor::get_drag_data_fw(const Point2 &p_point, Control *p_from) { |
| 248 | TreeItem *selected = action_tree->get_selected(); |
| 249 | if (!selected) { |
| 250 | return Variant(); |
| 251 | } |
| 252 | |
| 253 | String name = selected->get_text(0); |
| 254 | Label *label = memnew(Label(name)); |
| 255 | label->set_theme_type_variation("HeaderSmall" ); |
| 256 | label->set_modulate(Color(1, 1, 1, 1.0f)); |
| 257 | action_tree->set_drag_preview(label); |
| 258 | |
| 259 | Dictionary drag_data; |
| 260 | |
| 261 | if (selected->has_meta("__action" )) { |
| 262 | drag_data["input_type" ] = "action" ; |
| 263 | } |
| 264 | |
| 265 | if (selected->has_meta("__event" )) { |
| 266 | drag_data["input_type" ] = "event" ; |
| 267 | } |
| 268 | |
| 269 | action_tree->set_drop_mode_flags(Tree::DROP_MODE_INBETWEEN); |
| 270 | |
| 271 | return drag_data; |
| 272 | } |
| 273 | |
| 274 | bool ActionMapEditor::can_drop_data_fw(const Point2 &p_point, const Variant &p_data, Control *p_from) const { |
| 275 | Dictionary d = p_data; |
| 276 | if (!d.has("input_type" )) { |
| 277 | return false; |
| 278 | } |
| 279 | |
| 280 | TreeItem *selected = action_tree->get_selected(); |
| 281 | TreeItem *item = action_tree->get_item_at_position(p_point); |
| 282 | if (!selected || !item || item == selected) { |
| 283 | return false; |
| 284 | } |
| 285 | |
| 286 | // Don't allow moving an action in-between events. |
| 287 | if (d["input_type" ] == "action" && item->has_meta("__event" )) { |
| 288 | return false; |
| 289 | } |
| 290 | |
| 291 | // Don't allow moving an event to a different action. |
| 292 | if (d["input_type" ] == "event" && item->get_parent() != selected->get_parent()) { |
| 293 | return false; |
| 294 | } |
| 295 | |
| 296 | return true; |
| 297 | } |
| 298 | |
| 299 | void ActionMapEditor::drop_data_fw(const Point2 &p_point, const Variant &p_data, Control *p_from) { |
| 300 | if (!can_drop_data_fw(p_point, p_data, p_from)) { |
| 301 | return; |
| 302 | } |
| 303 | |
| 304 | TreeItem *selected = action_tree->get_selected(); |
| 305 | TreeItem *target = action_tree->get_item_at_position(p_point); |
| 306 | bool drop_above = action_tree->get_drop_section_at_position(p_point) == -1; |
| 307 | |
| 308 | if (!target) { |
| 309 | return; |
| 310 | } |
| 311 | |
| 312 | Dictionary d = p_data; |
| 313 | if (d["input_type" ] == "action" ) { |
| 314 | // Change action order. |
| 315 | String relative_to = target->get_meta("__name" ); |
| 316 | String action_name = selected->get_meta("__name" ); |
| 317 | emit_signal(SNAME("action_reordered" ), action_name, relative_to, drop_above); |
| 318 | |
| 319 | } else if (d["input_type" ] == "event" ) { |
| 320 | // Change event order |
| 321 | int current_index = selected->get_meta("__index" ); |
| 322 | int target_index = target->get_meta("__index" ); |
| 323 | |
| 324 | // Construct new events array. |
| 325 | Dictionary new_action = selected->get_parent()->get_meta("__action" ); |
| 326 | |
| 327 | Array events = new_action["events" ]; |
| 328 | Array new_events; |
| 329 | |
| 330 | // The following method was used to perform the array changes since `remove` followed by `insert` was not working properly at time of writing. |
| 331 | // Loop thought existing events |
| 332 | for (int i = 0; i < events.size(); i++) { |
| 333 | // If you come across the current index, just skip it, as it has been moved. |
| 334 | if (i == current_index) { |
| 335 | continue; |
| 336 | } else if (i == target_index) { |
| 337 | // We are at the target index. If drop above, add selected event there first, then target, so moved event goes on top. |
| 338 | if (drop_above) { |
| 339 | new_events.push_back(events[current_index]); |
| 340 | new_events.push_back(events[target_index]); |
| 341 | } else { |
| 342 | new_events.push_back(events[target_index]); |
| 343 | new_events.push_back(events[current_index]); |
| 344 | } |
| 345 | } else { |
| 346 | new_events.push_back(events[i]); |
| 347 | } |
| 348 | } |
| 349 | |
| 350 | new_action["events" ] = new_events; |
| 351 | emit_signal(SNAME("action_edited" ), selected->get_parent()->get_meta("__name" ), new_action); |
| 352 | } |
| 353 | } |
| 354 | |
| 355 | void ActionMapEditor::_notification(int p_what) { |
| 356 | switch (p_what) { |
| 357 | case NOTIFICATION_ENTER_TREE: |
| 358 | case NOTIFICATION_THEME_CHANGED: { |
| 359 | action_list_search->set_right_icon(get_editor_theme_icon(SNAME("Search" ))); |
| 360 | if (!actions_cache.is_empty()) { |
| 361 | update_action_list(); |
| 362 | } |
| 363 | } break; |
| 364 | } |
| 365 | } |
| 366 | |
| 367 | void ActionMapEditor::_bind_methods() { |
| 368 | ADD_SIGNAL(MethodInfo("action_added" , PropertyInfo(Variant::STRING, "name" ))); |
| 369 | ADD_SIGNAL(MethodInfo("action_edited" , PropertyInfo(Variant::STRING, "name" ), PropertyInfo(Variant::DICTIONARY, "new_action" ))); |
| 370 | ADD_SIGNAL(MethodInfo("action_removed" , PropertyInfo(Variant::STRING, "name" ))); |
| 371 | ADD_SIGNAL(MethodInfo("action_renamed" , PropertyInfo(Variant::STRING, "old_name" ), PropertyInfo(Variant::STRING, "new_name" ))); |
| 372 | ADD_SIGNAL(MethodInfo("action_reordered" , PropertyInfo(Variant::STRING, "action_name" ), PropertyInfo(Variant::STRING, "relative_to" ), PropertyInfo(Variant::BOOL, "before" ))); |
| 373 | ADD_SIGNAL(MethodInfo(SNAME("filter_focused" ))); |
| 374 | ADD_SIGNAL(MethodInfo(SNAME("filter_unfocused" ))); |
| 375 | } |
| 376 | |
| 377 | LineEdit *ActionMapEditor::get_search_box() const { |
| 378 | return action_list_search; |
| 379 | } |
| 380 | |
| 381 | InputEventConfigurationDialog *ActionMapEditor::get_configuration_dialog() { |
| 382 | return event_config_dialog; |
| 383 | } |
| 384 | |
| 385 | bool ActionMapEditor::_should_display_action(const String &p_name, const Array &p_events) const { |
| 386 | const Ref<InputEvent> search_ev = action_list_search_by_event->get_event(); |
| 387 | bool event_match = true; |
| 388 | if (search_ev.is_valid()) { |
| 389 | event_match = false; |
| 390 | for (int i = 0; i < p_events.size(); ++i) { |
| 391 | const Ref<InputEvent> ev = p_events[i]; |
| 392 | if (ev.is_valid() && ev->is_match(search_ev, true)) { |
| 393 | event_match = true; |
| 394 | } |
| 395 | } |
| 396 | } |
| 397 | |
| 398 | return event_match && action_list_search->get_text().is_subsequence_ofn(p_name); |
| 399 | } |
| 400 | |
| 401 | void ActionMapEditor::update_action_list(const Vector<ActionInfo> &p_action_infos) { |
| 402 | if (!p_action_infos.is_empty()) { |
| 403 | actions_cache = p_action_infos; |
| 404 | } |
| 405 | |
| 406 | action_tree->clear(); |
| 407 | TreeItem *root = action_tree->create_item(); |
| 408 | |
| 409 | for (int i = 0; i < actions_cache.size(); i++) { |
| 410 | ActionInfo action_info = actions_cache[i]; |
| 411 | |
| 412 | const Array events = action_info.action["events" ]; |
| 413 | if (!_should_display_action(action_info.name, events)) { |
| 414 | continue; |
| 415 | } |
| 416 | |
| 417 | if (!action_info.editable && !show_builtin_actions) { |
| 418 | continue; |
| 419 | } |
| 420 | |
| 421 | const Variant deadzone = action_info.action["deadzone" ]; |
| 422 | |
| 423 | // Update Tree... |
| 424 | |
| 425 | TreeItem *action_item = action_tree->create_item(root); |
| 426 | action_item->set_meta("__action" , action_info.action); |
| 427 | action_item->set_meta("__name" , action_info.name); |
| 428 | |
| 429 | // First Column - Action Name |
| 430 | action_item->set_text(0, action_info.name); |
| 431 | action_item->set_editable(0, action_info.editable); |
| 432 | action_item->set_icon(0, action_info.icon); |
| 433 | |
| 434 | // Second Column - Deadzone |
| 435 | action_item->set_editable(1, true); |
| 436 | action_item->set_cell_mode(1, TreeItem::CELL_MODE_RANGE); |
| 437 | action_item->set_range_config(1, 0.0, 1.0, 0.01); |
| 438 | action_item->set_range(1, deadzone); |
| 439 | |
| 440 | // Third column - buttons |
| 441 | if (action_info.has_initial) { |
| 442 | bool deadzone_eq = action_info.action_initial["deadzone" ] == action_info.action["deadzone" ]; |
| 443 | bool events_eq = Shortcut::is_event_array_equal(action_info.action_initial["events" ], action_info.action["events" ]); |
| 444 | bool action_eq = deadzone_eq && events_eq; |
| 445 | action_item->set_meta("__action_initial" , action_info.action_initial); |
| 446 | action_item->add_button(2, action_tree->get_editor_theme_icon(SNAME("ReloadSmall" )), BUTTON_REVERT_ACTION, action_eq, action_eq ? TTR("Cannot Revert - Action is same as initial" ) : TTR("Revert Action" )); |
| 447 | } |
| 448 | action_item->add_button(2, action_tree->get_editor_theme_icon(SNAME("Add" )), BUTTON_ADD_EVENT, false, TTR("Add Event" )); |
| 449 | action_item->add_button(2, action_tree->get_editor_theme_icon(SNAME("Remove" )), BUTTON_REMOVE_ACTION, !action_info.editable, action_info.editable ? TTR("Remove Action" ) : TTR("Cannot Remove Action" )); |
| 450 | |
| 451 | action_item->set_custom_bg_color(0, action_tree->get_theme_color(SNAME("prop_subsection" ), EditorStringName(Editor))); |
| 452 | action_item->set_custom_bg_color(1, action_tree->get_theme_color(SNAME("prop_subsection" ), EditorStringName(Editor))); |
| 453 | |
| 454 | for (int evnt_idx = 0; evnt_idx < events.size(); evnt_idx++) { |
| 455 | Ref<InputEvent> event = events[evnt_idx]; |
| 456 | if (event.is_null()) { |
| 457 | continue; |
| 458 | } |
| 459 | |
| 460 | TreeItem *event_item = action_tree->create_item(action_item); |
| 461 | |
| 462 | // First Column - Text |
| 463 | event_item->set_text(0, EventListenerLineEdit::get_event_text(event, true)); |
| 464 | event_item->set_meta("__event" , event); |
| 465 | event_item->set_meta("__index" , evnt_idx); |
| 466 | |
| 467 | // First Column - Icon |
| 468 | Ref<InputEventKey> k = event; |
| 469 | if (k.is_valid()) { |
| 470 | if (k->get_physical_keycode() == Key::NONE && k->get_keycode() == Key::NONE && k->get_key_label() != Key::NONE) { |
| 471 | event_item->set_icon(0, action_tree->get_editor_theme_icon(SNAME("KeyboardLabel" ))); |
| 472 | } else if (k->get_keycode() != Key::NONE) { |
| 473 | event_item->set_icon(0, action_tree->get_editor_theme_icon(SNAME("Keyboard" ))); |
| 474 | } else if (k->get_physical_keycode() != Key::NONE) { |
| 475 | event_item->set_icon(0, action_tree->get_editor_theme_icon(SNAME("KeyboardPhysical" ))); |
| 476 | } else { |
| 477 | event_item->set_icon(0, action_tree->get_editor_theme_icon(SNAME("KeyboardError" ))); |
| 478 | } |
| 479 | } |
| 480 | |
| 481 | Ref<InputEventMouseButton> mb = event; |
| 482 | if (mb.is_valid()) { |
| 483 | event_item->set_icon(0, action_tree->get_editor_theme_icon(SNAME("Mouse" ))); |
| 484 | } |
| 485 | |
| 486 | Ref<InputEventJoypadButton> jb = event; |
| 487 | if (jb.is_valid()) { |
| 488 | event_item->set_icon(0, action_tree->get_editor_theme_icon(SNAME("JoyButton" ))); |
| 489 | } |
| 490 | |
| 491 | Ref<InputEventJoypadMotion> jm = event; |
| 492 | if (jm.is_valid()) { |
| 493 | event_item->set_icon(0, action_tree->get_editor_theme_icon(SNAME("JoyAxis" ))); |
| 494 | } |
| 495 | |
| 496 | // Third Column - Buttons |
| 497 | event_item->add_button(2, action_tree->get_editor_theme_icon(SNAME("Edit" )), BUTTON_EDIT_EVENT, false, TTR("Edit Event" )); |
| 498 | event_item->add_button(2, action_tree->get_editor_theme_icon(SNAME("Remove" )), BUTTON_REMOVE_EVENT, false, TTR("Remove Event" )); |
| 499 | event_item->set_button_color(2, 0, Color(1, 1, 1, 0.75)); |
| 500 | event_item->set_button_color(2, 1, Color(1, 1, 1, 0.75)); |
| 501 | } |
| 502 | } |
| 503 | } |
| 504 | |
| 505 | void ActionMapEditor::show_message(const String &p_message) { |
| 506 | message->set_text(p_message); |
| 507 | message->popup_centered(); |
| 508 | } |
| 509 | |
| 510 | void ActionMapEditor::use_external_search_box(LineEdit *p_searchbox) { |
| 511 | memdelete(action_list_search); |
| 512 | action_list_search = p_searchbox; |
| 513 | action_list_search->connect("text_changed" , callable_mp(this, &ActionMapEditor::_search_term_updated)); |
| 514 | } |
| 515 | |
| 516 | void ActionMapEditor::_on_filter_focused() { |
| 517 | emit_signal(SNAME("filter_focused" )); |
| 518 | } |
| 519 | |
| 520 | void ActionMapEditor::_on_filter_unfocused() { |
| 521 | emit_signal(SNAME("filter_unfocused" )); |
| 522 | } |
| 523 | |
| 524 | ActionMapEditor::ActionMapEditor() { |
| 525 | // Main Vbox Container |
| 526 | VBoxContainer *main_vbox = memnew(VBoxContainer); |
| 527 | main_vbox->set_anchors_and_offsets_preset(PRESET_FULL_RECT); |
| 528 | add_child(main_vbox); |
| 529 | |
| 530 | HBoxContainer *top_hbox = memnew(HBoxContainer); |
| 531 | main_vbox->add_child(top_hbox); |
| 532 | |
| 533 | action_list_search = memnew(LineEdit); |
| 534 | action_list_search->set_h_size_flags(Control::SIZE_EXPAND_FILL); |
| 535 | action_list_search->set_placeholder(TTR("Filter by name..." )); |
| 536 | action_list_search->set_clear_button_enabled(true); |
| 537 | action_list_search->connect("text_changed" , callable_mp(this, &ActionMapEditor::_search_term_updated)); |
| 538 | top_hbox->add_child(action_list_search); |
| 539 | |
| 540 | action_list_search_by_event = memnew(EventListenerLineEdit); |
| 541 | action_list_search_by_event->set_h_size_flags(Control::SIZE_EXPAND_FILL); |
| 542 | action_list_search_by_event->set_stretch_ratio(0.75); |
| 543 | action_list_search_by_event->connect("event_changed" , callable_mp(this, &ActionMapEditor::_search_by_event)); |
| 544 | action_list_search_by_event->connect(SceneStringNames::get_singleton()->focus_entered, callable_mp(this, &ActionMapEditor::_on_filter_focused)); |
| 545 | action_list_search_by_event->connect(SceneStringNames::get_singleton()->focus_exited, callable_mp(this, &ActionMapEditor::_on_filter_unfocused)); |
| 546 | top_hbox->add_child(action_list_search_by_event); |
| 547 | |
| 548 | Button *clear_all_search = memnew(Button); |
| 549 | clear_all_search->set_text(TTR("Clear All" )); |
| 550 | clear_all_search->connect("pressed" , callable_mp(action_list_search_by_event, &EventListenerLineEdit::clear_event)); |
| 551 | clear_all_search->connect("pressed" , callable_mp(action_list_search, &LineEdit::clear)); |
| 552 | top_hbox->add_child(clear_all_search); |
| 553 | |
| 554 | // Adding Action line edit + button |
| 555 | add_hbox = memnew(HBoxContainer); |
| 556 | add_hbox->set_h_size_flags(Control::SIZE_EXPAND_FILL); |
| 557 | |
| 558 | add_edit = memnew(LineEdit); |
| 559 | add_edit->set_h_size_flags(Control::SIZE_EXPAND_FILL); |
| 560 | add_edit->set_placeholder(TTR("Add New Action" )); |
| 561 | add_edit->set_clear_button_enabled(true); |
| 562 | add_edit->connect("text_changed" , callable_mp(this, &ActionMapEditor::_add_edit_text_changed)); |
| 563 | add_edit->connect("text_submitted" , callable_mp(this, &ActionMapEditor::_add_action)); |
| 564 | add_hbox->add_child(add_edit); |
| 565 | |
| 566 | add_button = memnew(Button); |
| 567 | add_button->set_text(TTR("Add" )); |
| 568 | add_button->connect("pressed" , callable_mp(this, &ActionMapEditor::_add_action_pressed)); |
| 569 | add_hbox->add_child(add_button); |
| 570 | // Disable the button and set its tooltip. |
| 571 | _add_edit_text_changed(add_edit->get_text()); |
| 572 | |
| 573 | show_builtin_actions_checkbutton = memnew(CheckButton); |
| 574 | show_builtin_actions_checkbutton->set_text(TTR("Show Built-in Actions" )); |
| 575 | show_builtin_actions_checkbutton->connect("toggled" , callable_mp(this, &ActionMapEditor::set_show_builtin_actions)); |
| 576 | add_hbox->add_child(show_builtin_actions_checkbutton); |
| 577 | |
| 578 | show_builtin_actions = EditorSettings::get_singleton()->get_project_metadata("project_settings" , "show_builtin_actions" , false); |
| 579 | show_builtin_actions_checkbutton->set_pressed(show_builtin_actions); |
| 580 | |
| 581 | main_vbox->add_child(add_hbox); |
| 582 | |
| 583 | // Action Editor Tree |
| 584 | action_tree = memnew(Tree); |
| 585 | action_tree->set_v_size_flags(Control::SIZE_EXPAND_FILL); |
| 586 | action_tree->set_columns(3); |
| 587 | action_tree->set_hide_root(true); |
| 588 | action_tree->set_column_titles_visible(true); |
| 589 | action_tree->set_column_title(0, TTR("Action" )); |
| 590 | action_tree->set_column_clip_content(0, true); |
| 591 | action_tree->set_column_title(1, TTR("Deadzone" )); |
| 592 | action_tree->set_column_expand(1, false); |
| 593 | action_tree->set_column_custom_minimum_width(1, 80 * EDSCALE); |
| 594 | action_tree->set_column_expand(2, false); |
| 595 | action_tree->set_column_custom_minimum_width(2, 50 * EDSCALE); |
| 596 | action_tree->connect("item_edited" , callable_mp(this, &ActionMapEditor::_action_edited)); |
| 597 | action_tree->connect("item_activated" , callable_mp(this, &ActionMapEditor::_tree_item_activated)); |
| 598 | action_tree->connect("button_clicked" , callable_mp(this, &ActionMapEditor::_tree_button_pressed)); |
| 599 | main_vbox->add_child(action_tree); |
| 600 | |
| 601 | SET_DRAG_FORWARDING_GCD(action_tree, ActionMapEditor); |
| 602 | |
| 603 | // Adding event dialog |
| 604 | event_config_dialog = memnew(InputEventConfigurationDialog); |
| 605 | event_config_dialog->connect("confirmed" , callable_mp(this, &ActionMapEditor::_event_config_confirmed)); |
| 606 | add_child(event_config_dialog); |
| 607 | |
| 608 | message = memnew(AcceptDialog); |
| 609 | add_child(message); |
| 610 | } |
| 611 | |