Przeglądaj źródła

Save color palette as resources to reuse later

Co-authored-by: Micky <[email protected]>
Nông Văn Tình 1 rok temu
rodzic
commit
e6a49ab6ac

+ 16 - 0
doc/classes/ColorPalette.xml

@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<class name="ColorPalette" inherits="Resource" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../class.xsd">
+	<brief_description>
+		A resource class for managing a palette of colors, which can be loaded and saved using [ColorPicker].
+	</brief_description>
+	<description>
+		The [ColorPalette] resource is designed to store and manage a collection of colors. This resource is useful in scenarios where a predefined set of colors is required, such as for creating themes, designing user interfaces, or managing game assets. The built-in [ColorPicker] control can also make use of [ColorPalette] without additional code.
+	</description>
+	<tutorials>
+	</tutorials>
+	<members>
+		<member name="colors" type="PackedColorArray" setter="set_colors" getter="get_colors" default="PackedColorArray()">
+			A [PackedColorArray] containing the colors in the palette.
+		</member>
+	</members>
+</class>

+ 3 - 0
doc/classes/ColorPicker.xml

@@ -177,6 +177,9 @@
 		<theme_item name="folded_arrow" data_type="icon" type="Texture2D">
 			The icon for color preset drop down menu when folded.
 		</theme_item>
+		<theme_item name="menu_option" data_type="icon" type="Texture2D">
+			The icon for color preset option menu.
+		</theme_item>
 		<theme_item name="overbright_indicator" data_type="icon" type="Texture2D">
 			The indicator used to signalize that the color value is outside the 0-1 range.
 		</theme_item>

+ 12 - 0
editor/editor_node.cpp

@@ -3441,6 +3441,11 @@ void EditorNode::_update_file_menu_closed() {
 	file_menu->set_item_disabled(file_menu->get_item_index(FILE_OPEN_PREV), false);
 }
 
