summaryrefslogtreecommitdiffstats
path: root/editor/gui
diff options
context:
space:
mode:
Diffstat (limited to 'editor/gui')
-rw-r--r--editor/gui/SCsub5
-rw-r--r--editor/gui/editor_dir_dialog.cpp234
-rw-r--r--editor/gui/editor_dir_dialog.h78
-rw-r--r--editor/gui/editor_file_dialog.cpp1981
-rw-r--r--editor/gui/editor_file_dialog.h293
-rw-r--r--editor/gui/editor_object_selector.cpp256
-rw-r--r--editor/gui/editor_object_selector.h73
-rw-r--r--editor/gui/editor_spin_slider.cpp699
-rw-r--r--editor/gui/editor_spin_slider.h122
-rw-r--r--editor/gui/editor_title_bar.cpp86
-rw-r--r--editor/gui/editor_title_bar.h53
-rw-r--r--editor/gui/editor_toaster.cpp576
-rw-r--r--editor/gui/editor_toaster.h123
-rw-r--r--editor/gui/editor_zoom_widget.cpp209
-rw-r--r--editor/gui/editor_zoom_widget.h64
-rw-r--r--editor/gui/scene_tree_editor.cpp1545
-rw-r--r--editor/gui/scene_tree_editor.h197
17 files changed, 6594 insertions, 0 deletions
diff --git a/editor/gui/SCsub b/editor/gui/SCsub
new file mode 100644
index 0000000000..359d04e5df
--- /dev/null
+++ b/editor/gui/SCsub
@@ -0,0 +1,5 @@
+#!/usr/bin/env python
+
+Import("env")
+
+env.add_source_files(env.editor_sources, "*.cpp")
diff --git a/editor/gui/editor_dir_dialog.cpp b/editor/gui/editor_dir_dialog.cpp
new file mode 100644
index 0000000000..9da592d639
--- /dev/null
+++ b/editor/gui/editor_dir_dialog.cpp
@@ -0,0 +1,234 @@
+/**************************************************************************/
+/* editor_dir_dialog.cpp */
+/**************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/**************************************************************************/
+
+#include "editor_dir_dialog.h"
+
+#include "core/io/dir_access.h"
+#include "core/os/keyboard.h"
+#include "core/os/os.h"
+#include "editor/editor_file_system.h"
+#include "editor/editor_scale.h"
+#include "editor/editor_settings.h"
+#include "scene/gui/check_box.h"
+#include "scene/gui/tree.h"
+#include "servers/display_server.h"
+
+void EditorDirDialog::_update_dir(TreeItem *p_item, EditorFileSystemDirectory *p_dir, const String &p_select_path) {
+ updating = true;
+
+ String path = p_dir->get_path();
+
+ p_item->set_metadata(0, p_dir->get_path());
+ p_item->set_icon(0, tree->get_theme_icon(SNAME("Folder"), SNAME("EditorIcons")));
+ p_item->set_icon_modulate(0, tree->get_theme_color(SNAME("folder_icon_color"), SNAME("FileDialog")));
+
+ if (!p_item->get_parent()) {
+ p_item->set_text(0, "res://");
+ } else {
+ if (!opened_paths.has(path) && (p_select_path.is_empty() || !p_select_path.begins_with(path))) {
+ p_item->set_collapsed(true);
+ }
+
+ p_item->set_text(0, p_dir->get_name());
+ }
+
+ updating = false;
+ for (int i = 0; i < p_dir->get_subdir_count(); i++) {
+ TreeItem *ti = tree->create_item(p_item);
+ _update_dir(ti, p_dir->get_subdir(i));
+ }
+}
+
+void EditorDirDialog::reload(const String &p_path) {
+ if (!is_visible()) {
+ must_reload = true;
+ return;
+ }
+
+ tree->clear();
+ TreeItem *root = tree->create_item();
+ _update_dir(root, EditorFileSystem::get_singleton()->get_filesystem(), p_path);
+ _item_collapsed(root);
+ must_reload = false;
+}
+
+bool EditorDirDialog::is_copy_pressed() const {
+ return copy->is_pressed();
+}
+
+void EditorDirDialog::_notification(int p_what) {
+ switch (p_what) {
+ case NOTIFICATION_ENTER_TREE: {
+ EditorFileSystem::get_singleton()->connect("filesystem_changed", callable_mp(this, &EditorDirDialog::reload).bind(""));
+ reload();
+
+ if (!tree->is_connected("item_collapsed", callable_mp(this, &EditorDirDialog::_item_collapsed))) {
+ tree->connect("item_collapsed", callable_mp(this, &EditorDirDialog::_item_collapsed), CONNECT_DEFERRED);
+ }
+
+ if (!EditorFileSystem::get_singleton()->is_connected("filesystem_changed", callable_mp(this, &EditorDirDialog::reload))) {
+ EditorFileSystem::get_singleton()->connect("filesystem_changed", callable_mp(this, &EditorDirDialog::reload).bind(""));
+ }
+ } break;
+
+ case NOTIFICATION_EXIT_TREE: {
+ if (EditorFileSystem::get_singleton()->is_connected("filesystem_changed", callable_mp(this, &EditorDirDialog::reload))) {
+ EditorFileSystem::get_singleton()->disconnect("filesystem_changed", callable_mp(this, &EditorDirDialog::reload));
+ }
+ } break;
+
+ case NOTIFICATION_VISIBILITY_CHANGED: {
+ if (must_reload && is_visible()) {
+ reload();
+ }
+ } break;
+ }
+}
+
+void EditorDirDialog::_copy_toggled(bool p_pressed) {
+ if (p_pressed) {
+ set_ok_button_text(TTR("Copy"));
+ } else {
+ set_ok_button_text(TTR("Move"));
+ }
+}
+
+void EditorDirDialog::_item_collapsed(Object *p_item) {
+ TreeItem *item = Object::cast_to<TreeItem>(p_item);
+
+ if (updating) {
+ return;
+ }
+
+ if (item->is_collapsed()) {
+ opened_paths.erase(item->get_metadata(0));
+ } else {
+ opened_paths.insert(item->get_metadata(0));
+ }
+}
+
+void EditorDirDialog::_item_activated() {
+ _ok_pressed(); // From AcceptDialog.
+}
+
+void EditorDirDialog::ok_pressed() {
+ TreeItem *ti = tree->get_selected();
+ if (!ti) {
+ return;
+ }
+
+ String dir = ti->get_metadata(0);
+ emit_signal(SNAME("dir_selected"), dir);
+ hide();
+}
+
+void EditorDirDialog::_make_dir() {
+ TreeItem *ti = tree->get_selected();
+ if (!ti) {
+ mkdirerr->set_text(TTR("Please select a base directory first."));
+ mkdirerr->popup_centered();
+ return;
+ }
+
+ makedialog->popup_centered(Size2(250, 80));
+ makedirname->grab_focus();
+}
+
+void EditorDirDialog::_make_dir_confirm() {
+ TreeItem *ti = tree->get_selected();
+ if (!ti) {
+ return;
+ }
+
+ String dir = ti->get_metadata(0);
+
+ Ref<DirAccess> d = DirAccess::open(dir);
+ ERR_FAIL_COND_MSG(d.is_null(), "Cannot open directory '" + dir + "'.");
+
+ const String stripped_dirname = makedirname->get_text().strip_edges();
+
+ if (d->dir_exists(stripped_dirname)) {
+ mkdirerr->set_text(TTR("Could not create folder. File with that name already exists."));
+ mkdirerr->popup_centered();
+ return;
+ }
+
+ Error err = d->make_dir(stripped_dirname);
+ if (err != OK) {
+ mkdirerr->popup_centered(Size2(250, 80) * EDSCALE);
+ } else {
+ opened_paths.insert(dir);
+ EditorFileSystem::get_singleton()->scan_changes(); // We created a dir, so rescan changes.
+ }
+ makedirname->set_text(""); // reset label
+}
+
+void EditorDirDialog::_bind_methods() {
+ ADD_SIGNAL(MethodInfo("dir_selected", PropertyInfo(Variant::STRING, "dir")));
+}
+
+EditorDirDialog::EditorDirDialog() {
+ set_title(TTR("Choose a Directory"));
+ set_hide_on_ok(false);
+
+ VBoxContainer *vb = memnew(VBoxContainer);
+ add_child(vb);
+
+ tree = memnew(Tree);
+ vb->add_child(tree);
+ tree->set_v_size_flags(Control::SIZE_EXPAND_FILL);
+ tree->connect("item_activated", callable_mp(this, &EditorDirDialog::_item_activated));
+
+ copy = memnew(CheckBox);
+ vb->add_child(copy);
+ copy->set_text(TTR("Copy File(s)"));
+ copy->connect("toggled", callable_mp(this, &EditorDirDialog::_copy_toggled));
+
+ makedir = add_button(TTR("Create Folder"), DisplayServer::get_singleton()->get_swap_cancel_ok(), "makedir");
+ makedir->connect("pressed", callable_mp(this, &EditorDirDialog::_make_dir));
+
+ makedialog = memnew(ConfirmationDialog);
+ makedialog->set_title(TTR("Create Folder"));
+ add_child(makedialog);
+
+ VBoxContainer *makevb = memnew(VBoxContainer);
+ makedialog->add_child(makevb);
+
+ makedirname = memnew(LineEdit);
+ makevb->add_margin_child(TTR("Name:"), makedirname);
+ makedialog->register_text_enter(makedirname);
+ makedialog->connect("confirmed", callable_mp(this, &EditorDirDialog::_make_dir_confirm));
+
+ mkdirerr = memnew(AcceptDialog);
+ mkdirerr->set_text(TTR("Could not create folder."));
+ add_child(mkdirerr);
+
+ set_ok_button_text(TTR("Move"));
+}
diff --git a/editor/gui/editor_dir_dialog.h b/editor/gui/editor_dir_dialog.h
new file mode 100644
index 0000000000..9f2b48c164
--- /dev/null
+++ b/editor/gui/editor_dir_dialog.h
@@ -0,0 +1,78 @@
+/**************************************************************************/
+/* editor_dir_dialog.h */
+/**************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/**************************************************************************/
+
+#ifndef EDITOR_DIR_DIALOG_H
+#define EDITOR_DIR_DIALOG_H
+
+#include "scene/gui/dialogs.h"
+
+class CheckBox;
+class EditorFileSystemDirectory;
+class Tree;
+class TreeItem;
+
+class EditorDirDialog : public ConfirmationDialog {
+ GDCLASS(EditorDirDialog, ConfirmationDialog);
+
+ ConfirmationDialog *makedialog = nullptr;
+ LineEdit *makedirname = nullptr;
+ AcceptDialog *mkdirerr = nullptr;
+
+ Button *makedir = nullptr;
+ HashSet<String> opened_paths;
+
+ Tree *tree = nullptr;
+ bool updating = false;
+ CheckBox *copy = nullptr;
+
+ void _copy_toggled(bool p_pressed);
+ void _item_collapsed(Object *p_item);
+ void _item_activated();
+ void _update_dir(TreeItem *p_item, EditorFileSystemDirectory *p_dir, const String &p_select_path = String());
+
+ void _make_dir();
+ void _make_dir_confirm();
+
+ void ok_pressed() override;
+
+ bool must_reload = false;
+
+protected:
+ void _notification(int p_what);
+ static void _bind_methods();
+
+public:
+ void reload(const String &p_path = "");
+ bool is_copy_pressed() const;
+
+ EditorDirDialog();
+};
+
+#endif // EDITOR_DIR_DIALOG_H
diff --git a/editor/gui/editor_file_dialog.cpp b/editor/gui/editor_file_dialog.cpp
new file mode 100644
index 0000000000..62e0520799
--- /dev/null
+++ b/editor/gui/editor_file_dialog.cpp
@@ -0,0 +1,1981 @@
+/**************************************************************************/
+/* editor_file_dialog.cpp */
+/**************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/**************************************************************************/
+
+#include "editor_file_dialog.h"
+
+#include "core/config/project_settings.h"
+#include "core/io/file_access.h"
+#include "core/os/keyboard.h"
+#include "core/os/os.h"
+#include "editor/dependency_editor.h"
+#include "editor/editor_file_system.h"
+#include "editor/editor_node.h"
+#include "editor/editor_resource_preview.h"
+#include "editor/editor_scale.h"
+#include "editor/editor_settings.h"
+#include "scene/gui/center_container.h"
+#include "scene/gui/label.h"
+#include "scene/gui/margin_container.h"
+#include "scene/gui/option_button.h"
+#include "scene/gui/separator.h"
+#include "scene/gui/split_container.h"
+#include "scene/gui/texture_rect.h"
+#include "servers/display_server.h"
+
+EditorFileDialog::GetIconFunc EditorFileDialog::get_icon_func = nullptr;
+EditorFileDialog::GetIconFunc EditorFileDialog::get_thumbnail_func = nullptr;
+
+EditorFileDialog::RegisterFunc EditorFileDialog::register_func = nullptr;
+EditorFileDialog::RegisterFunc EditorFileDialog::unregister_func = nullptr;
+
+void EditorFileDialog::popup_file_dialog() {
+ popup_centered_clamped(Size2(1050, 700) * EDSCALE, 0.8);
+ _focus_file_text();
+}
+
+void EditorFileDialog::_focus_file_text() {
+ int lp = file->get_text().rfind(".");
+ if (lp != -1) {
+ file->select(0, lp);
+ file->grab_focus();
+ }
+}
+
+VBoxContainer *EditorFileDialog::get_vbox() {
+ return vbox;
+}
+
+void EditorFileDialog::_update_theme_item_cache() {
+ ConfirmationDialog::_update_theme_item_cache();
+
+ theme_cache.parent_folder = get_theme_icon(SNAME("ArrowUp"), SNAME("EditorIcons"));
+ theme_cache.forward_folder = get_theme_icon(SNAME("Forward"), SNAME("EditorIcons"));
+ theme_cache.back_folder = get_theme_icon(SNAME("Back"), SNAME("EditorIcons"));
+ theme_cache.reload = get_theme_icon(SNAME("Reload"), SNAME("EditorIcons"));
+ theme_cache.toggle_hidden = get_theme_icon(SNAME("GuiVisibilityVisible"), SNAME("EditorIcons"));
+ theme_cache.favorite = get_theme_icon(SNAME("Favorites"), SNAME("EditorIcons"));
+ theme_cache.mode_thumbnails = get_theme_icon(SNAME("FileThumbnail"), SNAME("EditorIcons"));
+ theme_cache.mode_list = get_theme_icon(SNAME("FileList"), SNAME("EditorIcons"));
+ theme_cache.favorites_up = get_theme_icon(SNAME("MoveUp"), SNAME("EditorIcons"));
+ theme_cache.favorites_down = get_theme_icon(SNAME("MoveDown"), SNAME("EditorIcons"));
+
+ theme_cache.folder = get_theme_icon(SNAME("Folder"), SNAME("EditorIcons"));
+ theme_cache.folder_icon_color = get_theme_color(SNAME("folder_icon_color"), SNAME("FileDialog"));
+
+ theme_cache.action_copy = get_theme_icon(SNAME("ActionCopy"), SNAME("EditorIcons"));
+ theme_cache.action_delete = get_theme_icon(SNAME("Remove"), SNAME("EditorIcons"));
+ theme_cache.filesystem = get_theme_icon(SNAME("Filesystem"), SNAME("EditorIcons"));
+
+ theme_cache.folder_medium_thumbnail = get_theme_icon(SNAME("FolderMediumThumb"), SNAME("EditorIcons"));
+ theme_cache.file_medium_thumbnail = get_theme_icon(SNAME("FileMediumThumb"), SNAME("EditorIcons"));
+ theme_cache.folder_big_thumbnail = get_theme_icon(SNAME("FolderBigThumb"), SNAME("EditorIcons"));
+ theme_cache.file_big_thumbnail = get_theme_icon(SNAME("FileBigThumb"), SNAME("EditorIcons"));
+
+ theme_cache.progress[0] = get_theme_icon("Progress1", SNAME("EditorIcons"));
+ theme_cache.progress[1] = get_theme_icon("Progress2", SNAME("EditorIcons"));
+ theme_cache.progress[2] = get_theme_icon("Progress3", SNAME("EditorIcons"));
+ theme_cache.progress[3] = get_theme_icon("Progress4", SNAME("EditorIcons"));
+ theme_cache.progress[4] = get_theme_icon("Progress5", SNAME("EditorIcons"));
+ theme_cache.progress[5] = get_theme_icon("Progress6", SNAME("EditorIcons"));
+ theme_cache.progress[6] = get_theme_icon("Progress7", SNAME("EditorIcons"));
+ theme_cache.progress[7] = get_theme_icon("Progress8", SNAME("EditorIcons"));
+}
+
+void EditorFileDialog::_notification(int p_what) {
+ switch (p_what) {
+ case NOTIFICATION_THEME_CHANGED:
+ case Control::NOTIFICATION_LAYOUT_DIRECTION_CHANGED:
+ case NOTIFICATION_TRANSLATION_CHANGED: {
+ _update_icons();
+ invalidate();
+ } break;
+
+ case NOTIFICATION_PROCESS: {
+ if (preview_waiting) {
+ preview_wheel_timeout -= get_process_delta_time();
+ if (preview_wheel_timeout <= 0) {
+ preview_wheel_index++;
+ if (preview_wheel_index >= 8) {
+ preview_wheel_index = 0;
+ }
+
+ Ref<Texture2D> frame = theme_cache.progress[preview_wheel_index];
+ preview->set_texture(frame);
+ preview_wheel_timeout = 0.1;
+ }
+ }
+ } break;
+
+ case EditorSettings::NOTIFICATION_EDITOR_SETTINGS_CHANGED: {
+ bool is_showing_hidden = EDITOR_GET("filesystem/file_dialog/show_hidden_files");
+ if (show_hidden_files != is_showing_hidden) {
+ set_show_hidden_files(is_showing_hidden);
+ }
+ set_display_mode((DisplayMode)EDITOR_GET("filesystem/file_dialog/display_mode").operator int());
+
+ // DO NOT CALL UPDATE FILE LIST HERE, ALL HUNDREDS OF HIDDEN DIALOGS WILL RESPOND, CALL INVALIDATE INSTEAD
+ invalidate();
+ } break;
+
+ case NOTIFICATION_VISIBILITY_CHANGED: {
+ if (!is_visible()) {
+ set_process_shortcut_input(false);
+ }
+
+ invalidate(); // For consistency with the standard FileDialog.
+ } break;
+
+ case NOTIFICATION_WM_WINDOW_FOCUS_IN: {
+ // Check if the current directory was removed externally (much less likely to happen while editor window is focused).
+ String previous_dir = get_current_dir();
+ while (!dir_access->dir_exists(get_current_dir())) {
+ _go_up();
+
+ // In case we can't go further up, use some fallback and break.
+ if (get_current_dir() == previous_dir) {
+ _dir_submitted(OS::get_singleton()->get_user_data_dir());
+ break;
+ }
+ }
+ } break;
+ }
+}
+
+void EditorFileDialog::shortcut_input(const Ref<InputEvent> &p_event) {
+ ERR_FAIL_COND(p_event.is_null());
+
+ Ref<InputEventKey> k = p_event;
+
+ if (k.is_valid()) {
+ if (k->is_pressed()) {
+ bool handled = false;
+
+ if (ED_IS_SHORTCUT("file_dialog/go_back", p_event)) {
+ _go_back();
+ handled = true;
+ }
+ if (ED_IS_SHORTCUT("file_dialog/go_forward", p_event)) {
+ _go_forward();
+ handled = true;
+ }
+ if (ED_IS_SHORTCUT("file_dialog/go_up", p_event)) {
+ _go_up();
+ handled = true;
+ }
+ if (ED_IS_SHORTCUT("file_dialog/refresh", p_event)) {
+ invalidate();
+ handled = true;
+ }
+ if (ED_IS_SHORTCUT("file_dialog/toggle_hidden_files", p_event)) {
+ set_show_hidden_files(!show_hidden_files);
+ handled = true;
+ }
+ if (ED_IS_SHORTCUT("file_dialog/toggle_favorite", p_event)) {
+ _favorite_pressed();
+ handled = true;
+ }
+ if (ED_IS_SHORTCUT("file_dialog/toggle_mode", p_event)) {
+ if (mode_thumbnails->is_pressed()) {
+ set_display_mode(DISPLAY_LIST);
+ } else {
+ set_display_mode(DISPLAY_THUMBNAILS);
+ }
+ handled = true;
+ }
+ if (ED_IS_SHORTCUT("file_dialog/create_folder", p_event)) {
+ _make_dir();
+ handled = true;
+ }
+ if (ED_IS_SHORTCUT("file_dialog/delete", p_event)) {
+ _delete_items();
+ handled = true;
+ }
+ if (ED_IS_SHORTCUT("file_dialog/focus_path", p_event)) {
+ dir->grab_focus();
+ handled = true;
+ }
+ if (ED_IS_SHORTCUT("file_dialog/move_favorite_up", p_event)) {
+ _favorite_move_up();
+ handled = true;
+ }
+ if (ED_IS_SHORTCUT("file_dialog/move_favorite_down", p_event)) {
+ _favorite_move_down();
+ handled = true;
+ }
+
+ if (handled) {
+ set_input_as_handled();
+ }
+ }
+ }
+}
+
+void EditorFileDialog::set_enable_multiple_selection(bool p_enable) {
+ item_list->set_select_mode(p_enable ? ItemList::SELECT_MULTI : ItemList::SELECT_SINGLE);
+};
+
+Vector<String> EditorFileDialog::get_selected_files() const {
+ Vector<String> list;
+ for (int i = 0; i < item_list->get_item_count(); i++) {
+ if (item_list->is_selected(i)) {
+ list.push_back(item_list->get_item_text(i));
+ }
+ }
+ return list;
+};
+
+void EditorFileDialog::update_dir() {
+ if (drives->is_visible()) {
+ if (dir_access->get_current_dir().is_network_share_path()) {
+ _update_drives(false);
+ drives->add_item(RTR("Network"));
+ drives->set_item_disabled(-1, true);
+ drives->select(drives->get_item_count() - 1);
+ } else {
+ drives->select(dir_access->get_current_drive());
+ }
+ }
+ dir->set_text(dir_access->get_current_dir(false));
+
+ // Disable "Open" button only when selecting file(s) mode.
+ get_ok_button()->set_disabled(_is_open_should_be_disabled());
+ switch (mode) {
+ case FILE_MODE_OPEN_FILE:
+ case FILE_MODE_OPEN_FILES:
+ set_ok_button_text(TTR("Open"));
+ break;
+ case FILE_MODE_OPEN_DIR:
+ set_ok_button_text(TTR("Select Current Folder"));
+ break;
+ case FILE_MODE_OPEN_ANY:
+ case FILE_MODE_SAVE_FILE:
+ // FIXME: Implement, or refactor to avoid duplication with set_mode
+ break;
+ }
+}
+
+void EditorFileDialog::_dir_submitted(String p_dir) {
+ dir_access->change_dir(p_dir);
+ invalidate();
+ update_dir();
+ _push_history();
+}
+
+void EditorFileDialog::_file_submitted(const String &p_file) {
+ _action_pressed();
+}
+
+void EditorFileDialog::_save_confirm_pressed() {
+ String f = dir_access->get_current_dir().path_join(file->get_text());
+ _save_to_recent();
+ hide();
+ emit_signal(SNAME("file_selected"), f);
+}
+
+void EditorFileDialog::_post_popup() {
+ ConfirmationDialog::_post_popup();
+
+ // Check if the current path doesn't exist and correct it.
+ String current = dir_access->get_current_dir();
+ while (!dir_access->dir_exists(current)) {
+ current = current.get_base_dir();
+ }
+ set_current_dir(current);
+
+ if (mode == FILE_MODE_SAVE_FILE) {
+ file->grab_focus();
+ } else {
+ item_list->grab_focus();
+ }
+
+ if (mode == FILE_MODE_OPEN_DIR) {
+ file_box->set_visible(false);
+ } else {
+ file_box->set_visible(true);
+ }
+
+ if (!get_current_file().is_empty()) {
+ _request_single_thumbnail(get_current_dir().path_join(get_current_file()));
+ }
+
+ local_history.clear();
+ local_history_pos = -1;
+ _push_history();
+
+ set_process_shortcut_input(true);
+}
+
+void EditorFileDialog::_thumbnail_result(const String &p_path, const Ref<Texture2D> &p_preview, const Ref<Texture2D> &p_small_preview, const Variant &p_udata) {
+ if (display_mode == DISPLAY_LIST || p_preview.is_null()) {
+ return;
+ }
+
+ for (int i = 0; i < item_list->get_item_count(); i++) {
+ Dictionary d = item_list->get_item_metadata(i);
+ String pname = d["path"];
+ if (pname == p_path) {
+ item_list->set_item_icon(i, p_preview);
+ item_list->set_item_tag_icon(i, Ref<Texture2D>());
+ }
+ }
+}
+
+void EditorFileDialog::_thumbnail_done(const String &p_path, const Ref<Texture2D> &p_preview, const Ref<Texture2D> &p_small_preview, const Variant &p_udata) {
+ set_process(false);
+ preview_waiting = false;
+
+ if (p_preview.is_valid() && get_current_path() == p_path) {
+ preview->set_texture(p_preview);
+ if (display_mode == DISPLAY_THUMBNAILS) {
+ preview_vb->hide();
+ } else {
+ preview_vb->show();
+ }
+
+ } else {
+ preview_vb->hide();
+ preview->set_texture(Ref<Texture2D>());
+ }
+}
+
+void EditorFileDialog::_request_single_thumbnail(const String &p_path) {
+ if (!FileAccess::exists(p_path) || !previews_enabled) {
+ return;
+ }
+
+ set_process(true);
+ preview_waiting = true;
+ preview_wheel_timeout = 0;
+ EditorResourcePreview::get_singleton()->queue_resource_preview(p_path, this, "_thumbnail_done", p_path);
+}
+
+void EditorFileDialog::_action_pressed() {
+ if (mode == FILE_MODE_OPEN_FILES) {
+ String fbase = dir_access->get_current_dir();
+
+ Vector<String> files;
+ for (int i = 0; i < item_list->get_item_count(); i++) {
+ if (item_list->is_selected(i)) {
+ files.push_back(fbase.path_join(item_list->get_item_text(i)));
+ }
+ }
+
+ if (files.size()) {
+ _save_to_recent();
+ hide();
+ emit_signal(SNAME("files_selected"), files);
+ }
+
+ return;
+ }
+
+ String file_text = file->get_text();
+ String f = file_text.is_absolute_path() ? file_text : dir_access->get_current_dir().path_join(file_text);
+
+ if ((mode == FILE_MODE_OPEN_ANY || mode == FILE_MODE_OPEN_FILE) && dir_access->file_exists(f)) {
+ _save_to_recent();
+ hide();
+ emit_signal(SNAME("file_selected"), f);
+ } else if (mode == FILE_MODE_OPEN_ANY || mode == FILE_MODE_OPEN_DIR) {
+ String path = dir_access->get_current_dir();
+
+ path = path.replace("\\", "/");
+
+ for (int i = 0; i < item_list->get_item_count(); i++) {
+ if (item_list->is_selected(i)) {
+ Dictionary d = item_list->get_item_metadata(i);
+ if (d["dir"]) {
+ path = path.path_join(d["name"]);
+
+ break;
+ }
+ }
+ }
+
+ _save_to_recent();
+ hide();
+ emit_signal(SNAME("dir_selected"), path);
+ }
+
+ if (mode == FILE_MODE_SAVE_FILE) {
+ bool valid = false;
+
+ if (filter->get_selected() == filter->get_item_count() - 1) {
+ valid = true; // match none
+ } else if (filters.size() > 1 && filter->get_selected() == 0) {
+ // match all filters
+ for (int i = 0; i < filters.size(); i++) {
+ String flt = filters[i].get_slice(";", 0);
+ for (int j = 0; j < flt.get_slice_count(","); j++) {
+ String str = flt.get_slice(",", j).strip_edges();
+ if (f.match(str)) {
+ valid = true;
+ break;
+ }
+ }
+ if (valid) {
+ break;
+ }
+ }
+ } else {
+ int idx = filter->get_selected();
+ if (filters.size() > 1) {
+ idx--;
+ }
+ if (idx >= 0 && idx < filters.size()) {
+ String flt = filters[idx].get_slice(";", 0);
+ int filterSliceCount = flt.get_slice_count(",");
+ for (int j = 0; j < filterSliceCount; j++) {
+ String str = (flt.get_slice(",", j).strip_edges());
+ if (f.match(str)) {
+ valid = true;
+ break;
+ }
+ }
+
+ if (!valid && filterSliceCount > 0) {
+ String str = (flt.get_slice(",", 0).strip_edges());
+ f += str.substr(1, str.length() - 1);
+ _request_single_thumbnail(get_current_dir().path_join(f.get_file()));
+ file->set_text(f.get_file());
+ valid = true;
+ }
+ } else {
+ valid = true;
+ }
+ }
+
+ // First check we're not having an empty name.
+ String file_name = file_text.strip_edges().get_file();
+ if (file_name.is_empty()) {
+ error_dialog->set_text(TTR("Cannot save file with an empty filename."));
+ error_dialog->popup_centered(Size2(250, 80) * EDSCALE);
+ return;
+ }
+
+ // Add first extension of filter if no valid extension is found.
+ if (!valid) {
+ int idx = filter->get_selected();
+ String flt = filters[idx].get_slice(";", 0);
+ String ext = flt.get_slice(",", 0).strip_edges().get_extension();
+ f += "." + ext;
+ }
+
+ if (file_name.begins_with(".")) { // Could still happen if typed manually.
+ error_dialog->set_text(TTR("Cannot save file with a name starting with a dot."));
+ error_dialog->popup_centered(Size2(250, 80) * EDSCALE);
+ return;
+ }
+
+ if (dir_access->file_exists(f) && !disable_overwrite_warning) {
+ confirm_save->set_text(vformat(TTR("File \"%s\" already exists.\nDo you want to overwrite it?"), f));
+ confirm_save->popup_centered(Size2(250, 80) * EDSCALE);
+ } else {
+ _save_to_recent();
+ hide();
+ emit_signal(SNAME("file_selected"), f);
+ }
+ }
+}
+
+void EditorFileDialog::_cancel_pressed() {
+ file->set_text("");
+ invalidate();
+ hide();
+}
+
+void EditorFileDialog::_item_selected(int p_item) {
+ int current = p_item;
+ if (current < 0 || current >= item_list->get_item_count()) {
+ return;
+ }
+
+ Dictionary d = item_list->get_item_metadata(current);
+
+ if (!d["dir"]) {
+ file->set_text(d["name"]);
+ _request_single_thumbnail(get_current_dir().path_join(get_current_file()));
+ } else if (mode == FILE_MODE_OPEN_DIR) {
+ set_ok_button_text(TTR("Select This Folder"));
+ }
+
+ get_ok_button()->set_disabled(_is_open_should_be_disabled());
+}
+
+void EditorFileDialog::_multi_selected(int p_item, bool p_selected) {
+ int current = p_item;
+ if (current < 0 || current >= item_list->get_item_count()) {
+ return;
+ }
+
+ Dictionary d = item_list->get_item_metadata(current);
+
+ if (!d["dir"] && p_selected) {
+ file->set_text(d["name"]);
+ _request_single_thumbnail(get_current_dir().path_join(get_current_file()));
+ }
+
+ get_ok_button()->set_disabled(_is_open_should_be_disabled());
+}
+
+void EditorFileDialog::_items_clear_selection(const Vector2 &p_pos, MouseButton p_mouse_button_index) {
+ if (p_mouse_button_index != MouseButton::LEFT) {
+ return;
+ }
+
+ item_list->deselect_all();
+
+ // If nothing is selected, then block Open button.
+ switch (mode) {
+ case FILE_MODE_OPEN_FILE:
+ case FILE_MODE_OPEN_FILES:
+ set_ok_button_text(TTR("Open"));
+ get_ok_button()->set_disabled(!item_list->is_anything_selected());
+ break;
+
+ case FILE_MODE_OPEN_DIR:
+ get_ok_button()->set_disabled(false);
+ set_ok_button_text(TTR("Select Current Folder"));
+ break;
+
+ case FILE_MODE_OPEN_ANY:
+ case FILE_MODE_SAVE_FILE:
+ // FIXME: Implement, or refactor to avoid duplication with set_mode
+ break;
+ }
+}
+
+void EditorFileDialog::_push_history() {
+ local_history.resize(local_history_pos + 1);
+ String new_path = dir_access->get_current_dir();
+ if (local_history.size() == 0 || new_path != local_history[local_history_pos]) {
+ local_history.push_back(new_path);
+ local_history_pos++;
+ dir_prev->set_disabled(local_history_pos == 0);
+ dir_next->set_disabled(true);
+ }
+}
+
+void EditorFileDialog::_item_dc_selected(int p_item) {
+ int current = p_item;
+ if (current < 0 || current >= item_list->get_item_count()) {
+ return;
+ }
+
+ Dictionary d = item_list->get_item_metadata(current);
+
+ if (d["dir"]) {
+ dir_access->change_dir(d["name"]);
+ callable_mp(this, &EditorFileDialog::update_file_list).call_deferred();
+ callable_mp(this, &EditorFileDialog::update_dir).call_deferred();
+
+ _push_history();
+
+ } else {
+ _action_pressed();
+ }
+}
+
+void EditorFileDialog::_item_list_item_rmb_clicked(int p_item, const Vector2 &p_pos, MouseButton p_mouse_button_index) {
+ if (p_mouse_button_index != MouseButton::RIGHT) {
+ return;
+ }
+
+ // Right click on specific file(s) or folder(s).
+ item_menu->clear();
+ item_menu->reset_size();
+
+ // Allow specific actions only on one item.
+ bool single_item_selected = item_list->get_selected_items().size() == 1;
+
+ // Disallow deleting the .import folder, Godot kills a cat if you do and it is possibly a senseless novice action.
+ bool allow_delete = true;
+ for (int i = 0; i < item_list->get_item_count(); i++) {
+ if (!item_list->is_selected(i)) {
+ continue;
+ }
+ Dictionary item_meta = item_list->get_item_metadata(i);
+ if (String(item_meta["path"]).begins_with(ProjectSettings::get_singleton()->get_project_data_path())) {
+ allow_delete = false;
+ break;
+ }
+ }
+
+ if (single_item_selected) {
+ item_menu->add_icon_item(theme_cache.action_copy, TTR("Copy Path"), ITEM_MENU_COPY_PATH);
+ }
+ if (allow_delete) {
+ item_menu->add_icon_item(theme_cache.action_delete, TTR("Delete"), ITEM_MENU_DELETE, Key::KEY_DELETE);
+ }
+
+#if !defined(ANDROID_ENABLED) && !defined(WEB_ENABLED)
+ // Opening the system file manager is not supported on the Android and web editors.
+ if (single_item_selected) {
+ item_menu->add_separator();
+ Dictionary item_meta = item_list->get_item_metadata(p_item);
+ String item_text = item_meta["dir"] ? TTR("Open in File Manager") : TTR("Show in File Manager");
+ item_menu->add_icon_item(theme_cache.filesystem, item_text, ITEM_MENU_SHOW_IN_EXPLORER);
+ }
+#endif
+
+ if (item_menu->get_item_count() > 0) {
+ item_menu->set_position(item_list->get_screen_position() + p_pos);
+ item_menu->reset_size();
+ item_menu->popup();
+ }
+}
+
+void EditorFileDialog::_item_list_empty_clicked(const Vector2 &p_pos, MouseButton p_mouse_button_index) {
+ if (p_mouse_button_index != MouseButton::RIGHT && p_mouse_button_index != MouseButton::LEFT) {
+ return;
+ }
+
+ // Left or right click on folder background. Deselect all files so that actions are applied on the current folder.
+ for (int i = 0; i < item_list->get_item_count(); i++) {
+ item_list->deselect(i);
+ }
+
+ if (p_mouse_button_index != MouseButton::RIGHT) {
+ return;
+ }
+
+ item_menu->clear();
+ item_menu->reset_size();
+
+ if (can_create_dir) {
+ item_menu->add_icon_item(theme_cache.folder, TTR("New Folder..."), ITEM_MENU_NEW_FOLDER, KeyModifierMask::CMD_OR_CTRL | Key::N);
+ }
+ item_menu->add_icon_item(theme_cache.reload, TTR("Refresh"), ITEM_MENU_REFRESH, Key::F5);
+#if !defined(ANDROID_ENABLED) && !defined(WEB_ENABLED)
+ // Opening the system file manager is not supported on the Android and web editors.
+ item_menu->add_separator();
+ item_menu->add_icon_item(theme_cache.filesystem, TTR("Open in File Manager"), ITEM_MENU_SHOW_IN_EXPLORER);
+#endif
+
+ item_menu->set_position(item_list->get_screen_position() + p_pos);
+ item_menu->reset_size();
+ item_menu->popup();
+}
+
+void EditorFileDialog::_item_menu_id_pressed(int p_option) {
+ switch (p_option) {
+ case ITEM_MENU_COPY_PATH: {
+ Dictionary item_meta = item_list->get_item_metadata(item_list->get_current());
+ DisplayServer::get_singleton()->clipboard_set(item_meta["path"]);
+ } break;
+
+ case ITEM_MENU_DELETE: {
+ _delete_items();
+ } break;
+
+ case ITEM_MENU_REFRESH: {
+ invalidate();
+ } break;
+
+ case ITEM_MENU_NEW_FOLDER: {
+ _make_dir();
+ } break;
+
+ case ITEM_MENU_SHOW_IN_EXPLORER: {
+ String path;
+ int idx = item_list->get_current();
+ if (idx == -1 || item_list->get_selected_items().size() == 0) {
+ // Folder background was clicked. Open this folder.
+ path = ProjectSettings::get_singleton()->globalize_path(dir_access->get_current_dir());
+ } else {
+ // Specific item was clicked. Open folders directly, or the folder containing a selected file.
+ Dictionary item_meta = item_list->get_item_metadata(idx);
+ path = ProjectSettings::get_singleton()->globalize_path(item_meta["path"]);
+ if (!item_meta["dir"]) {
+ path = path.get_base_dir();
+ }
+ }
+ OS::get_singleton()->shell_open(String("file://") + path);
+ } break;
+ }
+}
+
+bool EditorFileDialog::_is_open_should_be_disabled() {
+ if (mode == FILE_MODE_OPEN_ANY || mode == FILE_MODE_SAVE_FILE) {
+ return false;
+ }
+
+ Vector<int> items = item_list->get_selected_items();
+ if (items.size() == 0) {
+ return mode != FILE_MODE_OPEN_DIR; // In "Open folder" mode, having nothing selected picks the current folder.
+ }
+
+ for (int i = 0; i < items.size(); i++) {
+ Dictionary d = item_list->get_item_metadata(items.get(i));
+
+ if (((mode == FILE_MODE_OPEN_FILE || mode == FILE_MODE_OPEN_FILES) && d["dir"]) || (mode == FILE_MODE_OPEN_DIR && !d["dir"])) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+void EditorFileDialog::update_file_name() {
+ int idx = filter->get_selected() - 1;
+ if ((idx == -1 && filter->get_item_count() == 2) || (filter->get_item_count() > 2 && idx >= 0 && idx < filter->get_item_count() - 2)) {
+ if (idx == -1) {
+ idx += 1;
+ }
+ String filter_str = filters[idx];
+ String file_str = file->get_text();
+ String base_name = file_str.get_basename();
+ Vector<String> filter_substr = filter_str.split(";");
+ if (filter_substr.size() >= 2) {
+ file_str = base_name + "." + filter_substr[0].strip_edges().get_extension().to_lower();
+ } else {
+ file_str = base_name + "." + filter_str.strip_edges().get_extension().to_lower();
+ }
+ file->set_text(file_str);
+ }
+}
+
+// DO NOT USE THIS FUNCTION UNLESS NEEDED, CALL INVALIDATE() INSTEAD.
+void EditorFileDialog::update_file_list() {
+ int thumbnail_size = EDITOR_GET("filesystem/file_dialog/thumbnail_size");
+ thumbnail_size *= EDSCALE;
+ Ref<Texture2D> folder_thumbnail;
+ Ref<Texture2D> file_thumbnail;
+
+ item_list->clear();
+
+ // Scroll back to the top after opening a directory
+ item_list->get_v_scroll_bar()->set_value(0);
+
+ if (display_mode == DISPLAY_THUMBNAILS) {
+ item_list->set_max_columns(0);
+ item_list->set_icon_mode(ItemList::ICON_MODE_TOP);
+ item_list->set_fixed_column_width(thumbnail_size * 3 / 2);
+ item_list->set_max_text_lines(2);
+ item_list->set_text_overrun_behavior(TextServer::OVERRUN_TRIM_ELLIPSIS);
+ item_list->set_fixed_icon_size(Size2(thumbnail_size, thumbnail_size));
+
+ if (thumbnail_size < 64) {
+ folder_thumbnail = theme_cache.folder_medium_thumbnail;
+ file_thumbnail = theme_cache.file_medium_thumbnail;
+ } else {
+ folder_thumbnail = theme_cache.folder_big_thumbnail;
+ file_thumbnail = theme_cache.file_big_thumbnail;
+ }
+
+ preview_vb->hide();
+
+ } else {
+ item_list->set_icon_mode(ItemList::ICON_MODE_LEFT);
+ item_list->set_max_columns(1);
+ item_list->set_max_text_lines(1);
+ item_list->set_fixed_column_width(0);
+ item_list->set_fixed_icon_size(Size2());
+ if (preview->get_texture().is_valid()) {
+ preview_vb->show();
+ }
+ }
+
+ String cdir = dir_access->get_current_dir();
+
+ dir_access->list_dir_begin();
+
+ List<String> files;
+ List<String> dirs;
+
+ String item = dir_access->get_next();
+
+ while (!item.is_empty()) {
+ if (item == "." || item == "..") {
+ item = dir_access->get_next();
+ continue;
+ }
+
+ if (show_hidden_files) {
+ if (!dir_access->current_is_dir()) {
+ files.push_back(item);
+ } else {
+ dirs.push_back(item);
+ }
+ } else if (!dir_access->current_is_hidden()) {
+ String full_path = cdir == "res://" ? item : dir_access->get_current_dir() + "/" + item;
+ if (dir_access->current_is_dir() && (!EditorFileSystem::_should_skip_directory(full_path) || Engine::get_singleton()->is_project_manager_hint())) {
+ dirs.push_back(item);
+ } else {
+ files.push_back(item);
+ }
+ }
+ item = dir_access->get_next();
+ }
+
+ dirs.sort_custom<NaturalNoCaseComparator>();
+ files.sort_custom<NaturalNoCaseComparator>();
+
+ while (!dirs.is_empty()) {
+ const String &dir_name = dirs.front()->get();
+
+ item_list->add_item(dir_name);
+
+ if (display_mode == DISPLAY_THUMBNAILS) {
+ item_list->set_item_icon(-1, folder_thumbnail);
+ } else {
+ item_list->set_item_icon(-1, theme_cache.folder);
+ }
+
+ Dictionary d;
+ d["name"] = dir_name;
+ d["path"] = cdir.path_join(dir_name);
+ d["dir"] = true;
+
+ item_list->set_item_metadata(-1, d);
+ item_list->set_item_icon_modulate(-1, theme_cache.folder_icon_color);
+
+ dirs.pop_front();
+ }
+
+ List<String> patterns;
+ // build filter
+ if (filter->get_selected() == filter->get_item_count() - 1) {
+ // match all
+ } else if (filters.size() > 1 && filter->get_selected() == 0) {
+ // match all filters
+ for (int i = 0; i < filters.size(); i++) {
+ String f = filters[i].get_slice(";", 0);
+ for (int j = 0; j < f.get_slice_count(","); j++) {
+ patterns.push_back(f.get_slice(",", j).strip_edges());
+ }
+ }
+ } else {
+ int idx = filter->get_selected();
+ if (filters.size() > 1) {
+ idx--;
+ }
+
+ if (idx >= 0 && idx < filters.size()) {
+ String f = filters[idx].get_slice(";", 0);
+ for (int j = 0; j < f.get_slice_count(","); j++) {
+ patterns.push_back(f.get_slice(",", j).strip_edges());
+ }
+ }
+ }
+
+ while (!files.is_empty()) {
+ bool match = patterns.is_empty();
+
+ for (const String &E : patterns) {
+ if (files.front()->get().matchn(E)) {
+ match = true;
+ break;
+ }
+ }
+
+ if (match) {
+ item_list->add_item(files.front()->get());
+
+ if (get_icon_func) {
+ Ref<Texture2D> icon = get_icon_func(cdir.path_join(files.front()->get()));
+ if (display_mode == DISPLAY_THUMBNAILS) {
+ Ref<Texture2D> thumbnail;
+ if (get_thumbnail_func) {
+ thumbnail = get_thumbnail_func(cdir.path_join(files.front()->get()));
+ }
+ if (thumbnail.is_null()) {
+ thumbnail = file_thumbnail;
+ }
+
+ item_list->set_item_icon(-1, thumbnail);
+ item_list->set_item_tag_icon(-1, icon);
+ } else {
+ item_list->set_item_icon(-1, icon);
+ }
+ }
+
+ Dictionary d;
+ d["name"] = files.front()->get();
+ d["dir"] = false;
+ String fullpath = cdir.path_join(files.front()->get());
+ d["path"] = fullpath;
+ item_list->set_item_metadata(-1, d);
+
+ if (display_mode == DISPLAY_THUMBNAILS && previews_enabled) {
+ EditorResourcePreview::get_singleton()->queue_resource_preview(fullpath, this, "_thumbnail_result", fullpath);
+ }
+
+ if (file->get_text() == files.front()->get()) {
+ item_list->set_current(item_list->get_item_count() - 1);
+ }
+ }
+
+ files.pop_front();
+ }
+
+ if (favorites->get_current() >= 0) {
+ favorites->deselect(favorites->get_current());
+ }
+
+ favorite->set_pressed(false);
+ fav_up->set_disabled(true);
+ fav_down->set_disabled(true);
+ get_ok_button()->set_disabled(_is_open_should_be_disabled());
+ for (int i = 0; i < favorites->get_item_count(); i++) {
+ if (favorites->get_item_metadata(i) == cdir || favorites->get_item_metadata(i) == cdir + "/") {
+ favorites->select(i);
+ favorite->set_pressed(true);
+ if (i > 0) {
+ fav_up->set_disabled(false);
+ }
+ if (i < favorites->get_item_count() - 1) {
+ fav_down->set_disabled(false);
+ }
+ break;
+ }
+ }
+}
+
+void EditorFileDialog::_filter_selected(int) {
+ update_file_name();
+ update_file_list();
+}
+
+void EditorFileDialog::update_filters() {
+ filter->clear();
+
+ if (filters.size() > 1) {
+ String all_filters;
+
+ const int max_filters = 5;
+
+ for (int i = 0; i < MIN(max_filters, filters.size()); i++) {
+ String flt = filters[i].get_slice(";", 0).strip_edges();
+ if (i > 0) {
+ all_filters += ", ";
+ }
+ all_filters += flt;
+ }
+
+ if (max_filters < filters.size()) {
+ all_filters += ", ...";
+ }
+
+ filter->add_item(TTR("All Recognized") + " (" + all_filters + ")");
+ }
+ for (int i = 0; i < filters.size(); i++) {
+ String flt = filters[i].get_slice(";", 0).strip_edges();
+ String desc = filters[i].get_slice(";", 1).strip_edges();
+ if (desc.length()) {
+ filter->add_item(desc + " (" + flt + ")");
+ } else {
+ filter->add_item("(" + flt + ")");
+ }
+ }
+
+ filter->add_item(TTR("All Files (*)"));
+}
+
+void EditorFileDialog::clear_filters() {
+ filters.clear();
+ update_filters();
+ invalidate();
+}
+
+void EditorFileDialog::add_filter(const String &p_filter, const String &p_description) {
+ if (p_description.is_empty()) {
+ filters.push_back(p_filter);
+ } else {
+ filters.push_back(vformat("%s ; %s", p_filter, p_description));
+ }
+ update_filters();
+ invalidate();
+}
+
+void EditorFileDialog::set_filters(const Vector<String> &p_filters) {
+ if (filters == p_filters) {
+ return;
+ }
+ filters = p_filters;
+ update_filters();
+ invalidate();
+}
+
+Vector<String> EditorFileDialog::get_filters() const {
+ return filters;
+}
+
+String EditorFileDialog::get_current_dir() const {
+ return dir_access->get_current_dir();
+}
+
+String EditorFileDialog::get_current_file() const {
+ return file->get_text();
+}
+
+String EditorFileDialog::get_current_path() const {
+ return dir_access->get_current_dir().path_join(file->get_text());
+}
+
+void EditorFileDialog::set_current_dir(const String &p_dir) {
+ if (p_dir.is_relative_path()) {
+ dir_access->change_dir(OS::get_singleton()->get_resource_dir());
+ }
+ dir_access->change_dir(p_dir);
+ update_dir();
+ invalidate();
+}
+
+void EditorFileDialog::set_current_file(const String &p_file) {
+ file->set_text(p_file);
+ update_dir();
+ invalidate();
+ _focus_file_text();
+
+ if (is_visible()) {
+ _request_single_thumbnail(get_current_dir().path_join(get_current_file()));
+ }
+}
+
+void EditorFileDialog::set_current_path(const String &p_path) {
+ if (!p_path.size()) {
+ return;
+ }
+ int pos = MAX(p_path.rfind("/"), p_path.rfind("\\"));
+ if (pos == -1) {
+ set_current_file(p_path);
+ } else {
+ String path_dir = p_path.substr(0, pos);
+ String path_file = p_path.substr(pos + 1, p_path.length());
+ set_current_dir(path_dir);
+ set_current_file(path_file);
+ }
+}
+
+void EditorFileDialog::set_file_mode(FileMode p_mode) {
+ mode = p_mode;
+ switch (mode) {
+ case FILE_MODE_OPEN_FILE:
+ set_ok_button_text(TTR("Open"));
+ set_title(TTR("Open a File"));
+ can_create_dir = false;
+ break;
+ case FILE_MODE_OPEN_FILES:
+ set_ok_button_text(TTR("Open"));
+ set_title(TTR("Open File(s)"));
+ can_create_dir = false;
+ break;
+ case FILE_MODE_OPEN_DIR:
+ set_ok_button_text(TTR("Open"));
+ set_title(TTR("Open a Directory"));
+ can_create_dir = true;
+ break;
+ case FILE_MODE_OPEN_ANY:
+ set_ok_button_text(TTR("Open"));
+ set_title(TTR("Open a File or Directory"));
+ can_create_dir = true;
+ break;
+ case FILE_MODE_SAVE_FILE:
+ set_ok_button_text(TTR("Save"));
+ set_title(TTR("Save a File"));
+ can_create_dir = true;
+ break;
+ }
+
+ if (mode == FILE_MODE_OPEN_FILES) {
+ item_list->set_select_mode(ItemList::SELECT_MULTI);
+ } else {
+ item_list->set_select_mode(ItemList::SELECT_SINGLE);
+ }
+
+ if (can_create_dir) {
+ makedir->show();
+ } else {
+ makedir->hide();
+ }
+}
+
+EditorFileDialog::FileMode EditorFileDialog::get_file_mode() const {
+ return mode;
+}
+
+void EditorFileDialog::set_access(Access p_access) {
+ ERR_FAIL_INDEX(p_access, 3);
+ if (access == p_access) {
+ return;
+ }
+ switch (p_access) {
+ case ACCESS_FILESYSTEM: {
+ dir_access = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);
+ } break;
+ case ACCESS_RESOURCES: {
+ dir_access = DirAccess::create(DirAccess::ACCESS_RESOURCES);
+ } break;
+ case ACCESS_USERDATA: {
+ dir_access = DirAccess::create(DirAccess::ACCESS_USERDATA);
+ } break;
+ }
+ access = p_access;
+ _update_drives();
+ invalidate();
+ update_filters();
+ update_dir();
+}
+
+void EditorFileDialog::invalidate() {
+ if (!is_visible() || is_invalidating) {
+ return;
+ }
+
+ is_invalidating = true;
+ callable_mp(this, &EditorFileDialog::_invalidate).call_deferred();
+}
+
+void EditorFileDialog::_invalidate() {
+ if (!is_invalidating) {
+ return;
+ }
+
+ update_file_list();
+ _update_favorites();
+ _update_recent();
+
+ is_invalidating = false;
+}
+
+EditorFileDialog::Access EditorFileDialog::get_access() const {
+ return access;
+}
+
+void EditorFileDialog::_make_dir_confirm() {
+ const String stripped_dirname = makedirname->get_text().strip_edges();
+
+ if (dir_access->dir_exists(stripped_dirname)) {
+ error_dialog->set_text(TTR("Could not create folder. File with that name already exists."));
+ error_dialog->popup_centered(Size2(250, 50) * EDSCALE);
+ makedirname->set_text(""); // Reset label.
+ return;
+ }
+
+ Error err = dir_access->make_dir(stripped_dirname);
+ if (err == OK) {
+ dir_access->change_dir(stripped_dirname);
+ invalidate();
+ update_filters();
+ update_dir();
+ _push_history();
+ if (access != ACCESS_FILESYSTEM) {
+ EditorFileSystem::get_singleton()->scan_changes(); //we created a dir, so rescan changes
+ }
+ } else {
+ error_dialog->set_text(TTR("Could not create folder."));
+ error_dialog->popup_centered(Size2(250, 50) * EDSCALE);
+ }
+ makedirname->set_text(""); // reset label
+}
+
+void EditorFileDialog::_make_dir() {
+ makedialog->popup_centered(Size2(250, 80) * EDSCALE);
+ makedirname->grab_focus();
+}
+
+void EditorFileDialog::_delete_items() {
+ // Collect the selected folders and files to delete and check them in the deletion dependency dialog.
+ Vector<String> folders;
+ Vector<String> files;
+ for (int i = 0; i < item_list->get_item_count(); i++) {
+ if (!item_list->is_selected(i)) {
+ continue;
+ }
+ Dictionary item_meta = item_list->get_item_metadata(i);
+ if (item_meta["dir"]) {
+ folders.push_back(item_meta["path"]);
+ } else {
+ files.push_back(item_meta["path"]);
+ }
+ }
+ if (folders.size() + files.size() > 0) {
+ if (access == ACCESS_FILESYSTEM) {
+ global_remove_dialog->popup_centered();
+ } else {
+ dep_remove_dialog->reset_size();
+ dep_remove_dialog->show(folders, files);
+ }
+ }
+}
+
+void EditorFileDialog::_delete_files_global() {
+ // Delete files outside of the project directory without dependency checks.
+ for (int i = 0; i < item_list->get_item_count(); i++) {
+ if (!item_list->is_selected(i)) {
+ continue;
+ }
+ Dictionary item_meta = item_list->get_item_metadata(i);
+ // Only delete empty directories for safety.
+ dir_access->remove(item_meta["path"]);
+ }
+ update_file_list();
+}
+
+void EditorFileDialog::_select_drive(int p_idx) {
+ String d = drives->get_item_text(p_idx);
+ dir_access->change_dir(d);
+ file->set_text("");
+ invalidate();
+ update_dir();
+ _push_history();
+}
+
+void EditorFileDialog::_update_drives(bool p_select) {
+ int dc = dir_access->get_drive_count();
+ if (dc == 0 || access != ACCESS_FILESYSTEM) {
+ drives->hide();
+ } else {
+ drives->clear();
+ Node *dp = drives->get_parent();
+ if (dp) {
+ dp->remove_child(drives);
+ }
+ dp = dir_access->drives_are_shortcuts() ? shortcuts_container : drives_container;
+ dp->add_child(drives);
+ drives->show();
+
+ for (int i = 0; i < dir_access->get_drive_count(); i++) {
+ String d = dir_access->get_drive(i);
+ drives->add_item(dir_access->get_drive(i));
+ }
+ if (p_select) {
+ drives->select(dir_access->get_current_drive());
+ }
+ }
+}
+
+void EditorFileDialog::_update_icons() {
+ // Update icons.
+
+ mode_thumbnails->set_icon(theme_cache.mode_thumbnails);
+ mode_list->set_icon(theme_cache.mode_list);
+
+ if (is_layout_rtl()) {
+ dir_prev->set_icon(theme_cache.forward_folder);
+ dir_next->set_icon(theme_cache.back_folder);
+ } else {
+ dir_prev->set_icon(theme_cache.back_folder);
+ dir_next->set_icon(theme_cache.forward_folder);
+ }
+ dir_up->set_icon(theme_cache.parent_folder);
+
+ refresh->set_icon(theme_cache.reload);
+ favorite->set_icon(theme_cache.favorite);
+ show_hidden->set_icon(theme_cache.toggle_hidden);
+
+ fav_up->set_icon(theme_cache.favorites_up);
+ fav_down->set_icon(theme_cache.favorites_down);
+}
+
+void EditorFileDialog::_favorite_selected(int p_idx) {
+ Error change_dir_result = dir_access->change_dir(favorites->get_item_metadata(p_idx));
+ if (change_dir_result != OK) {
+ error_dialog->set_text(TTR("Favorited folder does not exist anymore and will be removed."));
+ error_dialog->popup_centered(Size2(250, 50) * EDSCALE);
+
+ bool res = (access == ACCESS_RESOURCES);
+
+ Vector<String> favorited = EditorSettings::get_singleton()->get_favorites();
+ String dir_to_remove = favorites->get_item_metadata(p_idx);
+
+ bool found = false;
+ for (int i = 0; i < favorited.size(); i++) {
+ bool cres = favorited[i].begins_with("res://");
+ if (cres != res) {
+ continue;
+ }
+
+ if (favorited[i] == dir_to_remove) {
+ found = true;
+ break;
+ }
+ }
+
+ if (found) {
+ favorited.erase(favorites->get_item_metadata(p_idx));
+ favorites->remove_item(p_idx);
+ EditorSettings::get_singleton()->set_favorites(favorited);
+ }
+ } else {
+ update_dir();
+ invalidate();
+ _push_history();
+ }
+}
+
+void EditorFileDialog::_favorite_move_up() {
+ int current = favorites->get_current();
+
+ if (current > 0 && current < favorites->get_item_count()) {
+ Vector<String> favorited = EditorSettings::get_singleton()->get_favorites();
+
+ int a_idx = favorited.find(String(favorites->get_item_metadata(current - 1)));
+ int b_idx = favorited.find(String(favorites->get_item_metadata(current)));
+
+ if (a_idx == -1 || b_idx == -1) {
+ return;
+ }
+ SWAP(favorited.write[a_idx], favorited.write[b_idx]);
+
+ EditorSettings::get_singleton()->set_favorites(favorited);
+
+ _update_favorites();
+ update_file_list();
+ }
+}
+
+void EditorFileDialog::_favorite_move_down() {
+ int current = favorites->get_current();
+
+ if (current >= 0 && current < favorites->get_item_count() - 1) {
+ Vector<String> favorited = EditorSettings::get_singleton()->get_favorites();
+
+ int a_idx = favorited.find(String(favorites->get_item_metadata(current + 1)));
+ int b_idx = favorited.find(String(favorites->get_item_metadata(current)));
+
+ if (a_idx == -1 || b_idx == -1) {
+ return;
+ }
+ SWAP(favorited.write[a_idx], favorited.write[b_idx]);
+
+ EditorSettings::get_singleton()->set_favorites(favorited);
+
+ _update_favorites();
+ update_file_list();
+ }
+}
+
+void EditorFileDialog::_update_favorites() {
+ bool res = (access == ACCESS_RESOURCES);
+
+ String current = get_current_dir();
+ favorites->clear();
+
+ favorite->set_pressed(false);
+
+ Vector<String> favorited = EditorSettings::get_singleton()->get_favorites();
+ Vector<String> favorited_paths;
+ Vector<String> favorited_names;
+
+ bool fav_changed = false;
+ int current_favorite = -1;
+ for (int i = 0; i < favorited.size(); i++) {
+ bool cres = favorited[i].begins_with("res://");
+ if (cres != res) {
+ continue;
+ }
+
+ if (!dir_access->dir_exists(favorited[i])) {
+ // Remove invalid directory from the list of Favorited directories.
+ favorited.remove_at(i--);
+ fav_changed = true;
+ continue;
+ }
+
+ // Compute favorite display text.
+ String name = favorited[i];
+ if (res && name == "res://") {
+ if (name == current) {
+ current_favorite = favorited_paths.size();
+ }
+ name = "/";
+ favorited_paths.append(favorited[i]);
+ favorited_names.append(name);
+ } else if (name.ends_with("/")) {
+ if (name == current || name == current + "/") {
+ current_favorite = favorited_paths.size();
+ }
+ name = name.substr(0, name.length() - 1);
+ name = name.get_file();
+ favorited_paths.append(favorited[i]);
+ favorited_names.append(name);
+ } else {
+ // Ignore favorited files.
+ }
+ }
+
+ if (fav_changed) {
+ EditorSettings::get_singleton()->set_favorites(favorited);
+ }
+
+ EditorNode::disambiguate_filenames(favorited_paths, favorited_names);
+
+ for (int i = 0; i < favorited_paths.size(); i++) {
+ favorites->add_item(favorited_names[i], theme_cache.folder);
+ favorites->set_item_metadata(-1, favorited_paths[i]);
+ favorites->set_item_icon_modulate(-1, theme_cache.folder_icon_color);
+
+ if (i == current_favorite) {
+ favorite->set_pressed(true);
+ favorites->set_current(favorites->get_item_count() - 1);
+ recent->deselect_all();
+ }
+ }
+}
+
+void EditorFileDialog::_favorite_pressed() {
+ bool res = (access == ACCESS_RESOURCES);
+
+ String cd = get_current_dir();
+ if (!cd.ends_with("/")) {
+ cd += "/";
+ }
+
+ Vector<String> favorited = EditorSettings::get_singleton()->get_favorites();
+
+ bool found = false;
+ for (int i = 0; i < favorited.size(); i++) {
+ bool cres = favorited[i].begins_with("res://");
+ if (cres != res) {
+ continue;
+ }
+
+ if (favorited[i] == cd) {
+ found = true;
+ break;
+ }
+ }
+
+ if (found) {
+ favorited.erase(cd);
+ } else {
+ favorited.push_back(cd);
+ }
+
+ EditorSettings::get_singleton()->set_favorites(favorited);
+
+ _update_favorites();
+}
+
+void EditorFileDialog::_update_recent() {
+ recent->clear();
+
+ bool res = (access == ACCESS_RESOURCES);
+ Vector<String> recentd = EditorSettings::get_singleton()->get_recent_dirs();
+ Vector<String> recentd_paths;
+ Vector<String> recentd_names;
+
+ for (int i = 0; i < recentd.size(); i++) {
+ bool cres = recentd[i].begins_with("res://");
+ if (cres != res) {
+ continue;
+ }
+
+ if (!dir_access->dir_exists(recentd[i])) {
+ // Remove invalid directory from the list of Recent directories.
+ recentd.remove_at(i--);
+ continue;
+ }
+
+ // Compute recent directory display text.
+ String name = recentd[i];
+ if (res && name == "res://") {
+ name = "/";
+ } else {
+ if (name.ends_with("/")) {
+ name = name.substr(0, name.length() - 1);
+ }
+ name = name.get_file();
+ }
+ recentd_paths.append(recentd[i]);
+ recentd_names.append(name);
+ }
+
+ EditorNode::disambiguate_filenames(recentd_paths, recentd_names);
+
+ for (int i = 0; i < recentd_paths.size(); i++) {
+ recent->add_item(recentd_names[i], theme_cache.folder);
+ recent->set_item_metadata(-1, recentd_paths[i]);
+ recent->set_item_icon_modulate(-1, theme_cache.folder_icon_color);
+ }
+ EditorSettings::get_singleton()->set_recent_dirs(recentd);
+}
+
+void EditorFileDialog::_recent_selected(int p_idx) {
+ Vector<String> recentd = EditorSettings::get_singleton()->get_recent_dirs();
+ ERR_FAIL_INDEX(p_idx, recentd.size());
+
+ dir_access->change_dir(recent->get_item_metadata(p_idx));
+ update_file_list();
+ update_dir();
+ _push_history();
+}
+
+void EditorFileDialog::_go_up() {
+ dir_access->change_dir(get_current_dir().get_base_dir());
+ update_file_list();
+ update_dir();
+ _push_history();
+}
+
+void EditorFileDialog::_go_back() {
+ if (local_history_pos <= 0) {
+ return;
+ }
+
+ local_history_pos--;
+ dir_access->change_dir(local_history[local_history_pos]);
+ update_file_list();
+ update_dir();
+
+ dir_prev->set_disabled(local_history_pos == 0);
+ dir_next->set_disabled(local_history_pos == local_history.size() - 1);
+}
+
+void EditorFileDialog::_go_forward() {
+ if (local_history_pos >= local_history.size() - 1) {
+ return;
+ }
+
+ local_history_pos++;
+ dir_access->change_dir(local_history[local_history_pos]);
+ update_file_list();
+ update_dir();
+
+ dir_prev->set_disabled(local_history_pos == 0);
+ dir_next->set_disabled(local_history_pos == local_history.size() - 1);
+}
+
+bool EditorFileDialog::default_show_hidden_files = false;
+
+EditorFileDialog::DisplayMode EditorFileDialog::default_display_mode = DISPLAY_THUMBNAILS;
+
+void EditorFileDialog::set_display_mode(DisplayMode p_mode) {
+ if (display_mode == p_mode) {
+ return;
+ }
+ if (p_mode == DISPLAY_THUMBNAILS) {
+ mode_list->set_pressed(false);
+ mode_thumbnails->set_pressed(true);
+ } else {
+ mode_thumbnails->set_pressed(false);
+ mode_list->set_pressed(true);
+ }
+ display_mode = p_mode;
+ invalidate();
+}
+
+EditorFileDialog::DisplayMode EditorFileDialog::get_display_mode() const {
+ return display_mode;
+}
+
+void EditorFileDialog::_bind_methods() {
+ ClassDB::bind_method(D_METHOD("_cancel_pressed"), &EditorFileDialog::_cancel_pressed);
+
+ ClassDB::bind_method(D_METHOD("clear_filters"), &EditorFileDialog::clear_filters);
+ ClassDB::bind_method(D_METHOD("add_filter", "filter", "description"), &EditorFileDialog::add_filter, DEFVAL(""));
+ ClassDB::bind_method(D_METHOD("set_filters", "filters"), &EditorFileDialog::set_filters);
+ ClassDB::bind_method(D_METHOD("get_filters"), &EditorFileDialog::get_filters);
+ ClassDB::bind_method(D_METHOD("get_current_dir"), &EditorFileDialog::get_current_dir);
+ ClassDB::bind_method(D_METHOD("get_current_file"), &EditorFileDialog::get_current_file);
+ ClassDB::bind_method(D_METHOD("get_current_path"), &EditorFileDialog::get_current_path);
+ ClassDB::bind_method(D_METHOD("set_current_dir", "dir"), &EditorFileDialog::set_current_dir);
+ ClassDB::bind_method(D_METHOD("set_current_file", "file"), &EditorFileDialog::set_current_file);
+ ClassDB::bind_method(D_METHOD("set_current_path", "path"), &EditorFileDialog::set_current_path);
+ ClassDB::bind_method(D_METHOD("set_file_mode", "mode"), &EditorFileDialog::set_file_mode);
+ ClassDB::bind_method(D_METHOD("get_file_mode"), &EditorFileDialog::get_file_mode);
+ ClassDB::bind_method(D_METHOD("get_vbox"), &EditorFileDialog::get_vbox);
+ ClassDB::bind_method(D_METHOD("get_line_edit"), &EditorFileDialog::get_line_edit);
+ ClassDB::bind_method(D_METHOD("set_access", "access"), &EditorFileDialog::set_access);
+ ClassDB::bind_method(D_METHOD("get_access"), &EditorFileDialog::get_access);
+ ClassDB::bind_method(D_METHOD("set_show_hidden_files", "show"), &EditorFileDialog::set_show_hidden_files);
+ ClassDB::bind_method(D_METHOD("is_showing_hidden_files"), &EditorFileDialog::is_showing_hidden_files);
+ ClassDB::bind_method(D_METHOD("_thumbnail_done"), &EditorFileDialog::_thumbnail_done);
+ ClassDB::bind_method(D_METHOD("set_display_mode", "mode"), &EditorFileDialog::set_display_mode);
+ ClassDB::bind_method(D_METHOD("get_display_mode"), &EditorFileDialog::get_display_mode);
+ ClassDB::bind_method(D_METHOD("_thumbnail_result"), &EditorFileDialog::_thumbnail_result);
+ ClassDB::bind_method(D_METHOD("set_disable_overwrite_warning", "disable"), &EditorFileDialog::set_disable_overwrite_warning);
+ ClassDB::bind_method(D_METHOD("is_overwrite_warning_disabled"), &EditorFileDialog::is_overwrite_warning_disabled);
+
+ ClassDB::bind_method(D_METHOD("invalidate"), &EditorFileDialog::invalidate);
+
+ ADD_SIGNAL(MethodInfo("file_selected", PropertyInfo(Variant::STRING, "path")));
+ ADD_SIGNAL(MethodInfo("files_selected", PropertyInfo(Variant::PACKED_STRING_ARRAY, "paths")));
+ ADD_SIGNAL(MethodInfo("dir_selected", PropertyInfo(Variant::STRING, "dir")));
+
+ ADD_PROPERTY(PropertyInfo(Variant::INT, "access", PROPERTY_HINT_ENUM, "Resources,User data,File system"), "set_access", "get_access");
+ ADD_PROPERTY(PropertyInfo(Variant::INT, "display_mode", PROPERTY_HINT_ENUM, "Thumbnails,List"), "set_display_mode", "get_display_mode");
+ ADD_PROPERTY(PropertyInfo(Variant::INT, "file_mode", PROPERTY_HINT_ENUM, "Open one,Open many,Open folder,Open any,Save"), "set_file_mode", "get_file_mode");
+ ADD_PROPERTY(PropertyInfo(Variant::STRING, "current_dir", PROPERTY_HINT_DIR, "", PROPERTY_USAGE_NONE), "set_current_dir", "get_current_dir");
+ ADD_PROPERTY(PropertyInfo(Variant::STRING, "current_file", PROPERTY_HINT_FILE, "*", PROPERTY_USAGE_NONE), "set_current_file", "get_current_file");
+ ADD_PROPERTY(PropertyInfo(Variant::STRING, "current_path", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NONE), "set_current_path", "get_current_path");
+ ADD_PROPERTY(PropertyInfo(Variant::PACKED_STRING_ARRAY, "filters"), "set_filters", "get_filters");
+ ADD_PROPERTY(PropertyInfo(Variant::BOOL, "show_hidden_files"), "set_show_hidden_files", "is_showing_hidden_files");
+ ADD_PROPERTY(PropertyInfo(Variant::BOOL, "disable_overwrite_warning"), "set_disable_overwrite_warning", "is_overwrite_warning_disabled");
+
+ BIND_ENUM_CONSTANT(FILE_MODE_OPEN_FILE);
+ BIND_ENUM_CONSTANT(FILE_MODE_OPEN_FILES);
+ BIND_ENUM_CONSTANT(FILE_MODE_OPEN_DIR);
+ BIND_ENUM_CONSTANT(FILE_MODE_OPEN_ANY);
+ BIND_ENUM_CONSTANT(FILE_MODE_SAVE_FILE);
+
+ BIND_ENUM_CONSTANT(ACCESS_RESOURCES);
+ BIND_ENUM_CONSTANT(ACCESS_USERDATA);
+ BIND_ENUM_CONSTANT(ACCESS_FILESYSTEM);
+
+ BIND_ENUM_CONSTANT(DISPLAY_THUMBNAILS);
+ BIND_ENUM_CONSTANT(DISPLAY_LIST);
+}
+
+void EditorFileDialog::set_show_hidden_files(bool p_show) {
+ if (p_show == show_hidden_files) {
+ return;
+ }
+
+ EditorSettings::get_singleton()->set("filesystem/file_dialog/show_hidden_files", p_show);
+ show_hidden_files = p_show;
+ show_hidden->set_pressed(p_show);
+ invalidate();
+}
+
+bool EditorFileDialog::is_showing_hidden_files() const {
+ return show_hidden_files;
+}
+
+void EditorFileDialog::set_default_show_hidden_files(bool p_show) {
+ default_show_hidden_files = p_show;
+}
+
+void EditorFileDialog::set_default_display_mode(DisplayMode p_mode) {
+ default_display_mode = p_mode;
+}
+
+void EditorFileDialog::_save_to_recent() {
+ String cur_dir = get_current_dir();
+ Vector<String> recent_new = EditorSettings::get_singleton()->get_recent_dirs();
+
+ const int max = 20;
+ int count = 0;
+ bool res = cur_dir.begins_with("res://");
+
+ for (int i = 0; i < recent_new.size(); i++) {
+ bool cres = recent_new[i].begins_with("res://");
+ if (recent_new[i] == cur_dir || (res == cres && count > max)) {
+ recent_new.remove_at(i);
+ i--;
+ } else {
+ count++;
+ }
+ }
+
+ recent_new.insert(0, cur_dir);
+
+ EditorSettings::get_singleton()->set_recent_dirs(recent_new);
+}
+
+void EditorFileDialog::set_disable_overwrite_warning(bool p_disable) {
+ disable_overwrite_warning = p_disable;
+}
+
+bool EditorFileDialog::is_overwrite_warning_disabled() const {
+ return disable_overwrite_warning;
+}
+
+void EditorFileDialog::set_previews_enabled(bool p_enabled) {
+ previews_enabled = p_enabled;
+}
+
+bool EditorFileDialog::are_previews_enabled() {
+ return previews_enabled;
+}
+
+EditorFileDialog::EditorFileDialog() {
+ show_hidden_files = default_show_hidden_files;
+ display_mode = default_display_mode;
+ VBoxContainer *vbc = memnew(VBoxContainer);
+ add_child(vbc);
+
+ set_title(TTR("Save a File"));
+
+ ED_SHORTCUT("file_dialog/go_back", TTR("Go Back"), KeyModifierMask::ALT | Key::LEFT);
+ ED_SHORTCUT("file_dialog/go_forward", TTR("Go Forward"), KeyModifierMask::ALT | Key::RIGHT);
+ ED_SHORTCUT("file_dialog/go_up", TTR("Go Up"), KeyModifierMask::ALT | Key::UP);
+ ED_SHORTCUT("file_dialog/refresh", TTR("Refresh"), Key::F5);
+ ED_SHORTCUT("file_dialog/toggle_hidden_files", TTR("Toggle Hidden Files"), KeyModifierMask::CMD_OR_CTRL | Key::H);
+ ED_SHORTCUT("file_dialog/toggle_favorite", TTR("Toggle Favorite"), KeyModifierMask::ALT | Key::F);
+ ED_SHORTCUT("file_dialog/toggle_mode", TTR("Toggle Mode"), KeyModifierMask::ALT | Key::V);
+ ED_SHORTCUT("file_dialog/create_folder", TTR("Create Folder"), KeyModifierMask::CMD_OR_CTRL | Key::N);
+ ED_SHORTCUT("file_dialog/delete", TTR("Delete"), Key::KEY_DELETE);
+ ED_SHORTCUT("file_dialog/focus_path", TTR("Focus Path"), KeyModifierMask::CMD_OR_CTRL | Key::D);
+ ED_SHORTCUT("file_dialog/move_favorite_up", TTR("Move Favorite Up"), KeyModifierMask::CMD_OR_CTRL | Key::UP);
+ ED_SHORTCUT("file_dialog/move_favorite_down", TTR("Move Favorite Down"), KeyModifierMask::CMD_OR_CTRL | Key::DOWN);
+
+ if (EditorSettings::get_singleton()) {
+ ED_SHORTCUT_OVERRIDE("file_dialog/toggle_favorite", "macos", KeyModifierMask::META | KeyModifierMask::CTRL | Key::F);
+ ED_SHORTCUT_OVERRIDE("file_dialog/toggle_mode", "macos", KeyModifierMask::META | KeyModifierMask::CTRL | Key::V);
+ }
+
+ HBoxContainer *pathhb = memnew(HBoxContainer);
+
+ dir_prev = memnew(Button);
+ dir_prev->set_flat(true);
+ dir_prev->set_tooltip_text(TTR("Go to previous folder."));
+ dir_next = memnew(Button);
+ dir_next->set_flat(true);
+ dir_next->set_tooltip_text(TTR("Go to next folder."));
+ dir_up = memnew(Button);
+ dir_up->set_flat(true);
+ dir_up->set_tooltip_text(TTR("Go to parent folder."));
+
+ pathhb->add_child(dir_prev);
+ pathhb->add_child(dir_next);
+ pathhb->add_child(dir_up);
+
+ dir_prev->connect("pressed", callable_mp(this, &EditorFileDialog::_go_back));
+ dir_next->connect("pressed", callable_mp(this, &EditorFileDialog::_go_forward));
+ dir_up->connect("pressed", callable_mp(this, &EditorFileDialog::_go_up));
+
+ Label *l = memnew(Label(TTR("Path:")));
+ l->set_theme_type_variation("HeaderSmall");
+ pathhb->add_child(l);
+
+ drives_container = memnew(HBoxContainer);
+ pathhb->add_child(drives_container);
+
+ dir = memnew(LineEdit);
+ dir->set_structured_text_bidi_override(TextServer::STRUCTURED_TEXT_FILE);
+ pathhb->add_child(dir);
+ dir->set_h_size_flags(Control::SIZE_EXPAND_FILL);
+
+ refresh = memnew(Button);
+ refresh->set_flat(true);
+ refresh->set_tooltip_text(TTR("Refresh files."));
+ refresh->connect("pressed", callable_mp(this, &EditorFileDialog::update_file_list));
+ pathhb->add_child(refresh);
+
+ favorite = memnew(Button);
+ favorite->set_flat(true);
+ favorite->set_toggle_mode(true);
+ favorite->set_tooltip_text(TTR("(Un)favorite current folder."));
+ favorite->connect("pressed", callable_mp(this, &EditorFileDialog::_favorite_pressed));
+ pathhb->add_child(favorite);
+
+ show_hidden = memnew(Button);
+ show_hidden->set_flat(true);
+ show_hidden->set_toggle_mode(true);
+ show_hidden->set_pressed(is_showing_hidden_files());
+ show_hidden->set_tooltip_text(TTR("Toggle the visibility of hidden files."));
+ show_hidden->connect("toggled", callable_mp(this, &EditorFileDialog::set_show_hidden_files));
+ pathhb->add_child(show_hidden);
+
+ pathhb->add_child(memnew(VSeparator));
+
+ Ref<ButtonGroup> view_mode_group;
+ view_mode_group.instantiate();
+
+ mode_thumbnails = memnew(Button);
+ mode_thumbnails->set_flat(true);
+ mode_thumbnails->connect("pressed", callable_mp(this, &EditorFileDialog::set_display_mode).bind(DISPLAY_THUMBNAILS));
+ mode_thumbnails->set_toggle_mode(true);
+ mode_thumbnails->set_pressed(display_mode == DISPLAY_THUMBNAILS);
+ mode_thumbnails->set_button_group(view_mode_group);
+ mode_thumbnails->set_tooltip_text(TTR("View items as a grid of thumbnails."));
+ pathhb->add_child(mode_thumbnails);
+
+ mode_list = memnew(Button);
+ mode_list->set_flat(true);
+ mode_list->connect("pressed", callable_mp(this, &EditorFileDialog::set_display_mode).bind(DISPLAY_LIST));
+ mode_list->set_toggle_mode(true);
+ mode_list->set_pressed(display_mode == DISPLAY_LIST);
+ mode_list->set_button_group(view_mode_group);
+ mode_list->set_tooltip_text(TTR("View items as a list."));
+ pathhb->add_child(mode_list);
+
+ shortcuts_container = memnew(HBoxContainer);
+ pathhb->add_child(shortcuts_container);
+
+ drives = memnew(OptionButton);
+ drives->connect("item_selected", callable_mp(this, &EditorFileDialog::_select_drive));
+ pathhb->add_child(drives);
+
+ makedir = memnew(Button);
+ makedir->set_text(TTR("Create Folder"));
+ makedir->connect("pressed", callable_mp(this, &EditorFileDialog::_make_dir));
+ pathhb->add_child(makedir);
+
+ list_hb = memnew(HSplitContainer);
+
+ vbc->add_child(pathhb);
+ vbc->add_child(list_hb);
+ list_hb->set_v_size_flags(Control::SIZE_EXPAND_FILL);
+
+ VSplitContainer *vsc = memnew(VSplitContainer);
+ list_hb->add_child(vsc);
+
+ VBoxContainer *fav_vb = memnew(VBoxContainer);
+ vsc->add_child(fav_vb);
+ fav_vb->set_custom_minimum_size(Size2(150, 100) * EDSCALE);
+ fav_vb->set_v_size_flags(Control::SIZE_EXPAND_FILL);
+ HBoxContainer *fav_hb = memnew(HBoxContainer);
+ fav_vb->add_child(fav_hb);
+
+ l = memnew(Label(TTR("Favorites:")));
+ l->set_theme_type_variation("HeaderSmall");
+ fav_hb->add_child(l);
+
+ fav_hb->add_spacer();
+ fav_up = memnew(Button);
+ fav_up->set_flat(true);
+ fav_hb->add_child(fav_up);
+ fav_up->connect("pressed", callable_mp(this, &EditorFileDialog::_favorite_move_up));
+ fav_down = memnew(Button);
+ fav_down->set_flat(true);
+ fav_hb->add_child(fav_down);
+ fav_down->connect("pressed", callable_mp(this, &EditorFileDialog::_favorite_move_down));
+
+ favorites = memnew(ItemList);
+ fav_vb->add_child(favorites);
+ favorites->set_v_size_flags(Control::SIZE_EXPAND_FILL);
+ favorites->connect("item_selected", callable_mp(this, &EditorFileDialog::_favorite_selected));
+
+ VBoxContainer *rec_vb = memnew(VBoxContainer);
+ vsc->add_child(rec_vb);
+ rec_vb->set_custom_minimum_size(Size2(150, 100) * EDSCALE);
+ rec_vb->set_v_size_flags(Control::SIZE_EXPAND_FILL);
+ recent = memnew(ItemList);
+ recent->set_allow_reselect(true);
+ rec_vb->add_margin_child(TTR("Recent:"), recent, true);
+ recent->connect("item_selected", callable_mp(this, &EditorFileDialog::_recent_selected));
+
+ VBoxContainer *item_vb = memnew(VBoxContainer);
+ list_hb->add_child(item_vb);
+ item_vb->set_custom_minimum_size(Size2(320, 0) * EDSCALE);
+
+ HBoxContainer *preview_hb = memnew(HBoxContainer);
+ preview_hb->set_v_size_flags(Control::SIZE_EXPAND_FILL);
+ item_vb->add_child(preview_hb);
+
+ VBoxContainer *list_vb = memnew(VBoxContainer);
+ list_vb->set_h_size_flags(Control::SIZE_EXPAND_FILL);
+
+ l = memnew(Label(TTR("Directories & Files:")));
+ l->set_theme_type_variation("HeaderSmall");
+ list_vb->add_child(l);
+ preview_hb->add_child(list_vb);
+
+ // Item (files and folders) list with context menu.
+
+ item_list = memnew(ItemList);
+ item_list->set_v_size_flags(Control::SIZE_EXPAND_FILL);
+ item_list->connect("item_clicked", callable_mp(this, &EditorFileDialog::_item_list_item_rmb_clicked));
+ item_list->connect("empty_clicked", callable_mp(this, &EditorFileDialog::_item_list_empty_clicked));
+ item_list->set_allow_rmb_select(true);
+
+ list_vb->add_child(item_list);
+
+ item_menu = memnew(PopupMenu);
+ item_menu->connect("id_pressed", callable_mp(this, &EditorFileDialog::_item_menu_id_pressed));
+ add_child(item_menu);
+
+ // Other stuff.
+
+ preview_vb = memnew(VBoxContainer);
+ preview_hb->add_child(preview_vb);
+ CenterContainer *prev_cc = memnew(CenterContainer);
+ preview_vb->add_margin_child(TTR("Preview:"), prev_cc);
+ preview = memnew(TextureRect);
+ prev_cc->add_child(preview);
+ preview_vb->hide();
+
+ file_box = memnew(HBoxContainer);
+
+ l = memnew(Label(TTR("File:")));
+ l->set_theme_type_variation("HeaderSmall");
+ file_box->add_child(l);
+
+ file = memnew(LineEdit);
+ file->set_structured_text_bidi_override(TextServer::STRUCTURED_TEXT_FILE);
+ file->set_stretch_ratio(4);
+ file->set_h_size_flags(Control::SIZE_EXPAND_FILL);
+ file_box->add_child(file);
+ filter = memnew(OptionButton);
+ filter->set_stretch_ratio(3);
+ filter->set_h_size_flags(Control::SIZE_EXPAND_FILL);
+ filter->set_clip_text(true); // Too many extensions overflow it.
+ file_box->add_child(filter);
+ file_box->set_h_size_flags(Control::SIZE_EXPAND_FILL);
+ item_vb->add_child(file_box);
+
+ dir_access = DirAccess::create(DirAccess::ACCESS_RESOURCES);
+ _update_drives();
+
+ connect("confirmed", callable_mp(this, &EditorFileDialog::_action_pressed));
+ item_list->connect("item_selected", callable_mp(this, &EditorFileDialog::_item_selected), CONNECT_DEFERRED);
+ item_list->connect("multi_selected", callable_mp(this, &EditorFileDialog::_multi_selected), CONNECT_DEFERRED);
+ item_list->connect("item_activated", callable_mp(this, &EditorFileDialog::_item_dc_selected).bind());
+ item_list->connect("empty_clicked", callable_mp(this, &EditorFileDialog::_items_clear_selection));
+ dir->connect("text_submitted", callable_mp(this, &EditorFileDialog::_dir_submitted));
+ file->connect("text_submitted", callable_mp(this, &EditorFileDialog::_file_submitted));
+ filter->connect("item_selected", callable_mp(this, &EditorFileDialog::_filter_selected));
+
+ confirm_save = memnew(ConfirmationDialog);
+ add_child(confirm_save);
+ confirm_save->connect("confirmed", callable_mp(this, &EditorFileDialog::_save_confirm_pressed));
+
+ dep_remove_dialog = memnew(DependencyRemoveDialog);
+ add_child(dep_remove_dialog);
+
+ global_remove_dialog = memnew(ConfirmationDialog);
+ global_remove_dialog->set_text(TTR("Remove the selected files? For safety only files and empty directories can be deleted from here. (Cannot be undone.)\nDepending on your filesystem configuration, the files will either be moved to the system trash or deleted permanently."));
+ global_remove_dialog->connect("confirmed", callable_mp(this, &EditorFileDialog::_delete_files_global));
+ add_child(global_remove_dialog);
+
+ makedialog = memnew(ConfirmationDialog);
+ makedialog->set_title(TTR("Create Folder"));
+ VBoxContainer *makevb = memnew(VBoxContainer);
+ makedialog->add_child(makevb);
+
+ makedirname = memnew(LineEdit);
+ makedirname->set_structured_text_bidi_override(TextServer::STRUCTURED_TEXT_FILE);
+ makevb->add_margin_child(TTR("Name:"), makedirname);
+ add_child(makedialog);
+ makedialog->register_text_enter(makedirname);
+ makedialog->connect("confirmed", callable_mp(this, &EditorFileDialog::_make_dir_confirm));
+ error_dialog = memnew(AcceptDialog);
+ add_child(error_dialog);
+
+ update_filters();
+ update_dir();
+
+ set_hide_on_ok(false);
+ vbox = vbc;
+
+ if (register_func) {
+ register_func(this);
+ }
+}
+
+EditorFileDialog::~EditorFileDialog() {
+ if (unregister_func) {
+ unregister_func(this);
+ }
+}
diff --git a/editor/gui/editor_file_dialog.h b/editor/gui/editor_file_dialog.h
new file mode 100644
index 0000000000..923c7080c5
--- /dev/null
+++ b/editor/gui/editor_file_dialog.h
@@ -0,0 +1,293 @@
+/**************************************************************************/
+/* editor_file_dialog.h */
+/**************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/**************************************************************************/
+
+#ifndef EDITOR_FILE_DIALOG_H
+#define EDITOR_FILE_DIALOG_H
+
+#include "core/io/dir_access.h"
+#include "scene/gui/dialogs.h"
+
+class DependencyRemoveDialog;
+class HSplitContainer;
+class ItemList;
+class OptionButton;
+class PopupMenu;
+class TextureRect;
+
+class EditorFileDialog : public ConfirmationDialog {
+ GDCLASS(EditorFileDialog, ConfirmationDialog);
+
+public:
+ enum DisplayMode {
+ DISPLAY_THUMBNAILS,
+ DISPLAY_LIST
+ };
+
+ enum Access {
+ ACCESS_RESOURCES,
+ ACCESS_USERDATA,
+ ACCESS_FILESYSTEM
+ };
+
+ enum FileMode {
+ FILE_MODE_OPEN_FILE,
+ FILE_MODE_OPEN_FILES,
+ FILE_MODE_OPEN_DIR,
+ FILE_MODE_OPEN_ANY,
+ FILE_MODE_SAVE_FILE
+ };
+
+ typedef Ref<Texture2D> (*GetIconFunc)(const String &);
+ typedef void (*RegisterFunc)(EditorFileDialog *);
+
+ static GetIconFunc get_icon_func;
+ static GetIconFunc get_thumbnail_func;
+ static RegisterFunc register_func;
+ static RegisterFunc unregister_func;
+
+private:
+ enum ItemMenu {
+ ITEM_MENU_COPY_PATH,
+ ITEM_MENU_DELETE,
+ ITEM_MENU_REFRESH,
+ ITEM_MENU_NEW_FOLDER,
+ ITEM_MENU_SHOW_IN_EXPLORER
+ };
+
+ ConfirmationDialog *makedialog = nullptr;
+ LineEdit *makedirname = nullptr;
+
+ Button *makedir = nullptr;
+ Access access = ACCESS_RESOURCES;
+
+ VBoxContainer *vbox = nullptr;
+ FileMode mode = FILE_MODE_SAVE_FILE;
+ bool can_create_dir = false;
+ LineEdit *dir = nullptr;
+
+ Button *dir_prev = nullptr;
+ Button *dir_next = nullptr;
+ Button *dir_up = nullptr;
+
+ HBoxContainer *drives_container = nullptr;
+ HBoxContainer *shortcuts_container = nullptr;
+ OptionButton *drives = nullptr;
+ ItemList *item_list = nullptr;
+ PopupMenu *item_menu = nullptr;
+ TextureRect *preview = nullptr;
+ VBoxContainer *preview_vb = nullptr;
+ HSplitContainer *list_hb = nullptr;
+ HBoxContainer *file_box = nullptr;
+ LineEdit *file = nullptr;
+ OptionButton *filter = nullptr;
+ AcceptDialog *error_dialog = nullptr;
+ Ref<DirAccess> dir_access;
+ ConfirmationDialog *confirm_save = nullptr;
+ DependencyRemoveDialog *dep_remove_dialog = nullptr;
+ ConfirmationDialog *global_remove_dialog = nullptr;
+
+ Button *mode_thumbnails = nullptr;
+ Button *mode_list = nullptr;
+
+ Button *refresh = nullptr;
+ Button *favorite = nullptr;
+ Button *show_hidden = nullptr;
+
+ Button *fav_up = nullptr;
+ Button *fav_down = nullptr;
+
+ ItemList *favorites = nullptr;
+ ItemList *recent = nullptr;
+
+ Vector<String> local_history;
+ int local_history_pos = 0;
+ void _push_history();
+
+ Vector<String> filters;
+
+ bool previews_enabled = true;
+ bool preview_waiting = false;
+ int preview_wheel_index = 0;
+ float preview_wheel_timeout = 0.0f;
+
+ static bool default_show_hidden_files;
+ static DisplayMode default_display_mode;
+ bool show_hidden_files;
+ DisplayMode display_mode;
+
+ bool disable_overwrite_warning = false;
+ bool is_invalidating = false;
+
+ struct ThemeCache {
+ Ref<Texture2D> parent_folder;
+ Ref<Texture2D> forward_folder;
+ Ref<Texture2D> back_folder;
+ Ref<Texture2D> reload;
+ Ref<Texture2D> toggle_hidden;
+ Ref<Texture2D> favorite;
+ Ref<Texture2D> mode_thumbnails;
+ Ref<Texture2D> mode_list;
+ Ref<Texture2D> favorites_up;
+ Ref<Texture2D> favorites_down;
+
+ Ref<Texture2D> folder;
+ Color folder_icon_color;
+
+ Ref<Texture2D> action_copy;
+ Ref<Texture2D> action_delete;
+ Ref<Texture2D> filesystem;
+
+ Ref<Texture2D> folder_medium_thumbnail;
+ Ref<Texture2D> file_medium_thumbnail;
+ Ref<Texture2D> folder_big_thumbnail;
+ Ref<Texture2D> file_big_thumbnail;
+
+ Ref<Texture2D> progress[8]{};
+ } theme_cache;
+
+ void update_dir();
+ void update_file_name();
+ void update_file_list();
+ void update_filters();
+
+ void _focus_file_text();
+
+ void _update_favorites();
+ void _favorite_pressed();
+ void _favorite_selected(int p_idx);
+ void _favorite_move_up();
+ void _favorite_move_down();
+
+ void _update_recent();
+ void _recent_selected(int p_idx);
+
+ void _item_selected(int p_item);
+ void _multi_selected(int p_item, bool p_selected);
+ void _items_clear_selection(const Vector2 &p_pos, MouseButton p_mouse_button_index);
+ void _item_dc_selected(int p_item);
+
+ void _item_list_item_rmb_clicked(int p_item, const Vector2 &p_pos, MouseButton p_mouse_button_index);
+ void _item_list_empty_clicked(const Vector2 &p_pos, MouseButton p_mouse_button_index);
+ void _item_menu_id_pressed(int p_option);
+
+ void _select_drive(int p_idx);
+ void _dir_submitted(String p_dir);
+ void _action_pressed();
+ void _save_confirm_pressed();
+ void _cancel_pressed();
+ void _filter_selected(int);
+ void _make_dir();
+ void _make_dir_confirm();
+
+ void _delete_items();
+ void _delete_files_global();
+
+ void _update_drives(bool p_select = true);
+ void _update_icons();
+
+ void _go_up();
+ void _go_back();
+ void _go_forward();
+
+ void _invalidate();
+
+ virtual void _post_popup() override;
+
+ void _save_to_recent();
+ // Callback function is callback(String p_path,Ref<Texture2D> preview,Variant udata) preview null if could not load.
+
+ void _thumbnail_result(const String &p_path, const Ref<Texture2D> &p_preview, const Ref<Texture2D> &p_small_preview, const Variant &p_udata);
+ void _thumbnail_done(const String &p_path, const Ref<Texture2D> &p_preview, const Ref<Texture2D> &p_small_preview, const Variant &p_udata);
+ void _request_single_thumbnail(const String &p_path);
+
+ virtual void shortcut_input(const Ref<InputEvent> &p_event) override;
+
+ bool _is_open_should_be_disabled();
+
+protected:
+ virtual void _update_theme_item_cache() override;
+
+ void _notification(int p_what);
+ static void _bind_methods();
+
+public:
+ // Public for use with callable_mp.
+ void _file_submitted(const String &p_file);
+
+ void popup_file_dialog();
+ void clear_filters();
+ void add_filter(const String &p_filter, const String &p_description = "");
+ void set_filters(const Vector<String> &p_filters);
+ Vector<String> get_filters() const;
+
+ void set_enable_multiple_selection(bool p_enable);
+ Vector<String> get_selected_files() const;
+
+ String get_current_dir() const;
+ String get_current_file() const;
+ String get_current_path() const;
+ void set_current_dir(const String &p_dir);
+ void set_current_file(const String &p_file);
+ void set_current_path(const String &p_path);
+
+ void set_display_mode(DisplayMode p_mode);
+ DisplayMode get_display_mode() const;
+
+ void set_file_mode(FileMode p_mode);
+ FileMode get_file_mode() const;
+
+ VBoxContainer *get_vbox();
+ LineEdit *get_line_edit() { return file; }
+
+ void set_access(Access p_access);
+ Access get_access() const;
+
+ static void set_default_show_hidden_files(bool p_show);
+ static void set_default_display_mode(DisplayMode p_mode);
+ void set_show_hidden_files(bool p_show);
+ bool is_showing_hidden_files() const;
+
+ void invalidate();
+
+ void set_disable_overwrite_warning(bool p_disable);
+ bool is_overwrite_warning_disabled() const;
+
+ void set_previews_enabled(bool p_enabled);
+ bool are_previews_enabled();
+
+ EditorFileDialog();
+ ~EditorFileDialog();
+};
+
+VARIANT_ENUM_CAST(EditorFileDialog::FileMode);
+VARIANT_ENUM_CAST(EditorFileDialog::Access);
+VARIANT_ENUM_CAST(EditorFileDialog::DisplayMode);
+
+#endif // EDITOR_FILE_DIALOG_H
diff --git a/editor/gui/editor_object_selector.cpp b/editor/gui/editor_object_selector.cpp
new file mode 100644
index 0000000000..9988e285c7
--- /dev/null
+++ b/editor/gui/editor_object_selector.cpp
@@ -0,0 +1,256 @@
+/**************************************************************************/
+/* editor_object_selector.cpp */
+/**************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/**************************************************************************/
+
+#include "editor_object_selector.h"
+
+#include "editor/editor_data.h"
+#include "editor/editor_node.h"
+#include "editor/editor_scale.h"
+#include "editor/multi_node_edit.h"
+
+Size2 EditorObjectSelector::get_minimum_size() const {
+ Ref<Font> font = get_theme_font(SNAME("font"));
+ int font_size = get_theme_font_size(SNAME("font_size"));
+ return Button::get_minimum_size() + Size2(0, font->get_height(font_size));
+}
+
+void EditorObjectSelector::_add_children_to_popup(Object *p_obj, int p_depth) {
+ if (p_depth > 8) {
+ return;
+ }
+
+ List<PropertyInfo> pinfo;
+ p_obj->get_property_list(&pinfo);
+ for (const PropertyInfo &E : pinfo) {
+ if (!(E.usage & PROPERTY_USAGE_EDITOR)) {
+ continue;
+ }
+ if (E.hint != PROPERTY_HINT_RESOURCE_TYPE) {
+ continue;
+ }
+
+ Variant value = p_obj->get(E.name);
+ if (value.get_type() != Variant::OBJECT) {
+ continue;
+ }
+ Object *obj = value;
+ if (!obj) {
+ continue;
+ }
+
+ Ref<Texture2D> obj_icon = EditorNode::get_singleton()->get_object_icon(obj);
+
+ String proper_name = "";
+ Vector<String> name_parts = E.name.split("/");
+
+ for (int i = 0; i < name_parts.size(); i++) {
+ if (i > 0) {
+ proper_name += " > ";
+ }
+ proper_name += name_parts[i].capitalize();
+ }
+
+ int index = sub_objects_menu->get_item_count();
+ sub_objects_menu->add_icon_item(obj_icon, proper_name, objects.size());
+ sub_objects_menu->set_item_indent(index, p_depth);
+ objects.push_back(obj->get_instance_id());
+
+ _add_children_to_popup(obj, p_depth + 1);
+ }
+}
+
+void EditorObjectSelector::_show_popup() {
+ if (sub_objects_menu->is_visible()) {
+ sub_objects_menu->hide();
+ return;
+ }
+
+ sub_objects_menu->clear();
+
+ Size2 size = get_size();
+ Point2 gp = get_screen_position();
+ gp.y += size.y;
+
+ sub_objects_menu->set_position(gp);
+ sub_objects_menu->set_size(Size2(size.width, 1));
+ sub_objects_menu->set_parent_rect(Rect2(Point2(gp - sub_objects_menu->get_position()), size));
+
+ sub_objects_menu->take_mouse_focus();
+ sub_objects_menu->popup();
+}
+
+void EditorObjectSelector::_about_to_show() {
+ Object *obj = ObjectDB::get_instance(history->get_path_object(history->get_path_size() - 1));
+ if (!obj) {
+ return;
+ }
+
+ objects.clear();
+
+ _add_children_to_popup(obj);
+ if (sub_objects_menu->get_item_count() == 0) {
+ sub_objects_menu->add_item(TTR("No sub-resources found."));
+ sub_objects_menu->set_item_disabled(0, true);
+ }
+}
+
+void EditorObjectSelector::update_path() {
+ for (int i = 0; i < history->get_path_size(); i++) {
+ Object *obj = ObjectDB::get_instance(history->get_path_object(i));
+ if (!obj) {
+ continue;
+ }
+
+ Ref<Texture2D> obj_icon;
+ if (Object::cast_to<MultiNodeEdit>(obj)) {
+ obj_icon = EditorNode::get_singleton()->get_class_icon(Object::cast_to<MultiNodeEdit>(obj)->get_edited_class_name());
+ } else {
+ obj_icon = EditorNode::get_singleton()->get_object_icon(obj);
+ }
+
+ if (obj_icon.is_valid()) {
+ current_object_icon->set_texture(obj_icon);
+ }
+
+ if (i == history->get_path_size() - 1) {
+ String name;
+ if (obj->has_method("_get_editor_name")) {
+ name = obj->call("_get_editor_name");
+ } else if (Object::cast_to<Resource>(obj)) {
+ Resource *r = Object::cast_to<Resource>(obj);
+ if (r->get_path().is_resource_file()) {
+ name = r->get_path().get_file();
+ } else {
+ name = r->get_name();
+ }
+
+ if (name.is_empty()) {
+ name = r->get_class();
+ }
+ } else if (obj->is_class("EditorDebuggerRemoteObject")) {
+ name = obj->call("get_title");
+ } else if (Object::cast_to<Node>(obj)) {
+ name = Object::cast_to<Node>(obj)->get_name();
+ } else if (Object::cast_to<Resource>(obj) && !Object::cast_to<Resource>(obj)->get_name().is_empty()) {
+ name = Object::cast_to<Resource>(obj)->get_name();
+ } else {
+ name = obj->get_class();
+ }
+
+ current_object_label->set_text(name);
+ set_tooltip_text(obj->get_class());
+ }
+ }
+}
+
+void EditorObjectSelector::clear_path() {
+ set_disabled(true);
+ set_tooltip_text("");
+
+ current_object_label->set_text("");
+ current_object_icon->set_texture(nullptr);
+ sub_objects_icon->hide();
+}
+
+void EditorObjectSelector::enable_path() {
+ set_disabled(false);
+ sub_objects_icon->show();
+}
+
+void EditorObjectSelector::_id_pressed(int p_idx) {
+ ERR_FAIL_INDEX(p_idx, objects.size());
+
+ Object *obj = ObjectDB::get_instance(objects[p_idx]);
+ if (!obj) {
+ return;
+ }
+
+ EditorNode::get_singleton()->push_item(obj);
+}
+
+void EditorObjectSelector::_notification(int p_what) {
+ switch (p_what) {
+ case NOTIFICATION_ENTER_TREE:
+ case NOTIFICATION_THEME_CHANGED: {
+ update_path();
+
+ int icon_size = get_theme_constant(SNAME("class_icon_size"), SNAME("Editor"));
+
+ current_object_icon->set_custom_minimum_size(Size2(icon_size, icon_size));
+ current_object_label->add_theme_font_override("font", get_theme_font(SNAME("main"), SNAME("EditorFonts")));
+ sub_objects_icon->set_texture(get_theme_icon(SNAME("arrow"), SNAME("OptionButton")));
+ sub_objects_menu->add_theme_constant_override("icon_max_width", icon_size);
+ } break;
+
+ case NOTIFICATION_READY: {
+ connect("pressed", callable_mp(this, &EditorObjectSelector::_show_popup));
+ } break;
+ }
+}
+
+void EditorObjectSelector::_bind_methods() {
+}
+
+EditorObjectSelector::EditorObjectSelector(EditorSelectionHistory *p_history) {
+ history = p_history;
+
+ MarginContainer *main_mc = memnew(MarginContainer);
+ main_mc->set_anchors_and_offsets_preset(PRESET_FULL_RECT);
+ main_mc->add_theme_constant_override("margin_left", 4 * EDSCALE);
+ main_mc->add_theme_constant_override("margin_right", 6 * EDSCALE);
+ add_child(main_mc);
+
+ HBoxContainer *main_hb = memnew(HBoxContainer);
+ main_mc->add_child(main_hb);
+
+ current_object_icon = memnew(TextureRect);
+ current_object_icon->set_stretch_mode(TextureRect::STRETCH_KEEP_ASPECT_CENTERED);
+ current_object_icon->set_expand_mode(TextureRect::EXPAND_IGNORE_SIZE);
+ main_hb->add_child(current_object_icon);
+
+ current_object_label = memnew(Label);
+ current_object_label->set_text_overrun_behavior(TextServer::OVERRUN_TRIM_ELLIPSIS);
+ current_object_label->set_h_size_flags(SIZE_EXPAND_FILL);
+ current_object_label->set_auto_translate(false);
+ main_hb->add_child(current_object_label);
+
+ sub_objects_icon = memnew(TextureRect);
+ sub_objects_icon->hide();
+ sub_objects_icon->set_stretch_mode(TextureRect::STRETCH_KEEP_CENTERED);
+ main_hb->add_child(sub_objects_icon);
+
+ sub_objects_menu = memnew(PopupMenu);
+ sub_objects_menu->set_auto_translate(false);
+ add_child(sub_objects_menu);
+ sub_objects_menu->connect("about_to_popup", callable_mp(this, &EditorObjectSelector::_about_to_show));
+ sub_objects_menu->connect("id_pressed", callable_mp(this, &EditorObjectSelector::_id_pressed));
+
+ set_tooltip_text(TTR("Open a list of sub-resources."));
+}
diff --git a/editor/gui/editor_object_selector.h b/editor/gui/editor_object_selector.h
new file mode 100644
index 0000000000..72ff285cf6
--- /dev/null
+++ b/editor/gui/editor_object_selector.h
@@ -0,0 +1,73 @@
+/**************************************************************************/
+/* editor_object_selector.h */
+/**************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/**************************************************************************/
+
+#ifndef EDITOR_OBJECT_SELECTOR_H
+#define EDITOR_OBJECT_SELECTOR_H
+
+#include "scene/gui/box_container.h"
+#include "scene/gui/button.h"
+#include "scene/gui/label.h"
+#include "scene/gui/popup_menu.h"
+#include "scene/gui/texture_rect.h"
+
+class EditorSelectionHistory;
+
+class EditorObjectSelector : public Button {
+ GDCLASS(EditorObjectSelector, Button);
+
+ EditorSelectionHistory *history = nullptr;
+
+ TextureRect *current_object_icon = nullptr;
+ Label *current_object_label = nullptr;
+ TextureRect *sub_objects_icon = nullptr;
+ PopupMenu *sub_objects_menu = nullptr;
+
+ Vector<ObjectID> objects;
+
+ void _show_popup();
+ void _id_pressed(int p_idx);
+ void _about_to_show();
+ void _add_children_to_popup(Object *p_obj, int p_depth = 0);
+
+protected:
+ void _notification(int p_what);
+ static void _bind_methods();
+
+public:
+ virtual Size2 get_minimum_size() const override;
+
+ void update_path();
+ void clear_path();
+ void enable_path();
+
+ EditorObjectSelector(EditorSelectionHistory *p_history);
+};
+
+#endif // EDITOR_OBJECT_SELECTOR_H
diff --git a/editor/gui/editor_spin_slider.cpp b/editor/gui/editor_spin_slider.cpp
new file mode 100644
index 0000000000..2e67893fac
--- /dev/null
+++ b/editor/gui/editor_spin_slider.cpp
@@ -0,0 +1,699 @@
+/**************************************************************************/
+/* editor_spin_slider.cpp */
+/**************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/**************************************************************************/
+
+#include "editor_spin_slider.h"
+
+#include "core/input/input.h"
+#include "core/math/expression.h"
+#include "core/os/keyboard.h"
+#include "editor/editor_scale.h"
+
+String EditorSpinSlider::get_tooltip(const Point2 &p_pos) const {
+ if (grabber->is_visible()) {
+ Key key = (OS::get_singleton()->has_feature("macos") || OS::get_singleton()->has_feature("web_macos") || OS::get_singleton()->has_feature("web_ios")) ? Key::META : Key::CTRL;
+ return TS->format_number(rtos(get_value())) + "\n\n" + vformat(TTR("Hold %s to round to integers.\nHold Shift for more precise changes."), find_keycode_name(key));
+ }
+ return TS->format_number(rtos(get_value()));
+}
+
+String EditorSpinSlider::get_text_value() const {
+ return TS->format_number(String::num(get_value(), Math::range_step_decimals(get_step())));
+}
+
+void EditorSpinSlider::gui_input(const Ref<InputEvent> &p_event) {
+ ERR_FAIL_COND(p_event.is_null());
+
+ if (read_only) {
+ return;
+ }
+
+ Ref<InputEventMouseButton> mb = p_event;
+ if (mb.is_valid()) {
+ if (mb->get_button_index() == MouseButton::LEFT) {
+ if (mb->is_pressed()) {
+ if (updown_offset != -1 && mb->get_position().x > updown_offset) {
+ //there is an updown, so use it.
+ if (mb->get_position().y < get_size().height / 2) {
+ set_value(get_value() + get_step());
+ } else {
+ set_value(get_value() - get_step());
+ }
+ return;
+ } else {
+ grabbing_spinner_attempt = true;
+ grabbing_spinner_dist_cache = 0;
+ pre_grab_value = get_value();
+ grabbing_spinner = false;
+ grabbing_spinner_mouse_pos = get_global_mouse_position();
+ emit_signal("grabbed");
+ }
+ } else {
+ if (grabbing_spinner_attempt) {
+ if (grabbing_spinner) {
+ Input::get_singleton()->set_mouse_mode(Input::MOUSE_MODE_VISIBLE);
+ Input::get_singleton()->warp_mouse(grabbing_spinner_mouse_pos);
+ queue_redraw();
+ emit_signal("ungrabbed");
+ } else {
+ _focus_entered();
+ }
+
+ grabbing_spinner = false;
+ grabbing_spinner_attempt = false;
+ }
+ }
+ } else if (mb->get_button_index() == MouseButton::WHEEL_UP || mb->get_button_index() == MouseButton::WHEEL_DOWN) {
+ if (grabber->is_visible()) {
+ call_deferred(SNAME("queue_redraw"));
+ }
+ }
+ }
+
+ Ref<InputEventMouseMotion> mm = p_event;
+ if (mm.is_valid()) {
+ if (grabbing_spinner_attempt) {
+ double diff_x = mm->get_relative().x;
+ if (mm->is_shift_pressed() && grabbing_spinner) {
+ diff_x *= 0.1;
+ }
+ grabbing_spinner_dist_cache += diff_x;
+
+ if (!grabbing_spinner && ABS(grabbing_spinner_dist_cache) > 4 * EDSCALE) {
+ Input::get_singleton()->set_mouse_mode(Input::MOUSE_MODE_CAPTURED);
+ grabbing_spinner = true;
+ }
+
+ if (grabbing_spinner) {
+ // Don't make the user scroll all the way back to 'in range' if they went off the end.
+ if (pre_grab_value < get_min() && !is_lesser_allowed()) {
+ pre_grab_value = get_min();
+ }
+ if (pre_grab_value > get_max() && !is_greater_allowed()) {
+ pre_grab_value = get_max();
+ }
+
+ if (mm->is_command_or_control_pressed()) {
+ // If control was just pressed, don't make the value do a huge jump in magnitude.
+ if (grabbing_spinner_dist_cache != 0) {
+ pre_grab_value += grabbing_spinner_dist_cache * get_step();
+ grabbing_spinner_dist_cache = 0;
+ }
+
+ set_value(Math::round(pre_grab_value + get_step() * grabbing_spinner_dist_cache * 10));
+ } else {
+ set_value(pre_grab_value + get_step() * grabbing_spinner_dist_cache);
+ }
+ }
+ } else if (updown_offset != -1) {
+ bool new_hover = (mm->get_position().x > updown_offset);
+ if (new_hover != hover_updown) {
+ hover_updown = new_hover;
+ queue_redraw();
+ }
+ }
+ }
+
+ Ref<InputEventKey> k = p_event;
+ if (k.is_valid() && k->is_pressed() && k->is_action("ui_accept", true)) {
+ _focus_entered();
+ }
+}
+
+void EditorSpinSlider::_grabber_gui_input(const Ref<InputEvent> &p_event) {
+ if (read_only) {
+ return;
+ }
+
+ Ref<InputEventMouseButton> mb = p_event;
+
+ if (is_read_only()) {
+ return;
+ }
+
+ if (grabbing_grabber) {
+ if (mb.is_valid()) {
+ if (mb->get_button_index() == MouseButton::WHEEL_UP) {
+ set_value(get_value() + get_step());
+ mousewheel_over_grabber = true;
+ } else if (mb->get_button_index() == MouseButton::WHEEL_DOWN) {
+ set_value(get_value() - get_step());
+ mousewheel_over_grabber = true;
+ }
+ }
+ }
+
+ if (mb.is_valid() && mb->get_button_index() == MouseButton::LEFT) {
+ if (mb->is_pressed()) {
+ grabbing_grabber = true;
+ if (!mousewheel_over_grabber) {
+ grabbing_ratio = get_as_ratio();
+ grabbing_from = grabber->get_transform().xform(mb->get_position()).x;
+ }
+ emit_signal("grabbed");
+ } else {
+ grabbing_grabber = false;
+ mousewheel_over_grabber = false;
+ emit_signal("ungrabbed");
+ }
+ }
+
+ Ref<InputEventMouseMotion> mm = p_event;
+ if (mm.is_valid() && grabbing_grabber) {
+ if (mousewheel_over_grabber) {
+ return;
+ }
+
+ float scale_x = get_global_transform_with_canvas().get_scale().x;
+ ERR_FAIL_COND(Math::is_zero_approx(scale_x));
+ float grabbing_ofs = (grabber->get_transform().xform(mm->get_position()).x - grabbing_from) / float(grabber_range) / scale_x;
+ set_as_ratio(grabbing_ratio + grabbing_ofs);
+ queue_redraw();
+ }
+}
+
+void EditorSpinSlider::_value_input_gui_input(const Ref<InputEvent> &p_event) {
+ Ref<InputEventKey> k = p_event;
+ if (k.is_valid() && k->is_pressed() && !is_read_only()) {
+ double step = get_step();
+ double real_step = step;
+ if (step < 1) {
+ double divisor = 1.0 / get_step();
+
+ if (trunc(divisor) == divisor) {
+ step = 1.0;
+ }
+ }
+
+ if (k->is_ctrl_pressed()) {
+ step *= 100.0;
+ } else if (k->is_shift_pressed()) {
+ step *= 10.0;
+ } else if (k->is_alt_pressed()) {
+ step *= 0.1;
+ }
+
+ Key code = k->get_keycode();
+ switch (code) {
+ case Key::UP: {
+ _evaluate_input_text();
+
+ double last_value = get_value();
+ set_value(last_value + step);
+ double new_value = get_value();
+
+ if (new_value < CLAMP(last_value + step, get_min(), get_max())) {
+ set_value(last_value + real_step);
+ }
+
+ value_input_dirty = true;
+ set_process_internal(true);
+ } break;
+ case Key::DOWN: {
+ _evaluate_input_text();
+
+ double last_value = get_value();
+ set_value(last_value - step);
+ double new_value = get_value();
+
+ if (new_value > CLAMP(last_value - step, get_min(), get_max())) {
+ set_value(last_value - real_step);
+ }
+
+ value_input_dirty = true;
+ set_process_internal(true);
+ } break;
+ case Key::ESCAPE: {
+ value_input_closed_frame = Engine::get_singleton()->get_frames_drawn();
+ if (value_input_popup) {
+ value_input_popup->hide();
+ }
+ } break;
+ default:
+ break;
+ }
+ }
+}
+
+void EditorSpinSlider::_update_value_input_stylebox() {
+ if (!value_input) {
+ return;
+ }
+
+ // Add a left margin to the stylebox to make the number align with the Label
+ // when it's edited. The LineEdit "focus" stylebox uses the "normal" stylebox's
+ // default margins.
+ Ref<StyleBox> stylebox = get_theme_stylebox(SNAME("normal"), SNAME("LineEdit"))->duplicate();
+ // EditorSpinSliders with a label have more space on the left, so add an
+ // higher margin to match the location where the text begins.
+ // The margin values below were determined by empirical testing.
+ if (is_layout_rtl()) {
+ stylebox->set_content_margin(SIDE_LEFT, 0);
+ stylebox->set_content_margin(SIDE_RIGHT, (!get_label().is_empty() ? 23 : 16) * EDSCALE);
+ } else {
+ stylebox->set_content_margin(SIDE_LEFT, (!get_label().is_empty() ? 23 : 16) * EDSCALE);
+ stylebox->set_content_margin(SIDE_RIGHT, 0);
+ }
+
+ value_input->add_theme_style_override("normal", stylebox);
+}
+
+void EditorSpinSlider::_draw_spin_slider() {
+ updown_offset = -1;
+
+ RID ci = get_canvas_item();
+ bool rtl = is_layout_rtl();
+ Vector2 size = get_size();
+
+ Ref<StyleBox> sb = get_theme_stylebox(is_read_only() ? SNAME("read_only") : SNAME("normal"), SNAME("LineEdit"));
+ if (!flat) {
+ draw_style_box(sb, Rect2(Vector2(), size));
+ }
+ Ref<Font> font = get_theme_font(SNAME("font"), SNAME("LineEdit"));
+ int font_size = get_theme_font_size(SNAME("font_size"), SNAME("LineEdit"));
+ int sep_base = 4 * EDSCALE;
+ int sep = sep_base + sb->get_offset().x; //make it have the same margin on both sides, looks better
+
+ int label_width = font->get_string_size(label, HORIZONTAL_ALIGNMENT_LEFT, -1, font_size).width;
+ int number_width = size.width - sb->get_minimum_size().width - label_width - sep;
+
+ Ref<Texture2D> updown = get_theme_icon(is_read_only() ? SNAME("updown_disabled") : SNAME("updown"), SNAME("SpinBox"));
+
+ String numstr = get_text_value();
+
+ int vofs = (size.height - font->get_height(font_size)) / 2 + font->get_ascent(font_size);
+
+ Color fc = get_theme_color(is_read_only() ? SNAME("font_uneditable_color") : SNAME("font_color"), SNAME("LineEdit"));
+ Color lc = get_theme_color(is_read_only() ? SNAME("read_only_label_color") : SNAME("label_color"));
+
+ if (flat && !label.is_empty()) {
+ Ref<StyleBox> label_bg = get_theme_stylebox(SNAME("label_bg"), SNAME("EditorSpinSlider"));
+ if (rtl) {
+ draw_style_box(label_bg, Rect2(Vector2(size.width - (sb->get_offset().x * 2 + label_width), 0), Vector2(sb->get_offset().x * 2 + label_width, size.height)));
+ } else {
+ draw_style_box(label_bg, Rect2(Vector2(), Vector2(sb->get_offset().x * 2 + label_width, size.height)));
+ }
+ }
+
+ if (has_focus()) {
+ Ref<StyleBox> focus = get_theme_stylebox(SNAME("focus"), SNAME("LineEdit"));
+ draw_style_box(focus, Rect2(Vector2(), size));
+ }
+
+ if (rtl) {
+ draw_string(font, Vector2(Math::round(size.width - sb->get_offset().x - label_width), vofs), label, HORIZONTAL_ALIGNMENT_RIGHT, -1, font_size, lc * Color(1, 1, 1, 0.5));
+ } else {
+ draw_string(font, Vector2(Math::round(sb->get_offset().x), vofs), label, HORIZONTAL_ALIGNMENT_LEFT, -1, font_size, lc * Color(1, 1, 1, 0.5));
+ }
+
+ int suffix_start = numstr.length();
+ RID num_rid = TS->create_shaped_text();
+ TS->shaped_text_add_string(num_rid, numstr + U"\u2009" + suffix, font->get_rids(), font_size, font->get_opentype_features());
+ for (int i = 0; i < TextServer::SPACING_MAX; i++) {
+ TS->shaped_text_set_spacing(num_rid, TextServer::SpacingType(i), font->get_spacing(TextServer::SpacingType(i)));
+ }
+
+ float text_start = rtl ? Math::round(sb->get_offset().x) : Math::round(sb->get_offset().x + label_width + sep);
+ Vector2 text_ofs = rtl ? Vector2(text_start + (number_width - TS->shaped_text_get_width(num_rid)), vofs) : Vector2(text_start, vofs);
+ int v_size = TS->shaped_text_get_glyph_count(num_rid);
+ const Glyph *glyphs = TS->shaped_text_get_glyphs(num_rid);
+ for (int i = 0; i < v_size; i++) {
+ for (int j = 0; j < glyphs[i].repeat; j++) {
+ if (text_ofs.x >= text_start && (text_ofs.x + glyphs[i].advance) <= (text_start + number_width)) {
+ Color color = fc;
+ if (glyphs[i].start >= suffix_start) {
+ color.a *= 0.4;
+ }
+ if (glyphs[i].font_rid != RID()) {
+ TS->font_draw_glyph(glyphs[i].font_rid, ci, glyphs[i].font_size, text_ofs + Vector2(glyphs[i].x_off, glyphs[i].y_off), glyphs[i].index, color);
+ } else if ((glyphs[i].flags & TextServer::GRAPHEME_IS_VIRTUAL) != TextServer::GRAPHEME_IS_VIRTUAL) {
+ TS->draw_hex_code_box(ci, glyphs[i].font_size, text_ofs + Vector2(glyphs[i].x_off, glyphs[i].y_off), glyphs[i].index, color);
+ }
+ }
+ text_ofs.x += glyphs[i].advance;
+ }
+ }
+ TS->free_rid(num_rid);
+
+ if (!hide_slider) {
+ if (get_step() == 1) {
+ Ref<Texture2D> updown2 = get_theme_icon(is_read_only() ? SNAME("updown_disabled") : SNAME("updown"), SNAME("SpinBox"));
+ int updown_vofs = (size.height - updown2->get_height()) / 2;
+ if (rtl) {
+ updown_offset = sb->get_margin(SIDE_LEFT);
+ } else {
+ updown_offset = size.width - sb->get_margin(SIDE_RIGHT) - updown2->get_width();
+ }
+ Color c(1, 1, 1);
+ if (hover_updown) {
+ c *= Color(1.2, 1.2, 1.2);
+ }
+ draw_texture(updown2, Vector2(updown_offset, updown_vofs), c);
+ if (grabber->is_visible()) {
+ grabber->hide();
+ }
+ } else {
+ const int grabber_w = 4 * EDSCALE;
+ const int width = size.width - sb->get_minimum_size().width - grabber_w;
+ const int ofs = sb->get_offset().x;
+ const int svofs = (size.height + vofs) / 2 - 1;
+ Color c = fc;
+
+ // Draw the horizontal slider's background.
+ c.a = 0.2;
+ draw_rect(Rect2(ofs, svofs + 1, width, 2 * EDSCALE), c);
+
+ // Draw the horizontal slider's filled part on the left.
+ const int gofs = get_as_ratio() * width;
+ c.a = 0.45;
+ draw_rect(Rect2(ofs, svofs + 1, gofs, 2 * EDSCALE), c);
+
+ // Draw the horizontal slider's grabber.
+ c.a = 0.9;
+ const Rect2 grabber_rect = Rect2(ofs + gofs, svofs, grabber_w, 4 * EDSCALE);
+ draw_rect(grabber_rect, c);
+
+ grabbing_spinner_mouse_pos = get_global_position() + grabber_rect.get_center();
+
+ bool display_grabber = (grabbing_grabber || mouse_over_spin || mouse_over_grabber) && !grabbing_spinner && !(value_input_popup && value_input_popup->is_visible());
+ if (grabber->is_visible() != display_grabber) {
+ if (display_grabber) {
+ grabber->show();
+ } else {
+ grabber->hide();
+ }
+ }
+
+ if (display_grabber) {
+ Ref<Texture2D> grabber_tex;
+ if (mouse_over_grabber) {
+ grabber_tex = get_theme_icon(SNAME("grabber_highlight"), SNAME("HSlider"));
+ } else {
+ grabber_tex = get_theme_icon(SNAME("grabber"), SNAME("HSlider"));
+ }
+
+ if (grabber->get_texture() != grabber_tex) {
+ grabber->set_texture(grabber_tex);
+ }
+
+ Vector2 scale = get_global_transform_with_canvas().get_scale();
+ grabber->set_scale(scale);
+ grabber->reset_size();
+ grabber->set_position(get_global_position() + (grabber_rect.get_center() - grabber->get_size() * 0.5) * scale);
+
+ if (mousewheel_over_grabber) {
+ Input::get_singleton()->warp_mouse(grabber->get_position() + grabber_rect.size);
+ }
+
+ grabber_range = width;
+ }
+ }
+ }
+}
+
+void EditorSpinSlider::_notification(int p_what) {
+ switch (p_what) {
+ case NOTIFICATION_ENTER_TREE:
+ case NOTIFICATION_THEME_CHANGED: {
+ _update_value_input_stylebox();
+ } break;
+
+ case NOTIFICATION_INTERNAL_PROCESS: {
+ if (value_input_dirty) {
+ value_input_dirty = false;
+ value_input->set_text(get_text_value());
+ }
+ set_process_internal(false);
+ } break;
+
+ case NOTIFICATION_DRAW: {
+ _draw_spin_slider();
+ } break;
+
+ case NOTIFICATION_WM_WINDOW_FOCUS_IN:
+ case NOTIFICATION_WM_WINDOW_FOCUS_OUT:
+ case NOTIFICATION_WM_CLOSE_REQUEST:
+ case NOTIFICATION_EXIT_TREE: {
+ if (grabbing_spinner) {
+ grabber->hide();
+ Input::get_singleton()->set_mouse_mode(Input::MOUSE_MODE_VISIBLE);
+ Input::get_singleton()->warp_mouse(grabbing_spinner_mouse_pos);
+ grabbing_spinner = false;
+ grabbing_spinner_attempt = false;
+ }
+ } break;
+
+ case NOTIFICATION_MOUSE_ENTER: {
+ mouse_over_spin = true;
+ queue_redraw();
+ } break;
+
+ case NOTIFICATION_MOUSE_EXIT: {
+ mouse_over_spin = false;
+ queue_redraw();
+ } break;
+
+ case NOTIFICATION_FOCUS_ENTER: {
+ if ((Input::get_singleton()->is_action_pressed("ui_focus_next") || Input::get_singleton()->is_action_pressed("ui_focus_prev")) && value_input_closed_frame != Engine::get_singleton()->get_frames_drawn()) {
+ _focus_entered();
+ }
+ value_input_closed_frame = 0;
+ } break;
+ }
+}
+
+LineEdit *EditorSpinSlider::get_line_edit() {
+ _ensure_input_popup();
+ return value_input;
+}
+
+Size2 EditorSpinSlider::get_minimum_size() const {
+ Ref<StyleBox> sb = get_theme_stylebox(SNAME("normal"), SNAME("LineEdit"));
+ Ref<Font> font = get_theme_font(SNAME("font"), SNAME("LineEdit"));
+ int font_size = get_theme_font_size(SNAME("font_size"), SNAME("LineEdit"));
+
+ Size2 ms = sb->get_minimum_size();
+ ms.height += font->get_height(font_size);
+
+ return ms;
+}
+
+void EditorSpinSlider::set_hide_slider(bool p_hide) {
+ hide_slider = p_hide;
+ queue_redraw();
+}
+
+bool EditorSpinSlider::is_hiding_slider() const {
+ return hide_slider;
+}
+
+void EditorSpinSlider::set_label(const String &p_label) {
+ label = p_label;
+ queue_redraw();
+}
+
+String EditorSpinSlider::get_label() const {
+ return label;
+}
+
+void EditorSpinSlider::set_suffix(const String &p_suffix) {
+ suffix = p_suffix;
+ queue_redraw();
+}
+
+String EditorSpinSlider::get_suffix() const {
+ return suffix;
+}
+
+void EditorSpinSlider::_evaluate_input_text() {
+ // Replace comma with dot to support it as decimal separator (GH-6028).
+ // This prevents using functions like `pow()`, but using functions
+ // in EditorSpinSlider is a barely known (and barely used) feature.
+ // Instead, we'd rather support German/French keyboard layouts out of the box.
+ const String text = TS->parse_number(value_input->get_text().replace(",", "."));
+
+ Ref<Expression> expr;
+ expr.instantiate();
+ Error err = expr->parse(text);
+ if (err != OK) {
+ return;
+ }
+
+ Variant v = expr->execute(Array(), nullptr, false, true);
+ if (v.get_type() == Variant::NIL) {
+ return;
+ }
+ set_value(v);
+}
+
+//text_submitted signal
+void EditorSpinSlider::_value_input_submitted(const String &p_text) {
+ value_input_closed_frame = Engine::get_singleton()->get_frames_drawn();
+ if (value_input_popup) {
+ value_input_popup->hide();
+ }
+}
+
+//modal_closed signal
+void EditorSpinSlider::_value_input_closed() {
+ _evaluate_input_text();
+ value_input_closed_frame = Engine::get_singleton()->get_frames_drawn();
+}
+
+//focus_exited signal
+void EditorSpinSlider::_value_focus_exited() {
+ // discontinue because the focus_exit was caused by right-click context menu
+ if (value_input->is_menu_visible()) {
+ return;
+ }
+
+ _evaluate_input_text();
+ // focus is not on the same element after the vlalue_input was exited
+ // -> focus is on next element
+ // -> TAB was pressed
+ // -> modal_close was not called
+ // -> need to close/hide manually
+ if (value_input_closed_frame != Engine::get_singleton()->get_frames_drawn()) {
+ if (value_input_popup) {
+ value_input_popup->hide();
+ }
+ //tab was pressed
+ } else {
+ //enter, click, esc
+ grab_focus();
+ }
+
+ emit_signal("value_focus_exited");
+}
+
+void EditorSpinSlider::_grabber_mouse_entered() {
+ mouse_over_grabber = true;
+ queue_redraw();
+}
+
+void EditorSpinSlider::_grabber_mouse_exited() {
+ mouse_over_grabber = false;
+ queue_redraw();
+}
+
+void EditorSpinSlider::set_read_only(bool p_enable) {
+ read_only = p_enable;
+ queue_redraw();
+}
+
+bool EditorSpinSlider::is_read_only() const {
+ return read_only;
+}
+
+void EditorSpinSlider::set_flat(bool p_enable) {
+ flat = p_enable;
+ queue_redraw();
+}
+
+bool EditorSpinSlider::is_flat() const {
+ return flat;
+}
+
+bool EditorSpinSlider::is_grabbing() const {
+ return grabbing_grabber || grabbing_spinner;
+}
+
+void EditorSpinSlider::_focus_entered() {
+ _ensure_input_popup();
+ value_input->set_text(get_text_value());
+ value_input_popup->set_size(get_size());
+ value_input_popup->call_deferred(SNAME("show"));
+ value_input->call_deferred(SNAME("grab_focus"));
+ value_input->call_deferred(SNAME("select_all"));
+ value_input->set_focus_next(find_next_valid_focus()->get_path());
+ value_input->set_focus_previous(find_prev_valid_focus()->get_path());
+ emit_signal("value_focus_entered");
+}
+
+void EditorSpinSlider::_bind_methods() {
+ ClassDB::bind_method(D_METHOD("set_label", "label"), &EditorSpinSlider::set_label);
+ ClassDB::bind_method(D_METHOD("get_label"), &EditorSpinSlider::get_label);
+
+ ClassDB::bind_method(D_METHOD("set_suffix", "suffix"), &EditorSpinSlider::set_suffix);
+ ClassDB::bind_method(D_METHOD("get_suffix"), &EditorSpinSlider::get_suffix);
+
+ ClassDB::bind_method(D_METHOD("set_read_only", "read_only"), &EditorSpinSlider::set_read_only);
+ ClassDB::bind_method(D_METHOD("is_read_only"), &EditorSpinSlider::is_read_only);
+
+ ClassDB::bind_method(D_METHOD("set_flat", "flat"), &EditorSpinSlider::set_flat);
+ ClassDB::bind_method(D_METHOD("is_flat"), &EditorSpinSlider::is_flat);
+
+ ClassDB::bind_method(D_METHOD("set_hide_slider", "hide_slider"), &EditorSpinSlider::set_hide_slider);
+ ClassDB::bind_method(D_METHOD("is_hiding_slider"), &EditorSpinSlider::is_hiding_slider);
+
+ ADD_PROPERTY(PropertyInfo(Variant::STRING, "label"), "set_label", "get_label");
+ ADD_PROPERTY(PropertyInfo(Variant::STRING, "suffix"), "set_suffix", "get_suffix");
+ ADD_PROPERTY(PropertyInfo(Variant::BOOL, "read_only"), "set_read_only", "is_read_only");
+ ADD_PROPERTY(PropertyInfo(Variant::BOOL, "flat"), "set_flat", "is_flat");
+ ADD_PROPERTY(PropertyInfo(Variant::BOOL, "hide_slider"), "set_hide_slider", "is_hiding_slider");
+
+ ADD_SIGNAL(MethodInfo("grabbed"));
+ ADD_SIGNAL(MethodInfo("ungrabbed"));
+ ADD_SIGNAL(MethodInfo("value_focus_entered"));
+ ADD_SIGNAL(MethodInfo("value_focus_exited"));
+}
+
+void EditorSpinSlider::_ensure_input_popup() {
+ if (value_input_popup) {
+ return;
+ }
+
+ value_input_popup = memnew(Control);
+ add_child(value_input_popup);
+
+ value_input = memnew(LineEdit);
+ value_input->set_focus_mode(FOCUS_CLICK);
+ value_input_popup->add_child(value_input);
+ value_input->set_anchors_and_offsets_preset(PRESET_FULL_RECT);
+ value_input_popup->connect("hidden", callable_mp(this, &EditorSpinSlider::_value_input_closed));
+ value_input->connect("text_submitted", callable_mp(this, &EditorSpinSlider::_value_input_submitted));
+ value_input->connect("focus_exited", callable_mp(this, &EditorSpinSlider::_value_focus_exited));
+ value_input->connect("gui_input", callable_mp(this, &EditorSpinSlider::_value_input_gui_input));
+
+ if (is_inside_tree()) {
+ _update_value_input_stylebox();
+ }
+}
+
+EditorSpinSlider::EditorSpinSlider() {
+ set_focus_mode(FOCUS_ALL);
+ grabber = memnew(TextureRect);
+ add_child(grabber);
+ grabber->hide();
+ grabber->set_as_top_level(true);
+ grabber->set_mouse_filter(MOUSE_FILTER_STOP);
+ grabber->connect("mouse_entered", callable_mp(this, &EditorSpinSlider::_grabber_mouse_entered));
+ grabber->connect("mouse_exited", callable_mp(this, &EditorSpinSlider::_grabber_mouse_exited));
+ grabber->connect("gui_input", callable_mp(this, &EditorSpinSlider::_grabber_gui_input));
+}
diff --git a/editor/gui/editor_spin_slider.h b/editor/gui/editor_spin_slider.h
new file mode 100644
index 0000000000..a4d810b18b
--- /dev/null
+++ b/editor/gui/editor_spin_slider.h
@@ -0,0 +1,122 @@
+/**************************************************************************/
+/* editor_spin_slider.h */
+/**************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/**************************************************************************/
+
+#ifndef EDITOR_SPIN_SLIDER_H
+#define EDITOR_SPIN_SLIDER_H
+
+#include "scene/gui/line_edit.h"
+#include "scene/gui/range.h"
+#include "scene/gui/texture_rect.h"
+
+class EditorSpinSlider : public Range {
+ GDCLASS(EditorSpinSlider, Range);
+
+ String label;
+ String suffix;
+ int updown_offset = -1;
+ bool hover_updown = false;
+ bool mouse_hover = false;
+
+ TextureRect *grabber = nullptr;
+ int grabber_range = 1;
+
+ bool mouse_over_spin = false;
+ bool mouse_over_grabber = false;
+ bool mousewheel_over_grabber = false;
+
+ bool grabbing_grabber = false;
+ int grabbing_from = 0;
+ float grabbing_ratio = 0.0f;
+
+ bool grabbing_spinner_attempt = false;
+ bool grabbing_spinner = false;
+
+ bool read_only = false;
+ float grabbing_spinner_dist_cache = 0.0f;
+ Vector2 grabbing_spinner_mouse_pos;
+ double pre_grab_value = 0.0;
+
+ Control *value_input_popup = nullptr;
+ LineEdit *value_input = nullptr;
+ uint64_t value_input_closed_frame = 0;
+ bool value_input_dirty = false;
+
+ bool hide_slider = false;
+ bool flat = false;
+
+ void _grabber_gui_input(const Ref<InputEvent> &p_event);
+ void _value_input_closed();
+ void _value_input_submitted(const String &);
+ void _value_focus_exited();
+ void _value_input_gui_input(const Ref<InputEvent> &p_event);
+
+ void _evaluate_input_text();
+
+ void _update_value_input_stylebox();
+ void _ensure_input_popup();
+ void _draw_spin_slider();
+
+protected:
+ void _notification(int p_what);
+ virtual void gui_input(const Ref<InputEvent> &p_event) override;
+ static void _bind_methods();
+ void _grabber_mouse_entered();
+ void _grabber_mouse_exited();
+ void _focus_entered();
+
+public:
+ String get_tooltip(const Point2 &p_pos) const override;
+
+ String get_text_value() const;
+ void set_label(const String &p_label);
+ String get_label() const;
+
+ void set_suffix(const String &p_suffix);
+ String get_suffix() const;
+
+ void set_hide_slider(bool p_hide);
+ bool is_hiding_slider() const;
+
+ void set_read_only(bool p_enable);
+ bool is_read_only() const;
+
+ void set_flat(bool p_enable);
+ bool is_flat() const;
+
+ bool is_grabbing() const;
+
+ void setup_and_show() { _focus_entered(); }
+ LineEdit *get_line_edit();
+
+ virtual Size2 get_minimum_size() const override;
+ EditorSpinSlider();
+};
+
+#endif // EDITOR_SPIN_SLIDER_H
diff --git a/editor/gui/editor_title_bar.cpp b/editor/gui/editor_title_bar.cpp
new file mode 100644
index 0000000000..c251c70c6d
--- /dev/null
+++ b/editor/gui/editor_title_bar.cpp
@@ -0,0 +1,86 @@
+/**************************************************************************/
+/* editor_title_bar.cpp */
+/**************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/**************************************************************************/
+
+#include "editor_title_bar.h"
+
+void EditorTitleBar::gui_input(const Ref<InputEvent> &p_event) {
+ if (!can_move) {
+ return;
+ }
+
+ Ref<InputEventMouseMotion> mm = p_event;
+ if (mm.is_valid() && moving) {
+ if (mm->get_button_mask().has_flag(MouseButtonMask::LEFT)) {
+ Window *w = Object::cast_to<Window>(get_viewport());
+ if (w) {
+ Point2 mouse = DisplayServer::get_singleton()->mouse_get_position();
+ w->set_position(mouse - click_pos);
+ }
+ } else {
+ moving = false;
+ }
+ }
+
+ Ref<InputEventMouseButton> mb = p_event;
+ if (mb.is_valid() && has_point(mb->get_position())) {
+ Window *w = Object::cast_to<Window>(get_viewport());
+ if (w) {
+ if (mb->get_button_index() == MouseButton::LEFT) {
+ if (mb->is_pressed()) {
+ click_pos = DisplayServer::get_singleton()->mouse_get_position() - w->get_position();
+ moving = true;
+ } else {
+ moving = false;
+ }
+ }
+ if (mb->get_button_index() == MouseButton::LEFT && mb->is_double_click() && mb->is_pressed()) {
+ if (DisplayServer::get_singleton()->window_maximize_on_title_dbl_click()) {
+ if (w->get_mode() == Window::MODE_WINDOWED) {
+ w->set_mode(Window::MODE_MAXIMIZED);
+ } else if (w->get_mode() == Window::MODE_MAXIMIZED) {
+ w->set_mode(Window::MODE_WINDOWED);
+ }
+ } else if (DisplayServer::get_singleton()->window_minimize_on_title_dbl_click()) {
+ w->set_mode(Window::MODE_MINIMIZED);
+ }
+ moving = false;
+ }
+ }
+ }
+}
+
+void EditorTitleBar::set_can_move_window(bool p_enabled) {
+ can_move = p_enabled;
+ set_process_input(can_move);
+}
+
+bool EditorTitleBar::get_can_move_window() const {
+ return can_move;
+}
diff --git a/editor/gui/editor_title_bar.h b/editor/gui/editor_title_bar.h
new file mode 100644
index 0000000000..4055476b82
--- /dev/null
+++ b/editor/gui/editor_title_bar.h
@@ -0,0 +1,53 @@
+/**************************************************************************/
+/* editor_title_bar.h */
+/**************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/**************************************************************************/
+
+#ifndef EDITOR_TITLE_BAR_H
+#define EDITOR_TITLE_BAR_H
+
+#include "scene/gui/box_container.h"
+#include "scene/main/window.h"
+
+class EditorTitleBar : public HBoxContainer {
+ GDCLASS(EditorTitleBar, HBoxContainer);
+
+ Point2i click_pos;
+ bool moving = false;
+ bool can_move = false;
+
+protected:
+ virtual void gui_input(const Ref<InputEvent> &p_event) override;
+ static void _bind_methods(){};
+
+public:
+ void set_can_move_window(bool p_enabled);
+ bool get_can_move_window() const;
+};
+
+#endif // EDITOR_TITLE_BAR_H
diff --git a/editor/gui/editor_toaster.cpp b/editor/gui/editor_toaster.cpp
new file mode 100644
index 0000000000..10c3e963af
--- /dev/null
+++ b/editor/gui/editor_toaster.cpp
@@ -0,0 +1,576 @@
+/**************************************************************************/
+/* editor_toaster.cpp */
+/**************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/**************************************************************************/
+
+#include "editor_toaster.h"
+
+#include "editor/editor_scale.h"
+#include "editor/editor_settings.h"
+#include "scene/gui/button.h"
+#include "scene/gui/label.h"
+#include "scene/gui/panel_container.h"
+
+EditorToaster *EditorToaster::singleton = nullptr;
+
+void EditorToaster::_notification(int p_what) {
+ switch (p_what) {
+ case NOTIFICATION_INTERNAL_PROCESS: {
+ double delta = get_process_delta_time();
+
+ // Check if one element is hovered, if so, don't elapse time.
+ bool hovered = false;
+ for (const KeyValue<Control *, Toast> &element : toasts) {
+ if (Rect2(Vector2(), element.key->get_size()).has_point(element.key->get_local_mouse_position())) {
+ hovered = true;
+ break;
+ }
+ }
+
+ // Elapses the time and remove toasts if needed.
+ if (!hovered) {
+ for (const KeyValue<Control *, Toast> &element : toasts) {
+ if (!element.value.popped || element.value.duration <= 0) {
+ continue;
+ }
+ toasts[element.key].remaining_time -= delta;
+ if (toasts[element.key].remaining_time < 0) {
+ close(element.key);
+ }
+ element.key->queue_redraw();
+ }
+ } else {
+ // Reset the timers when hovered.
+ for (const KeyValue<Control *, Toast> &element : toasts) {
+ if (!element.value.popped || element.value.duration <= 0) {
+ continue;
+ }
+ toasts[element.key].remaining_time = element.value.duration;
+ element.key->queue_redraw();
+ }
+ }
+
+ // Change alpha over time.
+ bool needs_update = false;
+ for (const KeyValue<Control *, Toast> &element : toasts) {
+ Color modulate_fade = element.key->get_modulate();
+
+ // Change alpha over time.
+ if (element.value.popped && modulate_fade.a < 1.0) {
+ modulate_fade.a += delta * 3;
+ element.key->set_modulate(modulate_fade);
+ } else if (!element.value.popped && modulate_fade.a > 0.0) {
+ modulate_fade.a -= delta * 2;
+ element.key->set_modulate(modulate_fade);
+ }
+
+ // Hide element if it is not visible anymore.
+ if (modulate_fade.a <= 0.0 && element.key->is_visible()) {
+ element.key->hide();
+ needs_update = true;
+ } else if (modulate_fade.a > 0.0 && !element.key->is_visible()) {
+ element.key->show();
+ needs_update = true;
+ }
+ }
+
+ if (needs_update) {
+ _update_vbox_position();
+ _update_disable_notifications_button();
+ main_button->queue_redraw();
+ }
+ } break;
+
+ case NOTIFICATION_ENTER_TREE:
+ case NOTIFICATION_THEME_CHANGED: {
+ if (vbox_container->is_visible()) {
+ main_button->set_icon(get_theme_icon(SNAME("Notification"), SNAME("EditorIcons")));
+ } else {
+ main_button->set_icon(get_theme_icon(SNAME("NotificationDisabled"), SNAME("EditorIcons")));
+ }
+ disable_notifications_button->set_icon(get_theme_icon(SNAME("NotificationDisabled"), SNAME("EditorIcons")));
+
+ // Styleboxes background.
+ info_panel_style_background->set_bg_color(get_theme_color(SNAME("base_color"), SNAME("Editor")));
+
+ warning_panel_style_background->set_bg_color(get_theme_color(SNAME("base_color"), SNAME("Editor")));
+ warning_panel_style_background->set_border_color(get_theme_color(SNAME("warning_color"), SNAME("Editor")));
+
+ error_panel_style_background->set_bg_color(get_theme_color(SNAME("base_color"), SNAME("Editor")));
+ error_panel_style_background->set_border_color(get_theme_color(SNAME("error_color"), SNAME("Editor")));
+
+ // Styleboxes progress.
+ info_panel_style_progress->set_bg_color(get_theme_color(SNAME("base_color"), SNAME("Editor")).lightened(0.03));
+
+ warning_panel_style_progress->set_bg_color(get_theme_color(SNAME("base_color"), SNAME("Editor")).lightened(0.03));
+ warning_panel_style_progress->set_border_color(get_theme_color(SNAME("warning_color"), SNAME("Editor")));
+
+ error_panel_style_progress->set_bg_color(get_theme_color(SNAME("base_color"), SNAME("Editor")).lightened(0.03));
+ error_panel_style_progress->set_border_color(get_theme_color(SNAME("error_color"), SNAME("Editor")));
+
+ main_button->queue_redraw();
+ disable_notifications_button->queue_redraw();
+ } break;
+
+ case NOTIFICATION_TRANSFORM_CHANGED: {
+ _update_vbox_position();
+ _update_disable_notifications_button();
+ } break;
+ }
+}
+
+void EditorToaster::_error_handler(void *p_self, const char *p_func, const char *p_file, int p_line, const char *p_error, const char *p_errorexp, bool p_editor_notify, ErrorHandlerType p_type) {
+ if (!EditorToaster::get_singleton() || !EditorToaster::get_singleton()->is_inside_tree()) {
+ return;
+ }
+
+#ifdef DEV_ENABLED
+ bool in_dev = true;
+#else
+ bool in_dev = false;
+#endif
+
+ int show_all_setting = EDITOR_GET("interface/editor/show_internal_errors_in_toast_notifications");
+
+ if (p_editor_notify || (show_all_setting == 0 && in_dev) || show_all_setting == 1) {
+ String err_str;
+ if (p_errorexp && p_errorexp[0]) {
+ err_str = String::utf8(p_errorexp);
+ } else {
+ err_str = String::utf8(p_error);
+ }
+ String tooltip_str = String::utf8(p_file) + ":" + itos(p_line);
+
+ if (!p_editor_notify) {
+ if (p_type == ERR_HANDLER_WARNING) {
+ err_str = "INTERNAL WARNING: " + err_str;
+ } else {
+ err_str = "INTERNAL ERROR: " + err_str;
+ }
+ }
+
+ Severity severity = (p_type == ERR_HANDLER_WARNING) ? SEVERITY_WARNING : SEVERITY_ERROR;
+ EditorToaster::get_singleton()->popup_str(err_str, severity, tooltip_str);
+ }
+}
+
+void EditorToaster::_update_vbox_position() {
+ // This is kind of a workaround because it's hard to keep the VBox anchroed to the bottom.
+ vbox_container->set_size(Vector2());
+ vbox_container->set_position(get_global_position() - vbox_container->get_size() + Vector2(get_size().x, -5 * EDSCALE));
+}
+
+void EditorToaster::_update_disable_notifications_button() {
+ bool any_visible = false;
+ for (KeyValue<Control *, Toast> element : toasts) {
+ if (element.key->is_visible()) {
+ any_visible = true;
+ break;
+ }
+ }
+
+ if (!any_visible || !vbox_container->is_visible()) {
+ disable_notifications_panel->hide();
+ } else {
+ disable_notifications_panel->show();
+ disable_notifications_panel->set_position(get_global_position() + Vector2(5 * EDSCALE, -disable_notifications_panel->get_minimum_size().y) + Vector2(get_size().x, -5 * EDSCALE));
+ }
+}
+
+void EditorToaster::_auto_hide_or_free_toasts() {
+ // Hide or free old temporary items.
+ int visible_temporary = 0;
+ int temporary = 0;
+ LocalVector<Control *> to_delete;
+ for (int i = vbox_container->get_child_count() - 1; i >= 0; i--) {
+ Control *control = Object::cast_to<Control>(vbox_container->get_child(i));
+ if (toasts[control].duration <= 0) {
+ continue; // Ignore non-temporary toasts.
+ }
+
+ temporary++;
+ if (control->is_visible()) {
+ visible_temporary++;
+ }
+
+ // Hide
+ if (visible_temporary > max_temporary_count) {
+ close(control);
+ }
+
+ // Free
+ if (temporary > max_temporary_count * 2) {
+ to_delete.push_back(control);
+ }
+ }
+
+ // Delete the control right away (removed as child) as it might cause issues otherwise when iterative over the vbox_container children.
+ for (Control *c : to_delete) {
+ vbox_container->remove_child(c);
+ c->queue_free();
+ toasts.erase(c);
+ }
+
+ if (toasts.is_empty()) {
+ main_button->set_tooltip_text(TTR("No notifications."));
+ main_button->set_modulate(Color(0.5, 0.5, 0.5));
+ main_button->set_disabled(true);
+ } else {
+ main_button->set_tooltip_text(TTR("Show notifications."));
+ main_button->set_modulate(Color(1, 1, 1));
+ main_button->set_disabled(false);
+ }
+}
+
+void EditorToaster::_draw_button() {
+ bool has_one = false;
+ Severity highest_severity = SEVERITY_INFO;
+ for (const KeyValue<Control *, Toast> &element : toasts) {
+ if (!element.key->is_visible()) {
+ continue;
+ }
+ has_one = true;
+ if (element.value.severity > highest_severity) {
+ highest_severity = element.value.severity;
+ }
+ }
+
+ if (!has_one) {
+ return;
+ }
+
+ Color color;
+ real_t button_radius = main_button->get_size().x / 8;
+ switch (highest_severity) {
+ case SEVERITY_INFO:
+ color = get_theme_color(SNAME("accent_color"), SNAME("Editor"));
+ break;
+ case SEVERITY_WARNING:
+ color = get_theme_color(SNAME("warning_color"), SNAME("Editor"));
+ break;
+ case SEVERITY_ERROR:
+ color = get_theme_color(SNAME("error_color"), SNAME("Editor"));
+ break;
+ default:
+ break;
+ }
+ main_button->draw_circle(Vector2(button_radius * 2, button_radius * 2), button_radius, color);
+}
+
+void EditorToaster::_draw_progress(Control *panel) {
+ if (toasts.has(panel) && toasts[panel].remaining_time > 0 && toasts[panel].duration > 0) {
+ Size2 size = panel->get_size();
+ size.x *= MIN(1, Math::remap(toasts[panel].remaining_time, 0, toasts[panel].duration, 0, 2));
+
+ Ref<StyleBoxFlat> stylebox;
+ switch (toasts[panel].severity) {
+ case SEVERITY_INFO:
+ stylebox = info_panel_style_progress;
+ break;
+ case SEVERITY_WARNING:
+ stylebox = warning_panel_style_progress;
+ break;
+ case SEVERITY_ERROR:
+ stylebox = error_panel_style_progress;
+ break;
+ default:
+ break;
+ }
+ panel->draw_style_box(stylebox, Rect2(Vector2(), size));
+ }
+}
+
+void EditorToaster::_set_notifications_enabled(bool p_enabled) {
+ vbox_container->set_visible(p_enabled);
+ if (p_enabled) {
+ main_button->set_icon(get_theme_icon(SNAME("Notification"), SNAME("EditorIcons")));
+ } else {
+ main_button->set_icon(get_theme_icon(SNAME("NotificationDisabled"), SNAME("EditorIcons")));
+ }
+ _update_disable_notifications_button();
+}
+
+void EditorToaster::_repop_old() {
+ // Repop olds, up to max_temporary_count
+ bool needs_update = false;
+ int visible_count = 0;
+ for (int i = vbox_container->get_child_count() - 1; i >= 0; i--) {
+ Control *control = Object::cast_to<Control>(vbox_container->get_child(i));
+ if (!control->is_visible()) {
+ control->show();
+ toasts[control].remaining_time = toasts[control].duration;
+ toasts[control].popped = true;
+ needs_update = true;
+ }
+ visible_count++;
+ if (visible_count >= max_temporary_count) {
+ break;
+ }
+ }
+ if (needs_update) {
+ _update_vbox_position();
+ _update_disable_notifications_button();
+ main_button->queue_redraw();
+ }
+}
+
+Control *EditorToaster::popup(Control *p_control, Severity p_severity, double p_time, String p_tooltip) {
+ // Create the panel according to the severity.
+ PanelContainer *panel = memnew(PanelContainer);
+ panel->set_tooltip_text(p_tooltip);
+ switch (p_severity) {
+ case SEVERITY_INFO:
+ panel->add_theme_style_override("panel", info_panel_style_background);
+ break;
+ case SEVERITY_WARNING:
+ panel->add_theme_style_override("panel", warning_panel_style_background);
+ break;
+ case SEVERITY_ERROR:
+ panel->add_theme_style_override("panel", error_panel_style_background);
+ break;
+ default:
+ break;
+ }
+ panel->set_modulate(Color(1, 1, 1, 0));
+ panel->connect("draw", callable_mp(this, &EditorToaster::_draw_progress).bind(panel));
+
+ // Horizontal container.
+ HBoxContainer *hbox_container = memnew(HBoxContainer);
+ hbox_container->set_h_size_flags(SIZE_EXPAND_FILL);
+ panel->add_child(hbox_container);
+
+ // Content control.
+ p_control->set_h_size_flags(SIZE_EXPAND_FILL);
+ hbox_container->add_child(p_control);
+
+ // Close button.
+ if (p_time > 0.0) {
+ Button *close_button = memnew(Button);
+ close_button->set_flat(true);
+ close_button->set_icon(get_theme_icon(SNAME("Close"), SNAME("EditorIcons")));
+ close_button->connect("pressed", callable_mp(this, &EditorToaster::close).bind(panel));
+ close_button->connect("theme_changed", callable_mp(this, &EditorToaster::_close_button_theme_changed).bind(close_button));
+ hbox_container->add_child(close_button);
+ }
+
+ toasts[panel].severity = p_severity;
+ if (p_time > 0.0) {
+ toasts[panel].duration = p_time;
+ toasts[panel].remaining_time = p_time;
+ } else {
+ toasts[panel].duration = -1.0;
+ }
+ toasts[panel].popped = true;
+ vbox_container->add_child(panel);
+ _auto_hide_or_free_toasts();
+ _update_vbox_position();
+ _update_disable_notifications_button();
+ main_button->queue_redraw();
+
+ return panel;
+}
+
+void EditorToaster::popup_str(String p_message, Severity p_severity, String p_tooltip) {
+ if (is_processing_error) {
+ return;
+ }
+
+ // Since "_popup_str" adds nodes to the tree, and since the "add_child" method is not
+ // thread-safe, it's better to defer the call to the next cycle to be thread-safe.
+ is_processing_error = true;
+ call_deferred(SNAME("_popup_str"), p_message, p_severity, p_tooltip);
+ is_processing_error = false;
+}
+
+void EditorToaster::_popup_str(String p_message, Severity p_severity, String p_tooltip) {
+ is_processing_error = true;
+ // Check if we already have a popup with the given message.
+ Control *control = nullptr;
+ for (KeyValue<Control *, Toast> element : toasts) {
+ if (element.value.message == p_message && element.value.severity == p_severity && element.value.tooltip == p_tooltip) {
+ control = element.key;
+ break;
+ }
+ }
+
+ // Create a new message if needed.
+ if (control == nullptr) {
+ HBoxContainer *hb = memnew(HBoxContainer);
+ hb->add_theme_constant_override("separation", 0);
+
+ Label *label = memnew(Label);
+ hb->add_child(label);
+
+ Label *count_label = memnew(Label);
+ hb->add_child(count_label);
+
+ control = popup(hb, p_severity, default_message_duration, p_tooltip);
+ toasts[control].message = p_message;
+ toasts[control].tooltip = p_tooltip;
+ toasts[control].count = 1;
+ toasts[control].message_label = label;
+ toasts[control].message_count_label = count_label;
+ } else {
+ if (toasts[control].popped) {
+ toasts[control].count += 1;
+ } else {
+ toasts[control].count = 1;
+ }
+ toasts[control].remaining_time = toasts[control].duration;
+ toasts[control].popped = true;
+ control->show();
+ vbox_container->move_child(control, vbox_container->get_child_count());
+ _auto_hide_or_free_toasts();
+ _update_vbox_position();
+ _update_disable_notifications_button();
+ main_button->queue_redraw();
+ }
+
+ // Retrieve the label back, then update the text.
+ Label *message_label = toasts[control].message_label;
+ ERR_FAIL_COND(!message_label);
+ message_label->set_text(p_message);
+ message_label->set_text_overrun_behavior(TextServer::OVERRUN_NO_TRIMMING);
+ message_label->set_custom_minimum_size(Size2());
+
+ Size2i size = message_label->get_combined_minimum_size();
+ int limit_width = get_viewport_rect().size.x / 2; // Limit label size to half the viewport size.
+ if (size.x > limit_width) {
+ message_label->set_text_overrun_behavior(TextServer::OVERRUN_TRIM_ELLIPSIS);
+ message_label->set_custom_minimum_size(Size2(limit_width, 0));
+ }
+
+ // Retrieve the count label back, then update the text.
+ Label *message_count_label = toasts[control].message_count_label;
+ if (toasts[control].count == 1) {
+ message_count_label->hide();
+ } else {
+ message_count_label->set_text(vformat("(%d)", toasts[control].count));
+ message_count_label->show();
+ }
+
+ vbox_container->reset_size();
+
+ is_processing_error = false;
+}
+
+void EditorToaster::close(Control *p_control) {
+ ERR_FAIL_COND(!toasts.has(p_control));
+ toasts[p_control].remaining_time = -1.0;
+ toasts[p_control].popped = false;
+}
+
+void EditorToaster::_close_button_theme_changed(Control *p_close_button) {
+ Button *close_button = Object::cast_to<Button>(p_close_button);
+ if (close_button) {
+ close_button->set_icon(get_theme_icon(SNAME("Close"), SNAME("EditorIcons")));
+ }
+}
+
+EditorToaster *EditorToaster::get_singleton() {
+ return singleton;
+}
+
+void EditorToaster::_bind_methods() {
+ // Binding method to make it defer-able.
+ ClassDB::bind_method(D_METHOD("_popup_str", "message", "severity", "tooltip"), &EditorToaster::_popup_str);
+}
+
+EditorToaster::EditorToaster() {
+ set_notify_transform(true);
+ set_process_internal(true);
+
+ // VBox.
+ vbox_container = memnew(VBoxContainer);
+ vbox_container->set_as_top_level(true);
+ vbox_container->connect("resized", callable_mp(this, &EditorToaster::_update_vbox_position));
+ add_child(vbox_container);
+
+ // Theming (background).
+ info_panel_style_background.instantiate();
+ info_panel_style_background->set_corner_radius_all(stylebox_radius * EDSCALE);
+
+ warning_panel_style_background.instantiate();
+ warning_panel_style_background->set_border_width(SIDE_LEFT, stylebox_radius * EDSCALE);
+ warning_panel_style_background->set_corner_radius_all(stylebox_radius * EDSCALE);
+
+ error_panel_style_background.instantiate();
+ error_panel_style_background->set_border_width(SIDE_LEFT, stylebox_radius * EDSCALE);
+ error_panel_style_background->set_corner_radius_all(stylebox_radius * EDSCALE);
+
+ Ref<StyleBoxFlat> boxes[] = { info_panel_style_background, warning_panel_style_background, error_panel_style_background };
+ for (int i = 0; i < 3; i++) {
+ boxes[i]->set_content_margin_individual(int(stylebox_radius * 2.5), 3, int(stylebox_radius * 2.5), 3);
+ }
+
+ // Theming (progress).
+ info_panel_style_progress.instantiate();
+ info_panel_style_progress->set_corner_radius_all(stylebox_radius * EDSCALE);
+
+ warning_panel_style_progress.instantiate();
+ warning_panel_style_progress->set_border_width(SIDE_LEFT, stylebox_radius * EDSCALE);
+ warning_panel_style_progress->set_corner_radius_all(stylebox_radius * EDSCALE);
+
+ error_panel_style_progress.instantiate();
+ error_panel_style_progress->set_border_width(SIDE_LEFT, stylebox_radius * EDSCALE);
+ error_panel_style_progress->set_corner_radius_all(stylebox_radius * EDSCALE);
+
+ // Main button.
+ main_button = memnew(Button);
+ main_button->set_tooltip_text(TTR("No notifications."));
+ main_button->set_modulate(Color(0.5, 0.5, 0.5));
+ main_button->set_disabled(true);
+ main_button->set_flat(true);
+ main_button->connect("pressed", callable_mp(this, &EditorToaster::_set_notifications_enabled).bind(true));
+ main_button->connect("pressed", callable_mp(this, &EditorToaster::_repop_old));
+ main_button->connect("draw", callable_mp(this, &EditorToaster::_draw_button));
+ add_child(main_button);
+
+ // Disable notification button.
+ disable_notifications_panel = memnew(PanelContainer);
+ disable_notifications_panel->set_as_top_level(true);
+ disable_notifications_panel->add_theme_style_override("panel", info_panel_style_background);
+ add_child(disable_notifications_panel);
+
+ disable_notifications_button = memnew(Button);
+ disable_notifications_button->set_tooltip_text(TTR("Silence the notifications."));
+ disable_notifications_button->set_flat(true);
+ disable_notifications_button->connect("pressed", callable_mp(this, &EditorToaster::_set_notifications_enabled).bind(false));
+ disable_notifications_panel->add_child(disable_notifications_button);
+
+ // Other
+ singleton = this;
+
+ eh.errfunc = _error_handler;
+ add_error_handler(&eh);
+};
+
+EditorToaster::~EditorToaster() {
+ singleton = nullptr;
+ remove_error_handler(&eh);
+}
diff --git a/editor/gui/editor_toaster.h b/editor/gui/editor_toaster.h
new file mode 100644
index 0000000000..6b834f8288
--- /dev/null
+++ b/editor/gui/editor_toaster.h
@@ -0,0 +1,123 @@
+/**************************************************************************/
+/* editor_toaster.h */
+/**************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/**************************************************************************/
+
+#ifndef EDITOR_TOASTER_H
+#define EDITOR_TOASTER_H
+
+#include "core/string/ustring.h"
+#include "core/templates/local_vector.h"
+#include "scene/gui/box_container.h"
+
+class Button;
+class PanelContainer;
+
+class EditorToaster : public HBoxContainer {
+ GDCLASS(EditorToaster, HBoxContainer);
+
+public:
+ enum Severity {
+ SEVERITY_INFO = 0,
+ SEVERITY_WARNING,
+ SEVERITY_ERROR,
+ };
+
+private:
+ ErrorHandlerList eh;
+
+ const int stylebox_radius = 3;
+
+ Ref<StyleBoxFlat> info_panel_style_background;
+ Ref<StyleBoxFlat> warning_panel_style_background;
+ Ref<StyleBoxFlat> error_panel_style_background;
+
+ Ref<StyleBoxFlat> info_panel_style_progress;
+ Ref<StyleBoxFlat> warning_panel_style_progress;
+ Ref<StyleBoxFlat> error_panel_style_progress;
+
+ Button *main_button = nullptr;
+ PanelContainer *disable_notifications_panel = nullptr;
+ Button *disable_notifications_button = nullptr;
+
+ VBoxContainer *vbox_container = nullptr;
+ const int max_temporary_count = 5;
+ struct Toast {
+ Severity severity = SEVERITY_INFO;
+
+ // Timing.
+ real_t duration = -1.0;
+ real_t remaining_time = 0.0;
+ bool popped = false;
+
+ // Messages
+ String message;
+ String tooltip;
+ int count = 0;
+ Label *message_label = nullptr;
+ Label *message_count_label = nullptr;
+ };
+ HashMap<Control *, Toast> toasts;
+
+ bool is_processing_error = false; // Makes sure that we don't handle errors that are triggered within the EditorToaster error processing.
+
+ const double default_message_duration = 5.0;
+
+ static void _error_handler(void *p_self, const char *p_func, const char *p_file, int p_line, const char *p_error, const char *p_errorexp, bool p_editor_notify, ErrorHandlerType p_type);
+ void _update_vbox_position();
+ void _update_disable_notifications_button();
+ void _auto_hide_or_free_toasts();
+
+ void _draw_button();
+ void _draw_progress(Control *panel);
+
+ void _set_notifications_enabled(bool p_enabled);
+ void _repop_old();
+ void _popup_str(String p_message, Severity p_severity, String p_tooltip);
+ void _close_button_theme_changed(Control *p_close_button);
+
+protected:
+ static EditorToaster *singleton;
+ static void _bind_methods();
+
+ void _notification(int p_what);
+
+public:
+ static EditorToaster *get_singleton();
+
+ Control *popup(Control *p_control, Severity p_severity = SEVERITY_INFO, double p_time = 0.0, String p_tooltip = String());
+ void popup_str(String p_message, Severity p_severity = SEVERITY_INFO, String p_tooltip = String());
+ void close(Control *p_control);
+
+ EditorToaster();
+ ~EditorToaster();
+};
+
+VARIANT_ENUM_CAST(EditorToaster::Severity);
+
+#endif // EDITOR_TOASTER_H
diff --git a/editor/gui/editor_zoom_widget.cpp b/editor/gui/editor_zoom_widget.cpp
new file mode 100644
index 0000000000..3998b33a53
--- /dev/null
+++ b/editor/gui/editor_zoom_widget.cpp
@@ -0,0 +1,209 @@
+/**************************************************************************/
+/* editor_zoom_widget.cpp */
+/**************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/**************************************************************************/
+
+#include "editor_zoom_widget.h"
+
+#include "core/os/keyboard.h"
+#include "editor/editor_scale.h"
+#include "editor/editor_settings.h"
+
+void EditorZoomWidget::_update_zoom_label() {
+ String zoom_text;
+ // The zoom level displayed is relative to the editor scale
+ // (like in most image editors). Its lower bound is clamped to 1 as some people
+ // lower the editor scale to increase the available real estate,
+ // even if their display doesn't have a particularly low DPI.
+ if (zoom >= 10) {
+ zoom_text = TS->format_number(rtos(Math::round((zoom / MAX(1, EDSCALE)) * 100)));
+ } else {
+ // 2 decimal places if the zoom is below 10%, 1 decimal place if it's below 1000%.
+ zoom_text = TS->format_number(rtos(Math::snapped((zoom / MAX(1, EDSCALE)) * 100, (zoom >= 0.1) ? 0.1 : 0.01)));
+ }
+ zoom_text += " " + TS->percent_sign();
+ zoom_reset->set_text(zoom_text);
+}
+
+void EditorZoomWidget::_button_zoom_minus() {
+ set_zoom_by_increments(-6, Input::get_singleton()->is_key_pressed(Key::ALT));
+ emit_signal(SNAME("zoom_changed"), zoom);
+}
+
+void EditorZoomWidget::_button_zoom_reset() {
+ set_zoom(1.0 * MAX(1, EDSCALE));
+ emit_signal(SNAME("zoom_changed"), zoom);
+}
+
+void EditorZoomWidget::_button_zoom_plus() {
+ set_zoom_by_increments(6, Input::get_singleton()->is_key_pressed(Key::ALT));
+ emit_signal(SNAME("zoom_changed"), zoom);
+}
+
+float EditorZoomWidget::get_zoom() {
+ return zoom;
+}
+
+void EditorZoomWidget::set_zoom(float p_zoom) {
+ if (p_zoom > 0 && p_zoom != zoom) {
+ zoom = p_zoom;
+ _update_zoom_label();
+ }
+}
+
+void EditorZoomWidget::set_zoom_by_increments(int p_increment_count, bool p_integer_only) {
+ // Remove editor scale from the index computation.
+ const float zoom_noscale = zoom / MAX(1, EDSCALE);
+
+ if (p_integer_only) {
+ // Only visit integer scaling factors above 100%, and fractions with an integer denominator below 100%
+ // (1/2 = 50%, 1/3 = 33.33%, 1/4 = 25%, …).
+ // This is useful when working on pixel art projects to avoid distortion.
+ // This algorithm is designed to handle fractional start zoom values correctly
+ // (e.g. 190% will zoom up to 200% and down to 100%).
+ if (zoom_noscale + p_increment_count * 0.001 >= 1.0 - CMP_EPSILON) {
+ // New zoom is certain to be above 100%.
+ if (p_increment_count >= 1) {
+ // Zooming.
+ set_zoom(Math::floor(zoom_noscale + p_increment_count) * MAX(1, EDSCALE));
+ } else {
+ // Dezooming.
+ set_zoom(Math::ceil(zoom_noscale + p_increment_count) * MAX(1, EDSCALE));
+ }
+ } else {
+ if (p_increment_count >= 1) {
+ // Zooming. Convert the current zoom into a denominator.
+ float new_zoom = 1.0 / Math::ceil(1.0 / zoom_noscale - p_increment_count);
+ if (Math::is_equal_approx(zoom_noscale, new_zoom)) {
+ // New zoom is identical to the old zoom, so try again.
+ // This can happen due to floating-point precision issues.
+ new_zoom = 1.0 / Math::ceil(1.0 / zoom_noscale - p_increment_count - 1);
+ }
+ set_zoom(new_zoom * MAX(1, EDSCALE));
+ } else {
+ // Dezooming. Convert the current zoom into a denominator.
+ float new_zoom = 1.0 / Math::floor(1.0 / zoom_noscale - p_increment_count);
+ if (Math::is_equal_approx(zoom_noscale, new_zoom)) {
+ // New zoom is identical to the old zoom, so try again.
+ // This can happen due to floating-point precision issues.
+ new_zoom = 1.0 / Math::floor(1.0 / zoom_noscale - p_increment_count + 1);
+ }
+ set_zoom(new_zoom * MAX(1, EDSCALE));
+ }
+ }
+ } else {
+ // Base increment factor defined as the twelveth root of two.
+ // This allow a smooth geometric evolution of the zoom, with the advantage of
+ // visiting all integer power of two scale factors.
+ // note: this is analogous to the 'semitones' interval in the music world
+ // In order to avoid numerical imprecisions, we compute and edit a zoom index
+ // with the following relation: zoom = 2 ^ (index / 12)
+
+ if (zoom < CMP_EPSILON || p_increment_count == 0) {
+ return;
+ }
+
+ // zoom = 2**(index/12) => log2(zoom) = index/12
+ float closest_zoom_index = Math::round(Math::log(zoom_noscale) * 12.f / Math::log(2.f));
+
+ float new_zoom_index = closest_zoom_index + p_increment_count;
+ float new_zoom = Math::pow(2.f, new_zoom_index / 12.f);
+
+ // Restore Editor scale transformation.
+ new_zoom *= MAX(1, EDSCALE);
+
+ set_zoom(new_zoom);
+ }
+}
+
+void EditorZoomWidget::_notification(int p_what) {
+ switch (p_what) {
+ case NOTIFICATION_ENTER_TREE:
+ case NOTIFICATION_THEME_CHANGED: {
+ zoom_minus->set_icon(get_theme_icon(SNAME("ZoomLess"), SNAME("EditorIcons")));
+ zoom_plus->set_icon(get_theme_icon(SNAME("ZoomMore"), SNAME("EditorIcons")));
+ } break;
+ }
+}
+
+void EditorZoomWidget::_bind_methods() {
+ ClassDB::bind_method(D_METHOD("set_zoom", "zoom"), &EditorZoomWidget::set_zoom);
+ ClassDB::bind_method(D_METHOD("get_zoom"), &EditorZoomWidget::get_zoom);
+ ClassDB::bind_method(D_METHOD("set_zoom_by_increments", "increment", "integer_only"), &EditorZoomWidget::set_zoom_by_increments);
+
+ ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "zoom"), "set_zoom", "get_zoom");
+
+ ADD_SIGNAL(MethodInfo("zoom_changed", PropertyInfo(Variant::FLOAT, "zoom")));
+}
+
+void EditorZoomWidget::set_shortcut_context(Node *p_node) const {
+ zoom_minus->set_shortcut_context(p_node);
+ zoom_plus->set_shortcut_context(p_node);
+ zoom_reset->set_shortcut_context(p_node);
+}
+
+EditorZoomWidget::EditorZoomWidget() {
+ // Zoom buttons
+ zoom_minus = memnew(Button);
+ zoom_minus->set_flat(true);
+ add_child(zoom_minus);
+ zoom_minus->connect("pressed", callable_mp(this, &EditorZoomWidget::_button_zoom_minus));
+ zoom_minus->set_shortcut(ED_SHORTCUT_ARRAY("canvas_item_editor/zoom_minus", TTR("Zoom Out"), { int32_t(KeyModifierMask::CMD_OR_CTRL | Key::MINUS), int32_t(KeyModifierMask::CMD_OR_CTRL | Key::KP_SUBTRACT) }));
+ zoom_minus->set_shortcut_context(this);
+ zoom_minus->set_focus_mode(FOCUS_NONE);
+
+ zoom_reset = memnew(Button);
+ zoom_reset->set_flat(true);
+ zoom_reset->add_theme_style_override("normal", memnew(StyleBoxEmpty));
+ zoom_reset->add_theme_style_override("hover", memnew(StyleBoxEmpty));
+ zoom_reset->add_theme_style_override("focus", memnew(StyleBoxEmpty));
+ zoom_reset->add_theme_style_override("pressed", memnew(StyleBoxEmpty));
+ add_child(zoom_reset);
+ zoom_reset->add_theme_constant_override("outline_size", Math::ceil(2 * EDSCALE));
+ zoom_reset->add_theme_color_override("font_outline_color", Color(0, 0, 0));
+ zoom_reset->add_theme_color_override("font_color", Color(1, 1, 1));
+ zoom_reset->connect("pressed", callable_mp(this, &EditorZoomWidget::_button_zoom_reset));
+ zoom_reset->set_shortcut(ED_GET_SHORTCUT("canvas_item_editor/zoom_100_percent"));
+ zoom_reset->set_shortcut_context(this);
+ zoom_reset->set_focus_mode(FOCUS_NONE);
+ zoom_reset->set_text_alignment(HORIZONTAL_ALIGNMENT_CENTER);
+ // Prevent the button's size from changing when the text size changes
+ zoom_reset->set_custom_minimum_size(Size2(56 * EDSCALE, 0));
+
+ zoom_plus = memnew(Button);
+ zoom_plus->set_flat(true);
+ add_child(zoom_plus);
+ zoom_plus->connect("pressed", callable_mp(this, &EditorZoomWidget::_button_zoom_plus));
+ zoom_plus->set_shortcut(ED_SHORTCUT_ARRAY("canvas_item_editor/zoom_plus", TTR("Zoom In"), { int32_t(KeyModifierMask::CMD_OR_CTRL | Key::EQUAL), int32_t(KeyModifierMask::CMD_OR_CTRL | Key::KP_ADD) }));
+ zoom_plus->set_shortcut_context(this);
+ zoom_plus->set_focus_mode(FOCUS_NONE);
+
+ _update_zoom_label();
+
+ add_theme_constant_override("separation", 0);
+}
diff --git a/editor/gui/editor_zoom_widget.h b/editor/gui/editor_zoom_widget.h
new file mode 100644
index 0000000000..be54043d93
--- /dev/null
+++ b/editor/gui/editor_zoom_widget.h
@@ -0,0 +1,64 @@
+/**************************************************************************/
+/* editor_zoom_widget.h */
+/**************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/**************************************************************************/
+
+#ifndef EDITOR_ZOOM_WIDGET_H
+#define EDITOR_ZOOM_WIDGET_H
+
+#include "scene/gui/box_container.h"
+#include "scene/gui/button.h"
+
+class EditorZoomWidget : public HBoxContainer {
+ GDCLASS(EditorZoomWidget, HBoxContainer);
+
+ Button *zoom_minus = nullptr;
+ Button *zoom_reset = nullptr;
+ Button *zoom_plus = nullptr;
+
+ float zoom = 1.0;
+ void _update_zoom_label();
+ void _button_zoom_minus();
+ void _button_zoom_reset();
+ void _button_zoom_plus();
+
+protected:
+ void _notification(int p_what);
+ static void _bind_methods();
+
+public:
+ EditorZoomWidget();
+
+ float get_zoom();
+ void set_zoom(float p_zoom);
+ void set_zoom_by_increments(int p_increment_count, bool p_integer_only = false);
+ // Sets the shortcut context for the zoom buttons. By default their context is this EditorZoomWidget control.
+ void set_shortcut_context(Node *p_node) const;
+};
+
+#endif // EDITOR_ZOOM_WIDGET_H
diff --git a/editor/gui/scene_tree_editor.cpp b/editor/gui/scene_tree_editor.cpp
new file mode 100644
index 0000000000..5b9f2c3b21
--- /dev/null
+++ b/editor/gui/scene_tree_editor.cpp
@@ -0,0 +1,1545 @@
+/**************************************************************************/
+/* scene_tree_editor.cpp */
+/**************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/**************************************************************************/
+
+#include "scene_tree_editor.h"
+
+#include "core/config/project_settings.h"
+#include "core/object/message_queue.h"
+#include "editor/editor_file_system.h"
+#include "editor/editor_node.h"
+#include "editor/editor_scale.h"
+#include "editor/editor_settings.h"
+#include "editor/editor_undo_redo_manager.h"
+#include "editor/node_dock.h"
+#include "editor/plugins/animation_player_editor_plugin.h"
+#include "editor/plugins/canvas_item_editor_plugin.h"
+#include "editor/plugins/script_editor_plugin.h"
+#include "scene/gui/label.h"
+#include "scene/gui/tab_container.h"
+#include "scene/gui/texture_rect.h"
+#include "scene/main/window.h"
+#include "scene/resources/packed_scene.h"
+
+Node *SceneTreeEditor::get_scene_node() {
+ ERR_FAIL_COND_V(!is_inside_tree(), nullptr);
+
+ return get_tree()->get_edited_scene_root();
+}
+
+void SceneTreeEditor::_cell_button_pressed(Object *p_item, int p_column, int p_id, MouseButton p_button) {
+ if (p_button != MouseButton::LEFT) {
+ return;
+ }
+
+ if (connect_to_script_mode) {
+ return; //don't do anything in this mode
+ }
+
+ TreeItem *item = Object::cast_to<TreeItem>(p_item);
+ ERR_FAIL_COND(!item);
+
+ NodePath np = item->get_metadata(0);
+
+ Node *n = get_node(np);
+ ERR_FAIL_COND(!n);
+
+ EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();
+ if (p_id == BUTTON_SUBSCENE) {
+ if (n == get_scene_node()) {
+ if (n && n->get_scene_inherited_state().is_valid()) {
+ emit_signal(SNAME("open"), n->get_scene_inherited_state()->get_path());
+ }
+ } else {
+ emit_signal(SNAME("open"), n->get_scene_file_path());
+ }
+ } else if (p_id == BUTTON_SCRIPT) {
+ Ref<Script> script_typed = n->get_script();
+ if (!script_typed.is_null()) {
+ emit_signal(SNAME("open_script"), script_typed);
+ }
+
+ } else if (p_id == BUTTON_VISIBILITY) {
+ undo_redo->create_action(TTR("Toggle Visible"));
+ _toggle_visible(n);
+ List<Node *> selection = editor_selection->get_selected_node_list();
+ if (selection.size() > 1 && selection.find(n) != nullptr) {
+ for (Node *nv : selection) {
+ ERR_FAIL_COND(!nv);
+ if (nv == n) {
+ continue;
+ }
+ _toggle_visible(nv);
+ }
+ }
+ undo_redo->commit_action();
+ } else if (p_id == BUTTON_LOCK) {
+ undo_redo->create_action(TTR("Unlock Node"));
+
+ if (n->is_class("CanvasItem") || n->is_class("Node3D")) {
+ undo_redo->add_do_method(n, "remove_meta", "_edit_lock_");
+ undo_redo->add_undo_method(n, "set_meta", "_edit_lock_", true);
+ undo_redo->add_do_method(this, "_update_tree");
+ undo_redo->add_undo_method(this, "_update_tree");
+ undo_redo->add_do_method(this, "emit_signal", "node_changed");
+ undo_redo->add_undo_method(this, "emit_signal", "node_changed");
+ }
+ undo_redo->commit_action();
+ } else if (p_id == BUTTON_PIN) {
+ if (n->is_class("AnimationPlayer")) {
+ AnimationPlayerEditor::get_singleton()->unpin();
+ _update_tree();
+ }
+
+ } else if (p_id == BUTTON_GROUP) {
+ undo_redo->create_action(TTR("Button Group"));
+
+ if (n->is_class("CanvasItem") || n->is_class("Node3D")) {
+ undo_redo->add_do_method(n, "remove_meta", "_edit_group_");
+ undo_redo->add_undo_method(n, "set_meta", "_edit_group_", true);
+ undo_redo->add_do_method(this, "_update_tree");
+ undo_redo->add_undo_method(this, "_update_tree");
+ undo_redo->add_do_method(this, "emit_signal", "node_changed");
+ undo_redo->add_undo_method(this, "emit_signal", "node_changed");
+ }
+ undo_redo->commit_action();
+ } else if (p_id == BUTTON_WARNING) {
+ String config_err = n->get_configuration_warnings_as_string();
+ if (config_err.is_empty()) {
+ return;
+ }
+
+ const PackedInt32Array boundaries = TS->string_get_word_breaks(config_err, "", 80);
+ PackedStringArray lines;
+ for (int i = 0; i < boundaries.size(); i += 2) {
+ const int start = boundaries[i];
+ const int end = boundaries[i + 1];
+ lines.append(config_err.substr(start, end - start + 1));
+ }
+
+ warning->set_text(String("\n").join(lines));
+ warning->popup_centered();
+
+ } else if (p_id == BUTTON_SIGNALS) {
+ editor_selection->clear();
+ editor_selection->add_node(n);
+
+ set_selected(n);
+
+ TabContainer *tab_container = Object::cast_to<TabContainer>(NodeDock::get_singleton()->get_parent());
+ NodeDock::get_singleton()->get_parent()->call("set_current_tab", tab_container->get_tab_idx_from_control(NodeDock::get_singleton()));
+ NodeDock::get_singleton()->show_connections();
+
+ } else if (p_id == BUTTON_GROUPS) {
+ editor_selection->clear();
+ editor_selection->add_node(n);
+
+ set_selected(n);
+
+ TabContainer *tab_container = Object::cast_to<TabContainer>(NodeDock::get_singleton()->get_parent());
+ NodeDock::get_singleton()->get_parent()->call("set_current_tab", tab_container->get_tab_idx_from_control(NodeDock::get_singleton()));
+ NodeDock::get_singleton()->show_groups();
+ } else if (p_id == BUTTON_UNIQUE) {
+ undo_redo->create_action(TTR("Disable Scene Unique Name"));
+ undo_redo->add_do_method(n, "set_unique_name_in_owner", false);
+ undo_redo->add_undo_method(n, "set_unique_name_in_owner", true);
+ undo_redo->add_do_method(this, "_update_tree");
+ undo_redo->add_undo_method(this, "_update_tree");
+ undo_redo->commit_action();
+ }
+}
+
+void SceneTreeEditor::_toggle_visible(Node *p_node) {
+ if (p_node->has_method("is_visible") && p_node->has_method("set_visible")) {
+ bool v = bool(p_node->call("is_visible"));
+ EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();
+ undo_redo->add_do_method(p_node, "set_visible", !v);
+ undo_redo->add_undo_method(p_node, "set_visible", v);
+ }
+}
+
+void SceneTreeEditor::_add_nodes(Node *p_node, TreeItem *p_parent) {
+ if (!p_node) {
+ return;
+ }
+
+ // only owned nodes are editable, since nodes can create their own (manually owned) child nodes,
+ // which the editor needs not to know about.
+
+ bool part_of_subscene = false;
+
+ if (!display_foreign && p_node->get_owner() != get_scene_node() && p_node != get_scene_node()) {
+ if ((show_enabled_subscene || can_open_instance) && p_node->get_owner() && (get_scene_node()->is_editable_instance(p_node->get_owner()))) {
+ part_of_subscene = true;
+ //allow
+ } else {
+ return;
+ }
+ } else {
+ part_of_subscene = p_node != get_scene_node() && get_scene_node()->get_scene_inherited_state().is_valid() && get_scene_node()->get_scene_inherited_state()->find_node_by_path(get_scene_node()->get_path_to(p_node)) >= 0;
+ }
+
+ TreeItem *item = tree->create_item(p_parent);
+
+ item->set_text(0, p_node->get_name());
+ if (can_rename && !part_of_subscene) {
+ item->set_editable(0, true);
+ }
+
+ item->set_selectable(0, true);
+ if (can_rename) {
+ bool collapsed = p_node->is_displayed_folded();
+ if (collapsed) {
+ item->set_collapsed(true);
+ }
+ }
+
+ Ref<Texture2D> icon = EditorNode::get_singleton()->get_object_icon(p_node, "Node");
+ item->set_icon(0, icon);
+ item->set_metadata(0, p_node->get_path());
+
+ if (connect_to_script_mode) {
+ Color accent = get_theme_color(SNAME("accent_color"), SNAME("Editor"));
+
+ Ref<Script> scr = p_node->get_script();
+ if (!scr.is_null() && EditorNode::get_singleton()->get_object_custom_type_base(p_node) != scr) {
+ //has script
+ item->add_button(0, get_theme_icon(SNAME("Script"), SNAME("EditorIcons")), BUTTON_SCRIPT);
+ } else {
+ //has no script (or script is a custom type)
+ item->set_custom_color(0, get_theme_color(SNAME("disabled_font_color"), SNAME("Editor")));
+ item->set_selectable(0, false);
+
+ if (!scr.is_null()) { // make sure to mark the script if a custom type
+ item->add_button(0, get_theme_icon(SNAME("Script"), SNAME("EditorIcons")), BUTTON_SCRIPT);
+ item->set_button_disabled(0, item->get_button_count(0) - 1, true);
+ }
+
+ accent.a *= 0.7;
+ }
+
+ if (marked.has(p_node)) {
+ String node_name = p_node->get_name();
+ if (connecting_signal) {
+ node_name += " " + TTR("(Connecting From)");
+ }
+ item->set_text(0, node_name);
+ item->set_custom_color(0, accent);
+ }
+ } else if (part_of_subscene) {
+ if (valid_types.size() == 0) {
+ item->set_custom_color(0, get_theme_color(SNAME("warning_color"), SNAME("Editor")));
+ }
+ } else if (marked.has(p_node)) {
+ String node_name = p_node->get_name();
+ if (connecting_signal) {
+ node_name += " " + TTR("(Connecting From)");
+ }
+ item->set_text(0, node_name);
+ item->set_selectable(0, marked_selectable);
+ item->set_custom_color(0, get_theme_color(SNAME("accent_color"), SNAME("Editor")));
+ } else if (!p_node->can_process()) {
+ item->set_custom_color(0, get_theme_color(SNAME("disabled_font_color"), SNAME("Editor")));
+ } else if (!marked_selectable && !marked_children_selectable) {
+ Node *node = p_node;
+ while (node) {
+ if (marked.has(node)) {
+ item->set_selectable(0, false);
+ item->set_custom_color(0, get_theme_color(SNAME("error_color"), SNAME("Editor")));
+ break;
+ }
+ node = node->get_parent();
+ }
+ }
+
+ if (can_rename) { //should be can edit..
+
+ String conf_warning = p_node->get_configuration_warnings_as_string();
+ if (!conf_warning.is_empty()) {
+ const int num_warnings = p_node->get_configuration_warnings().size();
+ String warning_icon;
+ if (num_warnings == 1) {
+ warning_icon = SNAME("NodeWarning");
+ } else if (num_warnings <= 3) {
+ warning_icon = vformat("NodeWarnings%d", num_warnings);
+ } else {
+ warning_icon = SNAME("NodeWarnings4Plus");
+ }
+
+ // Improve looks on tooltip, extra spacing on non-bullet point newlines.
+ const String bullet_point = String::utf8("• ");
+ int next_newline = 0;
+ while (next_newline != -1) {
+ next_newline = conf_warning.find("\n", next_newline + 2);
+ if (conf_warning.substr(next_newline + 1, bullet_point.length()) != bullet_point) {
+ conf_warning = conf_warning.insert(next_newline + 1, " ");
+ }
+ }
+
+ String newline = (num_warnings == 1 ? "\n" : "\n\n");
+
+ item->add_button(0, get_theme_icon(warning_icon, SNAME("EditorIcons")), BUTTON_WARNING, false, TTR("Node configuration warning:") + newline + conf_warning);
+ }
+
+ if (p_node->is_unique_name_in_owner()) {
+ item->add_button(0, get_theme_icon(SNAME("SceneUniqueName"), SNAME("EditorIcons")), BUTTON_UNIQUE, false, vformat(TTR("This node can be accessed from within anywhere in the scene by preceding it with the '%s' prefix in a node path.\nClick to disable this."), UNIQUE_NODE_PREFIX));
+ }
+
+ int num_connections = p_node->get_persistent_signal_connection_count();
+ int num_groups = p_node->get_persistent_group_count();
+
+ String msg_temp;
+ if (num_connections >= 1) {
+ Array arr;
+ arr.push_back(num_connections);
+ msg_temp += TTRN("Node has one connection.", "Node has {num} connections.", num_connections).format(arr, "{num}");
+ if (num_groups >= 1) {
+ msg_temp += "\n";
+ }
+ }
+ if (num_groups >= 1) {
+ msg_temp += TTRN("Node is in this group:", "Node is in the following groups:", num_groups) + "\n";
+
+ List<GroupInfo> groups;
+ p_node->get_groups(&groups);
+ for (const GroupInfo &E : groups) {
+ if (E.persistent) {
+ msg_temp += String::utf8("• ") + String(E.name) + "\n";
+ }
+ }
+ }
+ if (num_connections >= 1 || num_groups >= 1) {
+ if (num_groups < 1) {
+ msg_temp += "\n";
+ }
+ msg_temp += TTR("Click to show signals dock.");
+ }
+
+ Ref<Texture2D> icon_temp;
+ SceneTreeEditorButton signal_temp = BUTTON_SIGNALS;
+ if (num_connections >= 1 && num_groups >= 1) {
+ icon_temp = get_theme_icon(SNAME("SignalsAndGroups"), SNAME("EditorIcons"));
+ } else if (num_connections >= 1) {
+ icon_temp = get_theme_icon(SNAME("Signals"), SNAME("EditorIcons"));
+ } else if (num_groups >= 1) {
+ icon_temp = get_theme_icon(SNAME("Groups"), SNAME("EditorIcons"));
+ signal_temp = BUTTON_GROUPS;
+ }
+
+ if (num_connections >= 1 || num_groups >= 1) {
+ item->add_button(0, icon_temp, signal_temp, false, msg_temp);
+ }
+ }
+
+ // Display the node name in all tooltips so that long node names can be previewed
+ // without having to rename them.
+ if (p_node == get_scene_node() && p_node->get_scene_inherited_state().is_valid()) {
+ item->add_button(0, get_theme_icon(SNAME("InstanceOptions"), SNAME("EditorIcons")), BUTTON_SUBSCENE, false, TTR("Open in Editor"));
+
+ String tooltip = String(p_node->get_name()) + "\n" + TTR("Inherits:") + " " + p_node->get_scene_inherited_state()->get_path() + "\n" + TTR("Type:") + " " + p_node->get_class();
+ if (!p_node->get_editor_description().is_empty()) {
+ tooltip += "\n\n" + p_node->get_editor_description();
+ }
+
+ item->set_tooltip_text(0, tooltip);
+ } else if (p_node != get_scene_node() && !p_node->get_scene_file_path().is_empty() && can_open_instance) {
+ item->add_button(0, get_theme_icon(SNAME("InstanceOptions"), SNAME("EditorIcons")), BUTTON_SUBSCENE, false, TTR("Open in Editor"));
+
+ String tooltip = String(p_node->get_name()) + "\n" + TTR("Instance:") + " " + p_node->get_scene_file_path() + "\n" + TTR("Type:") + " " + p_node->get_class();
+ if (!p_node->get_editor_description().is_empty()) {
+ tooltip += "\n\n" + p_node->get_editor_description();
+ }
+
+ item->set_tooltip_text(0, tooltip);
+ } else {
+ StringName type = EditorNode::get_singleton()->get_object_custom_type_name(p_node);
+ if (type == StringName()) {
+ type = p_node->get_class();
+ }
+
+ String tooltip = String(p_node->get_name()) + "\n" + TTR("Type:") + " " + type;
+ if (!p_node->get_editor_description().is_empty()) {
+ tooltip += "\n\n" + p_node->get_editor_description();
+ }
+
+ item->set_tooltip_text(0, tooltip);
+ }
+
+ if (can_open_instance && is_scene_tree_dock) { // Show buttons only when necessary (SceneTreeDock) to avoid crashes.
+ if (!p_node->is_connected("script_changed", callable_mp(this, &SceneTreeEditor::_node_script_changed))) {
+ p_node->connect("script_changed", callable_mp(this, &SceneTreeEditor::_node_script_changed).bind(p_node));
+ }
+
+ Ref<Script> scr = p_node->get_script();
+ if (!scr.is_null()) {
+ String additional_notes;
+ Color button_color = Color(1, 1, 1);
+ // Can't set tooltip after adding button, need to do it before.
+ if (scr->is_tool()) {
+ additional_notes += "\n" + TTR("This script is currently running in the editor.");
+ button_color = get_theme_color(SNAME("accent_color"), SNAME("Editor"));
+ }
+ if (EditorNode::get_singleton()->get_object_custom_type_base(p_node) == scr) {
+ additional_notes += "\n" + TTR("This script is a custom type.");
+ button_color.a = 0.5;
+ }
+ item->add_button(0, get_theme_icon(SNAME("Script"), SNAME("EditorIcons")), BUTTON_SCRIPT, false, TTR("Open Script:") + " " + scr->get_path() + additional_notes);
+ item->set_button_color(0, item->get_button_count(0) - 1, button_color);
+ }
+
+ if (p_node->is_class("CanvasItem")) {
+ if (p_node->has_meta("_edit_lock_")) {
+ item->add_button(0, get_theme_icon(SNAME("Lock"), SNAME("EditorIcons")), BUTTON_LOCK, false, TTR("Node is locked.\nClick to unlock it."));
+ }
+
+ if (p_node->has_meta("_edit_group_")) {
+ item->add_button(0, get_theme_icon(SNAME("Group"), SNAME("EditorIcons")), BUTTON_GROUP, false, TTR("Children are not selectable.\nClick to make them selectable."));
+ }
+
+ bool v = p_node->call("is_visible");
+ if (v) {
+ item->add_button(0, get_theme_icon(SNAME("GuiVisibilityVisible"), SNAME("EditorIcons")), BUTTON_VISIBILITY, false, TTR("Toggle Visibility"));
+ } else {
+ item->add_button(0, get_theme_icon(SNAME("GuiVisibilityHidden"), SNAME("EditorIcons")), BUTTON_VISIBILITY, false, TTR("Toggle Visibility"));
+ }
+
+ if (!p_node->is_connected("visibility_changed", callable_mp(this, &SceneTreeEditor::_node_visibility_changed))) {
+ p_node->connect("visibility_changed", callable_mp(this, &SceneTreeEditor::_node_visibility_changed).bind(p_node));
+ }
+
+ _update_visibility_color(p_node, item);
+ } else if (p_node->is_class("CanvasLayer") || p_node->is_class("Window")) {
+ bool v = p_node->call("is_visible");
+ if (v) {
+ item->add_button(0, get_theme_icon(SNAME("GuiVisibilityVisible"), SNAME("EditorIcons")), BUTTON_VISIBILITY, false, TTR("Toggle Visibility"));
+ } else {
+ item->add_button(0, get_theme_icon(SNAME("GuiVisibilityHidden"), SNAME("EditorIcons")), BUTTON_VISIBILITY, false, TTR("Toggle Visibility"));
+ }
+
+ if (!p_node->is_connected("visibility_changed", callable_mp(this, &SceneTreeEditor::_node_visibility_changed))) {
+ p_node->connect("visibility_changed", callable_mp(this, &SceneTreeEditor::_node_visibility_changed).bind(p_node));
+ }
+ } else if (p_node->is_class("Node3D")) {
+ if (p_node->has_meta("_edit_lock_")) {
+ item->add_button(0, get_theme_icon(SNAME("Lock"), SNAME("EditorIcons")), BUTTON_LOCK, false, TTR("Node is locked.\nClick to unlock it."));
+ }
+
+ if (p_node->has_meta("_edit_group_")) {
+ item->add_button(0, get_theme_icon(SNAME("Group"), SNAME("EditorIcons")), BUTTON_GROUP, false, TTR("Children are not selectable.\nClick to make them selectable."));
+ }
+
+ bool v = p_node->call("is_visible");
+ if (v) {
+ item->add_button(0, get_theme_icon(SNAME("GuiVisibilityVisible"), SNAME("EditorIcons")), BUTTON_VISIBILITY, false, TTR("Toggle Visibility"));
+ } else {
+ item->add_button(0, get_theme_icon(SNAME("GuiVisibilityHidden"), SNAME("EditorIcons")), BUTTON_VISIBILITY, false, TTR("Toggle Visibility"));
+ }
+
+ if (!p_node->is_connected("visibility_changed", callable_mp(this, &SceneTreeEditor::_node_visibility_changed))) {
+ p_node->connect("visibility_changed", callable_mp(this, &SceneTreeEditor::_node_visibility_changed).bind(p_node));
+ }
+
+ _update_visibility_color(p_node, item);
+ } else if (p_node->is_class("AnimationPlayer")) {
+ bool is_pinned = AnimationPlayerEditor::get_singleton()->get_player() == p_node && AnimationPlayerEditor::get_singleton()->is_pinned();
+
+ if (is_pinned) {
+ item->add_button(0, get_theme_icon(SNAME("Pin"), SNAME("EditorIcons")), BUTTON_PIN, false, TTR("AnimationPlayer is pinned.\nClick to unpin."));
+ }
+ }
+ }
+
+ if (editor_selection) {
+ if (editor_selection->is_selected(p_node)) {
+ item->select(0);
+ }
+ }
+
+ if (selected == p_node) {
+ if (!editor_selection) {
+ item->select(0);
+ }
+ item->set_as_cursor(0);
+ }
+
+ for (int i = 0; i < p_node->get_child_count(); i++) {
+ _add_nodes(p_node->get_child(i), item);
+ }
+
+ if (valid_types.size()) {
+ bool valid = false;
+ for (const StringName &E : valid_types) {
+ if (p_node->is_class(E) ||
+ EditorNode::get_singleton()->is_object_of_custom_type(p_node, E)) {
+ valid = true;
+ break;
+ }
+ }
+
+ if (!valid) {
+ item->set_custom_color(0, get_theme_color(SNAME("disabled_font_color"), SNAME("Editor")));
+ item->set_selectable(0, false);
+ }
+ }
+}
+
+void SceneTreeEditor::_node_visibility_changed(Node *p_node) {
+ if (!p_node || (p_node != get_scene_node() && !p_node->get_owner())) {
+ return;
+ }
+
+ TreeItem *item = _find(tree->get_root(), p_node->get_path());
+
+ if (!item) {
+ return;
+ }
+
+ int idx = item->get_button_by_id(0, BUTTON_VISIBILITY);
+ ERR_FAIL_COND(idx == -1);
+
+ bool node_visible = false;
+
+ if (p_node->is_class("CanvasItem") || p_node->is_class("CanvasLayer") || p_node->is_class("Window")) {
+ node_visible = p_node->call("is_visible");
+ CanvasItemEditor::get_singleton()->get_viewport_control()->queue_redraw();
+ } else if (p_node->is_class("Node3D")) {
+ node_visible = p_node->call("is_visible");
+ }
+
+ if (node_visible) {
+ item->set_button(0, idx, get_theme_icon(SNAME("GuiVisibilityVisible"), SNAME("EditorIcons")));
+ } else {
+ item->set_button(0, idx, get_theme_icon(SNAME("GuiVisibilityHidden"), SNAME("EditorIcons")));
+ }
+
+ _update_visibility_color(p_node, item);
+}
+
+void SceneTreeEditor::_update_visibility_color(Node *p_node, TreeItem *p_item) {
+ if (p_node->is_class("CanvasItem") || p_node->is_class("Node3D")) {
+ Color color(1, 1, 1, 1);
+ bool visible_on_screen = p_node->call("is_visible_in_tree");
+ if (!visible_on_screen) {
+ color.a = 0.6;
+ }
+ int idx = p_item->get_button_by_id(0, BUTTON_VISIBILITY);
+ p_item->set_button_color(0, idx, color);
+ }
+}
+
+void SceneTreeEditor::_node_script_changed(Node *p_node) {
+ if (tree_dirty) {
+ return;
+ }
+
+ MessageQueue::get_singleton()->push_call(this, "_update_tree");
+ tree_dirty = true;
+}
+
+void SceneTreeEditor::_node_removed(Node *p_node) {
+ if (EditorNode::get_singleton()->is_exiting()) {
+ return; //speed up exit
+ }
+
+ if (p_node->is_connected("script_changed", callable_mp(this, &SceneTreeEditor::_node_script_changed))) {
+ p_node->disconnect("script_changed", callable_mp(this, &SceneTreeEditor::_node_script_changed));
+ }
+
+ if (p_node->is_class("Node3D") || p_node->is_class("CanvasItem") || p_node->is_class("CanvasLayer") || p_node->is_class("Window")) {
+ if (p_node->is_connected("visibility_changed", callable_mp(this, &SceneTreeEditor::_node_visibility_changed))) {
+ p_node->disconnect("visibility_changed", callable_mp(this, &SceneTreeEditor::_node_visibility_changed));
+ }
+ }
+
+ if (p_node == selected) {
+ selected = nullptr;
+ emit_signal(SNAME("node_selected"));
+ }
+}
+
+void SceneTreeEditor::_node_renamed(Node *p_node) {
+ if (p_node != get_scene_node() && !get_scene_node()->is_ancestor_of(p_node)) {
+ return;
+ }
+
+ emit_signal(SNAME("node_renamed"));
+
+ if (!tree_dirty) {
+ MessageQueue::get_singleton()->push_call(this, "_update_tree");
+ tree_dirty = true;
+ }
+}
+
+void SceneTreeEditor::_update_tree(bool p_scroll_to_selected) {
+ if (!is_inside_tree()) {
+ tree_dirty = false;
+ return;
+ }
+
+ if (tree->is_editing()) {
+ return;
+ }
+
+ updating_tree = true;
+ tree->clear();
+ if (get_scene_node()) {
+ _add_nodes(get_scene_node(), nullptr);
+ last_hash = hash_djb2_one_64(0);
+ _compute_hash(get_scene_node(), last_hash);
+ }
+ updating_tree = false;
+ tree_dirty = false;
+
+ if (!filter.strip_edges().is_empty()) {
+ _update_filter(nullptr, p_scroll_to_selected);
+ }
+}
+
+bool SceneTreeEditor::_update_filter(TreeItem *p_parent, bool p_scroll_to_selected) {
+ if (!p_parent) {
+ p_parent = tree->get_root();
+ filter_term_warning.clear();
+ }
+
+ if (!p_parent) {
+ // Tree is empty, nothing to do here.
+ return false;
+ }
+
+ bool keep_for_children = false;
+ for (TreeItem *child = p_parent->get_first_child(); child; child = child->get_next()) {
+ // Always keep if at least one of the children are kept.
+ keep_for_children = _update_filter(child, p_scroll_to_selected) || keep_for_children;
+ }
+
+ // Now find other reasons to keep this Node, too.
+ PackedStringArray terms = filter.to_lower().split_spaces();
+ bool keep = _item_matches_all_terms(p_parent, terms);
+
+ p_parent->set_visible(keep_for_children || keep);
+ if (keep_for_children) {
+ if (keep) {
+ p_parent->clear_custom_color(0);
+ p_parent->set_selectable(0, true);
+ } else {
+ p_parent->set_custom_color(0, get_theme_color(SNAME("disabled_font_color"), SNAME("Editor")));
+ p_parent->set_selectable(0, false);
+ p_parent->deselect(0);
+ }
+ }
+
+ if (editor_selection) {
+ Node *n = get_node(p_parent->get_metadata(0));
+ if (keep) {
+ if (p_scroll_to_selected && n && editor_selection->is_selected(n)) {
+ tree->scroll_to_item(p_parent);
+ }
+ } else {
+ if (n && p_parent->is_selected(0)) {
+ editor_selection->remove_node(n);
+ p_parent->deselect(0);
+ }
+ }
+ }
+
+ return keep || keep_for_children;
+}
+
+bool SceneTreeEditor::_item_matches_all_terms(TreeItem *p_item, PackedStringArray p_terms) {
+ if (p_terms.is_empty()) {
+ return true;
+ }
+
+ for (int i = 0; i < p_terms.size(); i++) {
+ String term = p_terms[i];
+
+ // Recognize special filter.
+ if (term.contains(":") && !term.get_slicec(':', 0).is_empty()) {
+ String parameter = term.get_slicec(':', 0);
+ String argument = term.get_slicec(':', 1);
+
+ if (parameter == "type" || parameter == "t") {
+ // Filter by Type.
+ String type = get_node(p_item->get_metadata(0))->get_class();
+ bool term_in_inherited_class = false;
+ // Every Node is is a Node, duh!
+ while (type != "Node") {
+ if (type.to_lower().contains(argument)) {
+ term_in_inherited_class = true;
+ break;
+ }
+
+ type = ClassDB::get_parent_class(type);
+ }
+ if (!term_in_inherited_class) {
+ return false;
+ }
+ } else if (parameter == "group" || parameter == "g") {
+ // Filter by Group.
+ Node *node = get_node(p_item->get_metadata(0));
+
+ if (argument.is_empty()) {
+ // When argument is empty, match all Nodes belonging to any exposed group.
+ if (node->get_persistent_group_count() == 0) {
+ return false;
+ }
+ } else {
+ List<Node::GroupInfo> group_info_list;
+ node->get_groups(&group_info_list);
+
+ bool term_in_groups = false;
+ for (int j = 0; j < group_info_list.size(); j++) {
+ if (!group_info_list[j].persistent) {
+ continue; // Ignore internal groups.
+ }
+ if (String(group_info_list[j].name).to_lower().contains(argument)) {
+ term_in_groups = true;
+ break;
+ }
+ }
+ if (!term_in_groups) {
+ return false;
+ }
+ }
+ } else if (filter_term_warning.is_empty()) {
+ filter_term_warning = vformat(TTR("\"%s\" is not a known filter."), parameter);
+ continue;
+ }
+ } else {
+ // Default.
+ if (!p_item->get_text(0).to_lower().contains(term)) {
+ return false;
+ }
+ }
+ }
+
+ return true;
+}
+
+void SceneTreeEditor::_compute_hash(Node *p_node, uint64_t &hash) {
+ hash = hash_djb2_one_64(p_node->get_instance_id(), hash);
+ if (p_node->get_parent()) {
+ hash = hash_djb2_one_64(p_node->get_parent()->get_instance_id(), hash); //so a reparent still produces a different hash
+ }
+
+ for (int i = 0; i < p_node->get_child_count(); i++) {
+ _compute_hash(p_node->get_child(i), hash);
+ }
+}
+
+void SceneTreeEditor::_test_update_tree() {
+ pending_test_update = false;
+
+ if (!is_inside_tree()) {
+ return;
+ }
+
+ if (tree_dirty) {
+ return; // don't even bother
+ }
+
+ uint64_t hash = hash_djb2_one_64(0);
+ if (get_scene_node()) {
+ _compute_hash(get_scene_node(), hash);
+ }
+ //test hash
+ if (hash == last_hash) {
+ return; // did not change
+ }
+
+ MessageQueue::get_singleton()->push_call(this, "_update_tree");
+ tree_dirty = true;
+}
+
+void SceneTreeEditor::_tree_process_mode_changed() {
+ MessageQueue::get_singleton()->push_call(this, "_update_tree");
+ tree_dirty = true;
+}
+
+void SceneTreeEditor::_tree_changed() {
+ if (EditorNode::get_singleton()->is_exiting()) {
+ return; //speed up exit
+ }
+ if (pending_test_update) {
+ return;
+ }
+ if (tree_dirty) {
+ return;
+ }
+
+ MessageQueue::get_singleton()->push_call(this, "_test_update_tree");
+ pending_test_update = true;
+}
+
+void SceneTreeEditor::_selected_changed() {
+ TreeItem *s = tree->get_selected();
+ ERR_FAIL_COND(!s);
+ NodePath np = s->get_metadata(0);
+
+ Node *n = get_node(np);
+
+ if (n == selected) {
+ return;
+ }
+
+ selected = get_node(np);
+
+ blocked++;
+ emit_signal(SNAME("node_selected"));
+ blocked--;
+}
+
+void SceneTreeEditor::_deselect_items() {
+ // Clear currently selected items in scene tree dock.
+ if (editor_selection) {
+ editor_selection->clear();
+ emit_signal(SNAME("node_changed"));
+ }
+}
+
+void SceneTreeEditor::_cell_multi_selected(Object *p_object, int p_cell, bool p_selected) {
+ TreeItem *item = Object::cast_to<TreeItem>(p_object);
+ ERR_FAIL_COND(!item);
+
+ if (!item->is_visible()) {
+ return;
+ }
+
+ NodePath np = item->get_metadata(0);
+
+ Node *n = get_node(np);
+
+ if (!n) {
+ return;
+ }
+
+ if (!editor_selection) {
+ return;
+ }
+
+ if (p_selected) {
+ editor_selection->add_node(n);
+
+ } else {
+ editor_selection->remove_node(n);
+ }
+
+ // Emitted "selected" in _selected_changed() when select single node, so select multiple node emit "changed"
+ if (editor_selection->get_selected_nodes().size() > 1) {
+ emit_signal(SNAME("node_changed"));
+ }
+}
+
+void SceneTreeEditor::_notification(int p_what) {
+ switch (p_what) {
+ case NOTIFICATION_ENTER_TREE: {
+ get_tree()->connect("tree_changed", callable_mp(this, &SceneTreeEditor::_tree_changed));
+ get_tree()->connect("tree_process_mode_changed", callable_mp(this, &SceneTreeEditor::_tree_process_mode_changed));
+ get_tree()->connect("node_removed", callable_mp(this, &SceneTreeEditor::_node_removed));
+ get_tree()->connect("node_renamed", callable_mp(this, &SceneTreeEditor::_node_renamed));
+ get_tree()->connect("node_configuration_warning_changed", callable_mp(this, &SceneTreeEditor::_warning_changed));
+
+ tree->connect("item_collapsed", callable_mp(this, &SceneTreeEditor::_cell_collapsed));
+
+ _update_tree();
+ } break;
+
+ case NOTIFICATION_EXIT_TREE: {
+ get_tree()->disconnect("tree_changed", callable_mp(this, &SceneTreeEditor::_tree_changed));
+ get_tree()->disconnect("tree_process_mode_changed", callable_mp(this, &SceneTreeEditor::_tree_process_mode_changed));
+ get_tree()->disconnect("node_removed", callable_mp(this, &SceneTreeEditor::_node_removed));
+ get_tree()->disconnect("node_renamed", callable_mp(this, &SceneTreeEditor::_node_renamed));
+ tree->disconnect("item_collapsed", callable_mp(this, &SceneTreeEditor::_cell_collapsed));
+ get_tree()->disconnect("node_configuration_warning_changed", callable_mp(this, &SceneTreeEditor::_warning_changed));
+ } break;
+
+ case NOTIFICATION_THEME_CHANGED: {
+ tree->add_theme_constant_override("icon_max_width", get_theme_constant(SNAME("class_icon_size"), SNAME("Editor")));
+
+ _update_tree();
+ } break;
+ }
+}
+
+TreeItem *SceneTreeEditor::_find(TreeItem *p_node, const NodePath &p_path) {
+ if (!p_node) {
+ return nullptr;
+ }
+
+ NodePath np = p_node->get_metadata(0);
+ if (np == p_path) {
+ return p_node;
+ }
+
+ TreeItem *children = p_node->get_first_child();
+ while (children) {
+ TreeItem *n = _find(children, p_path);
+ if (n) {
+ return n;
+ }
+ children = children->get_next();
+ }
+
+ return nullptr;
+}
+
+void SceneTreeEditor::set_selected(Node *p_node, bool p_emit_selected) {
+ ERR_FAIL_COND(blocked > 0);
+
+ if (pending_test_update) {
+ _test_update_tree();
+ }
+ if (tree_dirty) {
+ _update_tree();
+ }
+
+ if (selected == p_node) {
+ return;
+ }
+
+ TreeItem *item = p_node ? _find(tree->get_root(), p_node->get_path()) : nullptr;
+
+ if (item) {
+ if (auto_expand_selected) {
+ // Make visible when it's collapsed.
+ TreeItem *node = item->get_parent();
+ while (node && node != tree->get_root()) {
+ node->set_collapsed(false);
+ node = node->get_parent();
+ }
+ item->select(0);
+ item->set_as_cursor(0);
+ selected = p_node;
+ tree->ensure_cursor_is_visible();
+ }
+ } else {
+ if (!p_node) {
+ selected = nullptr;
+ }
+ _update_tree();
+ selected = p_node;
+ }
+
+ if (p_emit_selected) {
+ emit_signal(SNAME("node_selected"));
+ }
+}
+
+void SceneTreeEditor::_rename_node(ObjectID p_node, const String &p_name) {
+ Object *o = ObjectDB::get_instance(p_node);
+ ERR_FAIL_COND(!o);
+ Node *n = Object::cast_to<Node>(o);
+ ERR_FAIL_COND(!n);
+ TreeItem *item = _find(tree->get_root(), n->get_path());
+ ERR_FAIL_COND(!item);
+
+ n->set_name(p_name);
+ item->set_metadata(0, n->get_path());
+ item->set_text(0, p_name);
+}
+
+void SceneTreeEditor::_renamed() {
+ TreeItem *which = tree->get_edited();
+
+ ERR_FAIL_COND(!which);
+ NodePath np = which->get_metadata(0);
+ Node *n = get_node(np);
+ ERR_FAIL_COND(!n);
+
+ String raw_new_name = which->get_text(0);
+ if (raw_new_name.strip_edges().is_empty()) {
+ // If name is empty, fallback to class name.
+ if (GLOBAL_GET("editor/naming/node_name_casing").operator int() != NAME_CASING_PASCAL_CASE) {
+ raw_new_name = Node::adjust_name_casing(n->get_class());
+ } else {
+ raw_new_name = n->get_class();
+ }
+ }
+
+ String new_name = raw_new_name.validate_node_name();
+
+ if (new_name != raw_new_name) {
+ error->set_text(TTR("Invalid node name, the following characters are not allowed:") + "\n" + String::get_invalid_node_name_characters());
+ error->popup_centered();
+
+ if (new_name.is_empty()) {
+ which->set_text(0, n->get_name());
+ return;
+ }
+
+ which->set_text(0, new_name);
+ }
+
+ if (new_name == n->get_name()) {
+ if (which->get_text(0).is_empty()) {
+ which->set_text(0, new_name);
+ }
+
+ return;
+ }
+
+ // Trim leading/trailing whitespace to prevent node names from containing accidental whitespace, which would make it more difficult to get the node via `get_node()`.
+ new_name = new_name.strip_edges();
+
+ if (n->is_unique_name_in_owner() && get_tree()->get_edited_scene_root()->get_node_or_null("%" + new_name) != nullptr) {
+ error->set_text(TTR("Another node already uses this unique name in the scene."));
+ error->popup_centered();
+ which->set_text(0, n->get_name());
+ return;
+ }
+
+ if (!is_scene_tree_dock) {
+ n->set_name(new_name);
+ which->set_metadata(0, n->get_path());
+ emit_signal(SNAME("node_renamed"));
+ } else {
+ EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();
+ undo_redo->create_action(TTR("Rename Node"), UndoRedo::MERGE_DISABLE, n);
+ emit_signal(SNAME("node_prerename"), n, new_name);
+ undo_redo->add_do_method(this, "_rename_node", n->get_instance_id(), new_name);
+ undo_redo->add_undo_method(this, "_rename_node", n->get_instance_id(), n->get_name());
+ undo_redo->commit_action();
+ }
+}
+
+Node *SceneTreeEditor::get_selected() {
+ return selected;
+}
+
+void SceneTreeEditor::set_marked(const HashSet<Node *> &p_marked, bool p_selectable, bool p_children_selectable) {
+ if (tree_dirty) {
+ _update_tree();
+ }
+ marked = p_marked;
+ marked_selectable = p_selectable;
+ marked_children_selectable = p_children_selectable;
+ _update_tree();
+}
+
+void SceneTreeEditor::set_marked(Node *p_marked, bool p_selectable, bool p_children_selectable) {
+ HashSet<Node *> s;
+ if (p_marked) {
+ s.insert(p_marked);
+ }
+ set_marked(s, p_selectable, p_children_selectable);
+}
+
+void SceneTreeEditor::set_filter(const String &p_filter) {
+ filter = p_filter;
+ _update_filter(nullptr, true);
+}
+
+String SceneTreeEditor::get_filter() const {
+ return filter;
+}
+
+String SceneTreeEditor::get_filter_term_warning() {
+ return filter_term_warning;
+}
+
+void SceneTreeEditor::set_as_scene_tree_dock() {
+ is_scene_tree_dock = true;
+}
+
+void SceneTreeEditor::set_display_foreign_nodes(bool p_display) {
+ display_foreign = p_display;
+ _update_tree();
+}
+
+void SceneTreeEditor::set_valid_types(const Vector<StringName> &p_valid) {
+ valid_types = p_valid;
+}
+
+void SceneTreeEditor::set_editor_selection(EditorSelection *p_selection) {
+ editor_selection = p_selection;
+ tree->set_select_mode(Tree::SELECT_MULTI);
+ tree->set_cursor_can_exit_tree(false);
+ editor_selection->connect("selection_changed", callable_mp(this, &SceneTreeEditor::_selection_changed));
+}
+
+void SceneTreeEditor::_update_selection(TreeItem *item) {
+ ERR_FAIL_COND(!item);
+
+ NodePath np = item->get_metadata(0);
+
+ if (!has_node(np)) {
+ return;
+ }
+
+ Node *n = get_node(np);
+
+ if (!n) {
+ return;
+ }
+
+ if (editor_selection->is_selected(n)) {
+ item->select(0);
+ } else {
+ item->deselect(0);
+ }
+
+ TreeItem *c = item->get_first_child();
+
+ while (c) {
+ _update_selection(c);
+ c = c->get_next();
+ }
+}
+
+void SceneTreeEditor::_selection_changed() {
+ if (!editor_selection) {
+ return;
+ }
+
+ TreeItem *root = tree->get_root();
+
+ if (!root) {
+ return;
+ }
+ _update_selection(root);
+}
+
+void SceneTreeEditor::_cell_collapsed(Object *p_obj) {
+ if (updating_tree) {
+ return;
+ }
+ if (!can_rename) {
+ return;
+ }
+
+ TreeItem *ti = Object::cast_to<TreeItem>(p_obj);
+ if (!ti) {
+ return;
+ }
+
+ bool collapsed = ti->is_collapsed();
+
+ NodePath np = ti->get_metadata(0);
+
+ Node *n = get_node(np);
+ ERR_FAIL_COND(!n);
+
+ n->set_display_folded(collapsed);
+}
+
+Variant SceneTreeEditor::get_drag_data_fw(const Point2 &p_point, Control *p_from) {
+ if (!can_rename) {
+ return Variant(); //not editable tree
+ }
+
+ if (tree->get_button_id_at_position(p_point) != -1) {
+ return Variant(); //dragging from button
+ }
+
+ Vector<Node *> selected_nodes;
+ Vector<Ref<Texture2D>> icons;
+ TreeItem *next = tree->get_next_selected(nullptr);
+ while (next) {
+ NodePath np = next->get_metadata(0);
+
+ Node *n = get_node(np);
+ if (n) {
+ // Only allow selection if not part of an instantiated scene.
+ if (!n->get_owner() || n->get_owner() == get_scene_node() || n->get_owner()->get_scene_file_path().is_empty()) {
+ selected_nodes.push_back(n);
+ icons.push_back(next->get_icon(0));
+ }
+ }
+ next = tree->get_next_selected(next);
+ }
+
+ if (selected_nodes.is_empty()) {
+ return Variant();
+ }
+
+ VBoxContainer *vb = memnew(VBoxContainer);
+ Array objs;
+ int list_max = 10;
+ float opacity_step = 1.0f / list_max;
+ float opacity_item = 1.0f;
+ for (int i = 0; i < selected_nodes.size(); i++) {
+ if (i < list_max) {
+ HBoxContainer *hb = memnew(HBoxContainer);
+ TextureRect *tf = memnew(TextureRect);
+ tf->set_texture(icons[i]);
+ tf->set_stretch_mode(TextureRect::STRETCH_KEEP_CENTERED);
+ hb->add_child(tf);
+ Label *label = memnew(Label(selected_nodes[i]->get_name()));
+ hb->add_child(label);
+ vb->add_child(hb);
+ hb->set_modulate(Color(1, 1, 1, opacity_item));
+ opacity_item -= opacity_step;
+ }
+ NodePath p = selected_nodes[i]->get_path();
+ objs.push_back(p);
+ }
+
+ set_drag_preview(vb);
+ Dictionary drag_data;
+ drag_data["type"] = "nodes";
+ drag_data["nodes"] = objs;
+
+ tree->set_drop_mode_flags(Tree::DROP_MODE_INBETWEEN | Tree::DROP_MODE_ON_ITEM);
+ emit_signal(SNAME("nodes_dragged"));
+
+ return drag_data;
+}
+
+bool SceneTreeEditor::_is_script_type(const StringName &p_type) const {
+ return (script_types->find(p_type));
+}
+
+bool SceneTreeEditor::can_drop_data_fw(const Point2 &p_point, const Variant &p_data, Control *p_from) const {
+ if (!can_rename) {
+ return false; //not editable tree
+ }
+
+ Dictionary d = p_data;
+ if (!d.has("type")) {
+ return false;
+ }
+
+ TreeItem *item = tree->get_item_at_position(p_point);
+ if (!item) {
+ return false;
+ }
+
+ int section = tree->get_drop_section_at_position(p_point);
+ if (section < -1 || (section == -1 && !item->get_parent())) {
+ return false;
+ }
+
+ if (String(d["type"]) == "files") {
+ Vector<String> files = d["files"];
+
+ if (files.size() == 0) {
+ return false; //weird
+ }
+
+ if (_is_script_type(EditorFileSystem::get_singleton()->get_file_type(files[0]))) {
+ tree->set_drop_mode_flags(Tree::DROP_MODE_ON_ITEM);
+ return true;
+ }
+
+ bool scene_drop = true;
+ for (int i = 0; i < files.size(); i++) {
+ String file = files[i];
+ String ftype = EditorFileSystem::get_singleton()->get_file_type(file);
+ if (ftype != "PackedScene") {
+ scene_drop = false;
+ break;
+ }
+ }
+
+ if (scene_drop) {
+ tree->set_drop_mode_flags(Tree::DROP_MODE_INBETWEEN | Tree::DROP_MODE_ON_ITEM);
+ } else {
+ if (files.size() > 1) {
+ return false;
+ }
+ tree->set_drop_mode_flags(Tree::DROP_MODE_ON_ITEM);
+ }
+
+ return true;
+ }
+
+ if (String(d["type"]) == "script_list_element") {
+ ScriptEditorBase *se = Object::cast_to<ScriptEditorBase>(d["script_list_element"]);
+ if (se) {
+ String sp = se->get_edited_resource()->get_path();
+ if (_is_script_type(EditorFileSystem::get_singleton()->get_file_type(sp))) {
+ tree->set_drop_mode_flags(Tree::DROP_MODE_ON_ITEM);
+ return true;
+ }
+ }
+ }
+
+ return String(d["type"]) == "nodes" && filter.is_empty();
+}
+
+void SceneTreeEditor::drop_data_fw(const Point2 &p_point, const Variant &p_data, Control *p_from) {
+ if (!can_drop_data_fw(p_point, p_data, p_from)) {
+ return;
+ }
+
+ TreeItem *item = tree->get_item_at_position(p_point);
+ if (!item) {
+ return;
+ }
+ int section = tree->get_drop_section_at_position(p_point);
+ if (section < -1) {
+ return;
+ }
+
+ NodePath np = item->get_metadata(0);
+ Node *n = get_node(np);
+ if (!n) {
+ return;
+ }
+
+ Dictionary d = p_data;
+
+ if (String(d["type"]) == "nodes") {
+ Array nodes = d["nodes"];
+ emit_signal(SNAME("nodes_rearranged"), nodes, np, section);
+ }
+
+ if (String(d["type"]) == "files") {
+ Vector<String> files = d["files"];
+
+ String ftype = EditorFileSystem::get_singleton()->get_file_type(files[0]);
+ if (_is_script_type(ftype)) {
+ emit_signal(SNAME("script_dropped"), files[0], np);
+ } else {
+ emit_signal(SNAME("files_dropped"), files, np, section);
+ }
+ }
+
+ if (String(d["type"]) == "script_list_element") {
+ ScriptEditorBase *se = Object::cast_to<ScriptEditorBase>(d["script_list_element"]);
+ if (se) {
+ String sp = se->get_edited_resource()->get_path();
+ if (_is_script_type(EditorFileSystem::get_singleton()->get_file_type(sp))) {
+ emit_signal(SNAME("script_dropped"), sp, np);
+ }
+ }
+ }
+}
+
+void SceneTreeEditor::_empty_clicked(const Vector2 &p_pos, MouseButton p_button) {
+ if (p_button != MouseButton::RIGHT) {
+ return;
+ }
+ _rmb_select(p_pos);
+}
+
+void SceneTreeEditor::_rmb_select(const Vector2 &p_pos, MouseButton p_button) {
+ if (p_button != MouseButton::RIGHT) {
+ return;
+ }
+ emit_signal(SNAME("rmb_pressed"), tree->get_screen_position() + p_pos);
+}
+
+void SceneTreeEditor::update_warning() {
+ _warning_changed(nullptr);
+}
+
+void SceneTreeEditor::_warning_changed(Node *p_for_node) {
+ //should use a timer
+ update_timer->start();
+}
+
+void SceneTreeEditor::set_auto_expand_selected(bool p_auto, bool p_update_settings) {
+ if (p_update_settings) {
+ EditorSettings::get_singleton()->set("docks/scene_tree/auto_expand_to_selected", p_auto);
+ }
+ auto_expand_selected = p_auto;
+}
+
+void SceneTreeEditor::set_connect_to_script_mode(bool p_enable) {
+ connect_to_script_mode = p_enable;
+ update_tree();
+}
+
+void SceneTreeEditor::set_connecting_signal(bool p_enable) {
+ connecting_signal = p_enable;
+ update_tree();
+}
+
+void SceneTreeEditor::_bind_methods() {
+ ClassDB::bind_method(D_METHOD("_update_tree"), &SceneTreeEditor::_update_tree, DEFVAL(false)); // Still used by UndoRedo.
+ ClassDB::bind_method("_rename_node", &SceneTreeEditor::_rename_node);
+ ClassDB::bind_method("_test_update_tree", &SceneTreeEditor::_test_update_tree);
+
+ ClassDB::bind_method(D_METHOD("update_tree"), &SceneTreeEditor::update_tree);
+
+ ADD_SIGNAL(MethodInfo("node_selected"));
+ ADD_SIGNAL(MethodInfo("node_renamed"));
+ ADD_SIGNAL(MethodInfo("node_prerename"));
+ ADD_SIGNAL(MethodInfo("node_changed"));
+ ADD_SIGNAL(MethodInfo("nodes_dragged"));
+ ADD_SIGNAL(MethodInfo("nodes_rearranged", PropertyInfo(Variant::ARRAY, "paths"), PropertyInfo(Variant::NODE_PATH, "to_path"), PropertyInfo(Variant::INT, "type")));
+ ADD_SIGNAL(MethodInfo("files_dropped", PropertyInfo(Variant::PACKED_STRING_ARRAY, "files"), PropertyInfo(Variant::NODE_PATH, "to_path"), PropertyInfo(Variant::INT, "type")));
+ ADD_SIGNAL(MethodInfo("script_dropped", PropertyInfo(Variant::STRING, "file"), PropertyInfo(Variant::NODE_PATH, "to_path")));
+ ADD_SIGNAL(MethodInfo("rmb_pressed", PropertyInfo(Variant::VECTOR2, "position")));
+
+ ADD_SIGNAL(MethodInfo("open"));
+ ADD_SIGNAL(MethodInfo("open_script"));
+}
+
+SceneTreeEditor::SceneTreeEditor(bool p_label, bool p_can_rename, bool p_can_open_instance) {
+ selected = nullptr;
+
+ can_rename = p_can_rename;
+ can_open_instance = p_can_open_instance;
+ editor_selection = nullptr;
+
+ if (p_label) {
+ Label *label = memnew(Label);
+ label->set_theme_type_variation("HeaderSmall");
+ label->set_position(Point2(10, 0));
+ label->set_text(TTR("Scene Tree (Nodes):"));
+
+ add_child(label);
+ }
+
+ tree = memnew(Tree);
+ tree->set_anchor(SIDE_RIGHT, ANCHOR_END);
+ tree->set_anchor(SIDE_BOTTOM, ANCHOR_END);
+ tree->set_begin(Point2(0, p_label ? 18 : 0));
+ tree->set_end(Point2(0, 0));
+ tree->set_allow_reselect(true);
+ tree->add_theme_constant_override("button_margin", 0);
+
+ add_child(tree);
+
+ SET_DRAG_FORWARDING_GCD(tree, SceneTreeEditor);
+ if (p_can_rename) {
+ tree->set_allow_rmb_select(true);
+ tree->connect("item_mouse_selected", callable_mp(this, &SceneTreeEditor::_rmb_select));
+ tree->connect("empty_clicked", callable_mp(this, &SceneTreeEditor::_empty_clicked));
+ }
+
+ tree->connect("cell_selected", callable_mp(this, &SceneTreeEditor::_selected_changed));
+ tree->connect("item_edited", callable_mp(this, &SceneTreeEditor::_renamed));
+ tree->connect("multi_selected", callable_mp(this, &SceneTreeEditor::_cell_multi_selected));
+ tree->connect("button_clicked", callable_mp(this, &SceneTreeEditor::_cell_button_pressed));
+ tree->connect("nothing_selected", callable_mp(this, &SceneTreeEditor::_deselect_items));
+
+ error = memnew(AcceptDialog);
+ add_child(error);
+
+ warning = memnew(AcceptDialog);
+ add_child(warning);
+ warning->set_title(TTR("Node Configuration Warning!"));
+
+ last_hash = 0;
+ blocked = 0;
+
+ update_timer = memnew(Timer);
+ update_timer->connect("timeout", callable_mp(this, &SceneTreeEditor::_update_tree).bind(false));
+ update_timer->set_one_shot(true);
+ update_timer->set_wait_time(0.5);
+ add_child(update_timer);
+
+ script_types = memnew(List<StringName>);
+ ClassDB::get_inheriters_from_class("Script", script_types);
+}
+
+SceneTreeEditor::~SceneTreeEditor() {
+ memdelete(script_types);
+}
+
+/******** DIALOG *********/
+
+void SceneTreeDialog::popup_scenetree_dialog() {
+ popup_centered_clamped(Size2(350, 700) * EDSCALE);
+}
+
+void SceneTreeDialog::_update_theme() {
+ filter->set_right_icon(tree->get_theme_icon(SNAME("Search"), SNAME("EditorIcons")));
+}
+
+void SceneTreeDialog::_notification(int p_what) {
+ switch (p_what) {
+ case NOTIFICATION_VISIBILITY_CHANGED: {
+ if (is_visible()) {
+ tree->update_tree();
+
+ // Select the search bar by default.
+ filter->call_deferred(SNAME("grab_focus"));
+ }
+ } break;
+
+ case NOTIFICATION_ENTER_TREE: {
+ connect("confirmed", callable_mp(this, &SceneTreeDialog::_select));
+ _update_theme();
+ } break;
+
+ case NOTIFICATION_THEME_CHANGED: {
+ _update_theme();
+ } break;
+
+ case NOTIFICATION_EXIT_TREE: {
+ disconnect("confirmed", callable_mp(this, &SceneTreeDialog::_select));
+ } break;
+ }
+}
+
+void SceneTreeDialog::_cancel() {
+ hide();
+}
+
+void SceneTreeDialog::_select() {
+ if (tree->get_selected()) {
+ // The signal may cause another dialog to be displayed, so be sure to hide this one first.
+ hide();
+ emit_signal(SNAME("selected"), tree->get_selected()->get_path());
+ }
+}
+
+void SceneTreeDialog::_selected_changed() {
+ get_ok_button()->set_disabled(!tree->get_selected());
+}
+
+void SceneTreeDialog::_filter_changed(const String &p_filter) {
+ tree->set_filter(p_filter);
+}
+
+void SceneTreeDialog::_bind_methods() {
+ ClassDB::bind_method("_cancel", &SceneTreeDialog::_cancel);
+
+ ADD_SIGNAL(MethodInfo("selected", PropertyInfo(Variant::NODE_PATH, "path")));
+}
+
+SceneTreeDialog::SceneTreeDialog() {
+ set_title(TTR("Select a Node"));
+ VBoxContainer *vbc = memnew(VBoxContainer);
+ add_child(vbc);
+
+ filter = memnew(LineEdit);
+ filter->set_h_size_flags(Control::SIZE_EXPAND_FILL);
+ filter->set_placeholder(TTR("Filter Nodes"));
+ filter->set_clear_button_enabled(true);
+ filter->add_theme_constant_override("minimum_character_width", 0);
+ filter->connect("text_changed", callable_mp(this, &SceneTreeDialog::_filter_changed));
+ vbc->add_child(filter);
+
+ tree = memnew(SceneTreeEditor(false, false, true));
+ tree->set_v_size_flags(Control::SIZE_EXPAND_FILL);
+ tree->get_scene_tree()->connect("item_activated", callable_mp(this, &SceneTreeDialog::_select));
+ vbc->add_child(tree);
+
+ // Disable the OK button when no node is selected.
+ get_ok_button()->set_disabled(!tree->get_selected());
+ tree->connect("node_selected", callable_mp(this, &SceneTreeDialog::_selected_changed));
+}
+
+SceneTreeDialog::~SceneTreeDialog() {
+}
diff --git a/editor/gui/scene_tree_editor.h b/editor/gui/scene_tree_editor.h
new file mode 100644
index 0000000000..6a3213f8e4
--- /dev/null
+++ b/editor/gui/scene_tree_editor.h
@@ -0,0 +1,197 @@
+/**************************************************************************/
+/* scene_tree_editor.h */
+/**************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/**************************************************************************/
+
+#ifndef SCENE_TREE_EDITOR_H
+#define SCENE_TREE_EDITOR_H
+
+#include "editor/editor_data.h"
+#include "scene/gui/dialogs.h"
+#include "scene/gui/tree.h"
+
+class SceneTreeEditor : public Control {
+ GDCLASS(SceneTreeEditor, Control);
+
+ EditorSelection *editor_selection = nullptr;
+
+ enum SceneTreeEditorButton {
+ BUTTON_SUBSCENE = 0,
+ BUTTON_VISIBILITY = 1,
+ BUTTON_SCRIPT = 2,
+ BUTTON_LOCK = 3,
+ BUTTON_GROUP = 4,
+ BUTTON_WARNING = 5,
+ BUTTON_SIGNALS = 6,
+ BUTTON_GROUPS = 7,
+ BUTTON_PIN = 8,
+ BUTTON_UNIQUE = 9,
+ };
+
+ Tree *tree = nullptr;
+ Node *selected = nullptr;
+ ObjectID instance_node;
+
+ String filter;
+ String filter_term_warning;
+
+ AcceptDialog *error = nullptr;
+ AcceptDialog *warning = nullptr;
+
+ bool auto_expand_selected = true;
+ bool connect_to_script_mode = false;
+ bool connecting_signal = false;
+
+ int blocked;
+
+ void _compute_hash(Node *p_node, uint64_t &hash);
+
+ void _add_nodes(Node *p_node, TreeItem *p_parent);
+ void _test_update_tree();
+ bool _update_filter(TreeItem *p_parent = nullptr, bool p_scroll_to_selected = false);
+ bool _item_matches_all_terms(TreeItem *p_item, PackedStringArray p_terms);
+ void _tree_changed();
+ void _tree_process_mode_changed();
+ void _node_removed(Node *p_node);
+ void _node_renamed(Node *p_node);
+
+ TreeItem *_find(TreeItem *p_node, const NodePath &p_path);
+ void _notification(int p_what);
+ void _selected_changed();
+ void _deselect_items();
+ void _rename_node(ObjectID p_node, const String &p_name);
+
+ void _cell_collapsed(Object *p_obj);
+
+ uint64_t last_hash;
+
+ bool can_rename;
+ bool can_open_instance;
+ bool updating_tree = false;
+ bool show_enabled_subscene = false;
+ bool is_scene_tree_dock = false;
+
+ void _renamed();
+
+ HashSet<Node *> marked;
+ bool marked_selectable = false;
+ bool marked_children_selectable = false;
+ bool display_foreign = false;
+ bool tree_dirty = true;
+ bool pending_test_update = false;
+ static void _bind_methods();
+
+ void _cell_button_pressed(Object *p_item, int p_column, int p_id, MouseButton p_button);
+ void _toggle_visible(Node *p_node);
+ void _cell_multi_selected(Object *p_object, int p_cell, bool p_selected);
+ void _update_selection(TreeItem *item);
+ void _node_script_changed(Node *p_node);
+ void _node_visibility_changed(Node *p_node);
+ void _update_visibility_color(Node *p_node, TreeItem *p_item);
+
+ void _selection_changed();
+ Node *get_scene_node();
+
+ Variant get_drag_data_fw(const Point2 &p_point, Control *p_from);
+ bool can_drop_data_fw(const Point2 &p_point, const Variant &p_data, Control *p_from) const;
+ void drop_data_fw(const Point2 &p_point, const Variant &p_data, Control *p_from);
+
+ void _empty_clicked(const Vector2 &p_pos, MouseButton p_button);
+ void _rmb_select(const Vector2 &p_pos, MouseButton p_button = MouseButton::RIGHT);
+
+ void _warning_changed(Node *p_for_node);
+
+ Timer *update_timer = nullptr;
+
+ List<StringName> *script_types;
+ bool _is_script_type(const StringName &p_type) const;
+
+ Vector<StringName> valid_types;
+
+public:
+ // Public for use with callable_mp.
+ void _update_tree(bool p_scroll_to_selected = false);
+
+ void set_filter(const String &p_filter);
+ String get_filter() const;
+ String get_filter_term_warning();
+
+ void set_as_scene_tree_dock();
+ void set_display_foreign_nodes(bool p_display);
+
+ void set_marked(const HashSet<Node *> &p_marked, bool p_selectable = false, bool p_children_selectable = true);
+ void set_marked(Node *p_marked, bool p_selectable = false, bool p_children_selectable = true);
+ void set_selected(Node *p_node, bool p_emit_selected = true);
+ Node *get_selected();
+ void set_can_rename(bool p_can_rename) { can_rename = p_can_rename; }
+ void set_editor_selection(EditorSelection *p_selection);
+
+ void set_show_enabled_subscene(bool p_show) { show_enabled_subscene = p_show; }
+ void set_valid_types(const Vector<StringName> &p_valid);
+
+ void update_tree() { _update_tree(); }
+
+ void set_auto_expand_selected(bool p_auto, bool p_update_settings);
+ void set_connect_to_script_mode(bool p_enable);
+ void set_connecting_signal(bool p_enable);
+
+ Tree *get_scene_tree() { return tree; }
+
+ void update_warning();
+
+ SceneTreeEditor(bool p_label = true, bool p_can_rename = false, bool p_can_open_instance = false);
+ ~SceneTreeEditor();
+};
+
+class SceneTreeDialog : public ConfirmationDialog {
+ GDCLASS(SceneTreeDialog, ConfirmationDialog);
+
+ SceneTreeEditor *tree = nullptr;
+ //Button *select;
+ //Button *cancel;
+ LineEdit *filter = nullptr;
+
+ void _select();
+ void _cancel();
+ void _selected_changed();
+ void _filter_changed(const String &p_filter);
+ void _update_theme();
+
+protected:
+ void _notification(int p_what);
+ static void _bind_methods();
+
+public:
+ void popup_scenetree_dialog();
+ SceneTreeEditor *get_scene_tree() { return tree; }
+ LineEdit *get_filter_line_edit() { return filter; }
+ SceneTreeDialog();
+ ~SceneTreeDialog();
+};
+
+#endif // SCENE_TREE_EDITOR_H