UI: Add support for showing socket descriptions in tooltips

Currently, hovering over a socket itself shows no tooltip at all, while
hovering over its value field shows "Default value", which is not helpful.

This patch therefore implements socket tooltips following the proposal at
https://blender.community/c/rightclickselect/2Qgbbc/.

A lot of the basic functionality was already implemented for Geometry Nodes,
where hovering over the socket itself shows introspection info.

This patch extends this by:
- Supporting dynamic tooltips on labels, which is important for good tooltip
  coverage in a socket's region of the node.
- Adding a function to setting a dynamic tooltip for an entire uiLayout, which
  avoids needing to set it manually for a wide variety of socket types.
- Hiding the property label field in a tooltip when dynamic tooltip is also
  provided. If really needed, this label can be restored through the dynamic
  tooltip, but in all current cases the label is actually pointless anyways
  since the dynamic tooltip gives more accurate and specific information.
- Adding dynamic tooltips to a socket's UI layout row if it has a description
  configured, both in the Node Editor as well as in the Material Properties.

Note that the patch does not add any actual tooltip content yet, just the
infrastructure to show them. By default, sockets without a description still
show the old "Default value" tooltip.

For an example of how to add socket descriptions, check the Cylinder node
in the Geometry Nodes.

Differential Revision: https://developer.blender.org/D9967
This commit is contained in:
Lukas Stockner 2022-04-11 02:02:12 +02:00
parent b3525c3487
commit 484a914647
Notes: blender-bot 2023-02-14 10:35:28 +01:00
Referenced by commit 05b56d55e8, Fix T97386: Node socket labels swallow click/drag events
Referenced by issue #97386, Regression: Click Dragging on Socket Labels does not register
Referenced by issue #66765, Incorrect tooltip when hovering over the `Corner Pin` widget
7 changed files with 150 additions and 36 deletions

View File

@ -595,6 +595,7 @@ typedef void (*uiMenuHandleFunc)(struct bContext *C, void *arg, int event);
*/
typedef bool (*uiMenuStepFunc)(struct bContext *C, int direction, void *arg1);
typedef void *(*uiCopyArgFunc)(const void *arg);
typedef void (*uiFreeArgFunc)(void *arg);
/* interface_query.c */
@ -2065,6 +2066,24 @@ void uiLayoutSetFunc(uiLayout *layout, uiMenuHandleFunc handlefunc, void *argv);
void uiLayoutSetContextPointer(uiLayout *layout, const char *name, struct PointerRNA *ptr);
struct bContextStore *uiLayoutGetContextStore(uiLayout *layout);
void uiLayoutContextCopy(uiLayout *layout, struct bContextStore *context);
/**
* Set tooltip function for all buttons in the layout.
* func, arg and free_arg are passed on to UI_but_func_tooltip_set, so their meaning is the same.
*
* \param func: The callback function that gets called to get tooltip content
* \param arg: An optional opaque pointer that gets passed to func
* \param free_arg: An optional callback for freeing arg (can be set to e.g. MEM_freeN)
* \param copy_arg: An optional callback for duplicating arg in case UI_but_func_tooltip_set
* is being called on multiple buttons (can be set to e.g. MEM_dupallocN). If set to NULL, arg will
* be passed as-is to all buttons.
*/
void uiLayoutSetTooltipFunc(uiLayout *layout,
uiButToolTipFunc func,
void *arg,
uiCopyArgFunc copy_arg,
uiFreeArgFunc free_arg);
/**
* This is a bit of a hack but best keep it in one place at least.
*/

View File