+void EditorNode::_palette_quick_open_dialog() {
+	quick_open_color_palette->popup_dialog({ "ColorPalette" }, palette_file_selected_callback);
+	quick_open_color_palette->set_title(TTR("Quick Open Color Palette..."));
+}
+
 void EditorNode::replace_resources_in_object(Object *p_object, const Vector<Ref<Resource>> &p_source_resources, const Vector<Ref<Resource>> &p_target_resource) {
 	List<PropertyInfo> pi;
 	p_object->get_property_list(&pi);
@@ -3900,6 +3905,10 @@ void EditorNode::setup_color_picker(ColorPicker *p_picker) {
 
 	p_picker->set_color_mode((ColorPicker::ColorModeType)default_color_mode);
 	p_picker->set_picker_shape((ColorPicker::PickerShapeType)picker_shape);
+
+	p_picker->set_quick_open_callback(callable_mp(this, &EditorNode::_palette_quick_open_dialog));
+	p_picker->set_palette_saved_callback(callable_mp(EditorFileSystem::get_singleton(), &EditorFileSystem::update_file));
+	palette_file_selected_callback = callable_mp(p_picker, &ColorPicker::_quick_open_palette_file_selected);
 }
 
 bool EditorNode::is_scene_open(const String &p_path) {
@@ -7866,6 +7875,9 @@ EditorNode::EditorNode() {
 	quick_open_dialog = memnew(EditorQuickOpenDialog);
 	gui_base->add_child(quick_open_dialog);
 
+	quick_open_color_palette = memnew(EditorQuickOpenDialog);
+	gui_base->add_child(quick_open_color_palette);
+
 	_update_recent_scenes();
 
 	set_process_shortcut_input(true);

+ 3 - 0
editor/editor_node.h

@@ -264,6 +264,7 @@ private:
 	EditorPluginList *editor_plugins_force_input_forwarding = nullptr;
 	EditorPluginList *editor_plugins_force_over = nullptr;
 	EditorPluginList *editor_plugins_over = nullptr;
+	EditorQuickOpenDialog *quick_open_color_palette = nullptr;
 	EditorResourcePreview *resource_preview = nullptr;
 	EditorSelection *editor_selection = nullptr;
 	EditorSettingsDialog *editor_settings_dialog = nullptr;
@@ -413,6 +414,7 @@ private:
 	Timer *editor_layout_save_delay_timer = nullptr;
 	Timer *scan_changes_timer = nullptr;
 	Button *distraction_free = nullptr;
+	Callable palette_file_selected_callback;
 
 	EditorBottomPanel *bottom_panel = nullptr;
 
@@ -534,6 +536,7 @@ private:
 	void _export_as_menu_option(int p_idx);
 	void _update_file_menu_opened();
 	void _update_file_menu_closed();
+	void _palette_quick_open_dialog();
 
 	void _remove_plugin_from_enabled(const String &p_name);
 	void _plugin_over_edit(EditorPlugin *p_plugin, Object *p_object);

+ 288 - 11
scene/gui/color_picker.cpp

@@ -33,6 +33,7 @@
 #include "core/io/image.h"
 #include "scene/gui/aspect_ratio_container.h"
 #include "scene/gui/color_mode.h"
+#include "scene/gui/file_dialog.h"
 #include "scene/gui/grid_container.h"
 #include "scene/gui/label.h"
 #include "scene/gui/line_edit.h"
@@ -43,6 +44,7 @@
 #include "scene/gui/slider.h"
 #include "scene/gui/spin_box.h"
 #include "scene/gui/texture_rect.h"
+#include "scene/resources/color_palette.h"
 #include "scene/resources/image_texture.h"
 #include "scene/resources/style_box_flat.h"
 #include "scene/resources/style_box_texture.h"
@@ -88,6 +90,7 @@ void ColorPicker::_notification(int p_what) {
 			_update_drop_down_arrow(btn_preset->is_pressed(), btn_preset);
 			_update_drop_down_arrow(btn_recent_preset->is_pressed(), btn_recent_preset);
 			btn_add_preset->set_button_icon(theme_cache.add_preset);
+			menu_btn->set_button_icon(theme_cache.menu_option);
 
 			btn_pick->set_custom_minimum_size(Size2(28 * theme_cache.base_scale, 0));
 			btn_shape->set_custom_minimum_size(Size2(28 * theme_cache.base_scale, 0));
@@ -496,6 +499,15 @@ void ColorPicker::set_editor_settings(Object *p_editor_settings) {
 	_update_presets();
 	_update_recent_presets();
 }
+
+void ColorPicker::set_quick_open_callback(const Callable &p_file_selected) {
+	quick_open_callback = p_file_selected;
+}
+
+void ColorPicker::set_palette_saved_callback(const Callable &p_palette_saved) {
+	palette_saved_callback = p_palette_saved;
+}
+
 #endif
 
 HSlider *ColorPicker::get_slider(int p_idx) {
@@ -673,16 +685,34 @@ void ColorPicker::_update_presets() {
 
 #ifdef TOOLS_ENABLED
 	if (editor_settings) {
-		// Rebuild swatch color buttons, keeping the add-preset button in the first position.
-		for (int i = 1; i < preset_container->get_child_count(); i++) {
-			preset_container->get_child(i)->queue_free();
-		}
-		for (const Color &preset : preset_cache) {
-			_add_preset_button(preset_size, preset);
+		String cached_name = editor_settings->call(SNAME("get_project_metadata"), "color_picker", "palette_name", String());
+		palette_path = editor_settings->call(SNAME("get_project_metadata"), "color_picker", "palette_path", String());
+		bool palette_edited = editor_settings->call(SNAME("get_project_metadata"), "color_picker", "palette_edited", false);
+		if (!cached_name.is_empty()) {
+			palette_name->set_text(cached_name);
+			if (btn_preset->is_pressed() && !presets.is_empty()) {
+				palette_name->show();
+			}
+
+			if (palette_edited) {
+				palette_name->set_text(vformat("%s*", palette_name->get_text().replace("*", "")));
+				palette_name->set_tooltip_text(ETR("The changes to this palette have not been saved to a file."));
+			}
 		}
-		_notification(NOTIFICATION_VISIBILITY_CHANGED);
 	}
 #endif
+
+	// Rebuild swatch color buttons, keeping the add-preset button in the first position.
+	for (int i = 1; i < preset_container->get_child_count(); i++) {
+		preset_container->get_child(i)->queue_free();
+	}
+
+	presets = preset_cache;
+	for (const Color &preset : preset_cache) {
+		_add_preset_button(preset_size, preset);
+	}
+
+	_notification(NOTIFICATION_VISIBILITY_CHANGED);
 }
 
 void ColorPicker::_update_recent_presets() {
@@ -791,6 +821,107 @@ void ColorPicker::_add_recent_preset_button(int p_size, const Color &p_color) {
 	btn_preset_new->connect(SceneStringName(toggled), callable_mp(this, &ColorPicker::_recent_preset_pressed).bind(btn_preset_new));
 }
 
+void ColorPicker::_load_palette() {
+	List<String> extensions;
+	ResourceLoader::get_recognized_extensions_for_type("ColorPalette", &extensions);
+
+	file_dialog->set_title(RTR("Load Color Palette"));
+	file_dialog->clear_filters();
+	for (const String &K : extensions) {
+		file_dialog->add_filter("*." + K);
+	}
+
+	file_dialog->set_file_mode(FileDialog::FILE_MODE_OPEN_FILE);
+	file_dialog->set_current_file("");
+	file_dialog->popup_centered_ratio();
+}
+
+void ColorPicker::_save_palette(bool p_is_save_as) {
+	if (!p_is_save_as && !palette_path.is_empty()) {
+		file_dialog->set_file_mode(FileDialog::FILE_MODE_SAVE_FILE);
+		_palette_file_selected(palette_path);
+		return;
+	} else {
+		List<String> extensions;
+		ResourceLoader::get_recognized_extensions_for_type("ColorPalette", &extensions);
+
+		file_dialog->set_title(RTR("Save Color Palette"));
+		file_dialog->clear_filters();
+		for (const String &K : extensions) {
+			file_dialog->add_filter("*." + K);
+		}
+
+		file_dialog->set_file_mode(FileDialog::FILE_MODE_SAVE_FILE);
+		file_dialog->set_current_file("new_palette.tres");
+		file_dialog->popup_centered_ratio();
+	}
+}
+
+void ColorPicker::_quick_open_palette_file_selected(const String &p_path) {
+	if (!file_dialog) {
+		file_dialog = memnew(FileDialog);
+		add_child(file_dialog, false, INTERNAL_MODE_FRONT);
+		file_dialog->force_parent_owned();
+		file_dialog->connect("file_selected", callable_mp(this, &ColorPicker::_palette_file_selected));
+		file_dialog->set_access(FileDialog::ACCESS_FILESYSTEM);
+		file_dialog->set_current_dir(Engine::get_singleton()->is_editor_hint() ? "res://" : "user://");
+	}
+	file_dialog->set_file_mode(FileDialog::FILE_MODE_OPEN_FILE);
+	_palette_file_selected(p_path);
+}
+
+void ColorPicker::_palette_file_selected(const String &p_path) {
+	switch (file_dialog->get_file_mode()) {
+		case FileDialog::FileMode::FILE_MODE_OPEN_FILE: {
+			Ref<ColorPalette> palette = ResourceLoader::load(p_path, "", ResourceFormatLoader::CACHE_MODE_IGNORE);
+			ERR_FAIL_COND_MSG(palette.is_null(), vformat("Cannot open color palette file for reading at: %s", p_path));
+			preset_cache.clear();
+			presets.clear();
+
+			PackedColorArray saved_presets = palette->get_colors();
+			for (const Color &saved_preset : saved_presets) {
+				preset_cache.push_back(saved_preset);
+				presets.push_back(saved_preset);
+			}
+
+#ifdef TOOLS_ENABLED
+			if (editor_settings) {
+				const StringName set_project_metadata = SNAME("set_project_metadata");
+				editor_settings->call(set_project_metadata, "color_picker", "presets", saved_presets);
+				editor_settings->call(set_project_metadata, "color_picker", "palette_edited", false);
+			}
+#endif
+		} break;
+		case FileDialog::FileMode::FILE_MODE_SAVE_FILE: {
+			ColorPalette *palette = memnew(ColorPalette);
+			palette->set_colors(get_presets());
+			Error error = ResourceSaver::save(palette, p_path);
+			ERR_FAIL_COND_MSG(error != Error::OK, vformat("Cannot open color palette file for writing at: %s", p_path));
+#ifdef TOOLS_ENABLED
+			if (palette_saved_callback.is_valid()) {
+				palette_saved_callback.call_deferred(p_path);
+			}
+#endif // TOOLS_ENABLED
+		} break;
+		default:
+			break;
+	}
+
+	palette_name->set_text(p_path.get_file().get_basename());
+	palette_name->set_tooltip_text("");
+	palette_name->show();
+	palette_path = p_path;
+	btn_preset->set_pressed(true);
+#ifdef TOOLS_ENABLED
+	if (editor_settings) {
+		editor_settings->call(SNAME("set_project_metadata"), "color_picker", "palette_name", palette_name->get_text());
+		editor_settings->call(SNAME("set_project_metadata"), "color_picker", "palette_path", palette_path);
+		editor_settings->call(SNAME("set_project_metadata"), "color_picker", "palette_edited", false);
+	}
+#endif
+	_update_presets();
+}
+
 void ColorPicker::_show_hide_preset(const bool &p_is_btn_pressed, Button *p_btn_preset, Container *p_preset_container) {
 	if (p_is_btn_pressed) {
 		p_preset_container->show();
@@ -798,6 +929,11 @@ void ColorPicker::_show_hide_preset(const bool &p_is_btn_pressed, Button *p_btn_
 		p_preset_container->hide();
 	}
 	_update_drop_down_arrow(p_is_btn_pressed, p_btn_preset);
+
+	palette_name->hide();
+	if (btn_preset->is_pressed() && !palette_name->get_text().is_empty()) {
+		palette_name->show();
+	}
 }
 
 void ColorPicker::_update_drop_down_arrow(const bool &p_is_btn_pressed, Button *p_btn_preset) {
@@ -874,10 +1010,17 @@ void ColorPicker::add_preset(const Color &p_color) {
 		_add_preset_button(_get_preset_size(), p_color);
 	}
 
+	if (!palette_name->get_text().is_empty()) {
+		palette_name->set_text(vformat("%s*", palette_name->get_text().trim_suffix("*")));
+		palette_name->set_tooltip_text(ETR("The changes to this palette have not been saved to a file."));
+	}
+
 #ifdef TOOLS_ENABLED
 	if (editor_settings) {
 		PackedColorArray arr_to_save = get_presets();
-		editor_settings->call(SNAME("set_project_metadata"), "color_picker", "presets", arr_to_save);
+		const StringName set_project_metadata = SNAME("set_project_metadata");
+		editor_settings->call(set_project_metadata, "color_picker", "presets", arr_to_save);
+		editor_settings->call(set_project_metadata, "color_picker", "palette_edited", true);
 	}
 #endif
 }
@@ -918,10 +1061,22 @@ void ColorPicker::erase_preset(const Color &p_color) {
 			}
 		}
 
+		palette_name->set_text(vformat("%s*", palette_name->get_text().replace("*", "")));
+		palette_name->set_tooltip_text(ETR("The changes to this palette have not been saved to a file."));
+		if (presets.is_empty()) {
+			palette_name->set_text("");
+			palette_path = String();
+			palette_name->hide();
+		}
+
 #ifdef TOOLS_ENABLED
 		if (editor_settings) {
 			PackedColorArray arr_to_save = get_presets();
-			editor_settings->call(SNAME("set_project_metadata"), "color_picker", "presets", arr_to_save);
+			const StringName set_project_metadata = SNAME("set_project_metadata");
+			editor_settings->call(set_project_metadata, "color_picker", "presets", arr_to_save);
+			editor_settings->call(set_project_metadata, "color_picker", "palette_edited", true);
+			editor_settings->call(set_project_metadata, "color_picker", "palette_name", palette_name->get_text());
+			editor_settings->call(set_project_metadata, "color_picker", "palette_path", palette_path);
 		}
 #endif
 	}
@@ -1556,6 +1711,106 @@ void ColorPicker::_pick_finished() {
 	picker_window->hide();
 }
 
+void ColorPicker::_update_menu_items() {
+	if (!options_menu) {
+		options_menu = memnew(PopupMenu);
+		add_child(options_menu, false, INTERNAL_MODE_FRONT);
+		options_menu->force_parent_owned();
+		options_menu->connect("id_pressed", callable_mp(this, &ColorPicker::_options_menu_cbk));
+	}
+
+	options_menu->clear();
+	options_menu->reset_size();
+
+	if (!presets.is_empty()) {
+		options_menu->add_icon_item(get_theme_icon(SNAME("save"), SNAME("FileDialog")), RTR("Save"), static_cast<int>(MenuOption::MENU_SAVE));
+		options_menu->set_item_tooltip(-1, ETR("Save the current color palette to reuse later."));
+	}
+	if (!palette_path.is_empty()) {
+		options_menu->add_icon_item(get_theme_icon(SNAME("save"), SNAME("FileDialog")), RTR("Save As"), static_cast<int>(MenuOption::MENU_SAVE_AS));
+		options_menu->set_item_tooltip(-1, ETR("Save the current color palette as a new to reuse later."));
+	}
+	options_menu->add_icon_item(get_theme_icon(SNAME("load"), SNAME("FileDialog")), RTR("Load"), static_cast<int>(MenuOption::MENU_LOAD));
+	options_menu->set_item_tooltip(-1, ETR("Load existing color palette."));
+
+	if (Engine::get_singleton()->is_editor_hint()) {
+		options_menu->add_icon_item(get_theme_icon(SNAME("load"), SNAME("FileDialog")), RTR("Quick Load"), static_cast<int>(MenuOption::MENU_QUICKLOAD));
+		options_menu->set_item_tooltip(-1, ETR("Load existing color palette."));
+	}
+
+	if (!presets.is_empty()) {
+		options_menu->add_icon_item(get_theme_icon(SNAME("clear"), SNAME("FileDialog")), RTR("Clear"), static_cast<int>(MenuOption::MENU_CLEAR));
+		options_menu->set_item_tooltip(-1, ETR("Clear the currently loaded color palettes in the picker."));
+	}
+}
+
+void ColorPicker::_update_menu() {
+	_update_menu_items();
+	Rect2 gt = menu_btn->get_screen_rect();
+	menu_btn->reset_size();
+	int min_size = menu_btn->get_minimum_size().width;
+	Vector2 popup_pos = gt.get_end() - Vector2(min_size, 0);
+	options_menu->set_position(popup_pos);
+	options_menu->popup();
+}
+
+void ColorPicker::_options_menu_cbk(int p_which) {
+	if (!file_dialog) {
+		file_dialog = memnew(FileDialog);
+		add_child(file_dialog, false, INTERNAL_MODE_FRONT);
+		file_dialog->force_parent_owned();
+		file_dialog->connect("file_selected", callable_mp(this, &ColorPicker::_palette_file_selected));
+		file_dialog->set_access(FileDialog::ACCESS_FILESYSTEM);
+		file_dialog->set_current_dir(Engine::get_singleton()->is_editor_hint() ? "res://" : "user://");
+	}
+
+	MenuOption option = static_cast<MenuOption>(p_which);
+	switch (option) {
+		case MenuOption::MENU_SAVE:
+			_save_palette(false);
+			break;
+		case MenuOption::MENU_SAVE_AS:
+			_save_palette(true);
+			break;
+		case MenuOption::MENU_LOAD:
+			_load_palette();
+			break;
+
+#ifdef TOOLS_ENABLED
+		case MenuOption::MENU_QUICKLOAD:
+			if (quick_open_callback.is_valid()) {
+				file_dialog->set_file_mode(FileDialog::FILE_MODE_OPEN_FILE);
+				quick_open_callback.call_deferred();
+			}
+			break;
+#endif // TOOLS_ENABLED
+		case MenuOption::MENU_CLEAR: {
+			PackedColorArray colors = get_presets();
+			for (Color c : colors) {
+				erase_preset(c);
+			}
+
+			palette_name->set_text("");
+			palette_name->set_tooltip_text("");
+			palette_path = String();
+			btn_preset->set_pressed(false);
+
+#ifdef TOOLS_ENABLED
+			if (editor_settings) {
+				editor_settings->call(SNAME("set_project_metadata"), "color_picker", "palette_name", palette_name->get_text());
+				editor_settings->call(SNAME("set_project_metadata"), "color_picker", "palette_path", palette_path);
+				editor_settings->call(SNAME("set_project_metadata"), "color_picker", "palette_edited", false);
+			}
+#endif // TOOLS_ENABLED
+
+		}
+
+		break;
+		default:
+			break;
+	}
+}
+
 void ColorPicker::_pick_button_pressed_legacy() {
 	if (!is_inside_tree()) {
 		return;
@@ -1805,6 +2060,7 @@ void ColorPicker::_bind_methods() {
 
 	BIND_THEME_ITEM(Theme::DATA_TYPE_CONSTANT, ColorPicker, center_slider_grabbers);
 
+	BIND_THEME_ITEM(Theme::DATA_TYPE_ICON, ColorPicker, menu_option);
 	BIND_THEME_ITEM(Theme::DATA_TYPE_ICON, ColorPicker, screen_picker);
 	BIND_THEME_ITEM(Theme::DATA_TYPE_ICON, ColorPicker, expanded_arrow);
 	BIND_THEME_ITEM(Theme::DATA_TYPE_ICON, ColorPicker, folded_arrow);
@@ -2004,13 +2260,34 @@ ColorPicker::ColorPicker() {
 
 	preset_group.instantiate();
 
-	btn_preset = memnew(Button(ETR("Swatches")));
+	HBoxContainer *palette_box = memnew(HBoxContainer);
+	palette_box->set_h_size_flags(SIZE_EXPAND_FILL);
+	real_vbox->add_child(palette_box);
+
+	btn_preset = memnew(Button);
+	btn_preset->set_text("Swatches");
 	btn_preset->set_flat(true);
 	btn_preset->set_toggle_mode(true);
 	btn_preset->set_focus_mode(FOCUS_NONE);
 	btn_preset->set_text_alignment(HORIZONTAL_ALIGNMENT_LEFT);
 	btn_preset->connect(SceneStringName(toggled), callable_mp(this, &ColorPicker::_show_hide_preset).bind(btn_preset, preset_container));
-	real_vbox->add_child(btn_preset);
+	palette_box->add_child(btn_preset);
+
+	HBoxContainer *padding_box = memnew(HBoxContainer);
+	padding_box->set_h_size_flags(SIZE_EXPAND_FILL);
+	palette_box->add_child(padding_box);
+
+	menu_btn = memnew(Button);
+	menu_btn->set_flat(true);
+	menu_btn->set_tooltip_text(ETR("Show all options available."));
+	menu_btn->set_focus_mode(FOCUS_NONE);
+	menu_btn->connect(SceneStringName(pressed), callable_mp(this, &ColorPicker::_update_menu));
+	palette_box->add_child(menu_btn);
+
+	palette_name = memnew(Label);
+	palette_name->hide();
+	palette_name->set_mouse_filter(MOUSE_FILTER_PASS);
+	real_vbox->add_child(palette_name);
 
 	real_vbox->add_child(preset_container);
 

+ 29 - 0
scene/gui/color_picker.h

@@ -49,6 +49,7 @@ class PopupMenu;
 class SpinBox;
 class StyleBoxFlat;
 class TextureRect;
+class FileDialog;
 
 class ColorPresetButton : public BaseButton {
 	GDCLASS(ColorPresetButton, BaseButton);
@@ -106,6 +107,14 @@ public:
 	static const int SLIDER_COUNT = 4;
 
 private:
+	enum class MenuOption {
+		MENU_SAVE,
+		MENU_SAVE_AS,
+		MENU_LOAD,
+		MENU_QUICKLOAD,
+		MENU_CLEAR,
+	};
+
 	static inline Ref<Shader> wheel_shader;
 	static inline Ref<Shader> circle_shader;
 	static inline Ref<Shader> circle_ok_color_shader;
@@ -129,6 +138,9 @@ private:
 	Label *picker_preview_label = nullptr;
 	Ref<StyleBoxFlat> picker_preview_style_box;
 	Color picker_color;
+	FileDialog *file_dialog = nullptr;
+	Button *menu_btn = nullptr;
+	PopupMenu *options_menu = nullptr;
 
 	MarginContainer *internal_margin = nullptr;
 	Control *uv_edit = nullptr;
@@ -144,6 +156,8 @@ private:
 	HBoxContainer *recent_preset_hbc = nullptr;
 	Button *btn_add_preset = nullptr;
 	Button *btn_pick = nullptr;
+	Label *palette_name = nullptr;
+	String palette_path;
 	Button *btn_preset = nullptr;
 	Button *btn_recent_preset = nullptr;
 	PopupMenu *shape_popup = nullptr;
@@ -159,6 +173,10 @@ private:
 	ColorPresetButton *selected_recent_preset = nullptr;
 	Ref<ButtonGroup> preset_group;
 	Ref<ButtonGroup> recent_preset_group;
+#ifdef TOOLS_ENABLED
+	Callable quick_open_callback;
+	Callable palette_saved_callback;
+#endif // TOOLS_ENABLED
 
 	OptionButton *mode_option_button = nullptr;
 
@@ -226,6 +244,7 @@ private:
 
 		bool center_slider_grabbers = true;
 
+		Ref<Texture2D> menu_option;
 		Ref<Texture2D> screen_picker;
 		Ref<Texture2D> expanded_arrow;
 		Ref<Texture2D> folded_arrow;
@@ -279,6 +298,10 @@ private:
 	void _html_focus_exit();
 	void _pick_button_pressed();
 	void _pick_finished();
+	void _update_menu_items();
+	void _update_menu();
+	void _options_menu_cbk(int p_which);
+
 	// Legacy color picking.
 	void _pick_button_pressed_legacy();
 	void _picker_texture_input(const Ref<InputEvent> &p_event);
@@ -286,6 +309,8 @@ private:
 	inline int _get_preset_size();
 	void _add_preset_button(int p_size, const Color &p_color);
 	void _add_recent_preset_button(int p_size, const Color &p_color);
+	void _save_palette(bool p_is_save_as);
+	void _load_palette();
 
 	void _show_hide_preset(const bool &p_is_btn_pressed, Button *p_btn_preset, Container *p_preset_container);
 	void _update_drop_down_arrow(const bool &p_is_btn_pressed, Button *p_btn_preset);
@@ -305,6 +330,8 @@ protected:
 public:
 #ifdef TOOLS_ENABLED
 	void set_editor_settings(Object *p_editor_settings);
+	void set_quick_open_callback(const Callable &p_file_selected);
+	void set_palette_saved_callback(const Callable &p_palette_saved);
 #endif
 	HSlider *get_slider(int idx);
 	Vector<float> get_active_slider_values();
@@ -322,6 +349,8 @@ public:
 	Color get_pick_color() const;
 	void set_old_color(const Color &p_color);
 	Color get_old_color() const;
+	void _quick_open_palette_file_selected(const String &p_path);
+	void _palette_file_selected(const String &p_path);
 
 	void set_display_old_color(bool p_enabled);
 	bool is_displaying_old_color() const;

+ 2 - 0
scene/register_scene_types.cpp

@@ -113,6 +113,7 @@
 #include "scene/resources/bone_map.h"
 #include "scene/resources/camera_attributes.h"
 #include "scene/resources/camera_texture.h"
+#include "scene/resources/color_palette.h"
 #include "scene/resources/compositor.h"
 #include "scene/resources/compressed_texture.h"
 #include "scene/resources/curve_texture.h"
@@ -969,6 +970,7 @@ void register_scene_types() {
 	GDREGISTER_CLASS(FontFile);
 	GDREGISTER_CLASS(FontVariation);
 	GDREGISTER_CLASS(SystemFont);
+	GDREGISTER_CLASS(ColorPalette);
 
 	GDREGISTER_CLASS(Curve);
 

+ 46 - 0
scene/resources/color_palette.cpp

@@ -0,0 +1,46 @@
+/**************************************************************************/
+/*  color_palette.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 "color_palette.h"
+
+void ColorPalette::set_colors(const PackedColorArray &p_colors) {
+	colors = p_colors;
+}
+
+PackedColorArray ColorPalette::get_colors() const {
+	return colors;
+}
+
+void ColorPalette::_bind_methods() {
+	ClassDB::bind_method(D_METHOD("set_colors", "colors"), &ColorPalette::set_colors);
+	ClassDB::bind_method(D_METHOD("get_colors"), &ColorPalette::get_colors);
+
+	ADD_PROPERTY(PropertyInfo(Variant::PACKED_COLOR_ARRAY, "colors"), "set_colors", "get_colors");
+}

+ 50 - 0
scene/resources/color_palette.h

@@ -0,0 +1,50 @@
+/**************************************************************************/
+/*  color_palette.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 COLOR_PALETTE_H
+#define COLOR_PALETTE_H
+
+#include "core/io/resource.h"
+
+class ColorPalette : public Resource {
+	GDCLASS(ColorPalette, Resource)
+
+private:
+	PackedColorArray colors;
+
+protected:
+	static void _bind_methods();
+
+public:
+	void set_colors(const PackedColorArray &p_colors);
+	PackedColorArray get_colors() const;
+};
+
+#endif // COLOR_PALETTE_H

+ 4 - 0
scene/theme/default_theme.cpp

@@ -693,6 +693,9 @@ void fill_default_theme(Ref<Theme> &theme, const Ref<Font> &default_font, const
 
 	// File Dialog
 
+	theme->set_icon("load", "FileDialog", icons["load"]);
+	theme->set_icon("save", "FileDialog", icons["save"]);
+	theme->set_icon("clear", "FileDialog", icons["clear"]);
 	theme->set_icon("parent_folder", "FileDialog", icons["folder_up"]);
 	theme->set_icon("back_folder", "FileDialog", icons["arrow_left"]);
 	theme->set_icon("forward_folder", "FileDialog", icons["arrow_right"]);
@@ -1034,6 +1037,7 @@ void fill_default_theme(Ref<Theme> &theme, const Ref<Font> &default_font, const
 	theme->set_constant("label_width", "ColorPicker", Math::round(10 * scale));
 	theme->set_constant("center_slider_grabbers", "ColorPicker", 1);
 
+	theme->set_icon("menu_option", "ColorPicker", icons["tabs_menu_hl"]);
 	theme->set_icon("folded_arrow", "ColorPicker", icons["arrow_right"]);
 	theme->set_icon("expanded_arrow", "ColorPicker", icons["arrow_down"]);
 	theme->set_icon("screen_picker", "ColorPicker", icons["color_picker_pipette"]);

+ 1 - 0
scene/theme/icons/clear.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path fill="#e0e0e0" d="m8 1a1 1 0 0 0 -1 1v5h-2c-1.108 0-2 .892-2 2v1h10v-1c0-1.108-.892-2-2-2h-2v-5a1 1 0 0 0 -1-1zm-5 10v4l10-1v-3z"/></svg>

+ 1 - 0
scene/theme/icons/load.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path fill="#e0e0e0" d="M3 2a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h9c1.105 0 1.818-.91 2-2l1-6a1 1 0 0 0-1-1H6c-.552 0-.909.455-1 1l-1 6c-.091.545-.448 1-1 1a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1 1 1 0 0 0 1 1h5a1 1 0 0 0-1-1H7a2 2 0 0 0-2-2H3z"/></svg>

+ 1 - 0
scene/theme/icons/save.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path fill="#e0e0e0" d="M3 1a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V4l-3-3h-1v6H3zm1 0v5h3V1zm4 8a1 1 0 0 1 0 4 1 1 0 0 1 0-4z"/></svg>