Skip to content

Commit b9f59ae

Browse files
committed
Add color pickers to script editor
1 parent 80a3d20 commit b9f59ae

File tree

8 files changed

+458
-5
lines changed

8 files changed

+458
-5
lines changed

doc/classes/EditorSettings.xml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1216,6 +1216,9 @@
12161216
<member name="text_editor/appearance/caret/type" type="int" setter="" getter="">
12171217
The shape of the caret to use in the script editor. [b]Line[/b] displays a vertical line to the left of the current character, whereas [b]Block[/b] displays an outline over the current character.
12181218
</member>
1219+
<member name="text_editor/appearance/enable_inline_color_picker" type="bool" setter="" getter="">
1220+
If [code]true[/code], displays a colored button before any [Color] constructor in the script editor. Clicking on them allows the color to be modified through a color picker.
1221+
</member>
12191222
<member name="text_editor/appearance/guidelines/line_length_guideline_hard_column" type="int" setter="" getter="">
12201223
The column at which to display a subtle line as a line length guideline for scripts. This should generally be greater than [member text_editor/appearance/guidelines/line_length_guideline_soft_column].
12211224
</member>

editor/editor_settings.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -683,6 +683,8 @@ void EditorSettings::_load_defaults(Ref<ConfigFile> p_extra_config) {
683683
_load_godot2_text_editor_theme();
684684

685685
// Appearance
686+
EDITOR_SETTING_BASIC(Variant::BOOL, PROPERTY_HINT_NONE, "text_editor/appearance/enable_inline_color_picker", true, "");
687+
686688
// Appearance: Caret
687689
EDITOR_SETTING(Variant::INT, PROPERTY_HINT_ENUM, "text_editor/appearance/caret/type", 0, "Line,Block")
688690
_initial_set("text_editor/appearance/caret/caret_blink", true, true);

editor/plugins/script_text_editor.cpp

Lines changed: 249 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -396,8 +396,237 @@ bool ScriptTextEditor::show_members_overview() {
396396
return true;
397397
}
398398

399+
bool ScriptTextEditor::_is_valid_color_info(const Dictionary &p_info) {
400+
if (p_info.get_valid("color").get_type() != Variant::COLOR) {
401+
return false;
402+
}
403+
if (!p_info.get_valid("color_end").is_num() || !p_info.get_valid("color_mode").is_num()) {
404+
return false;
405+
}
406+
return true;
407+
}
408+
409+
Array ScriptTextEditor::_inline_object_parse(const String &p_text, int p_line) {
410+
Array result;
411+
int i_end_previous = 0;
412+
int i_start = p_text.find("Color");
413+
414+
while (i_start != -1) {
415+
// Ignore words that just have "Color" in them.
416+
if (i_start == 0 || !("_" + p_text.substr(i_start - 1, 1)).is_valid_ascii_identifier()) {
417+
int i_par_start = p_text.find_char('(', i_start + 5);
418+
if (i_par_start != -1) {
419+
int i_par_end = p_text.find_char(')', i_start + 5);
420+
if (i_par_end != -1) {
421+
Dictionary color_info;
422+
color_info["line"] = p_line;
423+
color_info["column"] = i_start;
424+
color_info["width_ratio"] = 1.0;
425+
color_info["color_end"] = i_par_end;
426+
427+
String fn_name = p_text.substr(i_start + 5, i_par_start - i_start - 5);
428+
String s_params = p_text.substr(i_par_start + 1, i_par_end - i_par_start - 1);
429+
bool has_added_color = false;
430+
431+
if (fn_name.is_empty()) {
432+
String stripped = s_params.strip_edges(true, true);
433+
// String constructor.
434+
if (stripped.length() > 0 && (stripped[0] == '\"')) {
435+
String color_string = stripped.substr(1, stripped.length() - 2);
436+
color_info["color"] = Color(color_string);
437+
color_info["color_mode"] = MODE_STRING;
438+
has_added_color = true;
439+
}
440+
// Hex constructor.
441+
else if (stripped.length() == 10 && stripped.substr(0, 2) == "0x") {
442+
color_info["color"] = Color(stripped.substr(2, stripped.length() - 2));
443+
color_info["color_mode"] = MODE_HEX;
444+
has_added_color = true;
445+
}
446+
// Empty Color() constructor.
447+
else if (stripped.is_empty()) {
448+
color_info["color"] = Color();
449+
color_info["color_mode"] = MODE_RGB;
450+
has_added_color = true;
451+
}
452+
}
453+
// Float & int parameters.
454+
if (!has_added_color && s_params.size() > 0) {
455+
PackedFloat64Array params = s_params.split_floats(",", false);
456+
if (params.size() == 3) {
457+
params.resize(4);
458+
params.set(3, 1.0);
459+
}
460+
if (params.size() == 4) {
461+
has_added_color = true;
462+
if (fn_name == ".from_ok_hsl") {
463+
color_info["color"] = Color::from_ok_hsl(params[0], params[1], params[2], params[3]);
464+
color_info["color_mode"] = MODE_OKHSL;
465+
} else if (fn_name == ".from_hsv") {
466+
color_info["color"] = Color::from_hsv(params[0], params[1], params[2], params[3]);
467+
color_info["color_mode"] = MODE_HSV;
468+
} else if (fn_name == ".from_rgba8") {
469+
color_info["color"] = Color::from_rgba8(int(params[0]), int(params[1]), int(params[2]), int(params[3]));
470+
color_info["color_mode"] = MODE_RGB8;
471+
} else if (fn_name.is_empty()) {
472+
color_info["color"] = Color(params[0], params[1], params[2], params[3]);
473+
color_info["color_mode"] = MODE_RGB;
474+
} else {
475+
has_added_color = false;
476+
}
477+
}
478+
}
479+
480+
if (has_added_color) {
481+
result.push_back(color_info);
482+
i_end_previous = i_par_end + 1;
483+
}
484+
}
485+
}
486+
}
487+
i_end_previous = MAX(i_end_previous, i_start);
488+
i_start = p_text.find("Color", i_start + 1);
489+
}
490+
return result;
491+
}
492+
493+
void ScriptTextEditor::_inline_object_draw(const Dictionary &p_info, const Rect2 &p_rect) {
494+
if (_is_valid_color_info(p_info)) {
495+
Rect2 col_rect = p_rect.grow(-4);
496+
if (color_alpha_texture.is_null()) {
497+
color_alpha_texture = inline_color_picker->get_theme_icon("sample_bg", "ColorPicker");
498+
}
499+
code_editor->get_text_editor()->draw_texture_rect(color_alpha_texture, col_rect, false);
500+
code_editor->get_text_editor()->draw_rect(col_rect, Color(p_info["color"]));
501+
code_editor->get_text_editor()->draw_rect(col_rect, Color(1, 1, 1), false, 1);
502+
}
503+
}
504+
505+
void ScriptTextEditor::_inline_object_handle_click(const Dictionary &p_info, const Rect2 &p_rect) {
506+
if (_is_valid_color_info(p_info)) {
507+
inline_color_picker->set_pick_color(p_info["color"]);
508+
inline_color_line = p_info["line"];
509+
inline_color_start = p_info["column"];
510+
inline_color_end = p_info["color_end"];
511+
512+
// Reset tooltip hover timer.
513+
code_editor->get_text_editor()->set_symbol_tooltip_on_hover_enabled(false);
514+
code_editor->get_text_editor()->set_symbol_tooltip_on_hover_enabled(true);
515+
516+
_update_color_constructor_options();
517+
inline_color_options->select(p_info["color_mode"]);
518+
EditorNode::get_singleton()->setup_color_picker(inline_color_picker);
519+
520+
// Move popup above the line if it's too low.
521+
float_t view_h = get_viewport_rect().size.y;
522+
float_t pop_h = inline_color_popup->get_contents_minimum_size().y;
523+
float_t pop_y = p_rect.get_end().y;
524+
float_t pop_x = p_rect.position.x;
525+
if (pop_y + pop_h > view_h) {
526+
pop_y = p_rect.position.y - pop_h;
527+
}
528+
// Move popup to the right if it's too high.
529+
if (pop_y < 0) {
530+
pop_x = p_rect.get_end().x;
531+
}
532+
533+
inline_color_popup->popup(Rect2(pop_x, pop_y, 0, 0));
534+
}
535+
}
536+
537+
String ScriptTextEditor::_picker_color_stringify(const Color &p_color, COLOR_MODE p_mode) {
538+
String result;
539+
String fname;
540+
Vector<String> str_params;
541+
switch (p_mode) {
542+
case ScriptTextEditor::MODE_STRING: {
543+
str_params.push_back("\"" + p_color.to_html() + "\"");
544+
} break;
545+
case ScriptTextEditor::MODE_HEX: {
546+
str_params.push_back("0x" + p_color.to_html());
547+
} break;
548+
case ScriptTextEditor::MODE_RGB: {
549+
str_params = {
550+
String::num(p_color.r, 3),
551+
String::num(p_color.g, 3),
552+
String::num(p_color.b, 3),
553+
String::num(p_color.a, 3)
554+
};
555+
} break;
556+
case ScriptTextEditor::MODE_HSV: {
557+
str_params = {
558+
String::num(p_color.get_h(), 3),
559+
String::num(p_color.get_s(), 3),
560+
String::num(p_color.get_v(), 3),
561+
String::num(p_color.a, 3)
562+
};
563+
fname = ".from_hsv";
564+
} break;
565+
case ScriptTextEditor::MODE_OKHSL: {
566+
str_params = {
567+
String::num(p_color.get_ok_hsl_h(), 3),
568+
String::num(p_color.get_ok_hsl_s(), 3),
569+
String::num(p_color.get_ok_hsl_l(), 3),
570+
String::num(p_color.a, 3)
571+
};
572+
fname = ".from_ok_hsl";
573+
} break;
574+
case ScriptTextEditor::MODE_RGB8: {
575+
str_params = {
576+
itos(p_color.get_r8()),
577+
itos(p_color.get_g8()),
578+
itos(p_color.get_b8()),
579+
itos(p_color.get_a8())
580+
};
581+
fname = ".from_rgba8";
582+
} break;
583+
default: {
584+
} break;
585+
}
586+
result = "Color" + fname + "(" + String(", ").join(str_params) + ")";
587+
return result;
588+
}
589+
590+
void ScriptTextEditor::_picker_color_changed(const Color &p_color) {
591+
_update_color_constructor_options();
592+
_update_color_text();
593+
}
594+
595+
void ScriptTextEditor::_update_color_constructor_options() {
596+
int item_count = inline_color_options->get_item_count();
597+
// Update or add each constructor as an option.
598+
for (int i = 0; i < MODE_MAX; i++) {
599+
String option_text = _picker_color_stringify(inline_color_picker->get_pick_color(), (COLOR_MODE)i);
600+
if (i >= item_count) {
601+
inline_color_options->add_item(option_text);
602+
} else {
603+
inline_color_options->set_item_text(i, option_text);
604+
}
605+
}
606+
}
607+
608+
void ScriptTextEditor::_update_color_text() {
609+
if (inline_color_line < 0) {
610+
return;
611+
}
612+
String result = inline_color_options->get_item_text(inline_color_options->get_selected_id());
613+
code_editor->get_text_editor()->begin_complex_operation();
614+
code_editor->get_text_editor()->remove_text(inline_color_line, inline_color_start, inline_color_line, inline_color_end + 1);
615+
inline_color_end = inline_color_start + result.size() - 2;
616+
code_editor->get_text_editor()->insert_text(result, inline_color_line, inline_color_start);
617+
code_editor->get_text_editor()->end_complex_operation();
618+
}
619+
399620
void ScriptTextEditor::update_settings() {
400621
code_editor->get_text_editor()->set_gutter_draw(connection_gutter, EDITOR_GET("text_editor/appearance/gutters/show_info_gutter"));
622+
if (EDITOR_GET("text_editor/appearance/enable_inline_color_picker")) {
623+
code_editor->get_text_editor()->set_inline_object_handlers(
624+
callable_mp(this, &ScriptTextEditor::_inline_object_parse),
625+
callable_mp(this, &ScriptTextEditor::_inline_object_draw),
626+
callable_mp(this, &ScriptTextEditor::_inline_object_handle_click));
627+
} else {
628+
code_editor->get_text_editor()->set_inline_object_handlers(Callable(), Callable(), Callable());
629+
}
401630
code_editor->update_editor_settings();
402631
}
403632

@@ -1812,6 +2041,9 @@ void ScriptTextEditor::_notification(int p_what) {
18122041
[[fallthrough]];
18132042
case NOTIFICATION_ENTER_TREE: {
18142043
code_editor->get_text_editor()->set_gutter_width(connection_gutter, code_editor->get_text_editor()->get_line_height());
2044+
Ref<Font> code_font = get_theme_font("font", "CodeEdit");
2045+
inline_color_options->add_theme_font_override("font", code_font);
2046+
inline_color_options->get_popup()->add_theme_font_override("font", code_font);
18152047
} break;
18162048
}
18172049
}
@@ -2594,6 +2826,23 @@ ScriptTextEditor::ScriptTextEditor() {
25942826
bookmarks_menu = memnew(PopupMenu);
25952827
breakpoints_menu = memnew(PopupMenu);
25962828

2829+
inline_color_popup = memnew(PopupPanel);
2830+
add_child(inline_color_popup);
2831+
2832+
inline_color_picker = memnew(ColorPicker);
2833+
inline_color_picker->set_mouse_filter(MOUSE_FILTER_STOP);
2834+
inline_color_picker->set_deferred_mode(true);
2835+
inline_color_picker->set_hex_visible(false);
2836+
inline_color_picker->connect("color_changed", callable_mp(this, &ScriptTextEditor::_picker_color_changed));
2837+
inline_color_popup->add_child(inline_color_picker);
2838+
2839+
inline_color_options = memnew(OptionButton);
2840+
inline_color_options->set_h_size_flags(SIZE_FILL);
2841+
inline_color_options->set_text_overrun_behavior(TextServer::OVERRUN_TRIM_ELLIPSIS);
2842+
inline_color_options->set_fit_to_longest_item(false);
2843+
inline_color_options->connect("item_selected", callable_mp(this, &ScriptTextEditor::_update_color_text).unbind(1));
2844+
inline_color_picker->get_slider(ColorPicker::SLIDER_COUNT)->get_parent()->add_sibling(inline_color_options);
2845+
25972846
connection_info_dialog = memnew(ConnectionInfoDialog);
25982847

25992848
SET_DRAG_FORWARDING_GCD(code_editor->get_text_editor(), ScriptTextEditor);

editor/plugins/script_text_editor.h

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
#include "editor/code_editor.h"
3636
#include "scene/gui/color_picker.h"
3737
#include "scene/gui/dialogs.h"
38+
#include "scene/gui/option_button.h"
3839
#include "scene/gui/tree.h"
3940

4041
class RichTextLabel;
@@ -84,6 +85,14 @@ class ScriptTextEditor : public ScriptEditorBase {
8485
PopupMenu *highlighter_menu = nullptr;
8586
PopupMenu *context_menu = nullptr;
8687

88+
int inline_color_line = -1;
89+
int inline_color_start = -1;
90+
int inline_color_end = -1;
91+
PopupPanel *inline_color_popup = nullptr;
92+
ColorPicker *inline_color_picker = nullptr;
93+
OptionButton *inline_color_options = nullptr;
94+
Ref<Texture2D> color_alpha_texture;
95+
8796
GotoLinePopup *goto_line_popup = nullptr;
8897
ScriptEditorQuickOpen *quick_open = nullptr;
8998
ConnectionInfoDialog *connection_info_dialog = nullptr;
@@ -160,6 +169,16 @@ class ScriptTextEditor : public ScriptEditorBase {
160169
EDIT_EMOJI_AND_SYMBOL,
161170
};
162171

172+
enum COLOR_MODE {
173+
MODE_RGB,
174+
MODE_STRING,
175+
MODE_HSV,
176+
MODE_OKHSL,
177+
MODE_RGB8,
178+
MODE_HEX,
179+
MODE_MAX
180+
};
181+
163182
void _enable_code_editor();
164183

165184
protected:
@@ -185,6 +204,15 @@ class ScriptTextEditor : public ScriptEditorBase {
185204
void _error_clicked(const Variant &p_line);
186205
void _warning_clicked(const Variant &p_line);
187206

207+
bool _is_valid_color_info(const Dictionary &p_info);
208+
Array _inline_object_parse(const String &p_text, int p_line);
209+
void _inline_object_draw(const Dictionary &p_info, const Rect2 &p_rect);
210+
void _inline_object_handle_click(const Dictionary &p_info, const Rect2 &p_rect);
211+
String _picker_color_stringify(const Color &p_color, COLOR_MODE p_mode);
212+
void _picker_color_changed(const Color &p_color);
213+
void _update_color_constructor_options();
214+
void _update_color_text();
215+
188216
void _notification(int p_what);
189217

190218
HashMap<String, Ref<EditorSyntaxHighlighter>> highlighters;

modules/text_server_adv/text_server_adv.cpp

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5204,7 +5204,8 @@ bool TextServerAdvanced::_shape_substr(ShapedTextDataAdvanced *p_new_sd, const S
52045204
int32_t bidi_run_end = _convert_pos(p_sd, ov_start + start + _bidi_run_start + _bidi_run_length);
52055205

52065206
for (int j = 0; j < sd_size; j++) {
5207-
if ((sd_glyphs[j].start >= bidi_run_start) && (sd_glyphs[j].end <= bidi_run_end)) {
5207+
int col_key_off = (sd_glyphs[j].start == sd_glyphs[j].end) ? 1 : 0;
5208+
if ((sd_glyphs[j].start >= bidi_run_start) && (sd_glyphs[j].end <= bidi_run_end - col_key_off)) {
52085209
// Copy glyphs.
52095210
Glyph gl = sd_glyphs[j];
52105211
if (gl.span_index >= 0) {
@@ -5994,7 +5995,11 @@ bool TextServerAdvanced::_shaped_text_update_breaks(const RID &p_shaped) {
59945995
while (i < span_size) {
59955996
String language = sd->spans[i].language;
59965997
int r_start = sd->spans[i].start;
5997-
while (i + 1 < span_size && language == sd->spans[i + 1].language) {
5998+
if (r_start == sd->spans[i].end) {
5999+
i++;
6000+
continue;
6001+
}
6002+
while (i + 1 < span_size && (language == sd->spans[i + 1].language || sd->spans[i + 1].start == sd->spans[i + 1].end)) {
59986003
i++;
59996004
}
60006005
int r_end = sd->spans[i].end;
@@ -6122,6 +6127,11 @@ bool TextServerAdvanced::_shaped_text_update_breaks(const RID &p_shaped) {
61226127
continue;
61236128
}
61246129
}
6130+
// Do not add extra space for color picker object.
6131+
if (((sd_glyphs[i].flags & GRAPHEME_IS_EMBEDDED_OBJECT) == GRAPHEME_IS_EMBEDDED_OBJECT && sd_glyphs[i].start == sd_glyphs[i].end) || (uint32_t(i + 1) < sd->glyphs.size() && (sd_glyphs[i + 1].flags & GRAPHEME_IS_EMBEDDED_OBJECT) == GRAPHEME_IS_EMBEDDED_OBJECT && sd_glyphs[i + 1].start == sd_glyphs[i + 1].end)) {
6132+
i += (sd_glyphs[i].count - 1);
6133+
continue;
6134+
}
61256135
Glyph gl;
61266136
gl.span_index = sd_glyphs[i].span_index;
61276137
gl.start = sd_glyphs[i].start;
@@ -6991,7 +7001,8 @@ bool TextServerAdvanced::_shaped_text_shape(const RID &p_shaped) {
69917001

69927002
for (int k = spn_from; k != spn_to; k += spn_delta) {
69937003
const ShapedTextDataAdvanced::Span &span = sd->spans[k];
6994-
if (span.start - sd->start >= script_run_end || span.end - sd->start <= script_run_start) {
7004+
int col_key_off = (span.start == span.end) ? 1 : 0;
7005+
if (span.start - sd->start >= script_run_end || span.end - sd->start <= script_run_start - col_key_off) {
69957006
continue;
69967007
}
69977008
if (span.embedded_key != Variant()) {

0 commit comments

Comments
 (0)