1 | /**************************************************************************/ |
2 | /* gradient_texture_2d_editor_plugin.cpp */ |
3 | /**************************************************************************/ |
4 | /* This file is part of: */ |
5 | /* GODOT ENGINE */ |
6 | /* https://godotengine.org */ |
7 | /**************************************************************************/ |
8 | /* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ |
9 | /* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ |
10 | /* */ |
11 | /* Permission is hereby granted, free of charge, to any person obtaining */ |
12 | /* a copy of this software and associated documentation files (the */ |
13 | /* "Software"), to deal in the Software without restriction, including */ |
14 | /* without limitation the rights to use, copy, modify, merge, publish, */ |
15 | /* distribute, sublicense, and/or sell copies of the Software, and to */ |
16 | /* permit persons to whom the Software is furnished to do so, subject to */ |
17 | /* the following conditions: */ |
18 | /* */ |
19 | /* The above copyright notice and this permission notice shall be */ |
20 | /* included in all copies or substantial portions of the Software. */ |
21 | /* */ |
22 | /* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ |
23 | /* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ |
24 | /* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ |
25 | /* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ |
26 | /* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ |
27 | /* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ |
28 | /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ |
29 | /**************************************************************************/ |
30 | |
31 | #include "gradient_texture_2d_editor_plugin.h" |
32 | |
33 | #include "editor/editor_node.h" |
34 | #include "editor/editor_scale.h" |
35 | #include "editor/editor_undo_redo_manager.h" |
36 | #include "editor/gui/editor_spin_slider.h" |
37 | #include "scene/gui/box_container.h" |
38 | #include "scene/gui/button.h" |
39 | #include "scene/gui/flow_container.h" |
40 | #include "scene/gui/separator.h" |
41 | #include "scene/resources/gradient_texture.h" |
42 | |
43 | Point2 GradientTexture2DEdit::_get_handle_pos(const Handle p_handle) { |
44 | // Get the handle's mouse position in pixels relative to offset. |
45 | return (p_handle == HANDLE_FROM ? texture->get_fill_from() : texture->get_fill_to()).clamp(Vector2(), Vector2(1, 1)) * size; |
46 | } |
47 | |
48 | GradientTexture2DEdit::Handle GradientTexture2DEdit::get_handle_at(const Vector2 &p_pos) { |
49 | Point2 from_pos = _get_handle_pos(HANDLE_FROM); |
50 | Point2 to_pos = _get_handle_pos(HANDLE_TO); |
51 | // If both handles are at the position, grab the one that's closer. |
52 | if (p_pos.distance_squared_to(from_pos) < p_pos.distance_squared_to(to_pos)) { |
53 | return Rect2(from_pos.round() - handle_size / 2, handle_size).has_point(p_pos) ? HANDLE_FROM : HANDLE_NONE; |
54 | } else { |
55 | return Rect2(to_pos.round() - handle_size / 2, handle_size).has_point(p_pos) ? HANDLE_TO : HANDLE_NONE; |
56 | } |
57 | } |
58 | |
59 | void GradientTexture2DEdit::set_fill_pos(const Vector2 &p_pos) { |
60 | if (p_pos.is_equal_approx(initial_grab_pos)) { |
61 | return; |
62 | } |
63 | |
64 | const StringName property_name = (grabbed == HANDLE_FROM) ? "fill_from" : "fill_to" ; |
65 | EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton(); |
66 | undo_redo->create_action(TTR("Move GradientTexture2D Fill Point" )); |
67 | undo_redo->add_do_property(texture.ptr(), property_name, p_pos); |
68 | undo_redo->add_undo_property(texture.ptr(), property_name, initial_grab_pos); |
69 | undo_redo->commit_action(); |
70 | } |
71 | |
72 | void GradientTexture2DEdit::gui_input(const Ref<InputEvent> &p_event) { |
73 | const Ref<InputEventMouseButton> mb = p_event; |
74 | if (mb.is_valid()) { |
75 | if (mb->get_button_index() == MouseButton::LEFT) { |
76 | if (mb->is_pressed()) { |
77 | grabbed = get_handle_at(mb->get_position() - offset); |
78 | |
79 | if (grabbed != HANDLE_NONE) { |
80 | initial_grab_pos = _get_handle_pos(grabbed) / size; |
81 | queue_redraw(); |
82 | } |
83 | } else { |
84 | // Release the handle. |
85 | if (grabbed != HANDLE_NONE) { |
86 | set_fill_pos(_get_handle_pos(grabbed) / size); |
87 | grabbed = HANDLE_NONE; |
88 | queue_redraw(); |
89 | } |
90 | } |
91 | } |
92 | |
93 | if (grabbed != HANDLE_NONE && mb->is_pressed() && mb->get_button_index() == MouseButton::RIGHT) { |
94 | texture->set((grabbed == HANDLE_FROM) ? SNAME("fill_from" ) : SNAME("fill_to" ), initial_grab_pos); |
95 | grabbed = HANDLE_NONE; |
96 | queue_redraw(); |
97 | } |
98 | } |
99 | |
100 | // Move handle. |
101 | const Ref<InputEventMouseMotion> mm = p_event; |
102 | if (mm.is_valid()) { |
103 | Vector2 mpos = mm->get_position() - offset; |
104 | |
105 | Handle handle_at_mpos = get_handle_at(mpos); |
106 | if (hovered != handle_at_mpos) { |
107 | hovered = handle_at_mpos; |
108 | queue_redraw(); |
109 | } |
110 | |
111 | if (grabbed == HANDLE_NONE) { |
112 | return; |
113 | } |
114 | |
115 | Vector2 new_pos = (mpos / size).clamp(Vector2(0, 0), Vector2(1, 1)); |
116 | if (snap_enabled || mm->is_ctrl_pressed()) { |
117 | new_pos = new_pos.snapped(Vector2(1.0 / snap_count, 1.0 / snap_count)); |
118 | } |
119 | |
120 | // Allow to snap to an axis with Shift. |
121 | if (mm->is_shift_pressed()) { |
122 | Vector2 initial_mpos = initial_grab_pos * size; |
123 | if (Math::abs(mpos.x - initial_mpos.x) > Math::abs(mpos.y - initial_mpos.y)) { |
124 | new_pos.y = initial_grab_pos.y; |
125 | } else { |
126 | new_pos.x = initial_grab_pos.x; |
127 | } |
128 | } |
129 | // Do it directly from the texture so there's no undo/redo until the handle is released. |
130 | texture->set((grabbed == HANDLE_FROM) ? SNAME("fill_from" ) : SNAME("fill_to" ), new_pos); |
131 | } |
132 | } |
133 | |
134 | void GradientTexture2DEdit::set_texture(Ref<GradientTexture2D> &p_texture) { |
135 | texture = p_texture; |
136 | texture->connect_changed(callable_mp((CanvasItem *)this, &CanvasItem::queue_redraw)); |
137 | } |
138 | |
139 | void GradientTexture2DEdit::set_snap_enabled(bool p_snap_enabled) { |
140 | snap_enabled = p_snap_enabled; |
141 | queue_redraw(); |
142 | if (texture.is_valid()) { |
143 | if (snap_enabled) { |
144 | texture->set_meta(SNAME("_snap_enabled" ), true); |
145 | } else { |
146 | texture->remove_meta(SNAME("_snap_enabled" )); |
147 | } |
148 | } |
149 | } |
150 | |
151 | void GradientTexture2DEdit::set_snap_count(int p_snap_count) { |
152 | snap_count = p_snap_count; |
153 | queue_redraw(); |
154 | if (texture.is_valid()) { |
155 | if (snap_count != GradientTexture2DEditor::DEFAULT_SNAP) { |
156 | texture->set_meta(SNAME("_snap_count" ), snap_count); |
157 | } else { |
158 | texture->remove_meta(SNAME("_snap_count" )); |
159 | } |
160 | } |
161 | } |
162 | |
163 | void GradientTexture2DEdit::_notification(int p_what) { |
164 | switch (p_what) { |
165 | case NOTIFICATION_MOUSE_EXIT: { |
166 | if (hovered != HANDLE_NONE) { |
167 | hovered = HANDLE_NONE; |
168 | queue_redraw(); |
169 | } |
170 | } break; |
171 | case NOTIFICATION_THEME_CHANGED: { |
172 | checkerboard->set_texture(get_editor_theme_icon(SNAME("GuiMiniCheckerboard" ))); |
173 | } break; |
174 | case NOTIFICATION_DRAW: { |
175 | _draw(); |
176 | } break; |
177 | } |
178 | } |
179 | |
180 | void GradientTexture2DEdit::_draw() { |
181 | if (texture.is_null()) { |
182 | return; |
183 | } |
184 | |
185 | const Ref<Texture2D> fill_from_icon = get_editor_theme_icon(SNAME("EditorPathSmoothHandle" )); |
186 | const Ref<Texture2D> fill_to_icon = get_editor_theme_icon(SNAME("EditorPathSharpHandle" )); |
187 | handle_size = fill_from_icon->get_size(); |
188 | |
189 | Size2 rect_size = get_size(); |
190 | |
191 | // Get the size and position to draw the texture and handles at. |
192 | // Subtract handle sizes so they stay inside the preview, but keep the texture's aspect ratio. |
193 | Size2 available_size = rect_size - handle_size; |
194 | Size2 ratio = available_size / texture->get_size(); |
195 | size = MIN(ratio.x, ratio.y) * texture->get_size(); |
196 | offset = ((rect_size - size) / 2).round(); |
197 | |
198 | checkerboard->set_rect(Rect2(offset, size)); |
199 | |
200 | draw_set_transform(offset); |
201 | draw_texture_rect(texture, Rect2(Point2(), size)); |
202 | |
203 | // Draw grid snap lines. |
204 | if (snap_enabled || (Input::get_singleton()->is_key_pressed(Key::CTRL) && grabbed != HANDLE_NONE)) { |
205 | const Color line_color = Color(0.5, 0.5, 0.5, 0.5); |
206 | |
207 | for (int idx = 0; idx < snap_count + 1; idx++) { |
208 | float x = float(idx * size.width) / snap_count; |
209 | float y = float(idx * size.height) / snap_count; |
210 | draw_line(Point2(x, 0), Point2(x, size.height), line_color); |
211 | draw_line(Point2(0, y), Point2(size.width, y), line_color); |
212 | } |
213 | } |
214 | |
215 | // Draw handles. |
216 | const Color focus_modulate = Color(0.5, 1, 2); |
217 | bool modulate_handle_from = grabbed == HANDLE_FROM || hovered == HANDLE_FROM; |
218 | bool modulate_handle_to = grabbed == HANDLE_TO || hovered == HANDLE_TO; |
219 | draw_texture(fill_from_icon, (_get_handle_pos(HANDLE_FROM) - handle_size / 2).round(), modulate_handle_from ? focus_modulate : Color(1, 1, 1)); |
220 | draw_texture(fill_to_icon, (_get_handle_pos(HANDLE_TO) - handle_size / 2).round(), modulate_handle_to ? focus_modulate : Color(1, 1, 1)); |
221 | } |
222 | |
223 | GradientTexture2DEdit::GradientTexture2DEdit() { |
224 | checkerboard = memnew(TextureRect); |
225 | checkerboard->set_stretch_mode(TextureRect::STRETCH_TILE); |
226 | checkerboard->set_expand_mode(TextureRect::EXPAND_IGNORE_SIZE); |
227 | checkerboard->set_draw_behind_parent(true); |
228 | add_child(checkerboard, false, INTERNAL_MODE_FRONT); |
229 | |
230 | set_custom_minimum_size(Size2(0, 250 * EDSCALE)); |
231 | } |
232 | |
233 | /////////////////////// |
234 | |
235 | const int GradientTexture2DEditor::DEFAULT_SNAP = 10; |
236 | |
237 | void GradientTexture2DEditor::_reverse_button_pressed() { |
238 | EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton(); |
239 | undo_redo->create_action(TTR("Swap GradientTexture2D Fill Points" )); |
240 | undo_redo->add_do_property(texture.ptr(), "fill_from" , texture->get_fill_to()); |
241 | undo_redo->add_do_property(texture.ptr(), "fill_to" , texture->get_fill_from()); |
242 | undo_redo->add_undo_property(texture.ptr(), "fill_from" , texture->get_fill_from()); |
243 | undo_redo->add_undo_property(texture.ptr(), "fill_to" , texture->get_fill_to()); |
244 | undo_redo->commit_action(); |
245 | } |
246 | |
247 | void GradientTexture2DEditor::_set_snap_enabled(bool p_enabled) { |
248 | texture_editor_rect->set_snap_enabled(p_enabled); |
249 | snap_count_edit->set_visible(p_enabled); |
250 | } |
251 | |
252 | void GradientTexture2DEditor::_set_snap_count(int p_snap_count) { |
253 | texture_editor_rect->set_snap_count(p_snap_count); |
254 | } |
255 | |
256 | void GradientTexture2DEditor::set_texture(Ref<GradientTexture2D> &p_texture) { |
257 | texture = p_texture; |
258 | texture_editor_rect->set_texture(p_texture); |
259 | } |
260 | |
261 | void GradientTexture2DEditor::_notification(int p_what) { |
262 | switch (p_what) { |
263 | case NOTIFICATION_ENTER_TREE: |
264 | case NOTIFICATION_THEME_CHANGED: { |
265 | reverse_button->set_icon(get_editor_theme_icon(SNAME("ReverseGradient" ))); |
266 | snap_button->set_icon(get_editor_theme_icon(SNAME("SnapGrid" ))); |
267 | } break; |
268 | case NOTIFICATION_READY: { |
269 | if (texture.is_valid()) { |
270 | // Set snapping settings based on the texture's meta. |
271 | snap_button->set_pressed(texture->get_meta("_snap_enabled" , false)); |
272 | snap_count_edit->set_value(texture->get_meta("_snap_count" , DEFAULT_SNAP)); |
273 | } |
274 | } break; |
275 | } |
276 | } |
277 | |
278 | GradientTexture2DEditor::GradientTexture2DEditor() { |
279 | HFlowContainer *toolbar = memnew(HFlowContainer); |
280 | add_child(toolbar); |
281 | |
282 | reverse_button = memnew(Button); |
283 | reverse_button->set_tooltip_text(TTR("Swap Gradient Fill Points" )); |
284 | toolbar->add_child(reverse_button); |
285 | reverse_button->connect("pressed" , callable_mp(this, &GradientTexture2DEditor::_reverse_button_pressed)); |
286 | |
287 | toolbar->add_child(memnew(VSeparator)); |
288 | |
289 | snap_button = memnew(Button); |
290 | snap_button->set_tooltip_text(TTR("Toggle Grid Snap" )); |
291 | snap_button->set_toggle_mode(true); |
292 | toolbar->add_child(snap_button); |
293 | snap_button->connect("toggled" , callable_mp(this, &GradientTexture2DEditor::_set_snap_enabled)); |
294 | |
295 | snap_count_edit = memnew(EditorSpinSlider); |
296 | snap_count_edit->set_min(2); |
297 | snap_count_edit->set_max(100); |
298 | snap_count_edit->set_value(DEFAULT_SNAP); |
299 | snap_count_edit->set_custom_minimum_size(Size2(65 * EDSCALE, 0)); |
300 | toolbar->add_child(snap_count_edit); |
301 | snap_count_edit->connect("value_changed" , callable_mp(this, &GradientTexture2DEditor::_set_snap_count)); |
302 | |
303 | texture_editor_rect = memnew(GradientTexture2DEdit); |
304 | add_child(texture_editor_rect); |
305 | |
306 | set_mouse_filter(MOUSE_FILTER_STOP); |
307 | _set_snap_enabled(snap_button->is_pressed()); |
308 | _set_snap_count(snap_count_edit->get_value()); |
309 | } |
310 | |
311 | /////////////////////// |
312 | |
313 | bool EditorInspectorPluginGradientTexture2D::can_handle(Object *p_object) { |
314 | return Object::cast_to<GradientTexture2D>(p_object) != nullptr; |
315 | } |
316 | |
317 | void EditorInspectorPluginGradientTexture2D::parse_begin(Object *p_object) { |
318 | GradientTexture2D *texture = Object::cast_to<GradientTexture2D>(p_object); |
319 | if (!texture) { |
320 | return; |
321 | } |
322 | Ref<GradientTexture2D> t(texture); |
323 | |
324 | GradientTexture2DEditor *editor = memnew(GradientTexture2DEditor); |
325 | editor->set_texture(t); |
326 | add_custom_control(editor); |
327 | } |
328 | |
329 | /////////////////////// |
330 | |
331 | GradientTexture2DEditorPlugin::GradientTexture2DEditorPlugin() { |
332 | Ref<EditorInspectorPluginGradientTexture2D> plugin; |
333 | plugin.instantiate(); |
334 | add_inspector_plugin(plugin); |
335 | } |
336 | |