Nodes: improve socket tooltips

The main goal of the patch is to make the socket tooltips more useful. This is
achieved in two ways:
* Use better text formatting in the tooltip to make it easier to parse and to
  better separate the current actual value from more general information about
  the socket.
* Add some more information like implicit field inputs when the node is not used
  currently or more details about a color.

This patch basically rewrites the entire tooltip generation, because it's quite
different from before (where we just created a single string for the entire
tooltip).

I'm using monospace for the actual current socket value and type, while the
normal font is used for general information about the socket like it's name,
description, allowed geometry types etc.

Pull Request: https://projects.blender.org/blender/blender/pulls/140540
This commit is contained in:
Jacques Lucke
2025-07-17 12:48:17 +02:00
parent 7495d5222b
commit f2ee95843c
12 changed files with 1102 additions and 870 deletions

View File

@@ -532,7 +532,7 @@ class NodeTreeMainUpdater {
if (node_tree_reference_lifetimes::analyse_reference_lifetimes(ntree)) {
result.interface_changed = true;
}
if (nodes::gizmos::update_tree_gizmo_propagation(ntree)) {
if (gizmos::update_tree_gizmo_propagation(ntree)) {
result.interface_changed = true;
}
}

View File

@@ -1035,6 +1035,9 @@ void UI_but_type_set_menu_from_pulldown(uiBut *but);
*/
void UI_but_color_set(uiBut *but, const uchar color[4]);
bool UI_but_is_color_gamma(uiBut &but);
const ColorManagedDisplay *UI_but_cm_display_get(uiBut &but);
/**
* Set at hint that describes the expected value when empty.
*/
@@ -1919,6 +1922,22 @@ void UI_tooltip_text_field_add(uiTooltipData &data,
*/
void UI_tooltip_image_field_add(uiTooltipData &data, const uiTooltipImage &image_data);
void UI_tooltip_color_field_add(uiTooltipData &data,
const blender::float4 &color,
bool has_alpha,
bool is_gamma,
const ColorManagedDisplay *display,
uiTooltipColorID color_id);
/**
* Add Python-related information to the tooltip. The caller is responsible for checking
* #USER_TOOLTIPS_PYTHON.
*/
void UI_tooltip_uibut_python_add(uiTooltipData &data,
bContext &C,
uiBut &but,
uiButExtraOpIcon *extra_icon);
/**
* Recreate tool-tip (use to update dynamic tips)
*/

View File

@@ -44,3 +44,8 @@ using uiButToolTipFunc = std::string (*)(bContext *C, void *argN, blender::Strin
* Mostly useful when using #uiLayoutSetTooltipCustomFunc.
*/
using uiButToolTipCustomFunc = void (*)(bContext &C, uiTooltipData &data, uiBut *but, void *argN);
namespace blender::ocio {
class Display;
} // namespace blender::ocio
using ColorManagedDisplay = blender::ocio::Display;

View File

@@ -6087,6 +6087,16 @@ void UI_but_color_set(uiBut *but, const uchar color[4])
copy_v4_v4_uchar(but->col, color);
}
const ColorManagedDisplay *UI_but_cm_display_get(uiBut &but)
{
return ui_block_cm_display_get(but.block);
}
bool UI_but_is_color_gamma(uiBut &but)
{
return ui_but_is_color_gamma(&but);
}
void UI_but_placeholder_set(uiBut *but, const StringRef placeholder_text)
{
MEM_SAFE_FREE(but->placeholder);

View File

@@ -50,11 +50,6 @@ struct wmKeyConfig;
struct wmOperatorType;
struct wmTimer;
namespace blender::ocio {
class Display;
} // namespace blender::ocio
using ColorManagedDisplay = blender::ocio::Display;
/* ****************** general defines ************** */
#define RNA_NO_INDEX -1

View File

@@ -58,6 +58,7 @@
#include "GPU_immediate_util.hh"
#include "GPU_state.hh"
#include "IMB_colormanagement.hh"
#include "IMB_imbuf.hh"
#include "IMB_imbuf_types.hh"
#include "IMB_thumbs.hh"
@@ -834,6 +835,125 @@ static std::string ui_tooltip_color_string(const blender::float4 &color,
return fmt::format("{}: {:.3f} {:.3f} {:.3f}", TIP_(title), color[0], color[1], color[2]);
};
void UI_tooltip_color_field_add(uiTooltipData &data,
const blender::float4 &original_color,
const bool has_alpha,
const bool is_gamma,
const ColorManagedDisplay *display,
const uiTooltipColorID color_id)
{
blender::float4 color = original_color;
if (!is_gamma && display) {
IMB_colormanagement_scene_linear_to_display_v3(color, display);
}
const std::string hex_st = ui_tooltip_color_string(color, "Hex", has_alpha, true);
const std::string rgba_st = ui_tooltip_color_string(
color, has_alpha ? "RGBA" : "RGB", has_alpha);
float hsva[4];
rgb_to_hsv_v(color, hsva);
hsva[3] = color[3];
const std::string hsva_st = ui_tooltip_color_string(hsva, has_alpha ? "HSVA" : "HSV", has_alpha);
const uiFontStyle *fs = &UI_style_get()->tooltip;
BLF_size(blf_mono_font, fs->points * UI_SCALE_FAC);
float w = BLF_width(blf_mono_font, hsva_st.c_str(), hsva_st.size());
uiTooltipImage image_data;
image_data.width = int(w);
image_data.height = int(w / (has_alpha ? 4.0f : 3.0f));
image_data.ibuf = IMB_allocImBuf(image_data.width, image_data.height, 32, IB_byte_data);
image_data.border = true;
image_data.premultiplied = false;
if (color[3] == 1.0f) {
/* No transparency so draw the entire area solid without checkerboard. */
image_data.background = uiTooltipImageBackground::None;
IMB_rectfill_area(image_data.ibuf, color, 1, 1, image_data.width, image_data.height, display);
}
else {
image_data.background = uiTooltipImageBackground::Checkerboard_Fixed;
/* Draw one half with transparency. */
IMB_rectfill_area(image_data.ibuf,
color,
image_data.width / 2,
1,
image_data.width,
image_data.height,
display);
/* Draw the other half with a solid color. */
color[3] = 1.0f;
IMB_rectfill_area(
image_data.ibuf, color, 1, 1, image_data.width / 2, image_data.height, display);
}
UI_tooltip_text_field_add(data, {}, {}, UI_TIP_STYLE_SPACER, color_id, false);
UI_tooltip_text_field_add(data, {}, {}, UI_TIP_STYLE_SPACER, color_id, false);
UI_tooltip_image_field_add(data, image_data);
UI_tooltip_text_field_add(data, {}, {}, UI_TIP_STYLE_SPACER, color_id, false);
UI_tooltip_text_field_add(data, hex_st, {}, UI_TIP_STYLE_MONO, color_id, false);
UI_tooltip_text_field_add(data, {}, {}, UI_TIP_STYLE_SPACER, color_id, false);
UI_tooltip_text_field_add(data, rgba_st, {}, UI_TIP_STYLE_MONO, color_id, false);
UI_tooltip_text_field_add(data, hsva_st, {}, UI_TIP_STYLE_MONO, color_id, false);
/* Tooltip now owns a copy of the ImBuf, so we can delete ours. */
IMB_freeImBuf(image_data.ibuf);
}
void UI_tooltip_uibut_python_add(uiTooltipData &data,
bContext &C,
uiBut &but,
uiButExtraOpIcon *extra_icon)
{
wmOperatorType *optype = extra_icon ? UI_but_extra_operator_icon_optype_get(extra_icon) :
but.optype;
PropertyRNA *rnaprop = extra_icon ? nullptr : but.rnaprop;
std::string rna_struct = UI_but_string_get_rna_struct_identifier(but);
std::string rna_prop = UI_but_string_get_rna_property_identifier(but);
if (optype && !rnaprop) {
PointerRNA *opptr = extra_icon ? UI_but_extra_operator_icon_opptr_get(extra_icon) :
/* Allocated when needed, the button owns it. */
UI_but_operator_ptr_ensure(&but);
/* So the context is passed to field functions (some Python field functions use it). */
WM_operator_properties_sanitize(opptr, false);
std::string str = ui_tooltip_text_python_from_op(&C, optype, opptr);
/* Operator info. */
UI_tooltip_text_field_add(data,
fmt::format(fmt::runtime(TIP_("Python: {}")), str),
{},
UI_TIP_STYLE_MONO,
UI_TIP_LC_PYTHON,
true);
}
if (!optype && !rna_struct.empty()) {
{
UI_tooltip_text_field_add(
data,
rna_prop.empty() ?
fmt::format(fmt::runtime(TIP_("Python: {}")), rna_struct) :
fmt::format(fmt::runtime(TIP_("Python: {}.{}")), rna_struct, rna_prop),
{},
UI_TIP_STYLE_MONO,
UI_TIP_LC_PYTHON,
(data.fields.size() > 0));
}
if (but.rnapoin.owner_id) {
std::optional<std::string> str = rnaprop ? RNA_path_full_property_py_ex(
&but.rnapoin, rnaprop, but.rnaindex, true) :
RNA_path_full_struct_py(&but.rnapoin);
UI_tooltip_text_field_add(data, str.value_or(""), {}, UI_TIP_STYLE_MONO, UI_TIP_LC_PYTHON);
}
}
}
static std::unique_ptr<uiTooltipData> ui_tooltip_data_from_button_or_extra_icon(
bContext *C, uiBut *but, uiButExtraOpIcon *extra_icon, const bool is_quick_tip)
{
@@ -1128,47 +1248,12 @@ static std::unique_ptr<uiTooltipData> ui_tooltip_data_from_button_or_extra_icon(
}
}
if (U.flag & USER_TOOLTIPS_PYTHON && optype && !rnaprop) {
PointerRNA *opptr = extra_icon ? UI_but_extra_operator_icon_opptr_get(extra_icon) :
/* Allocated when needed, the button owns it. */
UI_but_operator_ptr_ensure(but);
/* So the context is passed to field functions (some Python field functions use it). */
WM_operator_properties_sanitize(opptr, false);
std::string str = ui_tooltip_text_python_from_op(C, optype, opptr);
/* Operator info. */
UI_tooltip_text_field_add(*data,
fmt::format(fmt::runtime(TIP_("Python: {}")), str),
{},
UI_TIP_STYLE_MONO,
UI_TIP_LC_PYTHON,
true);
}
if ((U.flag & USER_TOOLTIPS_PYTHON) && !optype && !rna_struct.empty()) {
{
UI_tooltip_text_field_add(
*data,
rna_prop.empty() ?
fmt::format(fmt::runtime(TIP_("Python: {}")), rna_struct) :
fmt::format(fmt::runtime(TIP_("Python: {}.{}")), rna_struct, rna_prop),
{},
UI_TIP_STYLE_MONO,
UI_TIP_LC_PYTHON,
(data->fields.size() > 0));
}
if (but->rnapoin.owner_id) {
std::optional<std::string> str = rnaprop ? RNA_path_full_property_py_ex(
&but->rnapoin, rnaprop, but->rnaindex, true) :
RNA_path_full_struct_py(&but->rnapoin);
UI_tooltip_text_field_add(*data, str.value_or(""), {}, UI_TIP_STYLE_MONO, UI_TIP_LC_PYTHON);
}
if (U.flag & USER_TOOLTIPS_PYTHON) {
UI_tooltip_uibut_python_add(*data, *C, *but, extra_icon);
}
if (but->type == UI_BTYPE_COLOR) {
const ColorManagedDisplay *display = UI_but_cm_display_get(*but);
float color[4];
ui_but_v3_get(but, color);
@@ -1183,66 +1268,8 @@ static std::unique_ptr<uiTooltipData> ui_tooltip_data_from_button_or_extra_icon(
}
}
if (!ui_but_is_color_gamma(but)) {
ui_block_cm_to_display_space_v3(but->block, color);
}
const std::string hex_st = ui_tooltip_color_string(color, "Hex", has_alpha, true);
const std::string rgba_st = ui_tooltip_color_string(
color, has_alpha ? "RGBA" : "RGB", has_alpha);
float hsva[4];
rgb_to_hsv_v(color, hsva);
hsva[3] = color[3];
const std::string hsva_st = ui_tooltip_color_string(
hsva, has_alpha ? "HSVA" : "HSV", has_alpha);
const uiFontStyle *fs = &UI_style_get()->tooltip;
BLF_size(blf_mono_font, fs->points * UI_SCALE_FAC);
float w = BLF_width(blf_mono_font, hsva_st.c_str(), hsva_st.size());
uiTooltipImage image_data;
image_data.width = int(w);
image_data.height = int(w / (has_alpha ? 4.0f : 3.0f));
image_data.ibuf = IMB_allocImBuf(image_data.width, image_data.height, 32, IB_byte_data);
image_data.border = true;
image_data.premultiplied = false;
const ColorManagedDisplay *display = ui_block_cm_display_get(but->block);
if (color[3] == 1.0f) {
/* No transparency so draw the entire area solid without checkerboard. */
image_data.background = uiTooltipImageBackground::None;
IMB_rectfill_area(
image_data.ibuf, color, 1, 1, image_data.width, image_data.height, display);
}
else {
image_data.background = uiTooltipImageBackground::Checkerboard_Fixed;
/* Draw one half with transparency. */
IMB_rectfill_area(image_data.ibuf,
color,
image_data.width / 2,
1,
image_data.width,
image_data.height,
display);
/* Draw the other half with a solid color. */
color[3] = 1.0f;
IMB_rectfill_area(
image_data.ibuf, color, 1, 1, image_data.width / 2, image_data.height, display);
}
UI_tooltip_text_field_add(*data, {}, {}, UI_TIP_STYLE_SPACER, UI_TIP_LC_NORMAL, false);
UI_tooltip_text_field_add(*data, {}, {}, UI_TIP_STYLE_SPACER, UI_TIP_LC_NORMAL, false);
UI_tooltip_image_field_add(*data, image_data);
UI_tooltip_text_field_add(*data, {}, {}, UI_TIP_STYLE_SPACER, UI_TIP_LC_NORMAL, false);
UI_tooltip_text_field_add(*data, hex_st, {}, UI_TIP_STYLE_MONO, UI_TIP_LC_NORMAL, false);
UI_tooltip_text_field_add(*data, {}, {}, UI_TIP_STYLE_SPACER, UI_TIP_LC_NORMAL, false);
UI_tooltip_text_field_add(*data, rgba_st, {}, UI_TIP_STYLE_MONO, UI_TIP_LC_NORMAL, false);
UI_tooltip_text_field_add(*data, hsva_st, {}, UI_TIP_STYLE_MONO, UI_TIP_LC_NORMAL, false);
/* Tooltip now owns a copy of the ImBuf, so we can delete ours. */
IMB_freeImBuf(image_data.ibuf);
UI_tooltip_color_field_add(
*data, color, has_alpha, ui_but_is_color_gamma(but), display, UI_TIP_LC_NORMAL);
}
/* If the last field is a spacer, remove it. */

View File

@@ -37,6 +37,7 @@ set(SRC
node_relationships.cc
node_select.cc
node_shader_preview.cc
node_socket_tooltip.cc
node_sync_sockets.cc
node_templates.cc
node_view.cc

View File

@@ -19,7 +19,6 @@
#include "DNA_screen_types.h"
#include "DNA_space_types.h"
#include "DNA_text_types.h"
#include "DNA_texture_types.h"
#include "DNA_world_types.h"
#include "BLI_array.hh"
@@ -28,8 +27,6 @@
#include "BLI_function_ref.hh"
#include "BLI_listbase.h"
#include "BLI_map.hh"
#include "BLI_math_matrix.hh"
#include "BLI_math_quaternion.hh"
#include "BLI_set.hh"
#include "BLI_span.hh"
#include "BLI_string.h"
@@ -49,7 +46,6 @@
#include "BKE_main.hh"
#include "BKE_main_invariants.hh"
#include "BKE_node.hh"
#include "BKE_node_enum.hh"
#include "BKE_node_legacy_types.hh"
#include "BKE_node_runtime.hh"
#include "BKE_node_tree_update.hh"
@@ -58,7 +54,6 @@
#include "BKE_scene.hh"
#include "BKE_scene_runtime.hh"
#include "BKE_screen.hh"
#include "BKE_type_conversions.hh"
#include "IMB_imbuf.hh"
@@ -97,7 +92,6 @@
#include "NOD_geometry_nodes_log.hh"
#include "NOD_node_declaration.hh"
#include "NOD_node_extra_info.hh"
#include "NOD_socket_declarations_geometry.hh"
#include "GEO_fillet_curves.hh"
@@ -232,10 +226,6 @@ void tag_update_id(ID *id)
}
}
static std::string node_socket_get_tooltip(const SpaceNode *snode,
const bNodeTree &ntree,
const bNodeSocket &socket);
static const char *node_socket_get_translation_context(const bNodeSocket &socket)
{
/* The node is not explicitly defined. */
@@ -1451,14 +1441,16 @@ static void node_socket_tooltip_set(uiBlock &block,
0,
0,
std::nullopt);
UI_but_func_tooltip_set(
UI_but_func_tooltip_custom_set(
but,
[](bContext *C, void *argN, const StringRef /*tip*/) {
const SpaceNode &snode = *CTX_wm_space_node(C);
[](bContext &C, uiTooltipData &tip, uiBut *but, void *argN) {
const SpaceNode &snode = *CTX_wm_space_node(&C);
const bNodeTree &ntree = *snode.edittree;
const int index_in_tree = POINTER_AS_INT(argN);
ntree.ensure_topology_cache();
return node_socket_get_tooltip(&snode, ntree, *ntree.all_sockets()[index_in_tree]);
const bNodeSocket &socket = *ntree.all_sockets()[index_in_tree];
build_socket_tooltip(tip, C, but, ntree, socket);
},
POINTER_FROM_INT(socket_index_in_tree),
nullptr);
@@ -1507,735 +1499,17 @@ void node_socket_color_get(const bContext &C,
sock.typeinfo->draw_color((bContext *)&C, &ptr, &node_ptr, r_color);
}
static void create_inspection_string_for_generic_value(const bNodeSocket &socket,
const GPointer value,
fmt::memory_buffer &buf)
{
auto id_to_inspection_string = [&](const ID *id, const short idcode) {
fmt::format_to(fmt::appender(buf), "{}", id ? id->name + 2 : TIP_("None"));
fmt::format_to(fmt::appender(buf), " (");
fmt::format_to(fmt::appender(buf), "{}", TIP_(BKE_idtype_idcode_to_name(idcode)));
fmt::format_to(fmt::appender(buf), ")");
};
const CPPType &value_type = *value.type();
const void *buffer = value.get();
if (value_type.is<Object *>()) {
id_to_inspection_string(*static_cast<const ID *const *>(buffer), ID_OB);
return;
}
if (value_type.is<Material *>()) {
id_to_inspection_string(*static_cast<const ID *const *>(buffer), ID_MA);
return;
}
if (value_type.is<Tex *>()) {
id_to_inspection_string(*static_cast<const ID *const *>(buffer), ID_TE);
return;
}
if (value_type.is<Image *>()) {
id_to_inspection_string(*static_cast<const ID *const *>(buffer), ID_IM);
return;
}
if (value_type.is<Collection *>()) {
id_to_inspection_string(*static_cast<const ID *const *>(buffer), ID_GR);
return;
}
const CPPType &socket_type = *socket.typeinfo->base_cpp_type;
if (socket.type == SOCK_MENU) {
if (!value_type.is<int>()) {
return;
}
const int item_identifier = *static_cast<const int *>(buffer);
const auto *socket_storage = socket.default_value_typed<bNodeSocketValueMenu>();
if (!socket_storage->enum_items) {
return;
}
if (socket_storage->has_conflict()) {
return;
}
const bke::RuntimeNodeEnumItem *enum_item =
socket_storage->enum_items->find_item_by_identifier(item_identifier);
if (!enum_item) {
return;
}
fmt::format_to(fmt::appender(buf), fmt::runtime(TIP_("{} (Menu)")), enum_item->name);
return;
}
const bke::DataTypeConversions &convert = bke::get_implicit_type_conversions();
if (value_type != socket_type) {
if (!convert.is_convertible(value_type, socket_type)) {
return;
}
}
BUFFER_FOR_CPP_TYPE_VALUE(socket_type, socket_value);
/* This will just copy the value if the types are equal. */
convert.convert_to_uninitialized(value_type, socket_type, buffer, socket_value);
BLI_SCOPED_DEFER([&]() { socket_type.destruct(socket_value); });
if (socket_type.is<int>()) {
fmt::format_to(
fmt::appender(buf), fmt::runtime(TIP_("{} (Integer)")), *static_cast<int *>(socket_value));
}
else if (socket_type.is<float>()) {
const float float_value = *static_cast<float *>(socket_value);
/* Above that threshold floats can't represent fractions anymore. */
if (std::abs(float_value) > (1 << 24)) {
/* Use higher precision to display correct integer value instead of one that is rounded to
* fewer significant digits. */
fmt::format_to(fmt::appender(buf), fmt::runtime(TIP_("{:.10} (Float)")), float_value);
}
else {
fmt::format_to(fmt::appender(buf), fmt::runtime(TIP_("{} (Float)")), float_value);
}
}
else if (socket_type.is<blender::float3>()) {
const blender::float3 &vector = *static_cast<blender::float3 *>(socket_value);
fmt::format_to(fmt::appender(buf),
fmt::runtime(TIP_("({}, {}, {}) (Vector)")),
vector.x,
vector.y,
vector.z);
}
else if (socket_type.is<blender::ColorGeometry4f>()) {
const blender::ColorGeometry4f &color = *static_cast<blender::ColorGeometry4f *>(socket_value);
fmt::format_to(fmt::appender(buf),
fmt::runtime(TIP_("({}, {}, {}, {}) (Color)")),
color.r,
color.g,
color.b,
color.a);
}
else if (socket_type.is<math::Quaternion>()) {
const math::Quaternion &rotation = *static_cast<math::Quaternion *>(socket_value);
const math::EulerXYZ euler = math::to_euler(rotation);
fmt::format_to(fmt::appender(buf),
("({}" BLI_STR_UTF8_DEGREE_SIGN ", {}" BLI_STR_UTF8_DEGREE_SIGN
", {}" BLI_STR_UTF8_DEGREE_SIGN ")"),
euler.x().degree(),
euler.y().degree(),
euler.z().degree());
fmt::format_to(fmt::appender(buf), "{}", TIP_("(Rotation)"));
}
else if (socket_type.is<bool>()) {
fmt::format_to(fmt::appender(buf),
fmt::runtime(TIP_("{} (Boolean)")),
((*static_cast<bool *>(socket_value)) ? TIP_("True") : TIP_("False")));
}
else if (socket_type.is<float4x4>()) {
/* Transpose to be able to print row by row. */
const float4x4 value = math::transpose(*static_cast<const float4x4 *>(socket_value));
std::stringstream ss;
ss << value[0] << ",\n";
ss << value[1] << ",\n";
ss << value[2] << ",\n";
ss << value[3] << ",\n";
buf.append(ss.str());
fmt::format_to(fmt::appender(buf), "{}", TIP_("(Matrix)"));
}
}
static void create_inspection_string_for_field_info(const bNodeSocket &socket,
const geo_log::FieldInfoLog &value_log,
fmt::memory_buffer &buf)
{
const CPPType &socket_type = *socket.typeinfo->base_cpp_type;
const Span<std::string> input_tooltips = value_log.input_tooltips;
if (input_tooltips.is_empty()) {
/* Should have been logged as constant value. */
BLI_assert_unreachable();
fmt::format_to(fmt::appender(buf), "{}", TIP_("Value has not been logged"));
}
else {
if (socket_type.is<int>()) {
fmt::format_to(fmt::appender(buf), "{}", TIP_("Integer field based on:"));
}
else if (socket_type.is<float>()) {
fmt::format_to(fmt::appender(buf), "{}", TIP_("Float field based on:"));
}
else if (socket_type.is<blender::float3>()) {
fmt::format_to(fmt::appender(buf), "{}", TIP_("Vector field based on:"));
}
else if (socket_type.is<bool>()) {
fmt::format_to(fmt::appender(buf), "{}", TIP_("Boolean field based on:"));
}
else if (socket_type.is<std::string>()) {
fmt::format_to(fmt::appender(buf), "{}", TIP_("String field based on:"));
}
else if (socket_type.is<blender::ColorGeometry4f>()) {
fmt::format_to(fmt::appender(buf), "{}", TIP_("Color field based on:"));
}
else if (socket_type.is<math::Quaternion>()) {
fmt::format_to(fmt::appender(buf), "{}", TIP_("Rotation field based on:"));
}
fmt::format_to(fmt::appender(buf), "\n");
for (const int i : input_tooltips.index_range()) {
const blender::StringRef tooltip = input_tooltips[i];
fmt::format_to(fmt::appender(buf), fmt::runtime(TIP_("\u2022 {}")), TIP_(tooltip));
if (i < input_tooltips.size() - 1) {
fmt::format_to(fmt::appender(buf), ".\n");
}
}
}
}
static void create_inspection_string_for_geometry_info(const geo_log::GeometryInfoLog &value_log,
fmt::memory_buffer &buf)
{
auto to_string = [](int value) {
char str[BLI_STR_FORMAT_INT32_GROUPED_SIZE];
BLI_str_format_int_grouped(str, value);
return std::string(str);
};
if (value_log.grid_info) {
const geo_log::GeometryInfoLog::GridInfo &grid_info = *value_log.grid_info;
fmt::format_to(
fmt::appender(buf), "{}", grid_info.is_empty ? TIP_("Empty Grid") : TIP_("\u2022 Grid"));
return;
}
Span<bke::GeometryComponent::Type> component_types = value_log.component_types;
if (component_types.is_empty()) {
fmt::format_to(fmt::appender(buf), "{}", TIP_("Empty Geometry"));
return;
}
fmt::format_to(fmt::appender(buf), "{}", TIP_("Geometry:"));
if (!value_log.name.empty()) {
fmt::format_to(fmt::appender(buf), " \"{}\"", value_log.name);
}
fmt::format_to(fmt::appender(buf), "\n");
for (bke::GeometryComponent::Type type : component_types) {
switch (type) {
case bke::GeometryComponent::Type::Mesh: {
const geo_log::GeometryInfoLog::MeshInfo &mesh_info = *value_log.mesh_info;
fmt::format_to(fmt::appender(buf),
fmt::runtime(TIP_("\u2022 Mesh: {} vertices, {} edges, {} faces")),
to_string(mesh_info.verts_num),
to_string(mesh_info.edges_num),
to_string(mesh_info.faces_num));
break;
}
case bke::GeometryComponent::Type::PointCloud: {
const geo_log::GeometryInfoLog::PointCloudInfo &pointcloud_info =
*value_log.pointcloud_info;
fmt::format_to(fmt::appender(buf),
fmt::runtime(TIP_("\u2022 Point Cloud: {} points")),
to_string(pointcloud_info.points_num));
break;
}
case bke::GeometryComponent::Type::Curve: {
const geo_log::GeometryInfoLog::CurveInfo &curve_info = *value_log.curve_info;
fmt::format_to(fmt::appender(buf),
fmt::runtime(TIP_("\u2022 Curve: {} points, {} splines")),
to_string(curve_info.points_num),
to_string(curve_info.splines_num));
break;
}
case bke::GeometryComponent::Type::Instance: {
const geo_log::GeometryInfoLog::InstancesInfo &instances_info = *value_log.instances_info;
fmt::format_to(fmt::appender(buf),
fmt::runtime(TIP_("\u2022 Instances: {}")),
to_string(instances_info.instances_num));
break;
}
case bke::GeometryComponent::Type::Volume: {
const geo_log::GeometryInfoLog::VolumeInfo &volume_info = *value_log.volume_info;
fmt::format_to(fmt::appender(buf),
fmt::runtime(TIP_("\u2022 Volume: {} grids")),
volume_info.grids_num);
break;
}
case bke::GeometryComponent::Type::Edit: {
if (value_log.edit_data_info.has_value()) {
const geo_log::GeometryInfoLog::EditDataInfo &edit_info = *value_log.edit_data_info;
fmt::format_to(fmt::appender(buf),
fmt::runtime(TIP_("\u2022 Edit: {}, {}, {}")),
edit_info.has_deformed_positions ? TIP_("positions") :
TIP_("no positions"),
edit_info.has_deform_matrices ? TIP_("matrices") : TIP_("no matrices"),
edit_info.gizmo_transforms_num > 0 ? TIP_("gizmos") : TIP_("no gizmos"));
}
break;
}
case bke::GeometryComponent::Type::GreasePencil: {
const geo_log::GeometryInfoLog::GreasePencilInfo &grease_pencil_info =
*value_log.grease_pencil_info;
fmt::format_to(fmt::appender(buf),
fmt::runtime(TIP_("\u2022 Grease Pencil: {} layers")),
to_string(grease_pencil_info.layers_num));
break;
}
}
if (type != component_types.last()) {
fmt::format_to(fmt::appender(buf), ".\n");
}
}
}
static void create_inspection_string_for_geometry_socket(fmt::memory_buffer &buf,
const nodes::decl::Geometry *socket_decl)
{
/* If the geometry declaration is null, as is the case for input to group output,
* or it is an output socket don't show supported types. */
if (socket_decl == nullptr || socket_decl->in_out == SOCK_OUT) {
return;
}
Span<bke::GeometryComponent::Type> supported_types = socket_decl->supported_types();
if (supported_types.is_empty()) {
fmt::format_to(fmt::appender(buf), "{}", TIP_("Supported: All Types"));
return;
}
fmt::format_to(fmt::appender(buf), "{}", TIP_("Supported: "));
for (bke::GeometryComponent::Type type : supported_types) {
switch (type) {
case bke::GeometryComponent::Type::Mesh: {
fmt::format_to(fmt::appender(buf), "{}", TIP_("Mesh"));
break;
}
case bke::GeometryComponent::Type::PointCloud: {
fmt::format_to(fmt::appender(buf), "{}", TIP_("Point Cloud"));
break;
}
case bke::GeometryComponent::Type::Curve: {
fmt::format_to(fmt::appender(buf), "{}", TIP_("Curve"));
break;
}
case bke::GeometryComponent::Type::Instance: {
fmt::format_to(fmt::appender(buf), "{}", TIP_("Instances"));
break;
}
case bke::GeometryComponent::Type::Volume: {
fmt::format_to(fmt::appender(buf), "{}", CTX_TIP_(BLT_I18NCONTEXT_ID_ID, "Volume"));
break;
}
case bke::GeometryComponent::Type::Edit: {
break;
}
case bke::GeometryComponent::Type::GreasePencil: {
fmt::format_to(fmt::appender(buf), "{}", TIP_("Grease Pencil"));
break;
}
}
if (type != supported_types.last()) {
fmt::format_to(fmt::appender(buf), ", ");
}
}
}
static void create_inspection_string_for_bundle(const geo_log::BundleValueLog &value_log,
fmt::memory_buffer &buf)
{
if (value_log.items.is_empty()) {
fmt::format_to(fmt::appender(buf), "{}", TIP_("Empty Bundle"));
return;
}
fmt::format_to(fmt::appender(buf), "{}", TIP_("Bundle values:\n"));
for (const geo_log::BundleValueLog::Item &item : value_log.items) {
fmt::format_to(fmt::appender(buf),
fmt::runtime("\u2022 \"{}\" ({})\n"),
item.key.identifiers().first(),
TIP_(item.type->label));
}
}
static void create_inspection_string_for_closure(const geo_log::ClosureValueLog &value_log,
fmt::memory_buffer &buf)
{
if (value_log.inputs.is_empty() && value_log.outputs.is_empty()) {
fmt::format_to(fmt::appender(buf), "{}", TIP_("Empty Closure"));
}
if (!value_log.inputs.is_empty()) {
fmt::format_to(fmt::appender(buf), "{}:\n", TIP_("Inputs"));
for (const geo_log::ClosureValueLog::Item &item : value_log.inputs) {
fmt::format_to(fmt::appender(buf),
fmt::runtime("\u2022 {} ({})\n"),
item.key.identifiers().first(),
IFACE_(item.type->label));
}
}
if (!value_log.outputs.is_empty()) {
fmt::format_to(fmt::appender(buf), "{}:\n", TIP_("Outputs"));
for (const geo_log::ClosureValueLog::Item &item : value_log.outputs) {
fmt::format_to(fmt::appender(buf),
fmt::runtime("\u2022 {} ({})\n"),
item.key.identifiers().first(),
IFACE_(item.type->label));
}
}
}
static void create_inspection_string_for_default_socket_value(const bNodeSocket &socket,
fmt::memory_buffer &buf)
{
if (!socket.is_input()) {
return;
}
if (socket.is_multi_input()) {
return;
}
if (socket.owner_node().is_reroute()) {
return;
}
const Span<const bNodeSocket *> connected_sockets = socket.directly_linked_sockets();
if (!connected_sockets.is_empty() && !connected_sockets[0]->owner_node().is_dangling_reroute()) {
return;
}
if (const nodes::SocketDeclaration *socket_decl = socket.runtime->declaration) {
if (socket_decl->input_field_type == nodes::InputSocketFieldType::Implicit) {
return;
}
}
if (socket.typeinfo->base_cpp_type == nullptr) {
return;
}
const CPPType &value_type = *socket.typeinfo->base_cpp_type;
BUFFER_FOR_CPP_TYPE_VALUE(value_type, socket_value);
socket.typeinfo->get_base_cpp_value(socket.default_value, socket_value);
create_inspection_string_for_generic_value(socket, GPointer(value_type, socket_value), buf);
value_type.destruct(socket_value);
}
static std::optional<std::string> create_description_inspection_string(const bNodeSocket &socket)
{
if (socket.runtime->declaration == nullptr) {
if (socket.description[0]) {
return socket.description;
}
return std::nullopt;
}
const blender::nodes::SocketDeclaration &socket_decl = *socket.runtime->declaration;
blender::StringRefNull description = socket_decl.description;
if (description.is_empty()) {
return std::nullopt;
}
return TIP_(description);
}
static std::optional<std::string> create_log_inspection_string(geo_log::GeoTreeLog *geo_tree_log,
const bNodeSocket &socket)
{
if (geo_tree_log == nullptr) {
return std::nullopt;
}
if (socket.typeinfo->base_cpp_type == nullptr) {
return std::nullopt;
}
geo_tree_log->ensure_socket_values();
geo_log::ValueLog *value_log = geo_tree_log->find_socket_value_log(socket);
fmt::memory_buffer buf;
if (const geo_log::GenericValueLog *generic_value_log =
dynamic_cast<const geo_log::GenericValueLog *>(value_log))
{
create_inspection_string_for_generic_value(socket, generic_value_log->value, buf);
}
else if (const geo_log::StringLog *string_log = dynamic_cast<const geo_log::StringLog *>(
value_log))
{
if (string_log->truncated) {
fmt::format_to(fmt::appender(buf), fmt::runtime(TIP_("{}... (String)")), string_log->value);
}
else {
fmt::format_to(fmt::appender(buf), fmt::runtime(TIP_("{} (String)")), string_log->value);
}
}
else if (const geo_log::FieldInfoLog *gfield_value_log =
dynamic_cast<const geo_log::FieldInfoLog *>(value_log))
{
create_inspection_string_for_field_info(socket, *gfield_value_log, buf);
}
else if (const geo_log::GeometryInfoLog *geo_value_log =
dynamic_cast<const geo_log::GeometryInfoLog *>(value_log))
{
create_inspection_string_for_geometry_info(*geo_value_log, buf);
}
else if (const geo_log::BundleValueLog *bundle_value_log =
dynamic_cast<const geo_log::BundleValueLog *>(value_log))
{
create_inspection_string_for_bundle(*bundle_value_log, buf);
}
else if (const geo_log::ClosureValueLog *closure_value_log =
dynamic_cast<const geo_log::ClosureValueLog *>(value_log))
{
create_inspection_string_for_closure(*closure_value_log, buf);
}
std::string str = fmt::to_string(buf);
if (str.empty()) {
return std::nullopt;
}
return str;
}
static std::optional<std::string> create_declaration_inspection_string(const bNodeSocket &socket)
{
fmt::memory_buffer buf;
if (const nodes::decl::Geometry *socket_decl = dynamic_cast<const nodes::decl::Geometry *>(
socket.runtime->declaration))
{
create_inspection_string_for_geometry_socket(buf, socket_decl);
}
std::string str = fmt::to_string(buf);
if (str.empty()) {
return std::nullopt;
}
return str;
}
static Vector<std::string> lines_of_text(std::string text)
{
Vector<std::string> result;
std::istringstream text_stream(text);
for (std::string line; std::getline(text_stream, line);) {
result.append(line);
}
return result;
}
static std::optional<std::string> create_multi_input_log_inspection_string(
const bNodeSocket &socket, TreeDrawContext &tree_draw_ctx)
{
if (!socket.is_multi_input()) {
return std::nullopt;
}
Vector<std::pair<int, std::string>, 8> numerated_info;
const Span<const bNodeLink *> connected_links = socket.directly_linked_links();
for (const int index : connected_links.index_range()) {
const bNodeLink *link = connected_links[index];
const int connection_number = index + 1;
if (!link->is_used()) {
continue;
}
if (!(link->flag & NODE_LINK_VALID)) {
continue;
}
if (link->fromnode->is_dangling_reroute()) {
continue;
}
const bNodeSocket &connected_socket = *link->fromsock;
geo_log::GeoTreeLog *geo_tree_log = tree_draw_ctx.tree_logs.get_main_tree_log(
connected_socket);
const std::optional<std::string> input_log = create_log_inspection_string(geo_tree_log,
connected_socket);
if (!input_log.has_value()) {
continue;
}
numerated_info.append({connection_number, std::move(*input_log)});
}
if (numerated_info.is_empty()) {
return std::nullopt;
}
fmt::memory_buffer buf;
for (const std::pair<int, std::string> &info : numerated_info) {
const Vector<std::string> lines = lines_of_text(info.second);
fmt::format_to(fmt::appender(buf), "{}", info.first);
fmt::format_to(fmt::appender(buf), ". ");
fmt::format_to(fmt::appender(buf), "{}", lines.first());
for (const std::string &line : lines.as_span().drop_front(1)) {
fmt::format_to(fmt::appender(buf), "\n {}", line);
}
if (&info != &numerated_info.last()) {
buf.append(StringRef(".\n"));
}
}
const std::string str = fmt::to_string(buf);
if (str.empty()) {
return std::nullopt;
}
return str;
}
static std::optional<std::string> create_default_value_inspection_string(const bNodeSocket &socket)
{
fmt::memory_buffer buf;
create_inspection_string_for_default_socket_value(socket, buf);
std::string str = fmt::to_string(buf);
if (str.empty()) {
return std::nullopt;
}
return str;
}
static const bNodeSocket *target_for_reroute(const bNodeSocket &reroute_output)
{
const bNodeSocket *output = &reroute_output;
Set<const bNode *> visited_nodes;
visited_nodes.add(&reroute_output.owner_node());
while (true) {
const Span<const bNodeSocket *> linked_sockets = output->directly_linked_sockets();
if (linked_sockets.size() != 1) {
return nullptr;
}
const bNode &target_node = linked_sockets[0]->owner_node();
if (!visited_nodes.add(&target_node)) {
return nullptr;
}
if (!target_node.is_dangling_reroute()) {
return linked_sockets[0];
}
output = target_node.output_sockets()[0];
}
}
static std::optional<std::string> create_dangling_reroute_inspection_string(
const bNodeTree &ntree, const bNodeSocket &socket)
{
if (ntree.type != NTREE_GEOMETRY) {
return std::nullopt;
}
const bNode &node = socket.owner_node();
if (!node.is_dangling_reroute()) {
return std::nullopt;
}
const bNodeSocket &output_socket = *node.output_sockets()[0];
const bNodeSocket *target_socket = target_for_reroute(output_socket);
if (target_socket == nullptr) {
if (!output_socket.directly_linked_sockets().is_empty()) {
return TIP_("Dangling reroute is ignored by all targets");
}
return std::nullopt;
}
if (target_socket->is_multi_input()) {
return TIP_("Dangling reroute branch is ignored by multi input socket");
}
fmt::memory_buffer buf;
create_inspection_string_for_default_socket_value(*target_socket, buf);
std::string str = fmt::to_string(buf);
if (str.empty()) {
return TIP_("Dangling reroute is ignored");
}
fmt::format_to(fmt::appender(buf), ".\n\n");
fmt::format_to(fmt::appender(buf),
"{}",
TIP_("Dangling reroute is ignored, default value of target socket is used"));
return str;
}
static std::string node_socket_get_tooltip(const SpaceNode *snode,
const bNodeTree &ntree,
const bNodeSocket &socket)
{
TreeDrawContext tree_draw_ctx;
if (snode != nullptr) {
if (ntree.type == NTREE_GEOMETRY) {
tree_draw_ctx.tree_logs = geo_log::GeoNodesLog::get_contextual_tree_logs(*snode);
}
}
geo_log::GeoTreeLog *geo_tree_log = tree_draw_ctx.tree_logs.get_main_tree_log(socket);
Vector<std::string> inspection_strings;
if (std::optional<std::string> info = create_description_inspection_string(socket)) {
inspection_strings.append(std::move(*info));
}
if (std::optional<std::string> info = create_log_inspection_string(geo_tree_log, socket)) {
inspection_strings.append(std::move(*info));
}
else if (std::optional<std::string> info = create_dangling_reroute_inspection_string(ntree,
socket))
{
inspection_strings.append(std::move(*info));
}
else if (std::optional<std::string> info = create_default_value_inspection_string(socket)) {
inspection_strings.append(std::move(*info));
}
else if (std::optional<std::string> info = create_multi_input_log_inspection_string(
socket, tree_draw_ctx))
{
inspection_strings.append(std::move(*info));
}
if (std::optional<std::string> info = create_declaration_inspection_string(socket)) {
inspection_strings.append(std::move(*info));
}
if (U.experimental.use_socket_structure_type) {
if (socket.runtime->declaration) {
switch (socket.runtime->declaration->structure_type) {
case nodes::StructureType::Single:
inspection_strings.append(TIP_("(Single Value)"));
break;
case nodes::StructureType::Dynamic:
inspection_strings.append(TIP_("(Dynamic Structure Type)"));
break;
case nodes::StructureType::Field:
inspection_strings.append(TIP_("(Field)"));
break;
case nodes::StructureType::Grid:
inspection_strings.append(TIP_("(Volume Grid)"));
break;
}
}
}
std::stringstream output;
for (const std::string &info : inspection_strings) {
output << info;
if (&info != &inspection_strings.last()) {
output << ".\n\n";
}
}
if (inspection_strings.is_empty()) {
const bool is_extend = StringRef(socket.idname) == "NodeSocketVirtual";
const bNode &node = socket.owner_node();
if (node.is_reroute()) {
output << bke::node_label(ntree, node);
}
else if (is_extend) {
output << TIP_("Connect a link to create a new socket");
}
else {
const StringRefNull socket_label = bke::node_socket_label(socket);
const char *socket_translation_context = node_socket_get_translation_context(socket);
const char *translated_socket_label = CTX_TIP_(socket_translation_context,
socket_label.c_str());
output << translated_socket_label;
}
if (ntree.type == NTREE_GEOMETRY && !is_extend) {
output << ".\n\n";
output << TIP_(
"Unknown socket value. Either the socket was not used or its value was not logged "
"during the last evaluation");
}
}
return output.str();
}
static void node_socket_add_tooltip_in_node_editor(const bNodeSocket &sock, uiLayout &layout)
{
uiLayoutSetTooltipFunc(
uiLayoutSetTooltipCustomFunc(
&layout,
[](bContext *C, void *argN, const StringRef /*tip*/) {
const SpaceNode &snode = *CTX_wm_space_node(C);
[](bContext &C, uiTooltipData &tip, uiBut *but, void *argN) {
const SpaceNode &snode = *CTX_wm_space_node(&C);
const bNodeTree &ntree = *snode.edittree;
const int index_in_tree = POINTER_AS_INT(argN);
ntree.ensure_topology_cache();
return node_socket_get_tooltip(&snode, ntree, *ntree.all_sockets()[index_in_tree]);
const bNodeSocket &socket = *ntree.all_sockets()[index_in_tree];
build_socket_tooltip(tip, C, but, ntree, socket);
},
POINTER_FROM_INT(sock.index_in_tree()),
nullptr,
@@ -2253,12 +1527,11 @@ void node_socket_add_tooltip(const bNodeTree &ntree, const bNodeSocket &sock, ui
data->ntree = &ntree;
data->socket = &sock;
uiLayoutSetTooltipFunc(
uiLayoutSetTooltipCustomFunc(
&layout,
[](bContext *C, void *argN, const StringRef /*tip*/) {
[](bContext &C, uiTooltipData &tip, uiBut *but, void *argN) {
SocketTooltipData *data = static_cast<SocketTooltipData *>(argN);
const SpaceNode *snode = CTX_wm_space_node(C);
return node_socket_get_tooltip(snode, *data->ntree, *data->socket);
build_socket_tooltip(tip, C, but, *data->ntree, *data->socket);
},
data,
MEM_dupallocN,
@@ -2641,20 +1914,32 @@ static void node_draw_panels(bNodeTree &ntree, const bNode &node, uiBlock &block
if (input_socket && !input_socket->is_logically_linked()) {
PointerRNA socket_ptr = RNA_pointer_create_discrete(
&ntree.id, &RNA_NodeSocket, input_socket);
uiDefButR(&block,
UI_BTYPE_CHECKBOX,
-1,
"",
offsetx,
int(*panel_runtime.header_center_y - NODE_DYS),
UI_UNIT_X,
NODE_DY,
&socket_ptr,
"default_value",
0,
0,
0,
"");
uiBut *panel_toggle_but = uiDefButR(&block,
UI_BTYPE_CHECKBOX,
-1,
"",
offsetx,
int(*panel_runtime.header_center_y - NODE_DYS),
UI_UNIT_X,
NODE_DY,
&socket_ptr,
"default_value",
0,
0,
0,
"");
UI_but_func_tooltip_custom_set(
panel_toggle_but,
[](bContext &C, uiTooltipData &tip, uiBut *but, void *argN) {
const SpaceNode &snode = *CTX_wm_space_node(&C);
const bNodeTree &ntree = *snode.edittree;
const int index_in_tree = POINTER_AS_INT(argN);
ntree.ensure_topology_cache();
const bNodeSocket &socket = *ntree.all_sockets()[index_in_tree];
build_socket_tooltip(tip, C, but, ntree, socket);
},
POINTER_FROM_INT(input_socket->index_in_tree()),
nullptr);
offsetx += UI_UNIT_X;
}

View File

@@ -447,4 +447,12 @@ MenuType add_root_catalogs_menu_type();
void NODE_OT_sockets_sync(wmOperatorType *ot);
/* node_socket_tooltip.cc */
void build_socket_tooltip(uiTooltipData &tip_data,
bContext &C,
uiBut *but,
const bNodeTree &tree,
const bNodeSocket &socket);
} // namespace blender::ed::space_node

View File

@@ -0,0 +1,881 @@
/* SPDX-FileCopyrightText: 2025 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include <fmt/format.h>
#include <sstream>
#include "BKE_context.hh"
#include "BKE_idtype.hh"
#include "BKE_lib_id.hh"
#include "BKE_node_enum.hh"
#include "BKE_node_runtime.hh"
#include "BKE_type_conversions.hh"
#include "BLI_math_euler.hh"
#include "BLI_string.h"
#include "BLT_translation.hh"
#include "DNA_collection_types.h"
#include "DNA_material_types.h"
#include "NOD_geometry_nodes_log.hh"
#include "NOD_node_declaration.hh"
#include "NOD_socket.hh"
#include "node_intern.hh"
namespace geo_log = blender::nodes::geo_eval_log;
namespace blender::ed::space_node {
class SocketTooltipBuilder {
private:
uiTooltipData &tip_data_;
const bNodeTree &tree_;
const bNode &node_;
const bNodeSocket &socket_;
uiBut *but_ = nullptr;
bContext &C_;
int indentation_ = 0;
enum class TooltipBlockType {
Label,
Description,
Value,
Python,
};
std::optional<TooltipBlockType> last_block_type_;
public:
SocketTooltipBuilder(uiTooltipData &tip_data,
const bNodeTree &tree,
const bNodeSocket &socket,
bContext &C,
uiBut *but)
: tip_data_(tip_data),
tree_(tree),
node_(socket.owner_node()),
socket_(socket),
but_(but),
C_(C)
{
}
void build()
{
const bool is_extend = StringRef(socket_.idname) == "NodeSocketVirtual";
if (is_extend) {
this->build_tooltip_extend_socket();
return;
}
if (node_.is_dangling_reroute()) {
this->build_tooltip_dangling_reroute();
return;
}
if (this->should_show_label()) {
this->build_tooltip_label();
}
this->build_tooltip_description();
this->build_tooltip_value();
this->build_python();
/* Extra padding at the bottom. */
this->add_space();
}
private:
void build_tooltip_extend_socket()
{
this->add_text_field(TIP_("Connect a link to create a new socket."));
}
void build_tooltip_dangling_reroute()
{
this->add_text_field(TIP_("Dangling reroute nodes are ignored."), UI_TIP_LC_ALERT);
}
bool should_show_label()
{
if (this->get_socket_description().value_or("").empty()) {
/* Show label when the description is empty so that the tooltip is never empty. */
return true;
}
if (socket_.is_output()) {
return false;
}
if (socket_.type == SOCK_MENU) {
return true;
}
if (socket_.flag & SOCK_HIDE_LABEL) {
return true;
}
return false;
}
void build_tooltip_label()
{
this->start_block(TooltipBlockType::Label);
if (node_.is_reroute()) {
this->add_text_field_header(TIP_("Reroute"));
return;
}
const StringRefNull translated_socket_label = node_socket_get_label(&socket_, nullptr);
this->add_text_field_header(translated_socket_label);
}
void build_tooltip_description()
{
std::optional<std::string> description_opt = this->get_socket_description();
if (!description_opt) {
return;
}
std::string description = std::move(*description_opt);
if (description.empty()) {
return;
}
if (description[description.size() - 1] != '.') {
description += '.';
}
this->start_block(TooltipBlockType::Description);
this->add_text_field(std::move(description));
}
std::optional<std::string> get_socket_description()
{
if (socket_.runtime->declaration == nullptr) {
if (socket_.description[0]) {
return socket_.description;
}
return std::nullopt;
}
const nodes::SocketDeclaration &socket_decl = *socket_.runtime->declaration;
if (!socket_decl.description.empty()) {
return TIP_(socket_decl.description);
}
if (socket_decl.align_with_previous_socket) {
const Span<nodes::ItemDeclarationPtr> all_items = node_.runtime->declaration->all_items;
for (const int i : all_items.index_range()) {
if (&*all_items[i] != &socket_decl) {
continue;
}
if (i == 0) {
break;
}
const nodes::SocketDeclaration *previous_socket_decl =
dynamic_cast<const nodes::SocketDeclaration *>(all_items[i - 1].get());
if (!previous_socket_decl) {
break;
}
if (!previous_socket_decl->description.empty()) {
return TIP_(previous_socket_decl->description);
}
}
}
return std::nullopt;
}
void build_tooltip_value()
{
SpaceNode *snode = CTX_wm_space_node(&C_);
geo_log::ContextualGeoTreeLogs geo_tree_logs;
if (snode) {
geo_tree_logs = geo_log::GeoNodesLog::get_contextual_tree_logs(*snode);
}
geo_log::GeoTreeLog *geo_tree_log = geo_tree_logs.get_main_tree_log(socket_);
if (geo_tree_log && this->build_tooltip_value_from_geometry_nodes_log(*geo_tree_log)) {
return;
}
const bool always_show_value = socket_.owner_tree().type == NTREE_GEOMETRY;
if (node_.is_reroute()) {
if (always_show_value) {
this->start_block(TooltipBlockType::Value);
this->build_tooltip_value_unknown();
}
return;
}
if (socket_.is_input()) {
if (this->is_socket_default_value_used()) {
this->build_tooltip_value_socket_default();
return;
}
}
if (always_show_value) {
this->start_block(TooltipBlockType::Value);
this->build_tooltip_value_unknown();
}
}
void build_tooltip_value_unknown()
{
this->add_text_field_mono(TIP_("Value: Unknown (not evaluated)"));
}
void build_tooltip_value_socket_default()
{
if (socket_.is_multi_input()) {
this->start_block(TooltipBlockType::Value);
this->add_text_field_mono(TIP_("Values: None"));
return;
}
const nodes::SocketDeclaration *socket_decl = socket_.runtime->declaration;
if (socket_decl && socket_decl->input_field_type == nodes::InputSocketFieldType::Implicit) {
this->start_block(TooltipBlockType::Value);
build_tooltip_value_implicit_default(socket_decl->default_input_type);
return;
}
if (socket_.typeinfo->base_cpp_type == nullptr) {
return;
}
const CPPType &cpp_type = *socket_.typeinfo->base_cpp_type;
BUFFER_FOR_CPP_TYPE_VALUE(cpp_type, socket_value);
socket_.typeinfo->get_base_cpp_value(socket_.default_value, socket_value);
BLI_SCOPED_DEFER([&]() { cpp_type.destruct(socket_value); });
this->start_block(TooltipBlockType::Value);
this->build_tooltip_value_generic({cpp_type, socket_value});
}
[[nodiscard]] bool build_tooltip_value_from_geometry_nodes_log(geo_log::GeoTreeLog &geo_tree_log)
{
if (socket_.typeinfo->base_cpp_type == nullptr) {
return false;
}
geo_tree_log.ensure_socket_values();
if (socket_.is_multi_input()) {
return this->build_tooltip_last_value_multi_input(geo_tree_log);
}
geo_log::ValueLog *value_log = geo_tree_log.find_socket_value_log(socket_);
if (!value_log) {
return false;
}
this->start_block(TooltipBlockType::Value);
this->build_tooltip_value_geo_log(*value_log);
return true;
}
bool build_tooltip_last_value_multi_input(geo_log::GeoTreeLog &geo_tree_log)
{
const Span<const bNodeLink *> connected_links = socket_.directly_linked_links();
Vector<std::pair<int, geo_log::ValueLog *>> value_logs;
bool all_value_logs_missing = true;
for (const int i : connected_links.index_range()) {
const bNodeLink &link = *connected_links[i];
if (!link.is_used()) {
continue;
}
if (!(link.flag & NODE_LINK_VALID)) {
continue;
}
const bNodeSocket &from_socket = *link.fromsock;
geo_log::ValueLog *value_log = geo_tree_log.find_socket_value_log(from_socket);
value_logs.append({i, value_log});
if (value_log) {
all_value_logs_missing = false;
}
}
if (all_value_logs_missing) {
return false;
}
this->start_block(TooltipBlockType::Value);
for (const auto &[i, value_log] : value_logs) {
const int connection_number = i + 1;
if (i > 0) {
this->add_space();
}
this->add_text_field_mono(fmt::format("{}:", connection_number));
this->add_space();
indentation_++;
BLI_SCOPED_DEFER([&]() { indentation_--; });
if (value_log) {
this->build_tooltip_value_geo_log(*value_log);
}
else {
this->build_tooltip_value_unknown();
}
}
return true;
}
void build_tooltip_value_geo_log(geo_log::ValueLog &value_log)
{
if (const auto *generic_value_log = dynamic_cast<const geo_log::GenericValueLog *>(&value_log))
{
this->build_tooltip_value_generic(generic_value_log->value);
}
else if (const auto *string_value_log = dynamic_cast<const geo_log::StringLog *>(&value_log)) {
this->build_tooltip_value_string_log(*string_value_log);
}
else if (const auto *field_value_log = dynamic_cast<const geo_log::FieldInfoLog *>(&value_log))
{
this->build_tooltip_value_field_log(*field_value_log);
}
else if (const auto *geometry_log = dynamic_cast<const geo_log::GeometryInfoLog *>(&value_log))
{
this->build_tooltip_value_geometry_log(*geometry_log);
}
else if (const auto *grid_log = dynamic_cast<const geo_log::GridInfoLog *>(&value_log)) {
build_tooltip_value_grid_log(*grid_log);
}
else if (const auto *bundle_log = dynamic_cast<const geo_log::BundleValueLog *>(&value_log)) {
this->build_tooltip_value_bundle_log(*bundle_log);
}
else if (const auto *closure_log = dynamic_cast<const geo_log::ClosureValueLog *>(&value_log))
{
this->build_tooltip_value_closure_log(*closure_log);
}
}
void build_tooltip_value_and_type_oneline(const StringRef value, const StringRef type)
{
this->add_text_field_mono(fmt::format("{}: {}", TIP_("Value"), value));
this->add_space();
this->add_text_field_mono(fmt::format("{}: {}", TIP_("Type"), type));
}
template<typename T> [[nodiscard]] bool build_tooltip_value_data_block(const GPointer &value)
{
const CPPType &type = *value.type();
if (!type.is<T *>()) {
return false;
}
const T *data = *value.get<T *>();
std::string value_str;
if (data) {
value_str = BKE_id_name(id_cast<const ID &>(*data));
}
else {
value_str = TIP_("None");
}
const ID_Type id_type = T::id_type;
const char *id_type_name = BKE_idtype_idcode_to_name(id_type);
this->build_tooltip_value_and_type_oneline(value_str, TIP_(id_type_name));
return true;
}
void build_tooltip_value_enum(const int item_identifier)
{
const auto *storage = socket_.default_value_typed<bNodeSocketValueMenu>();
if (!storage->enum_items || storage->has_conflict()) {
this->build_tooltip_value_and_type_oneline(TIP_("Unknown"), TIP_("Menu"));
return;
}
const bke::RuntimeNodeEnumItem *enum_item = storage->enum_items->find_item_by_identifier(
item_identifier);
if (!enum_item) {
return;
}
if (!enum_item->description.empty()) {
this->add_text_field(enum_item->description, UI_TIP_LC_VALUE);
this->add_space();
}
this->build_tooltip_value_and_type_oneline(enum_item->name, TIP_("Menu"));
}
void build_tooltip_value_int(const int value)
{
std::string value_str = fmt::format("{}", value);
this->build_tooltip_value_and_type_oneline(value_str, TIP_("Integer"));
}
void build_tooltip_value_float(const float value)
{
std::string value_str;
/* Above that threshold floats can't represent fractions anymore. */
if (std::abs(value) > (1 << 24)) {
/* Use higher precision to display correct integer value instead of one that is rounded to
* fewer significant digits. */
value_str = fmt::format("{:.10}", value);
}
else {
value_str = fmt::format("{}", value);
}
this->build_tooltip_value_and_type_oneline(value_str, TIP_("Float"));
}
void build_tooltip_value_float3(const float3 &value)
{
const std::string value_str = fmt::format("{} {} {}", value.x, value.y, value.z);
this->build_tooltip_value_and_type_oneline(value_str, TIP_("3D Float Vector"));
}
void build_tooltip_value_color(const ColorGeometry4f &value)
{
const std::string value_str = fmt::format(
"{} {} {} {} ({})", value.r, value.g, value.b, value.a, TIP_("Linear"));
this->build_tooltip_value_and_type_oneline(value_str, TIP_("Float Color"));
this->add_space();
this->add_text_field_mono(TIP_("Display:"), UI_TIP_LC_NORMAL);
bool is_gamma = false;
const ColorManagedDisplay *display = nullptr;
if (but_) {
is_gamma = UI_but_is_color_gamma(*but_);
display = UI_but_cm_display_get(*but_);
}
UI_tooltip_color_field_add(tip_data_, float4(value), true, is_gamma, display, UI_TIP_LC_VALUE);
}
void build_tooltip_value_quaternion(const math::Quaternion &value)
{
const math::EulerXYZ euler = math::to_euler(value);
const std::string value_str = fmt::format("{}" BLI_STR_UTF8_DEGREE_SIGN
" {}" BLI_STR_UTF8_DEGREE_SIGN
" {}" BLI_STR_UTF8_DEGREE_SIGN,
euler.x().degree(),
euler.y().degree(),
euler.z().degree());
this->build_tooltip_value_and_type_oneline(value_str, TIP_("Rotation"));
}
void build_tooltip_value_bool(const bool value)
{
std::string value_str = value ? TIP_("True") : TIP_("False");
this->build_tooltip_value_and_type_oneline(value_str, TIP_("Boolean"));
}
void build_tooltip_value_float4x4(const float4x4 &value)
{
/* Transpose to be able to print row by row. */
const float4x4 value_transposed = math::transpose(value);
std::stringstream ss;
for (const int row_i : IndexRange(4)) {
const float4 row = value_transposed[row_i];
ss << fmt::format("{:7.3} {:7.3} {:7.3} {:7.3}\n", row[0], row[1], row[2], row[3]);
}
this->add_text_field_mono(fmt::format("{}:", TIP_("Value")));
this->add_space();
this->add_text_field_mono(ss.str());
this->add_space();
this->add_text_field_mono(fmt::format("{}: {}", TIP_("Type"), TIP_("4x4 Float Matrix")));
}
void build_tooltip_value_generic(const GPointer &value)
{
const CPPType &value_type = *value.type();
if (this->build_tooltip_value_data_block<Object>(value)) {
return;
}
if (this->build_tooltip_value_data_block<Material>(value)) {
return;
}
if (this->build_tooltip_value_data_block<Tex>(value)) {
return;
}
if (this->build_tooltip_value_data_block<Image>(value)) {
return;
}
if (this->build_tooltip_value_data_block<Collection>(value)) {
return;
}
if (socket_.type == SOCK_MENU) {
if (!value_type.is<int>()) {
this->build_tooltip_value_unknown();
return;
}
const int item_identifier = *value.get<int>();
this->build_tooltip_value_enum(item_identifier);
return;
}
const CPPType &socket_base_cpp_type = *socket_.typeinfo->base_cpp_type;
const bke::DataTypeConversions &conversions = bke::get_implicit_type_conversions();
if (value_type != socket_base_cpp_type) {
if (!conversions.is_convertible(value_type, socket_base_cpp_type)) {
this->build_tooltip_value_unknown();
return;
}
}
BUFFER_FOR_CPP_TYPE_VALUE(socket_base_cpp_type, socket_value);
conversions.convert_to_uninitialized(
value_type, socket_base_cpp_type, value.get(), socket_value);
BLI_SCOPED_DEFER([&]() { socket_base_cpp_type.destruct(socket_value); });
if (socket_base_cpp_type.is<int>()) {
this->build_tooltip_value_int(*static_cast<int *>(socket_value));
return;
}
if (socket_base_cpp_type.is<float>()) {
this->build_tooltip_value_float(*static_cast<float *>(socket_value));
return;
}
if (socket_base_cpp_type.is<float3>()) {
this->build_tooltip_value_float3(*static_cast<float3 *>(socket_value));
return;
}
if (socket_base_cpp_type.is<ColorGeometry4f>()) {
this->build_tooltip_value_color(*static_cast<ColorGeometry4f *>(socket_value));
return;
}
if (socket_base_cpp_type.is<math::Quaternion>()) {
this->build_tooltip_value_quaternion(*static_cast<math::Quaternion *>(socket_value));
return;
}
if (socket_base_cpp_type.is<bool>()) {
this->build_tooltip_value_bool(*static_cast<bool *>(socket_value));
return;
}
if (socket_base_cpp_type.is<float4x4>()) {
this->build_tooltip_value_float4x4(*static_cast<float4x4 *>(socket_value));
return;
}
this->build_tooltip_value_unknown();
}
void build_tooltip_value_string_log(const geo_log::StringLog &value_log)
{
std::string value_str = value_log.value;
if (value_log.truncated) {
value_str += "...";
}
this->build_tooltip_value_and_type_oneline(value_str, TIP_("String"));
}
const char *get_field_type_name(const CPPType &base_type)
{
if (base_type.is<int>()) {
return TIP_("Integer Field");
}
if (base_type.is<float>()) {
return TIP_("Float Field");
}
if (base_type.is<blender::float3>()) {
return TIP_("3D Float Vector Field");
}
if (base_type.is<bool>()) {
return TIP_("Boolean Field");
}
if (base_type.is<std::string>()) {
return TIP_("String Field");
}
if (base_type.is<blender::ColorGeometry4f>()) {
return TIP_("Color Field");
}
if (base_type.is<math::Quaternion>()) {
return TIP_("Rotation Field");
}
BLI_assert_unreachable();
return TIP_("Field");
}
void build_tooltip_value_field_log(const geo_log::FieldInfoLog &value_log)
{
const CPPType &socket_base_cpp_type = *socket_.typeinfo->base_cpp_type;
const Span<std::string> input_tooltips = value_log.input_tooltips;
if (input_tooltips.is_empty()) {
/* Should have been logged as constant value. */
BLI_assert_unreachable();
return;
}
this->add_text_field_mono(TIP_("Field dependending on:"));
for (const std::string &input_tooltip : input_tooltips) {
this->add_space();
this->add_text_field_mono(fmt::format(" \u2022 {}", input_tooltip));
}
this->add_space();
std::string type_str = this->get_field_type_name(socket_base_cpp_type);
this->add_text_field_mono(fmt::format("{}: {}", TIP_("Type"), type_str));
}
std::string count_to_string(const int count)
{
char str[BLI_STR_FORMAT_INT32_GROUPED_SIZE];
BLI_str_format_int_grouped(str, count);
return std::string(str);
}
void build_tooltip_value_geometry_log(const geo_log::GeometryInfoLog &geometry_log)
{
Span<bke::GeometryComponent::Type> component_types = geometry_log.component_types;
if (component_types.is_empty()) {
this->build_tooltip_value_and_type_oneline(TIP_("None"), TIP_("Geometry Set"));
return;
}
this->add_text_field_mono(TIP_("Geometry components:"));
for (const bke::GeometryComponent::Type type : component_types) {
std::string component_str;
switch (type) {
case bke::GeometryComponent::Type::Mesh: {
const geo_log::GeometryInfoLog::MeshInfo &info = *geometry_log.mesh_info;
component_str = fmt::format(fmt::runtime(TIP_("Mesh: {} vertices, {} edges, {} faces")),
this->count_to_string(info.verts_num),
this->count_to_string(info.edges_num),
this->count_to_string(info.faces_num));
break;
}
case bke::GeometryComponent::Type::PointCloud: {
const geo_log::GeometryInfoLog::PointCloudInfo &info = *geometry_log.pointcloud_info;
component_str = fmt::format(fmt::runtime(TIP_("Point Cloud: {} points")),
this->count_to_string(info.points_num));
break;
}
case bke::GeometryComponent::Type::Instance: {
const geo_log::GeometryInfoLog::InstancesInfo &info = *geometry_log.instances_info;
component_str = fmt::format(fmt::runtime(TIP_("Instances: {}")),
this->count_to_string(info.instances_num));
break;
}
case bke::GeometryComponent::Type::Volume: {
const geo_log::GeometryInfoLog::VolumeInfo &info = *geometry_log.volume_info;
component_str = fmt::format(fmt::runtime(TIP_("Volume: {} grids")),
this->count_to_string(info.grids_num));
break;
}
case bke::GeometryComponent::Type::Curve: {
const geo_log::GeometryInfoLog::CurveInfo &info = *geometry_log.curve_info;
component_str = fmt::format(fmt::runtime(TIP_("Curve: {} points, {} splines")),
this->count_to_string(info.points_num),
this->count_to_string(info.splines_num));
break;
}
case bke::GeometryComponent::Type::GreasePencil: {
const geo_log::GeometryInfoLog::GreasePencilInfo &info =
*geometry_log.grease_pencil_info;
component_str = fmt::format(fmt::runtime(TIP_("Grease Pencil: {} layers")),
this->count_to_string(info.layers_num));
break;
}
case bke::GeometryComponent::Type::Edit: {
if (geometry_log.edit_data_info.has_value()) {
const geo_log::GeometryInfoLog::EditDataInfo &info = *geometry_log.edit_data_info;
component_str = fmt::format(
fmt::runtime(TIP_("Edit: {}, {}, {}")),
info.has_deformed_positions ? TIP_("positions") : TIP_("no positions"),
info.has_deform_matrices ? TIP_("matrices") : TIP_("no matrices"),
info.gizmo_transforms_num > 0 ? TIP_("gizmos") : TIP_("no gizmos"));
}
break;
}
}
if (!component_str.empty()) {
this->add_space();
this->add_text_field_mono(fmt::format(" \u2022 {}", component_str));
}
}
this->add_space();
this->add_text_field_mono(TIP_("Type: Geometry Set"));
}
void build_tooltip_value_grid_log(const geo_log::GridInfoLog &grid_log)
{
std::string value_str;
if (grid_log.is_empty) {
value_str = TIP_("None");
}
else {
value_str = TIP_("Volume Grid");
}
this->build_tooltip_value_and_type_oneline(value_str, TIP_("Volume Grid"));
}
void build_tooltip_value_bundle_log(const geo_log::BundleValueLog &bundle_log)
{
if (bundle_log.items.is_empty()) {
this->add_text_field_mono(TIP_("Values: None"));
}
else {
this->add_text_field_mono(TIP_("Values:"));
Vector<geo_log::BundleValueLog::Item> sorted_items = bundle_log.items;
std::sort(sorted_items.begin(), sorted_items.end(), [](const auto &a, const auto &b) {
return BLI_strcasecmp_natural(a.key.identifiers().first().c_str(),
b.key.identifiers().first().c_str()) < 0;
});
for (const geo_log::BundleValueLog::Item &item : sorted_items) {
this->add_space();
const std::string type_name = TIP_(item.type->label);
this->add_text_field_mono(fmt::format(
fmt::runtime("\u2022 \"{}\" ({})\n"), item.key.identifiers().first(), type_name));
}
}
this->add_space();
this->add_text_field_mono(TIP_("Type: Bundle"));
}
void build_tooltip_value_closure_log(const geo_log::ClosureValueLog &closure_log)
{
if (closure_log.inputs.is_empty() && closure_log.outputs.is_empty()) {
this->add_text_field_mono(TIP_("Value: None"));
}
else {
if (!closure_log.inputs.is_empty()) {
this->add_text_field_mono(TIP_("Inputs:"));
for (const geo_log::ClosureValueLog::Item &item : closure_log.inputs) {
this->add_space();
const std::string type_name = TIP_(item.type->label);
this->add_text_field_mono(fmt::format(
fmt::runtime("\u2022 \"{}\" ({})\n"), item.key.identifiers().first(), type_name));
}
}
if (!closure_log.outputs.is_empty()) {
this->add_space();
this->add_text_field_mono(TIP_("Outputs:"));
for (const geo_log::ClosureValueLog::Item &item : closure_log.outputs) {
this->add_space();
const std::string type_name = TIP_(item.type->label);
this->add_text_field_mono(fmt::format(
fmt::runtime("\u2022 \"{}\" ({})\n"), item.key.identifiers().first(), type_name));
}
}
}
this->add_space();
this->add_text_field_mono(TIP_("Type: Closure"));
}
void build_tooltip_value_implicit_default(const NodeDefaultInputType &type)
{
switch (type) {
case NODE_DEFAULT_INPUT_VALUE: {
/* Should be handled elsewhere. */
BLI_assert_unreachable();
break;
}
case NODE_DEFAULT_INPUT_INDEX_FIELD: {
this->build_tooltip_value_and_type_oneline(TIP_("Index Field"),
this->get_field_type_name(CPPType::get<int>()));
break;
}
case NODE_DEFAULT_INPUT_ID_INDEX_FIELD: {
this->build_tooltip_value_and_type_oneline(TIP_("ID or Index Field"),
this->get_field_type_name(CPPType::get<int>()));
break;
}
case NODE_DEFAULT_INPUT_NORMAL_FIELD: {
this->build_tooltip_value_and_type_oneline(
TIP_("Normal Field"), this->get_field_type_name(CPPType::get<float3>()));
break;
}
case NODE_DEFAULT_INPUT_POSITION_FIELD: {
this->build_tooltip_value_and_type_oneline(
TIP_("Position Field"), this->get_field_type_name(CPPType::get<float3>()));
break;
}
case NODE_DEFAULT_INPUT_INSTANCE_TRANSFORM_FIELD: {
this->build_tooltip_value_and_type_oneline(
TIP_("Instance Transform Field"), this->get_field_type_name(CPPType::get<float4x4>()));
break;
}
case NODE_DEFAULT_INPUT_HANDLE_LEFT_FIELD: {
this->build_tooltip_value_and_type_oneline(
TIP_("Left Handle Field"), this->get_field_type_name(CPPType::get<float3>()));
break;
}
case NODE_DEFAULT_INPUT_HANDLE_RIGHT_FIELD:
this->build_tooltip_value_and_type_oneline(
TIP_("Right Handle Field"), this->get_field_type_name(CPPType::get<float3>()));
break;
}
}
bool is_socket_default_value_used()
{
BLI_assert(socket_.is_input());
for (const bNodeLink *link : socket_.directly_linked_links()) {
if (!link->is_used()) {
continue;
}
const bNodeSocket &from_socket = *link->fromsock;
const bNode &from_node = from_socket.owner_node();
if (from_node.is_dangling_reroute()) {
continue;
}
return false;
}
return true;
}
StringRef get_structure_type_tooltip(const nodes::StructureType &structure_type)
{
switch (structure_type) {
case nodes::StructureType::Single: {
return TIP_("Single Value");
}
case nodes::StructureType::Dynamic: {
return TIP_("Dynamic");
}
case nodes::StructureType::Field: {
return TIP_("Field");
}
case nodes::StructureType::Grid: {
return TIP_("Volume Grid");
}
}
BLI_assert_unreachable();
return "Unknown";
}
void build_python()
{
if (!(U.flag & USER_TOOLTIPS_PYTHON)) {
return;
}
if (!but_) {
return;
}
UI_tooltip_uibut_python_add(tip_data_, C_, *but_, nullptr);
}
void start_block(const TooltipBlockType new_block_type)
{
if (last_block_type_.has_value()) {
this->add_space(2);
}
last_block_type_ = new_block_type;
}
void add_text_field_header(std::string text)
{
UI_tooltip_text_field_add(
tip_data_, this->indent(text), {}, UI_TIP_STYLE_HEADER, UI_TIP_LC_MAIN);
}
void add_text_field(std::string text, const uiTooltipColorID color_id = UI_TIP_LC_NORMAL)
{
UI_tooltip_text_field_add(tip_data_, this->indent(text), {}, UI_TIP_STYLE_NORMAL, color_id);
}
void add_text_field_mono(std::string text, const uiTooltipColorID color_id = UI_TIP_LC_VALUE)
{
UI_tooltip_text_field_add(tip_data_, this->indent(text), {}, UI_TIP_STYLE_MONO, color_id);
}
void add_space(const int amount = 1)
{
for ([[maybe_unused]] const int i : IndexRange(amount)) {
UI_tooltip_text_field_add(tip_data_, {}, {}, UI_TIP_STYLE_SPACER, UI_TIP_LC_NORMAL);
}
}
std::string indent(std::string text)
{
if (indentation_ == 0) {
return text;
}
return fmt::format("{: <{}}{}", "", indentation_, text);
}
};
void build_socket_tooltip(uiTooltipData &tip_data,
bContext &C,
uiBut *but,
const bNodeTree &tree,
const bNodeSocket &socket)
{
SocketTooltipBuilder builder(tip_data, tree, socket, C, but);
builder.build();
}
} // namespace blender::ed::space_node

View File

@@ -168,9 +168,6 @@ class GeometryInfoLog : public ValueLog {
struct VolumeInfo {
int grids_num;
};
struct GridInfo {
bool is_empty;
};
std::optional<MeshInfo> mesh_info;
std::optional<CurveInfo> curve_info;
@@ -179,10 +176,15 @@ class GeometryInfoLog : public ValueLog {
std::optional<InstancesInfo> instances_info;
std::optional<EditDataInfo> edit_data_info;
std::optional<VolumeInfo> volume_info;
std::optional<GridInfo> grid_info;
GeometryInfoLog(const bke::GeometrySet &geometry_set);
GeometryInfoLog(const bke::GVolumeGrid &grid);
};
class GridInfoLog : public ValueLog {
public:
bool is_empty = false;
GridInfoLog(const bke::GVolumeGrid &grid);
};
class BundleValueLog : public ValueLog {

View File

@@ -208,9 +208,8 @@ struct GridIsEmptyOp {
};
#endif /* WITH_OPENVDB */
GeometryInfoLog::GeometryInfoLog(const bke::GVolumeGrid &grid)
GridInfoLog::GridInfoLog(const bke::GVolumeGrid &grid)
{
GridInfo &info = this->grid_info.emplace();
#ifdef WITH_OPENVDB
bke::VolumeTreeAccessToken token;
const openvdb::GridBase &vdb_grid = grid->grid(token);
@@ -218,14 +217,14 @@ GeometryInfoLog::GeometryInfoLog(const bke::GVolumeGrid &grid)
GridIsEmptyOp is_empty_op{vdb_grid};
if (BKE_volume_grid_type_operation(grid_type, is_empty_op)) {
info.is_empty = is_empty_op.result;
this->is_empty = is_empty_op.result;
}
else {
info.is_empty = true;
this->is_empty = true;
}
#else
UNUSED_VARS(grid);
info.is_empty = true;
this->is_empty = true;
#endif
}
@@ -312,7 +311,7 @@ void GeoTreeLogger::log_value(const bNode &node, const bNodeSocket &socket, cons
#ifdef WITH_OPENVDB
else if (value_variant.is_volume_grid()) {
const bke::GVolumeGrid grid = value_variant.extract<bke::GVolumeGrid>();
store_logged_value(this->allocator->construct<GeometryInfoLog>(grid));
store_logged_value(this->allocator->construct<GridInfoLog>(grid));
}
#endif
else if (value_variant.valid_for_socket(SOCK_BUNDLE)) {