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 | |