@ -5681,6 +5681,41 @@ void uiLayoutContextCopy(uiLayout *layout, bContextStore *context)
layout->context = CTX_store_add_all(&block->contexts, context);
}
void uiLayoutSetTooltipFunc(uiLayout *layout,
uiButToolTipFunc func,
void *arg,
uiCopyArgFunc copy_arg,
uiFreeArgFunc free_arg)
{
bool arg_used = false;
LISTBASE_FOREACH (uiItem *, item, &layout->items) {
/* Each button will call free_arg for "its" argument, so we need to
* duplicate the allocation for each button after the first. */
if (copy_arg != NULL && arg_used) {
arg = copy_arg(arg);
}
arg_used = true;
if (item->type == ITEM_BUTTON) {
uiButtonItem *bitem = (uiButtonItem *)item;
if (bitem->but->type == UI_BTYPE_DECORATOR) {
continue;
}
UI_but_func_tooltip_set(bitem->but, func, arg, free_arg);
}
else {
uiLayoutSetTooltipFunc(
(uiLayout *)item, func, arg, copy_arg, free_arg);
}
}
if (!arg_used) {
/* Free the original copy of arg in case the layout is empty. */
free_arg(arg);
}
}
void uiLayoutSetContextFromBut(uiLayout *layout, uiBut *but)
{
if (but->opptr) {

View File

@ -61,7 +61,7 @@ bool ui_but_is_toggle(const uiBut *but)
bool ui_but_is_interactive(const uiBut *but, const bool labeledit)
{
/* NOTE: #UI_BTYPE_LABEL is included for highlights, this allows drags. */
if ((but->type == UI_BTYPE_LABEL) && but->dragpoin == nullptr) {
if ((but->type == UI_BTYPE_LABEL) && but->dragpoin == nullptr && but->tip_func == nullptr) {
return false;
}
if (ELEM(but->type, UI_BTYPE_ROUNDBOX, UI_BTYPE_SEPR, UI_BTYPE_SEPR_LINE, UI_BTYPE_LISTBOX)) {

View File

@ -788,8 +788,10 @@ static uiTooltipData *ui_tooltip_data_from_button_or_extra_icon(bContext *C,
}
/* Tip Label (only for buttons not already showing the label).
* Check prefix instead of comparing because the button may include the shortcut. */
if (but_label.strinfo && !STRPREFIX(but->drawstr, but_label.strinfo)) {
* Check prefix instead of comparing because the button may include the shortcut.
* Buttons with dynamic tooltips also don't get their default label here since they
* can already provide more accurate and specific tooltip content. */
if (but_label.strinfo && !STRPREFIX(but->drawstr, but_label.strinfo) && !but->tip_func) {
uiTooltipField *field = text_field_add(data,
&(uiTooltipFormat){
.style = UI_TIP_STYLE_HEADER,

View File

@ -26,12 +26,10 @@
#include "BLI_span.hh"
#include "BLI_string_ref.hh"
#include "BLI_vector.hh"
#include "BLI_vector_set.hh"
#include "BLT_translation.h"
#include "BKE_context.h"
#include "BKE_geometry_set.hh"
#include "BKE_idtype.h"
#include "BKE_lib_id.h"
#include "BKE_main.h"
@ -75,8 +73,6 @@
#include "node_intern.hh" /* own include */
using blender::GPointer;
using blender::fn::FieldCPPType;
using blender::fn::FieldInput;
using blender::fn::GField;
namespace geo_log = blender::nodes::geometry_nodes_eval_log;
@ -374,6 +370,8 @@ static void node_update_basis(const bContext &C, bNodeTree &ntree, bNode &node,
const char *socket_label = nodeSocketLabel(nsock);
nsock->typeinfo->draw((bContext *)&C, row, &sockptr, &nodeptr, IFACE_(socket_label));
node_socket_add_tooltip(&ntree, &node, nsock, row);
UI_block_align_end(&block);
UI_block_layout_resolve(&block, nullptr, &buty);
@ -504,6 +502,8 @@ static void node_update_basis(const bContext &C, bNodeTree &ntree, bNode &node,
const char *socket_label = nodeSocketLabel(nsock);
nsock->typeinfo->draw((bContext *)&C, row, &sockptr, &nodeptr, IFACE_(socket_label));
node_socket_add_tooltip(&ntree, &node, nsock, row);
UI_block_align_end(&block);
UI_block_layout_resolve(&block, nullptr, &buty);
@ -943,6 +943,10 @@ static std::optional<std::string> create_socket_inspection_string(bContext *C,
bNodeSocket &socket)
{
SpaceNode *snode = CTX_wm_space_node(C);
if (snode == nullptr) {
return {};
};
const geo_log::SocketLog *socket_log = geo_log::ModifierLog::find_socket_by_node_editor_context(
*snode, node, socket);
if (socket_log == nullptr) {
@ -970,6 +974,78 @@ static std::optional<std::string> create_socket_inspection_string(bContext *C,
return ss.str();
}
static bool node_socket_has_tooltip(bNodeTree *ntree, bNodeSocket *socket)
{
if (ntree->type == NTREE_GEOMETRY) {
return true;
}
if (socket->declaration != nullptr) {
const blender::nodes::SocketDeclaration &socket_decl = *socket->declaration;
return !socket_decl.description().is_empty();
}
return false;
}
static char *node_socket_get_tooltip(bContext *C,
bNodeTree *ntree,
bNode *node,
bNodeSocket *socket)
{
std::stringstream output;
if (socket->declaration != nullptr) {
const blender::nodes::SocketDeclaration &socket_decl = *socket->declaration;
blender::StringRef description = socket_decl.description();
if (!description.is_empty()) {
output << TIP_(description.data());
}
}
if (ntree->type == NTREE_GEOMETRY) {
if (!output.str().empty()) {
output << ".\n\n";
}
std::optional<std::string> socket_inspection_str = create_socket_inspection_string(
C, *node, *socket);
if (socket_inspection_str.has_value()) {
output << *socket_inspection_str;
}
else {
output << TIP_("The socket value has not been computed yet");
}
}
if (output.str().empty()) {
output << nodeSocketLabel(socket);
}
return BLI_strdup(output.str().c_str());
}
void node_socket_add_tooltip(bNodeTree *ntree, bNode *node, bNodeSocket *sock, uiLayout *layout)
{
if (!node_socket_has_tooltip(ntree, sock)) {
return;
}
SocketTooltipData *data = MEM_cnew<SocketTooltipData>(__func__);
data->ntree = ntree;
data->node = node;
data->socket = sock;
uiLayoutSetTooltipFunc(
layout,
[](bContext *C, void *argN, const char *UNUSED(tip)) {
SocketTooltipData *data = static_cast<SocketTooltipData *>(argN);
return node_socket_get_tooltip(C, data->ntree, data->node, data->socket);
},
data,
MEM_dupallocN,
MEM_freeN);
}
static void node_socket_draw_nested(const bContext &C,
bNodeTree &ntree,
PointerRNA &node_ptr,
@ -1001,8 +1077,7 @@ static void node_socket_draw_nested(const bContext &C,
size_id,
outline_col_id);
if (ntree.type != NTREE_GEOMETRY) {
/* Only geometry nodes has socket value tooltips currently. */
if (!node_socket_has_tooltip(&ntree, &sock)) {
return;
}
@ -1034,24 +1109,7 @@ static void node_socket_draw_nested(const bContext &C,
but,
[](bContext *C, void *argN, const char *UNUSED(tip)) {
SocketTooltipData *data = (SocketTooltipData *)argN;
std::optional<std::string> socket_inspection_str = create_socket_inspection_string(
C, *data->node, *data->socket);
std::stringstream output;
if (data->socket->declaration != nullptr) {
const blender::nodes::SocketDeclaration &socket_decl = *data->socket->declaration;
blender::StringRef description = socket_decl.description();
if (!description.is_empty()) {
output << TIP_(description.data()) << ".\n\n";
}
}
if (socket_inspection_str.has_value()) {
output << *socket_inspection_str;
}
else {
output << TIP_("The socket value has not been computed yet");
}
return BLI_strdup(output.str().c_str());
return node_socket_get_tooltip(C, data->ntree, data->node, data->socket);
},
data,
MEM_freeN);

View File

@ -134,6 +134,8 @@ void node_socket_color_get(const bContext &C,
void node_draw_space(const bContext &C, ARegion &region);
void node_socket_add_tooltip(bNodeTree *ntree, bNode *node, bNodeSocket *sock, uiLayout *layout);
/**
* Sort nodes by selection: unselected nodes first, then selected,
* then the active node at the very end. Relative order is kept intact.

View File

@ -852,23 +852,19 @@ static void ui_node_draw_input(
}
}
else {
row = uiLayoutRow(row, true);
uiLayout *sub = uiLayoutRow(row, true);
uiTemplateNodeLink(row, C, ntree, node, input);
uiTemplateNodeLink(sub, C, ntree, node, input);
if (input->flag & SOCK_HIDE_VALUE) {
add_dummy_decorator = true;
}
/* input not linked, show value */
else {
uiLayout *sub = row;
switch (input->type) {
case SOCK_VECTOR:
if (input->type == SOCK_VECTOR) {
uiItemS(row);
sub = uiLayoutColumn(row, true);
}
uiItemS(sub);
sub = uiLayoutColumn(sub, true);
ATTR_FALLTHROUGH;
case SOCK_FLOAT:
case SOCK_INT:
@ -884,7 +880,7 @@ static void ui_node_draw_input(
if (node_tree->type == NTREE_GEOMETRY && snode != nullptr) {
/* Only add the attribute search in the node editor, in other places there is not
* enough context. */
node_geometry_add_attribute_search_button(*C, *node, inputptr, *row);
node_geometry_add_attribute_search_button(*C, *node, inputptr, *sub);
}
else {
uiItemR(sub, &inputptr, "default_value", 0, "", ICON_NONE);
@ -903,6 +899,8 @@ static void ui_node_draw_input(
uiItemDecoratorR(split_wrapper.decorate_column, nullptr, nullptr, 0);
}
node_socket_add_tooltip(ntree, node, input, row);
/* clear */
node->flag &= ~NODE_TEST;
}