1 | /**************************************************************************/ |
2 | /* editor_settings_dialog.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_settings_dialog.h" |
32 | |
33 | #include "core/config/project_settings.h" |
34 | #include "core/input/input_map.h" |
35 | #include "core/os/keyboard.h" |
36 | #include "editor/debugger/editor_debugger_node.h" |
37 | #include "editor/editor_file_system.h" |
38 | #include "editor/editor_log.h" |
39 | #include "editor/editor_node.h" |
40 | #include "editor/editor_property_name_processor.h" |
41 | #include "editor/editor_scale.h" |
42 | #include "editor/editor_settings.h" |
43 | #include "editor/editor_string_names.h" |
44 | #include "editor/editor_undo_redo_manager.h" |
45 | #include "editor/event_listener_line_edit.h" |
46 | #include "editor/input_event_configuration_dialog.h" |
47 | #include "scene/gui/margin_container.h" |
48 | |
49 | void EditorSettingsDialog::ok_pressed() { |
50 | if (!EditorSettings::get_singleton()) { |
51 | return; |
52 | } |
53 | |
54 | _settings_save(); |
55 | timer->stop(); |
56 | } |
57 | |
58 | void EditorSettingsDialog::_settings_changed() { |
59 | timer->start(); |
60 | } |
61 | |
62 | void EditorSettingsDialog::_settings_property_edited(const String &p_name) { |
63 | String full_name = inspector->get_full_item_path(p_name); |
64 | |
65 | if (full_name == "interface/theme/accent_color" || full_name == "interface/theme/base_color" || full_name == "interface/theme/contrast" || full_name == "interface/theme/draw_extra_borders" ) { |
66 | EditorSettings::get_singleton()->set_manually("interface/theme/preset" , "Custom" ); // set preset to Custom |
67 | } else if (full_name.begins_with("text_editor/theme/highlighting" )) { |
68 | EditorSettings::get_singleton()->set_manually("text_editor/theme/color_theme" , "Custom" ); |
69 | } |
70 | } |
71 | |
72 | void EditorSettingsDialog::_settings_save() { |
73 | EditorSettings::get_singleton()->notify_changes(); |
74 | EditorSettings::get_singleton()->save(); |
75 | } |
76 | |
77 | void EditorSettingsDialog::cancel_pressed() { |
78 | if (!EditorSettings::get_singleton()) { |
79 | return; |
80 | } |
81 | |
82 | EditorSettings::get_singleton()->notify_changes(); |
83 | } |
84 | |
85 | void EditorSettingsDialog::() { |
86 | if (!EditorSettings::get_singleton()) { |
87 | return; |
88 | } |
89 | |
90 | EditorSettings::get_singleton()->list_text_editor_themes(); // make sure we have an up to date list of themes |
91 | |
92 | inspector->edit(EditorSettings::get_singleton()); |
93 | inspector->get_inspector()->update_tree(); |
94 | |
95 | search_box->select_all(); |
96 | search_box->grab_focus(); |
97 | |
98 | _update_shortcuts(); |
99 | set_process_shortcut_input(true); |
100 | |
101 | // Restore valid window bounds or pop up at default size. |
102 | Rect2 saved_size = EditorSettings::get_singleton()->get_project_metadata("dialog_bounds" , "editor_settings" , Rect2()); |
103 | if (saved_size != Rect2()) { |
104 | popup(saved_size); |
105 | } else { |
106 | popup_centered_clamped(Size2(900, 700) * EDSCALE, 0.8); |
107 | } |
108 | |
109 | _focus_current_search_box(); |
110 | } |
111 | |
112 | void EditorSettingsDialog::_filter_shortcuts(const String &) { |
113 | _update_shortcuts(); |
114 | } |
115 | |
116 | void EditorSettingsDialog::_filter_shortcuts_by_event(const Ref<InputEvent> &p_event) { |
117 | if (p_event.is_null() || (p_event->is_pressed() && !p_event->is_echo())) { |
118 | _update_shortcuts(); |
119 | } |
120 | } |
121 | |
122 | void EditorSettingsDialog::_undo_redo_callback(void *p_self, const String &p_name) { |
123 | EditorNode::get_log()->add_message(p_name, EditorLog::MSG_TYPE_EDITOR); |
124 | } |
125 | |
126 | void EditorSettingsDialog::_notification(int p_what) { |
127 | switch (p_what) { |
128 | case NOTIFICATION_VISIBILITY_CHANGED: { |
129 | if (!is_visible()) { |
130 | EditorSettings::get_singleton()->set_project_metadata("dialog_bounds" , "editor_settings" , Rect2(get_position(), get_size())); |
131 | set_process_shortcut_input(false); |
132 | } |
133 | } break; |
134 | |
135 | case NOTIFICATION_READY: { |
136 | EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton(); |
137 | undo_redo->get_or_create_history(EditorUndoRedoManager::GLOBAL_HISTORY).undo_redo->set_method_notify_callback(EditorDebuggerNode::_method_changeds, nullptr); |
138 | undo_redo->get_or_create_history(EditorUndoRedoManager::GLOBAL_HISTORY).undo_redo->set_property_notify_callback(EditorDebuggerNode::_property_changeds, nullptr); |
139 | undo_redo->get_or_create_history(EditorUndoRedoManager::GLOBAL_HISTORY).undo_redo->set_commit_notify_callback(_undo_redo_callback, this); |
140 | } break; |
141 | |
142 | case NOTIFICATION_ENTER_TREE: { |
143 | _update_icons(); |
144 | } break; |
145 | |
146 | case EditorSettings::NOTIFICATION_EDITOR_SETTINGS_CHANGED: { |
147 | _update_icons(); |
148 | |
149 | bool update_shortcuts_tab = |
150 | EditorSettings::get_singleton()->check_changed_settings_in_group("shortcuts" ) || |
151 | EditorSettings::get_singleton()->check_changed_settings_in_group("builtin_action_overrides" ); |
152 | if (update_shortcuts_tab) { |
153 | _update_shortcuts(); |
154 | } |
155 | |
156 | if (EditorSettings::get_singleton()->check_changed_settings_in_group("interface/editor/localize_settings" )) { |
157 | inspector->update_category_list(); |
158 | } |
159 | } break; |
160 | } |
161 | } |
162 | |
163 | void EditorSettingsDialog::shortcut_input(const Ref<InputEvent> &p_event) { |
164 | ERR_FAIL_COND(p_event.is_null()); |
165 | EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton(); |
166 | |
167 | const Ref<InputEventKey> k = p_event; |
168 | if (k.is_valid() && k->is_pressed()) { |
169 | bool handled = false; |
170 | |
171 | if (ED_IS_SHORTCUT("ui_undo" , p_event)) { |
172 | String action = undo_redo->get_current_action_name(); |
173 | if (!action.is_empty()) { |
174 | EditorNode::get_log()->add_message(vformat(TTR("Undo: %s" ), action), EditorLog::MSG_TYPE_EDITOR); |
175 | } |
176 | undo_redo->undo(); |
177 | handled = true; |
178 | } |
179 | |
180 | if (ED_IS_SHORTCUT("ui_redo" , p_event)) { |
181 | undo_redo->redo(); |
182 | String action = undo_redo->get_current_action_name(); |
183 | if (!action.is_empty()) { |
184 | EditorNode::get_log()->add_message(vformat(TTR("Redo: %s" ), action), EditorLog::MSG_TYPE_EDITOR); |
185 | } |
186 | handled = true; |
187 | } |
188 | |
189 | if (k->is_match(InputEventKey::create_reference(KeyModifierMask::CMD_OR_CTRL | Key::F))) { |
190 | _focus_current_search_box(); |
191 | handled = true; |
192 | } |
193 | |
194 | if (handled) { |
195 | set_input_as_handled(); |
196 | } |
197 | } |
198 | } |
199 | |
200 | void EditorSettingsDialog::_update_icons() { |
201 | search_box->set_right_icon(shortcuts->get_editor_theme_icon(SNAME("Search" ))); |
202 | search_box->set_clear_button_enabled(true); |
203 | shortcut_search_box->set_right_icon(shortcuts->get_editor_theme_icon(SNAME("Search" ))); |
204 | shortcut_search_box->set_clear_button_enabled(true); |
205 | |
206 | restart_close_button->set_icon(shortcuts->get_editor_theme_icon(SNAME("Close" ))); |
207 | restart_container->add_theme_style_override("panel" , shortcuts->get_theme_stylebox(SNAME("panel" ), SNAME("Tree" ))); |
208 | restart_icon->set_texture(shortcuts->get_editor_theme_icon(SNAME("StatusWarning" ))); |
209 | restart_label->add_theme_color_override("font_color" , shortcuts->get_theme_color(SNAME("warning_color" ), EditorStringName(Editor))); |
210 | } |
211 | |
212 | void EditorSettingsDialog::_event_config_confirmed() { |
213 | Ref<InputEventKey> k = shortcut_editor->get_event(); |
214 | if (k.is_null()) { |
215 | return; |
216 | } |
217 | |
218 | if (current_event_index == -1) { |
219 | // Add new event |
220 | current_events.push_back(k); |
221 | } else { |
222 | // Edit existing event |
223 | current_events[current_event_index] = k; |
224 | } |
225 | |
226 | if (is_editing_action) { |
227 | _update_builtin_action(current_edited_identifier, current_events); |
228 | } else { |
229 | _update_shortcut_events(current_edited_identifier, current_events); |
230 | } |
231 | } |
232 | |
233 | void EditorSettingsDialog::_update_builtin_action(const String &p_name, const Array &p_events) { |
234 | Array old_input_array = EditorSettings::get_singleton()->get_builtin_action_overrides(p_name); |
235 | |
236 | EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton(); |
237 | undo_redo->create_action(vformat(TTR("Edit Built-in Action: %s" ), p_name)); |
238 | undo_redo->add_do_method(EditorSettings::get_singleton(), "mark_setting_changed" , "builtin_action_overrides" ); |
239 | undo_redo->add_undo_method(EditorSettings::get_singleton(), "mark_setting_changed" , "builtin_action_overrides" ); |
240 | undo_redo->add_do_method(EditorSettings::get_singleton(), "set_builtin_action_override" , p_name, p_events); |
241 | undo_redo->add_undo_method(EditorSettings::get_singleton(), "set_builtin_action_override" , p_name, old_input_array); |
242 | undo_redo->add_do_method(this, "_settings_changed" ); |
243 | undo_redo->add_undo_method(this, "_settings_changed" ); |
244 | undo_redo->commit_action(); |
245 | |
246 | _update_shortcuts(); |
247 | } |
248 | |
249 | void EditorSettingsDialog::_update_shortcut_events(const String &p_path, const Array &p_events) { |
250 | Ref<Shortcut> current_sc = EditorSettings::get_singleton()->get_shortcut(p_path); |
251 | |
252 | EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton(); |
253 | undo_redo->create_action(vformat(TTR("Edit Shortcut: %s" ), p_path)); |
254 | undo_redo->add_do_method(current_sc.ptr(), "set_events" , p_events); |
255 | undo_redo->add_undo_method(current_sc.ptr(), "set_events" , current_sc->get_events()); |
256 | undo_redo->add_do_method(EditorSettings::get_singleton(), "mark_setting_changed" , "shortcuts" ); |
257 | undo_redo->add_undo_method(EditorSettings::get_singleton(), "mark_setting_changed" , "shortcuts" ); |
258 | undo_redo->add_do_method(this, "_update_shortcuts" ); |
259 | undo_redo->add_undo_method(this, "_update_shortcuts" ); |
260 | undo_redo->add_do_method(this, "_settings_changed" ); |
261 | undo_redo->add_undo_method(this, "_settings_changed" ); |
262 | undo_redo->commit_action(); |
263 | } |
264 | |
265 | Array EditorSettingsDialog::_event_list_to_array_helper(const List<Ref<InputEvent>> &p_events) { |
266 | Array events; |
267 | |
268 | // Convert the list to an array, and only keep key events as this is for the editor. |
269 | for (const List<Ref<InputEvent>>::Element *E = p_events.front(); E; E = E->next()) { |
270 | Ref<InputEventKey> k = E->get(); |
271 | if (k.is_valid()) { |
272 | events.append(E->get()); |
273 | } |
274 | } |
275 | |
276 | return events; |
277 | } |
278 | |
279 | void EditorSettingsDialog::_create_shortcut_treeitem(TreeItem *p_parent, const String &p_shortcut_identifier, const String &p_display, Array &p_events, bool p_allow_revert, bool p_is_action, bool p_is_collapsed) { |
280 | TreeItem *shortcut_item = shortcuts->create_item(p_parent); |
281 | shortcut_item->set_collapsed(p_is_collapsed); |
282 | shortcut_item->set_text(0, p_display); |
283 | |
284 | Ref<InputEvent> primary = p_events.size() > 0 ? Ref<InputEvent>(p_events[0]) : Ref<InputEvent>(); |
285 | Ref<InputEvent> secondary = p_events.size() > 1 ? Ref<InputEvent>(p_events[1]) : Ref<InputEvent>(); |
286 | |
287 | String sc_text = "None" ; |
288 | if (primary.is_valid()) { |
289 | sc_text = primary->as_text(); |
290 | |
291 | if (secondary.is_valid()) { |
292 | sc_text += ", " + secondary->as_text(); |
293 | |
294 | if (p_events.size() > 2) { |
295 | sc_text += " (+" + itos(p_events.size() - 2) + ")" ; |
296 | } |
297 | } |
298 | } |
299 | |
300 | shortcut_item->set_text(1, sc_text); |
301 | if (sc_text == "None" ) { |
302 | // Fade out unassigned shortcut labels for easier visual grepping. |
303 | shortcut_item->set_custom_color(1, shortcuts->get_theme_color(SNAME("font_color" ), SNAME("Label" )) * Color(1, 1, 1, 0.5)); |
304 | } |
305 | |
306 | if (p_allow_revert) { |
307 | shortcut_item->add_button(1, shortcuts->get_editor_theme_icon(SNAME("Reload" )), SHORTCUT_REVERT); |
308 | } |
309 | |
310 | shortcut_item->add_button(1, shortcuts->get_editor_theme_icon(SNAME("Add" )), SHORTCUT_ADD); |
311 | shortcut_item->add_button(1, shortcuts->get_editor_theme_icon(SNAME("Close" )), SHORTCUT_ERASE); |
312 | |
313 | shortcut_item->set_meta("is_action" , p_is_action); |
314 | shortcut_item->set_meta("type" , "shortcut" ); |
315 | shortcut_item->set_meta("shortcut_identifier" , p_shortcut_identifier); |
316 | shortcut_item->set_meta("events" , p_events); |
317 | |
318 | // Shortcut Input Events |
319 | for (int i = 0; i < p_events.size(); i++) { |
320 | Ref<InputEvent> ie = p_events[i]; |
321 | if (ie.is_null()) { |
322 | continue; |
323 | } |
324 | |
325 | TreeItem *event_item = shortcuts->create_item(shortcut_item); |
326 | |
327 | event_item->set_text(0, shortcut_item->get_child_count() == 1 ? "Primary" : "" ); |
328 | event_item->set_text(1, ie->as_text()); |
329 | |
330 | event_item->add_button(1, shortcuts->get_editor_theme_icon(SNAME("Edit" )), SHORTCUT_EDIT); |
331 | event_item->add_button(1, shortcuts->get_editor_theme_icon(SNAME("Close" )), SHORTCUT_ERASE); |
332 | |
333 | event_item->set_custom_bg_color(0, shortcuts->get_theme_color(SNAME("dark_color_3" ), EditorStringName(Editor))); |
334 | event_item->set_custom_bg_color(1, shortcuts->get_theme_color(SNAME("dark_color_3" ), EditorStringName(Editor))); |
335 | |
336 | event_item->set_meta("is_action" , p_is_action); |
337 | event_item->set_meta("type" , "event" ); |
338 | event_item->set_meta("event_index" , i); |
339 | } |
340 | } |
341 | |
342 | bool EditorSettingsDialog::_should_display_shortcut(const String &p_name, const Array &p_events) const { |
343 | const Ref<InputEvent> search_ev = shortcut_search_by_event->get_event(); |
344 | bool event_match = true; |
345 | if (search_ev.is_valid()) { |
346 | event_match = false; |
347 | for (int i = 0; i < p_events.size(); ++i) { |
348 | const Ref<InputEvent> ev = p_events[i]; |
349 | if (ev.is_valid() && ev->is_match(search_ev, true)) { |
350 | event_match = true; |
351 | } |
352 | } |
353 | } |
354 | |
355 | return event_match && shortcut_search_box->get_text().is_subsequence_ofn(p_name); |
356 | } |
357 | |
358 | void EditorSettingsDialog::_update_shortcuts() { |
359 | // Before clearing the tree, take note of which categories are collapsed so that this state can be maintained when the tree is repopulated. |
360 | HashMap<String, bool> collapsed; |
361 | |
362 | if (shortcuts->get_root() && shortcuts->get_root()->get_first_child()) { |
363 | TreeItem *ti = shortcuts->get_root()->get_first_child(); |
364 | while (ti) { |
365 | // Not all items have valid or unique text in the first column - so if it has an identifier, use that, as it should be unique. |
366 | if (ti->get_first_child() && ti->has_meta("shortcut_identifier" )) { |
367 | collapsed[ti->get_meta("shortcut_identifier" )] = ti->is_collapsed(); |
368 | } else { |
369 | collapsed[ti->get_text(0)] = ti->is_collapsed(); |
370 | } |
371 | |
372 | // Try go down tree |
373 | TreeItem *ti_next = ti->get_first_child(); |
374 | // Try go to the next node via in-order traversal |
375 | if (!ti_next) { |
376 | ti_next = ti; |
377 | while (ti_next && !ti_next->get_next()) { |
378 | ti_next = ti_next->get_parent(); |
379 | } |
380 | if (ti_next) { |
381 | ti_next = ti_next->get_next(); |
382 | } |
383 | } |
384 | |
385 | ti = ti_next; |
386 | } |
387 | } |
388 | |
389 | shortcuts->clear(); |
390 | |
391 | TreeItem *root = shortcuts->create_item(); |
392 | HashMap<String, TreeItem *> sections; |
393 | |
394 | // Set up section for Common/Built-in actions |
395 | TreeItem *common_section = shortcuts->create_item(root); |
396 | sections["Common" ] = common_section; |
397 | common_section->set_text(0, TTR("Common" )); |
398 | common_section->set_selectable(0, false); |
399 | common_section->set_selectable(1, false); |
400 | if (collapsed.has("Common" )) { |
401 | common_section->set_collapsed(collapsed["Common" ]); |
402 | } |
403 | common_section->set_custom_bg_color(0, shortcuts->get_theme_color(SNAME("prop_subsection" ), EditorStringName(Editor))); |
404 | common_section->set_custom_bg_color(1, shortcuts->get_theme_color(SNAME("prop_subsection" ), EditorStringName(Editor))); |
405 | |
406 | // Get the action map for the editor, and add each item to the "Common" section. |
407 | for (const KeyValue<StringName, InputMap::Action> &E : InputMap::get_singleton()->get_action_map()) { |
408 | const String &action_name = E.key; |
409 | const InputMap::Action &action = E.value; |
410 | |
411 | // Skip non-builtin actions. |
412 | if (!InputMap::get_singleton()->get_builtins_with_feature_overrides_applied().has(action_name)) { |
413 | continue; |
414 | } |
415 | |
416 | const List<Ref<InputEvent>> &all_default_events = InputMap::get_singleton()->get_builtins_with_feature_overrides_applied().find(action_name)->value; |
417 | Array action_events = _event_list_to_array_helper(action.inputs); |
418 | if (!_should_display_shortcut(action_name, action_events)) { |
419 | continue; |
420 | } |
421 | |
422 | Array default_events = _event_list_to_array_helper(all_default_events); |
423 | bool same_as_defaults = Shortcut::is_event_array_equal(default_events, action_events); |
424 | bool collapse = !collapsed.has(action_name) || (collapsed.has(action_name) && collapsed[action_name]); |
425 | |
426 | _create_shortcut_treeitem(common_section, action_name, action_name, action_events, !same_as_defaults, true, collapse); |
427 | } |
428 | |
429 | // Editor Shortcuts |
430 | |
431 | List<String> slist; |
432 | EditorSettings::get_singleton()->get_shortcut_list(&slist); |
433 | slist.sort(); // Sort alphabetically. |
434 | |
435 | const EditorPropertyNameProcessor::Style name_style = EditorPropertyNameProcessor::get_settings_style(); |
436 | const EditorPropertyNameProcessor::Style tooltip_style = EditorPropertyNameProcessor::get_tooltip_style(name_style); |
437 | |
438 | // Create all sections first. |
439 | for (const String &E : slist) { |
440 | Ref<Shortcut> sc = EditorSettings::get_singleton()->get_shortcut(E); |
441 | String section_name = E.get_slice("/" , 0); |
442 | |
443 | if (sections.has(section_name)) { |
444 | continue; |
445 | } |
446 | |
447 | TreeItem *section = shortcuts->create_item(root); |
448 | |
449 | const String item_name = EditorPropertyNameProcessor::get_singleton()->process_name(section_name, name_style); |
450 | const String tooltip = EditorPropertyNameProcessor::get_singleton()->process_name(section_name, tooltip_style); |
451 | |
452 | section->set_text(0, item_name); |
453 | section->set_tooltip_text(0, tooltip); |
454 | section->set_selectable(0, false); |
455 | section->set_selectable(1, false); |
456 | section->set_custom_bg_color(0, shortcuts->get_theme_color(SNAME("prop_subsection" ), EditorStringName(Editor))); |
457 | section->set_custom_bg_color(1, shortcuts->get_theme_color(SNAME("prop_subsection" ), EditorStringName(Editor))); |
458 | |
459 | if (collapsed.has(item_name)) { |
460 | section->set_collapsed(collapsed[item_name]); |
461 | } |
462 | |
463 | sections[section_name] = section; |
464 | } |
465 | |
466 | // Add shortcuts to sections. |
467 | for (const String &E : slist) { |
468 | Ref<Shortcut> sc = EditorSettings::get_singleton()->get_shortcut(E); |
469 | if (!sc->has_meta("original" )) { |
470 | continue; |
471 | } |
472 | |
473 | String section_name = E.get_slice("/" , 0); |
474 | TreeItem *section = sections[section_name]; |
475 | |
476 | if (!_should_display_shortcut(sc->get_name(), sc->get_events())) { |
477 | continue; |
478 | } |
479 | |
480 | Array original = sc->get_meta("original" ); |
481 | Array shortcuts_array = sc->get_events().duplicate(true); |
482 | bool same_as_defaults = Shortcut::is_event_array_equal(original, shortcuts_array); |
483 | bool collapse = !collapsed.has(E) || (collapsed.has(E) && collapsed[E]); |
484 | |
485 | _create_shortcut_treeitem(section, E, sc->get_name(), shortcuts_array, !same_as_defaults, false, collapse); |
486 | } |
487 | |
488 | // remove sections with no shortcuts |
489 | for (KeyValue<String, TreeItem *> &E : sections) { |
490 | TreeItem *section = E.value; |
491 | if (section->get_first_child() == nullptr) { |
492 | root->remove_child(section); |
493 | memdelete(section); |
494 | } |
495 | } |
496 | } |
497 | |
498 | void EditorSettingsDialog::_shortcut_button_pressed(Object *p_item, int p_column, int p_idx, MouseButton p_button) { |
499 | if (p_button != MouseButton::LEFT) { |
500 | return; |
501 | } |
502 | TreeItem *ti = Object::cast_to<TreeItem>(p_item); |
503 | ERR_FAIL_NULL_MSG(ti, "Object passed is not a TreeItem." ); |
504 | |
505 | ShortcutButton button_idx = (ShortcutButton)p_idx; |
506 | |
507 | is_editing_action = ti->get_meta("is_action" ); |
508 | |
509 | String type = ti->get_meta("type" ); |
510 | |
511 | if (type == "event" ) { |
512 | current_edited_identifier = ti->get_parent()->get_meta("shortcut_identifier" ); |
513 | current_events = ti->get_parent()->get_meta("events" ); |
514 | current_event_index = ti->get_meta("event_index" ); |
515 | } else { // Type is "shortcut" |
516 | current_edited_identifier = ti->get_meta("shortcut_identifier" ); |
517 | current_events = ti->get_meta("events" ); |
518 | current_event_index = -1; |
519 | } |
520 | |
521 | switch (button_idx) { |
522 | case EditorSettingsDialog::SHORTCUT_ADD: { |
523 | // Only for "shortcut" types |
524 | shortcut_editor->popup_and_configure(); |
525 | } break; |
526 | case EditorSettingsDialog::SHORTCUT_EDIT: { |
527 | // Only for "event" types |
528 | shortcut_editor->popup_and_configure(current_events[current_event_index]); |
529 | } break; |
530 | case EditorSettingsDialog::SHORTCUT_ERASE: { |
531 | if (type == "shortcut" ) { |
532 | if (is_editing_action) { |
533 | _update_builtin_action(current_edited_identifier, Array()); |
534 | } else { |
535 | _update_shortcut_events(current_edited_identifier, Array()); |
536 | } |
537 | } else if (type == "event" ) { |
538 | current_events.remove_at(current_event_index); |
539 | |
540 | if (is_editing_action) { |
541 | _update_builtin_action(current_edited_identifier, current_events); |
542 | } else { |
543 | _update_shortcut_events(current_edited_identifier, current_events); |
544 | } |
545 | } |
546 | } break; |
547 | case EditorSettingsDialog::SHORTCUT_REVERT: { |
548 | // Only for "shortcut" types |
549 | if (is_editing_action) { |
550 | List<Ref<InputEvent>> defaults = InputMap::get_singleton()->get_builtins_with_feature_overrides_applied()[current_edited_identifier]; |
551 | Array events = _event_list_to_array_helper(defaults); |
552 | |
553 | _update_builtin_action(current_edited_identifier, events); |
554 | } else { |
555 | Ref<Shortcut> sc = EditorSettings::get_singleton()->get_shortcut(current_edited_identifier); |
556 | Array original = sc->get_meta("original" ); |
557 | _update_shortcut_events(current_edited_identifier, original); |
558 | } |
559 | } break; |
560 | default: |
561 | break; |
562 | } |
563 | } |
564 | |
565 | void EditorSettingsDialog::_shortcut_cell_double_clicked() { |
566 | // When a shortcut cell is double clicked: |
567 | // If the cell has children and is in the bindings column, and if its first child is editable, |
568 | // then uncollapse the cell, and if the first child is the only child, then edit that child. |
569 | // If the cell is in the bindings column and can be edited, then edit it. |
570 | // If the cell is in the name column, then toggle collapse. |
571 | const ShortcutButton edit_btn_id = EditorSettingsDialog::SHORTCUT_EDIT; |
572 | const int edit_btn_col = 1; |
573 | TreeItem *ti = shortcuts->get_selected(); |
574 | if (ti == nullptr) { |
575 | return; |
576 | } |
577 | |
578 | String type = ti->get_meta("type" ); |
579 | int col = shortcuts->get_selected_column(); |
580 | if (type == "shortcut" && col == 0) { |
581 | if (ti->get_first_child()) { |
582 | ti->set_collapsed(!ti->is_collapsed()); |
583 | } |
584 | } else if (type == "shortcut" && col == 1) { |
585 | if (ti->get_first_child()) { |
586 | TreeItem *child_ti = ti->get_first_child(); |
587 | if (child_ti->get_button_by_id(edit_btn_col, edit_btn_id) != -1) { |
588 | ti->set_collapsed(false); |
589 | if (ti->get_child_count() == 1) { |
590 | _shortcut_button_pressed(child_ti, edit_btn_col, edit_btn_id); |
591 | } |
592 | } |
593 | } |
594 | } else if (type == "event" && col == 1) { |
595 | if (ti->get_button_by_id(edit_btn_col, edit_btn_id) != -1) { |
596 | _shortcut_button_pressed(ti, edit_btn_col, edit_btn_id); |
597 | } |
598 | } |
599 | } |
600 | |
601 | Variant EditorSettingsDialog::get_drag_data_fw(const Point2 &p_point, Control *p_from) { |
602 | TreeItem *selected = shortcuts->get_selected(); |
603 | |
604 | // Only allow drag for events |
605 | if (!selected || (String)selected->get_meta("type" , "" ) != "event" ) { |
606 | return Variant(); |
607 | } |
608 | |
609 | String label_text = "Event " + itos(selected->get_meta("event_index" )); |
610 | Label *label = memnew(Label(label_text)); |
611 | label->set_modulate(Color(1, 1, 1, 1.0f)); |
612 | shortcuts->set_drag_preview(label); |
613 | |
614 | shortcuts->set_drop_mode_flags(Tree::DROP_MODE_INBETWEEN); |
615 | |
616 | return Dictionary(); // No data required |
617 | } |
618 | |
619 | bool EditorSettingsDialog::can_drop_data_fw(const Point2 &p_point, const Variant &p_data, Control *p_from) const { |
620 | TreeItem *selected = shortcuts->get_selected(); |
621 | TreeItem *item = shortcuts->get_item_at_position(p_point); |
622 | if (!selected || !item || item == selected || (String)item->get_meta("type" , "" ) != "event" ) { |
623 | return false; |
624 | } |
625 | |
626 | // Don't allow moving an events in-between shortcuts. |
627 | if (selected->get_parent()->get_meta("shortcut_identifier" ) != item->get_parent()->get_meta("shortcut_identifier" )) { |
628 | return false; |
629 | } |
630 | |
631 | return true; |
632 | } |
633 | |
634 | void EditorSettingsDialog::drop_data_fw(const Point2 &p_point, const Variant &p_data, Control *p_from) { |
635 | if (!can_drop_data_fw(p_point, p_data, p_from)) { |
636 | return; |
637 | } |
638 | |
639 | TreeItem *selected = shortcuts->get_selected(); |
640 | TreeItem *target = shortcuts->get_item_at_position(p_point); |
641 | |
642 | if (!target) { |
643 | return; |
644 | } |
645 | |
646 | int target_event_index = target->get_meta("event_index" ); |
647 | int index_moving_from = selected->get_meta("event_index" ); |
648 | |
649 | Array events = selected->get_parent()->get_meta("events" ); |
650 | |
651 | Variant event_moved = events[index_moving_from]; |
652 | events.remove_at(index_moving_from); |
653 | events.insert(target_event_index, event_moved); |
654 | |
655 | String ident = selected->get_parent()->get_meta("shortcut_identifier" ); |
656 | if (selected->get_meta("is_action" )) { |
657 | _update_builtin_action(ident, events); |
658 | } else { |
659 | _update_shortcut_events(ident, events); |
660 | } |
661 | } |
662 | |
663 | void EditorSettingsDialog::_tabs_tab_changed(int p_tab) { |
664 | _focus_current_search_box(); |
665 | } |
666 | |
667 | void EditorSettingsDialog::_focus_current_search_box() { |
668 | Control *tab = tabs->get_current_tab_control(); |
669 | LineEdit *current_search_box = nullptr; |
670 | if (tab == tab_general) { |
671 | current_search_box = search_box; |
672 | } else if (tab == tab_shortcuts) { |
673 | current_search_box = shortcut_search_box; |
674 | } |
675 | |
676 | if (current_search_box) { |
677 | current_search_box->grab_focus(); |
678 | current_search_box->select_all(); |
679 | } |
680 | } |
681 | |
682 | void EditorSettingsDialog::_editor_restart() { |
683 | EditorNode::get_singleton()->save_all_scenes(); |
684 | EditorNode::get_singleton()->restart_editor(); |
685 | } |
686 | |
687 | void EditorSettingsDialog::_editor_restart_request() { |
688 | restart_container->show(); |
689 | } |
690 | |
691 | void EditorSettingsDialog::_editor_restart_close() { |
692 | restart_container->hide(); |
693 | } |
694 | |
695 | void EditorSettingsDialog::_bind_methods() { |
696 | ClassDB::bind_method(D_METHOD("_update_shortcuts" ), &EditorSettingsDialog::_update_shortcuts); |
697 | ClassDB::bind_method(D_METHOD("_settings_changed" ), &EditorSettingsDialog::_settings_changed); |
698 | } |
699 | |
700 | EditorSettingsDialog::EditorSettingsDialog() { |
701 | set_title(TTR("Editor Settings" )); |
702 | set_clamp_to_embedder(true); |
703 | |
704 | tabs = memnew(TabContainer); |
705 | tabs->set_theme_type_variation("TabContainerOdd" ); |
706 | tabs->connect("tab_changed" , callable_mp(this, &EditorSettingsDialog::_tabs_tab_changed)); |
707 | add_child(tabs); |
708 | |
709 | // General Tab |
710 | |
711 | tab_general = memnew(VBoxContainer); |
712 | tabs->add_child(tab_general); |
713 | tab_general->set_name(TTR("General" )); |
714 | |
715 | HBoxContainer *hbc = memnew(HBoxContainer); |
716 | hbc->set_h_size_flags(Control::SIZE_EXPAND_FILL); |
717 | tab_general->add_child(hbc); |
718 | |
719 | search_box = memnew(LineEdit); |
720 | search_box->set_placeholder(TTR("Filter Settings" )); |
721 | search_box->set_h_size_flags(Control::SIZE_EXPAND_FILL); |
722 | hbc->add_child(search_box); |
723 | |
724 | inspector = memnew(SectionedInspector); |
725 | inspector->get_inspector()->set_use_filter(true); |
726 | inspector->register_search_box(search_box); |
727 | inspector->set_v_size_flags(Control::SIZE_EXPAND_FILL); |
728 | tab_general->add_child(inspector); |
729 | inspector->get_inspector()->connect("property_edited" , callable_mp(this, &EditorSettingsDialog::_settings_property_edited)); |
730 | inspector->get_inspector()->connect("restart_requested" , callable_mp(this, &EditorSettingsDialog::_editor_restart_request)); |
731 | |
732 | restart_container = memnew(PanelContainer); |
733 | tab_general->add_child(restart_container); |
734 | HBoxContainer *restart_hb = memnew(HBoxContainer); |
735 | restart_container->add_child(restart_hb); |
736 | restart_icon = memnew(TextureRect); |
737 | restart_icon->set_v_size_flags(Control::SIZE_SHRINK_CENTER); |
738 | restart_hb->add_child(restart_icon); |
739 | restart_label = memnew(Label); |
740 | restart_label->set_text(TTR("The editor must be restarted for changes to take effect." )); |
741 | restart_hb->add_child(restart_label); |
742 | restart_hb->add_spacer(); |
743 | Button *restart_button = memnew(Button); |
744 | restart_button->connect("pressed" , callable_mp(this, &EditorSettingsDialog::_editor_restart)); |
745 | restart_hb->add_child(restart_button); |
746 | restart_button->set_text(TTR("Save & Restart" )); |
747 | restart_close_button = memnew(Button); |
748 | restart_close_button->set_flat(true); |
749 | restart_close_button->connect("pressed" , callable_mp(this, &EditorSettingsDialog::_editor_restart_close)); |
750 | restart_hb->add_child(restart_close_button); |
751 | restart_container->hide(); |
752 | |
753 | // Shortcuts Tab |
754 | |
755 | tab_shortcuts = memnew(VBoxContainer); |
756 | |
757 | tabs->add_child(tab_shortcuts); |
758 | tab_shortcuts->set_name(TTR("Shortcuts" )); |
759 | |
760 | HBoxContainer *top_hbox = memnew(HBoxContainer); |
761 | top_hbox->set_h_size_flags(Control::SIZE_EXPAND_FILL); |
762 | tab_shortcuts->add_child(top_hbox); |
763 | |
764 | shortcut_search_box = memnew(LineEdit); |
765 | shortcut_search_box->set_placeholder(TTR("Filter by name..." )); |
766 | shortcut_search_box->set_h_size_flags(Control::SIZE_EXPAND_FILL); |
767 | top_hbox->add_child(shortcut_search_box); |
768 | shortcut_search_box->connect("text_changed" , callable_mp(this, &EditorSettingsDialog::_filter_shortcuts)); |
769 | |
770 | shortcut_search_by_event = memnew(EventListenerLineEdit); |
771 | shortcut_search_by_event->set_h_size_flags(Control::SIZE_EXPAND_FILL); |
772 | shortcut_search_by_event->set_stretch_ratio(0.75); |
773 | shortcut_search_by_event->set_allowed_input_types(INPUT_KEY); |
774 | shortcut_search_by_event->connect("event_changed" , callable_mp(this, &EditorSettingsDialog::_filter_shortcuts_by_event)); |
775 | top_hbox->add_child(shortcut_search_by_event); |
776 | |
777 | Button *clear_all_search = memnew(Button); |
778 | clear_all_search->set_text(TTR("Clear All" )); |
779 | clear_all_search->connect("pressed" , callable_mp(shortcut_search_box, &LineEdit::clear)); |
780 | clear_all_search->connect("pressed" , callable_mp(shortcut_search_by_event, &EventListenerLineEdit::clear_event)); |
781 | top_hbox->add_child(clear_all_search); |
782 | |
783 | shortcuts = memnew(Tree); |
784 | shortcuts->set_v_size_flags(Control::SIZE_EXPAND_FILL); |
785 | shortcuts->set_columns(2); |
786 | shortcuts->set_hide_root(true); |
787 | shortcuts->set_column_titles_visible(true); |
788 | shortcuts->set_column_title(0, TTR("Name" )); |
789 | shortcuts->set_column_title(1, TTR("Binding" )); |
790 | shortcuts->connect("button_clicked" , callable_mp(this, &EditorSettingsDialog::_shortcut_button_pressed)); |
791 | shortcuts->connect("item_activated" , callable_mp(this, &EditorSettingsDialog::_shortcut_cell_double_clicked)); |
792 | tab_shortcuts->add_child(shortcuts); |
793 | |
794 | SET_DRAG_FORWARDING_GCD(shortcuts, EditorSettingsDialog); |
795 | |
796 | // Adding event dialog |
797 | shortcut_editor = memnew(InputEventConfigurationDialog); |
798 | shortcut_editor->connect("confirmed" , callable_mp(this, &EditorSettingsDialog::_event_config_confirmed)); |
799 | shortcut_editor->set_allowed_input_types(INPUT_KEY); |
800 | add_child(shortcut_editor); |
801 | |
802 | set_hide_on_ok(true); |
803 | |
804 | timer = memnew(Timer); |
805 | timer->set_wait_time(1.5); |
806 | timer->connect("timeout" , callable_mp(this, &EditorSettingsDialog::_settings_save)); |
807 | timer->set_one_shot(true); |
808 | add_child(timer); |
809 | EditorSettings::get_singleton()->connect("settings_changed" , callable_mp(this, &EditorSettingsDialog::_settings_changed)); |
810 | set_ok_button_text(TTR("Close" )); |
811 | } |
812 | |
813 | EditorSettingsDialog::~EditorSettingsDialog() { |
814 | } |
815 | |