From f88b51995b79a66b455fc9ee83fc5101a198775c Mon Sep 17 00:00:00 2001 From: kobewi Date: Mon, 30 Jun 2025 19:07:12 +0200 Subject: [PATCH] Expose FileDialog callbacks for getting custom icons --- doc/classes/FileDialog.xml | 28 +++++++++++++++ editor/editor_node.cpp | 21 +++++++++-- editor/editor_node.h | 5 +++ scene/gui/file_dialog.cpp | 74 ++++++++++++++++++++++++++++++++++---- scene/gui/file_dialog.h | 10 ++++-- scene/gui/item_list.cpp | 13 +++++-- 6 files changed, 138 insertions(+), 13 deletions(-) diff --git a/doc/classes/FileDialog.xml b/doc/classes/FileDialog.xml index bfa0c329490..c9869cc232c 100644 --- a/doc/classes/FileDialog.xml +++ b/doc/classes/FileDialog.xml @@ -131,6 +131,34 @@ [b]Note:[/b] [FileDialog] will update its internal [ItemList] of favorites when its visibility changes. Be sure to call this method earlier if you want your changes to have effect. + + + + + Sets the callback used by the [FileDialog] nodes to get a file icon, when [constant DISPLAY_LIST] mode is used. The callback should take a single [String] argument (file path), and return a [Texture2D]. If an invalid texture is returned, the [theme_item file] icon will be used instead. + + + + + + + Sets the callback used by the [FileDialog] nodes to get a file icon, when [constant DISPLAY_THUMBNAILS] mode is used. The callback should take a single [String] argument (file path), and return a [Texture2D]. If an invalid texture is returned, the [theme_item file_thumbnail] icon will be used instead. + Thumbnails are usually more complex and may take a while to load. To avoid stalling the application, you can use [ImageTexture] to asynchronously create the thumbnail. + [codeblock] + func _ready(): + FileDialog.set_get_thumbnail_callback(thumbnail_method) + + func thumbnail_method(path): + var image_texture = ImageTexture.new() + make_thumbnail_async(path, image_texture) + return image_texture + + func make_thumbnail_async(path, image_texture): + var thumbnail_texture = await generate_thumbnail(path) # Some method that generates a thumbnail. + image_texture.set_image(thumbnail_texture.get_image()) + [/codeblock] + + diff --git a/editor/editor_node.cpp b/editor/editor_node.cpp index c32393a7803..8208b175c95 100644 --- a/editor/editor_node.cpp +++ b/editor/editor_node.cpp @@ -687,6 +687,10 @@ void EditorNode::_update_theme(bool p_skip_creation) { _update_renderer_color(); } + Ref thumbnail_icon = gui_base->get_theme_icon(SNAME("file_thumbnail"), SNAME("FileDialog")); + default_thumbnail.instantiate(); + default_thumbnail->set_image(thumbnail_icon->get_image()); + editor_dock_manager->update_tab_styles(); editor_dock_manager->update_docks_menu(); editor_dock_manager->set_tab_icon_max_width(theme->get_constant(SNAME("class_icon_size"), EditorStringName(Editor))); @@ -5625,6 +5629,19 @@ Ref EditorNode::_file_dialog_get_icon(const String &p_path) { return singleton->icon_type_cache["Object"]; } +Ref EditorNode::_file_dialog_get_thumbnail(const String &p_path) { + Ref texture = singleton->default_thumbnail->duplicate(); + EditorResourcePreview::get_singleton()->queue_resource_preview(p_path, callable_mp_static(EditorNode::_file_dialog_thumbnail_callback).bind(texture)); + return texture; +} + +void EditorNode::_file_dialog_thumbnail_callback(const String &p_path, const Ref &p_preview, const Ref &p_small_preview, Ref p_texture) { + ERR_FAIL_COND(p_texture.is_null()); + if (p_preview.is_valid()) { + p_texture->set_image(p_preview->get_image()); + } +} + void EditorNode::_build_icon_type_cache() { List tl; theme->get_icon_list(EditorStringName(EditorIcons), &tl); @@ -7817,7 +7834,8 @@ EditorNode::EditorNode() { EditorContextMenuPluginManager::create(); // Used for previews. - FileDialog::get_icon_func = _file_dialog_get_icon; + FileDialog::set_get_icon_callback(callable_mp_static(_file_dialog_get_icon)); + FileDialog::set_get_thumbnail_callback(callable_mp_static(_file_dialog_get_thumbnail)); FileDialog::register_func = _file_dialog_register; FileDialog::unregister_func = _file_dialog_unregister; @@ -8919,7 +8937,6 @@ EditorNode::~EditorNode() { GDExtensionEditorPlugins::editor_node_add_plugin = nullptr; GDExtensionEditorPlugins::editor_node_remove_plugin = nullptr; - FileDialog::get_icon_func = nullptr; FileDialog::register_func = nullptr; FileDialog::unregister_func = nullptr; diff --git a/editor/editor_node.h b/editor/editor_node.h index ca147a05e0a..f60536df2df 100644 --- a/editor/editor_node.h +++ b/editor/editor_node.h @@ -46,6 +46,7 @@ class ConfirmationDialog; class Control; class FileDialog; class HBoxContainer; +class ImageTexture; class MenuBar; class MenuButton; class OptionButton; @@ -512,6 +513,10 @@ private: } static Ref _file_dialog_get_icon(const String &p_path); + static Ref _file_dialog_get_thumbnail(const String &p_path); + static void _file_dialog_thumbnail_callback(const String &p_path, const Ref &p_preview, const Ref &p_small_preview, Ref p_texture); + Ref default_thumbnail; + static void _file_dialog_register(FileDialog *p_dialog); static void _file_dialog_unregister(FileDialog *p_dialog); static void _editor_file_dialog_register(EditorFileDialog *p_dialog); diff --git a/scene/gui/file_dialog.cpp b/scene/gui/file_dialog.cpp index 52db20327ba..dc7f57a0d5c 100644 --- a/scene/gui/file_dialog.cpp +++ b/scene/gui/file_dialog.cpp @@ -577,6 +577,24 @@ bool FileDialog::_is_open_should_be_disabled() { return false; } +void FileDialog::_thumbnail_callback(const Ref &p_texture, const String &p_path) { + if (display_mode == DISPLAY_LIST || p_texture.is_null()) { + return; + } + + if (!p_path.begins_with(full_dir)) { + return; + } + + const String file_name = p_path.get_file(); + for (int i = 0; i < file_list->get_item_count(); i++) { + if (file_list->get_item_text(i) == file_name) { + file_list->set_item_icon(i, p_texture); + break; + } + } +} + void FileDialog::_go_up() { _change_dir(".."); _push_history(); @@ -778,7 +796,7 @@ void FileDialog::update_file_list() { file_list->set_max_columns(1); file_list->set_max_text_lines(1); file_list->set_fixed_column_width(0); - file_list->set_fixed_icon_size(Size2()); + file_list->set_fixed_icon_size(theme_cache.file->get_size()); } dir_access->list_dir_begin(); @@ -948,13 +966,44 @@ void FileDialog::update_file_list() { for (const FileInfo &info : filtered_files) { file_list->add_item(info.name); - if (get_icon_func) { - Ref icon = get_icon_func(base_dir.path_join(info.name)); + const String path = base_dir.path_join(info.name); + + if (display_mode == DISPLAY_LIST) { + Ref icon; + if (get_icon_callback.is_valid()) { + const Variant &v = path; + const Variant *argptrs[1] = { &v }; + Variant vicon; + + Callable::CallError ce; + get_icon_callback.callp(argptrs, 1, vicon, ce); + if (unlikely(ce.error != Callable::CallError::CALL_OK)) { + ERR_PRINT(vformat("Error calling FileDialog's icon callback: %s.", Variant::get_callable_error_text(get_icon_callback, argptrs, 1, ce))); + } + icon = vicon; + } + if (icon.is_null()) { + icon = theme_cache.file; + } + file_list->set_item_icon(-1, icon); + } else { // DISPLAY_THUMBNAILS + Ref icon; + if (get_thumbnail_callback.is_valid()) { + const Variant &v = path; + const Variant *argptrs[1] = { &v }; + Variant vicon; + + Callable::CallError ce; + get_thumbnail_callback.callp(argptrs, 1, vicon, ce); + if (unlikely(ce.error != Callable::CallError::CALL_OK)) { + ERR_PRINT(vformat("Error calling FileDialog's thumbnail callback: %s.", Variant::get_callable_error_text(get_thumbnail_callback, argptrs, 1, ce))); + } + icon = vicon; + } + if (icon.is_null()) { + icon = theme_cache.file; + } file_list->set_item_icon(-1, icon); - } else if (display_mode == DISPLAY_THUMBNAILS) { - file_list->set_item_icon(-1, theme_cache.file_thumbnail); - } else { - file_list->set_item_icon(-1, theme_cache.file); } file_list->set_item_icon_modulate(-1, theme_cache.file_icon_color); @@ -1983,6 +2032,8 @@ void FileDialog::_bind_methods() { ClassDB::bind_static_method("FileDialog", D_METHOD("get_favorite_list"), &FileDialog::get_favorite_list); ClassDB::bind_static_method("FileDialog", D_METHOD("set_recent_list", "recents"), &FileDialog::set_recent_list); ClassDB::bind_static_method("FileDialog", D_METHOD("get_recent_list"), &FileDialog::get_recent_list); + ClassDB::bind_static_method("FileDialog", D_METHOD("set_get_icon_callback", "callback"), &FileDialog::set_get_icon_callback); + ClassDB::bind_static_method("FileDialog", D_METHOD("set_get_thumbnail_callback", "callback"), &FileDialog::set_get_thumbnail_callback); ClassDB::bind_method(D_METHOD("invalidate"), &FileDialog::invalidate); @@ -2118,6 +2169,14 @@ void FileDialog::set_default_show_hidden_files(bool p_show) { default_show_hidden_files = p_show; } +void FileDialog::set_get_icon_callback(const Callable &p_callback) { + get_icon_callback = p_callback; +} + +void FileDialog::set_get_thumbnail_callback(const Callable &p_callback) { + get_thumbnail_callback = p_callback; +} + void FileDialog::set_use_native_dialog(bool p_native) { use_native_dialog = p_native; @@ -2143,6 +2202,7 @@ FileDialog::FileDialog() { set_hide_on_ok(false); set_size(Size2(640, 360)); set_default_ok_text(ETR("Save")); // Default mode text. + thumbnail_callback = callable_mp(this, &FileDialog::_thumbnail_callback); for (int i = 0; i < CUSTOMIZATION_MAX; i++) { customization_flags[i] = true; diff --git a/scene/gui/file_dialog.h b/scene/gui/file_dialog.h index e8b94fba286..f8b31a7ff6b 100644 --- a/scene/gui/file_dialog.h +++ b/scene/gui/file_dialog.h @@ -146,10 +146,11 @@ public: CUSTOMIZATION_MAX }; - typedef Ref (*GetIconFunc)(const String &); typedef void (*RegisterFunc)(FileDialog *); - inline static GetIconFunc get_icon_func = nullptr; + inline static Callable get_icon_callback; + inline static Callable get_thumbnail_callback; + inline static RegisterFunc register_func = nullptr; inline static RegisterFunc unregister_func = nullptr; @@ -189,6 +190,7 @@ private: String root_prefix; String full_dir; + Callable thumbnail_callback; bool is_invalidating = false; VBoxContainer *main_vbox = nullptr; @@ -338,6 +340,7 @@ private: void _native_dialog_cb_with_options(bool p_ok, const Vector &p_files, int p_filter, const Dictionary &p_selected_options); bool _is_open_should_be_disabled(); + void _thumbnail_callback(const Ref &p_texture, const String &p_path); TypedArray _get_options() const; void _update_option_controls(); @@ -430,6 +433,9 @@ public: static void set_default_show_hidden_files(bool p_show); + static void set_get_icon_callback(const Callable &p_callback); + static void set_get_thumbnail_callback(const Callable &p_callback); + void invalidate(); void deselect_all(); diff --git a/scene/gui/item_list.cpp b/scene/gui/item_list.cpp index 587136878b9..14fd7466c4d 100644 --- a/scene/gui/item_list.cpp +++ b/scene/gui/item_list.cpp @@ -208,11 +208,20 @@ void ItemList::set_item_icon(int p_idx, const Ref &p_icon) { } ERR_FAIL_INDEX(p_idx, items.size()); - if (items[p_idx].icon == p_icon) { + Item &item = items.write[p_idx]; + if (item.icon == p_icon) { return; } - items.write[p_idx].icon = p_icon; + const Callable redraw = callable_mp((CanvasItem *)this, &CanvasItem::queue_redraw); + if (item.icon.is_valid()) { + item.icon->disconnect_changed(redraw); + } + item.icon = p_icon; + if (p_icon.is_valid()) { + p_icon->connect_changed(redraw); + } + queue_redraw(); shape_changed = true; }