Tool System: Show popup accelerators in tooltip
[blender.git] / source / blender / editors / interface / interface_region_tooltip.c
1 /*
2  * ***** BEGIN GPL LICENSE BLOCK *****
3  *
4  * This program is free software; you can redistribute it and/or
5  * modify it under the terms of the GNU General Public License
6  * as published by the Free Software Foundation; either version 2
7  * of the License, or (at your option) any later version.
8  *
9  * This program is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12  * GNU General Public License for more details.
13  *
14  * You should have received a copy of the GNU General Public License
15  * along with this program; if not, write to the Free Software Foundation,
16  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17  *
18  * The Original Code is Copyright (C) 2008 Blender Foundation.
19  * All rights reserved.
20  *
21  * Contributor(s): Blender Foundation
22  *
23  * ***** END GPL LICENSE BLOCK *****
24  */
25
26 /** \file blender/editors/interface/interface_region_tooltip.c
27  *  \ingroup edinterface
28  *
29  * ToolTip Region and Construction
30  */
31
32 /* TODO(campbell):
33  * We may want to have a higher level API that initializes a timer,
34  * checks for mouse motion and clears the tool-tip afterwards.
35  * We never want multiple tool-tips at once so this could be handled on the window / window-manager level.
36  *
37  * For now it's not a priority, so leave as-is.
38  */
39
40 #include <stdarg.h>
41 #include <stdlib.h>
42 #include <string.h>
43 #include <assert.h>
44
45 #include "MEM_guardedalloc.h"
46
47 #include "DNA_userdef_types.h"
48
49 #include "BLI_math.h"
50 #include "BLI_string.h"
51 #include "BLI_string_utf8.h"
52 #include "BLI_rect.h"
53 #include "BLI_utildefines.h"
54
55 #include "BKE_context.h"
56 #include "BKE_screen.h"
57
58 #include "WM_api.h"
59 #include "WM_types.h"
60
61 #include "RNA_access.h"
62
63 #include "BIF_gl.h"
64
65 #include "UI_interface.h"
66
67 #include "BLF_api.h"
68 #include "BLT_translation.h"
69
70 #ifdef WITH_PYTHON
71 #  include "BPY_extern.h"
72 #endif
73
74 #include "ED_screen.h"
75
76 #include "interface_intern.h"
77 #include "interface_regions_intern.h"
78
79 #define UI_TIP_PAD_FAC      1.3f
80 #define UI_TIP_PADDING      (int)(UI_TIP_PAD_FAC * UI_UNIT_Y)
81 #define UI_TIP_MAXWIDTH     600
82
83
84 typedef struct uiTooltipFormat {
85         enum {
86                 UI_TIP_STYLE_NORMAL = 0,
87                 UI_TIP_STYLE_HEADER,
88                 UI_TIP_STYLE_MONO,
89         } style : 3;
90         enum {
91                 UI_TIP_LC_MAIN = 0,     /* primary text */
92                 UI_TIP_LC_VALUE,        /* the value of buttons (also shortcuts) */
93                 UI_TIP_LC_ACTIVE,       /* titles of active enum values */
94                 UI_TIP_LC_NORMAL,       /* regular text */
95                 UI_TIP_LC_PYTHON,       /* Python snippet */
96                 UI_TIP_LC_ALERT,        /* description of why operator can't run */
97         } color_id : 4;
98         int is_pad : 1;
99 } uiTooltipFormat;
100
101 typedef struct uiTooltipField {
102         char *text;
103         char *text_suffix;
104         struct {
105                 uint x_pos;     /* x cursor position at the end of the last line */
106                 uint lines;     /* number of lines, 1 or more with word-wrap */
107         } geom;
108         uiTooltipFormat format;
109
110 } uiTooltipField;
111
112 typedef struct uiTooltipData {
113         rcti bbox;
114         uiTooltipField *fields;
115         uint            fields_len;
116         uiFontStyle fstyle;
117         int wrap_width;
118         int toth, lineh;
119 } uiTooltipData;
120
121 #define UI_TIP_LC_MAX 6
122
123 BLI_STATIC_ASSERT(UI_TIP_LC_MAX == UI_TIP_LC_ALERT + 1, "invalid lc-max");
124 BLI_STATIC_ASSERT(sizeof(uiTooltipFormat) <= sizeof(int), "oversize");
125
126 static uiTooltipField *text_field_add_only(
127         uiTooltipData *data)
128 {
129         data->fields_len += 1;
130         data->fields = MEM_recallocN(data->fields, sizeof(*data->fields) * data->fields_len);
131         return &data->fields[data->fields_len - 1];
132 }
133
134 static uiTooltipField *text_field_add(
135         uiTooltipData *data,
136         const uiTooltipFormat *format)
137 {
138         uiTooltipField *field = text_field_add_only(data);
139         field->format = *format;
140         return field;
141 }
142
143 /* -------------------------------------------------------------------- */
144 /** \name ToolTip Callbacks (Draw & Free)
145  * \{ */
146
147 static void rgb_tint(
148         float col[3],
149         float h, float h_strength,
150         float v, float v_strength)
151 {
152         float col_hsv_from[3];
153         float col_hsv_to[3];
154
155         rgb_to_hsv_v(col, col_hsv_from);
156
157         col_hsv_to[0] = h;
158         col_hsv_to[1] = h_strength;
159         col_hsv_to[2] = (col_hsv_from[2] * (1.0f - v_strength)) + (v * v_strength);
160
161         hsv_to_rgb_v(col_hsv_to, col);
162 }
163
164 static void ui_tooltip_region_draw_cb(const bContext *UNUSED(C), ARegion *ar)
165 {
166         const float pad_px = UI_TIP_PADDING;
167         uiTooltipData *data = ar->regiondata;
168         uiWidgetColors *theme = ui_tooltip_get_theme();
169         rcti bbox = data->bbox;
170         float tip_colors[UI_TIP_LC_MAX][3];
171         unsigned char drawcol[4] = {0, 0, 0, 255}; /* to store color in while drawing (alpha is always 255) */
172
173         float *main_color    = tip_colors[UI_TIP_LC_MAIN]; /* the color from the theme */
174         float *value_color   = tip_colors[UI_TIP_LC_VALUE];
175         float *active_color  = tip_colors[UI_TIP_LC_ACTIVE];
176         float *normal_color  = tip_colors[UI_TIP_LC_NORMAL];
177         float *python_color  = tip_colors[UI_TIP_LC_PYTHON];
178         float *alert_color   = tip_colors[UI_TIP_LC_ALERT];
179
180         float background_color[3];
181         float tone_bg;
182         int i;
183
184         wmOrtho2_region_pixelspace(ar);
185
186         /* draw background */
187         ui_draw_tooltip_background(UI_style_get(), NULL, &bbox);
188
189         /* set background_color */
190         rgb_uchar_to_float(background_color, (const unsigned char *)theme->inner);
191
192         /* calculate normal_color */
193         rgb_uchar_to_float(main_color, (const unsigned char *)theme->text);
194         copy_v3_v3(active_color, main_color);
195         copy_v3_v3(normal_color, main_color);
196         copy_v3_v3(python_color, main_color);
197         copy_v3_v3(alert_color, main_color);
198         copy_v3_v3(value_color, main_color);
199
200         /* find the brightness difference between background and text colors */
201
202         tone_bg = rgb_to_grayscale(background_color);
203         /* tone_fg = rgb_to_grayscale(main_color); */
204
205         /* mix the colors */
206         rgb_tint(value_color,  0.0f, 0.0f, tone_bg, 0.2f);  /* light gray */
207         rgb_tint(active_color, 0.6f, 0.2f, tone_bg, 0.2f);  /* light blue */
208         rgb_tint(normal_color, 0.0f, 0.0f, tone_bg, 0.4f);  /* gray       */
209         rgb_tint(python_color, 0.0f, 0.0f, tone_bg, 0.5f);  /* dark gray  */
210         rgb_tint(alert_color,  0.0f, 0.8f, tone_bg, 0.1f);  /* red        */
211
212         /* draw text */
213         BLF_wordwrap(data->fstyle.uifont_id, data->wrap_width);
214         BLF_wordwrap(blf_mono_font, data->wrap_width);
215
216         bbox.xmin += 0.5f * pad_px;  /* add padding to the text */
217         bbox.ymax -= 0.25f * pad_px;
218
219         for (i = 0; i < data->fields_len; i++) {
220                 const uiTooltipField *field = &data->fields[i];
221                 const uiTooltipField *field_next = (i + 1) != data->fields_len ? &data->fields[i + 1] : NULL;
222
223                 bbox.ymin = bbox.ymax - (data->lineh * field->geom.lines);
224                 if (field->format.style == UI_TIP_STYLE_HEADER) {
225                         /* draw header and active data (is done here to be able to change color) */
226                         uiFontStyle fstyle_header = data->fstyle;
227
228                         /* override text-style */
229                         fstyle_header.shadow = 1;
230                         fstyle_header.shadowcolor = rgb_to_grayscale(tip_colors[UI_TIP_LC_MAIN]);
231                         fstyle_header.shadx = fstyle_header.shady = 0;
232                         fstyle_header.shadowalpha = 1.0f;
233                         fstyle_header.word_wrap = true;
234
235                         rgb_float_to_uchar(drawcol, tip_colors[UI_TIP_LC_MAIN]);
236                         UI_fontstyle_set(&fstyle_header);
237                         UI_fontstyle_draw(&fstyle_header, &bbox, field->text, drawcol);
238
239                         fstyle_header.shadow = 0;
240
241                         /* offset to the end of the last line */
242                         if (field->text_suffix) {
243                                 float xofs = field->geom.x_pos;
244                                 float yofs = data->lineh * (field->geom.lines - 1);
245                                 bbox.xmin += xofs;
246                                 bbox.ymax -= yofs;
247
248                                 rgb_float_to_uchar(drawcol, tip_colors[UI_TIP_LC_ACTIVE]);
249                                 UI_fontstyle_draw(&fstyle_header, &bbox, field->text_suffix, drawcol);
250
251                                 /* undo offset */
252                                 bbox.xmin -= xofs;
253                                 bbox.ymax += yofs;
254                         }
255                 }
256                 else if (field->format.style == UI_TIP_STYLE_MONO) {
257                         uiFontStyle fstyle_mono = data->fstyle;
258                         fstyle_mono.uifont_id = blf_mono_font;
259                         fstyle_mono.word_wrap = true;
260
261                         UI_fontstyle_set(&fstyle_mono);
262                         /* XXX, needed because we dont have mono in 'U.uifonts' */
263                         BLF_size(fstyle_mono.uifont_id, fstyle_mono.points * U.pixelsize, U.dpi);
264                         rgb_float_to_uchar(drawcol, tip_colors[field->format.color_id]);
265                         UI_fontstyle_draw(&fstyle_mono, &bbox, field->text, drawcol);
266                 }
267                 else {
268                         uiFontStyle fstyle_normal = data->fstyle;
269                         BLI_assert(field->format.style == UI_TIP_STYLE_NORMAL);
270                         fstyle_normal.word_wrap = true;
271
272                         /* draw remaining data */
273                         rgb_float_to_uchar(drawcol, tip_colors[field->format.color_id]);
274                         UI_fontstyle_set(&fstyle_normal);
275                         UI_fontstyle_draw(&fstyle_normal, &bbox, field->text, drawcol);
276                 }
277
278                 bbox.ymax -= data->lineh * field->geom.lines;
279
280                 if (field_next && field_next->format.is_pad) {
281                         bbox.ymax -= data->lineh * (UI_TIP_PAD_FAC - 1);
282                 }
283         }
284
285         BLF_disable(data->fstyle.uifont_id, BLF_WORD_WRAP);
286         BLF_disable(blf_mono_font, BLF_WORD_WRAP);
287 }
288
289 static void ui_tooltip_region_free_cb(ARegion *ar)
290 {
291         uiTooltipData *data;
292
293         data = ar->regiondata;
294
295         for (int  i = 0; i < data->fields_len; i++) {
296                 const uiTooltipField *field = &data->fields[i];
297                 MEM_freeN(field->text);
298                 if (field->text_suffix) {
299                         MEM_freeN(field->text_suffix);
300                 }
301         }
302         MEM_freeN(data->fields);
303         MEM_freeN(data);
304         ar->regiondata = NULL;
305 }
306
307 /** \} */
308
309 /* -------------------------------------------------------------------- */
310 /** \name ToolTip Creation
311  * \{ */
312
313 static bool ui_tooltip_data_append_from_keymap(
314         bContext *C, uiTooltipData *data,
315         wmKeyMap *keymap)
316 {
317         const int fields_len_init = data->fields_len;
318         char buf[512];
319
320         for (wmKeyMapItem *kmi = keymap->items.first; kmi; kmi = kmi->next) {
321                 wmOperatorType *ot = WM_operatortype_find(kmi->idname, true);
322                 if (ot != NULL) {
323                         /* Tip */
324                         {
325                                 uiTooltipField *field = text_field_add(
326                                         data, &(uiTooltipFormat){
327                                             .style = UI_TIP_STYLE_NORMAL,
328                                             .color_id = UI_TIP_LC_MAIN,
329                                             .is_pad = true,
330                                         });
331                                 field->text = BLI_strdup(ot->description[0] ? ot->description : ot->name);
332                         }
333                         /* Shortcut */
334                         {
335                                 uiTooltipField *field = text_field_add(
336                                         data, &(uiTooltipFormat){
337                                             .style = UI_TIP_STYLE_NORMAL,
338                                             .color_id = UI_TIP_LC_NORMAL,
339                                         });
340                                 bool found = false;
341                                 if (WM_keymap_item_to_string(kmi, false, buf, sizeof(buf))) {
342                                         found = true;
343                                 }
344                                 field->text = BLI_sprintfN(TIP_("Shortcut: %s"), found ? buf : "None");
345                         }
346
347                         /* Python */
348                         if (U.flag & USER_TOOLTIPS_PYTHON) {
349                                 uiTooltipField *field = text_field_add(
350                                         data, &(uiTooltipFormat){
351                                             .style = UI_TIP_STYLE_NORMAL,
352                                             .color_id = UI_TIP_LC_PYTHON,
353                                         });
354                                 char *str = WM_operator_pystring_ex(C, NULL, false, false, ot, kmi->ptr);
355                                 WM_operator_pystring_abbreviate(str, 32);
356                                 field->text = BLI_sprintfN(TIP_("Python: %s"), str);
357                                 MEM_freeN(str);
358                         }
359                 }
360         }
361
362         return (fields_len_init != data->fields_len);
363 }
364
365
366 /**
367  * Special tool-system exception.
368  */
369 static uiTooltipData *ui_tooltip_data_from_tool(bContext *C, uiBut *but)
370 {
371         if (but->optype == NULL) {
372                 return NULL;
373         }
374
375         if (!STREQ(but->optype->idname, "WM_OT_tool_set_by_name")) {
376                 return NULL;
377         }
378
379         char tool_name[MAX_NAME];
380         RNA_string_get(but->opptr, "name", tool_name);
381         BLI_assert(tool_name[0] != '\0');
382
383         /* We have a tool, now extract the info. */
384         uiTooltipData *data = MEM_callocN(sizeof(uiTooltipData), "uiTooltipData");
385
386 #ifdef WITH_PYTHON
387         /* it turns out to be most simple to do this via Python since C
388          * doesn't have access to information about non-active tools.
389          */
390         char expr[256];
391
392         /* Tip. */
393         {
394                 SNPRINTF(
395                         expr,
396                         "__import__('bl_ui').space_toolsystem_common.description_from_name("
397                         "__import__('bpy').context, "
398                         "__import__('bpy').context.space_data.type, "
399                         "'%s') + '.'",
400                         tool_name);
401
402                 char *expr_result = NULL;
403                 if (BPY_execute_string_as_string(C, expr, true, &expr_result)) {
404                         if (!STREQ(expr_result, ".")) {
405                                 uiTooltipField *field = text_field_add(
406                                         data, &(uiTooltipFormat){
407                                             .style = UI_TIP_STYLE_NORMAL,
408                                             .color_id = UI_TIP_LC_MAIN,
409                                             .is_pad = true,
410                                         });
411                                 field->text = expr_result;
412                         }
413                         else {
414                                 MEM_freeN(expr_result);
415                         }
416                 }
417                 else {
418                         BLI_assert(0);
419                 }
420         }
421
422         /* Shortcut. */
423         {
424                 /* There are two kinds of shortcuts, either direct access to the tool,
425                  * when a key is bound directly to the tool (as if the toolbar button is pressed),
426                  * or when a key is assigned to the operator it's self (bypassing the tool).
427                  *
428                  * Either way case it's useful to show the shortcut.
429                  */
430                 char *shortcut = NULL;
431
432                 {
433                         uiStringInfo op_keymap = {BUT_GET_OP_KEYMAP, NULL};
434                         UI_but_string_info_get(C, but, &op_keymap, NULL);
435                         shortcut = op_keymap.strinfo;
436                 }
437
438                 if (shortcut == NULL) {
439                         /* Check for direct access to the tool. */
440                         char shortcut_toolbar[128] = "";
441                         if (WM_key_event_operator_string(
442                                     C, "WM_OT_toolbar", WM_OP_INVOKE_REGION_WIN, NULL, true,
443                                     shortcut_toolbar, ARRAY_SIZE(shortcut_toolbar)))
444                         {
445                                 /* Generate keymap in order to inspect it.
446                                  * Note, we could make a utility to avoid the keymap generation part of this. */
447                                 const char *expr_ptr = (
448                                         "getattr("
449                                         "__import__('bl_ui').space_toolsystem_common.keymap_from_context("
450                                         "__import__('bpy').context, "
451                                         "__import__('bpy').context.space_data.type), "
452                                         "'as_pointer', lambda: 0)()");
453
454                                 intptr_t expr_result = 0;
455                                 if (BPY_execute_string_as_intptr(C, expr_ptr, true, &expr_result)) {
456                                         if (expr_result != 0) {
457                                                 wmKeyMap *keymap = (wmKeyMap *)expr_result;
458                                                 for (wmKeyMapItem *kmi = keymap->items.first; kmi; kmi = kmi->next) {
459                                                         if (STREQ(kmi->idname, but->optype->idname)) {
460                                                                 char tool_name_test[MAX_NAME];
461                                                                 RNA_string_get(kmi->ptr, "name", tool_name_test);
462                                                                 if (STREQ(tool_name, tool_name_test)) {
463                                                                         char buf[128];
464                                                                         WM_keymap_item_to_string(kmi, false, buf, sizeof(buf));
465                                                                         shortcut = BLI_sprintfN("%s, %s", shortcut_toolbar, buf);
466                                                                         break;
467                                                                 }
468                                                         }
469                                                 }
470                                         }
471                                 }
472                                 else {
473                                         BLI_assert(0);
474                                 }
475                         }
476                 }
477
478                 if (shortcut != NULL) {
479                         uiTooltipField *field = text_field_add(
480                                 data, &(uiTooltipFormat){
481                                     .style = UI_TIP_STYLE_NORMAL,
482                                     .color_id = UI_TIP_LC_VALUE,
483                                     .is_pad = true,
484                                 });
485                         field->text = BLI_sprintfN(TIP_("Shortcut: %s"), shortcut);
486                         MEM_freeN(shortcut);
487                 }
488         }
489
490         /* Keymap */
491
492         /* This is too handy not to expose somehow, let's be sneaky for now. */
493         if (CTX_wm_window(C)->eventstate->shift) {
494
495                 SNPRINTF(
496                         expr,
497                         "getattr("
498                         "__import__('bl_ui').space_toolsystem_common.keymap_from_name("
499                         "__import__('bpy').context, "
500                         "__import__('bpy').context.space_data.type, "
501                         "'%s'), "
502                         "'as_pointer', lambda: 0)()",
503                         tool_name);
504
505                 intptr_t expr_result = 0;
506                 if (BPY_execute_string_as_intptr(C, expr, true, &expr_result)) {
507                         if (expr_result != 0) {
508                                 {
509                                         uiTooltipField *field = text_field_add(
510                                                 data, &(uiTooltipFormat){
511                                                     .style = UI_TIP_STYLE_NORMAL,
512                                                     .color_id = UI_TIP_LC_NORMAL,
513                                                     .is_pad = true,
514                                                 });
515                                         field->text = BLI_strdup("Tool Keymap:");
516                                 }
517                                 wmKeyMap *keymap = (wmKeyMap *)expr_result;
518                                 ui_tooltip_data_append_from_keymap(C, data, keymap);
519                         }
520                 }
521                 else {
522                         BLI_assert(0);
523                 }
524         }
525 #endif  /* WITH_PYTHON */
526
527         if (data->fields_len == 0) {
528                 MEM_freeN(data);
529                 return NULL;
530         }
531         else {
532                 return data;
533         }
534 }
535
536 static uiTooltipData *ui_tooltip_data_from_button(bContext *C, uiBut *but)
537 {
538         uiStringInfo but_tip = {BUT_GET_TIP, NULL};
539         uiStringInfo enum_label = {BUT_GET_RNAENUM_LABEL, NULL};
540         uiStringInfo enum_tip = {BUT_GET_RNAENUM_TIP, NULL};
541         uiStringInfo op_keymap = {BUT_GET_OP_KEYMAP, NULL};
542         uiStringInfo prop_keymap = {BUT_GET_PROP_KEYMAP, NULL};
543         uiStringInfo rna_struct = {BUT_GET_RNASTRUCT_IDENTIFIER, NULL};
544         uiStringInfo rna_prop = {BUT_GET_RNAPROP_IDENTIFIER, NULL};
545
546         char buf[512];
547
548         /* create tooltip data */
549         uiTooltipData *data = MEM_callocN(sizeof(uiTooltipData), "uiTooltipData");
550
551         UI_but_string_info_get(C, but, &but_tip, &enum_label, &enum_tip, &op_keymap, &prop_keymap, &rna_struct, &rna_prop, NULL);
552
553         /* Tip */
554         if (but_tip.strinfo) {
555                 {
556                         uiTooltipField *field = text_field_add(
557                                 data, &(uiTooltipFormat){
558                                     .style = UI_TIP_STYLE_HEADER,
559                                     .color_id = UI_TIP_LC_NORMAL,
560                                 });
561                         if (enum_label.strinfo) {
562                                 field->text = BLI_sprintfN("%s:  ", but_tip.strinfo);
563                                 field->text_suffix = BLI_strdup(enum_label.strinfo);
564                         }
565                         else {
566                                 field->text = BLI_sprintfN("%s.", but_tip.strinfo);
567                         }
568                 }
569
570                 /* special case enum rna buttons */
571                 if ((but->type & UI_BTYPE_ROW) && but->rnaprop && RNA_property_flag(but->rnaprop) & PROP_ENUM_FLAG) {
572                         uiTooltipField *field = text_field_add(
573                                 data, &(uiTooltipFormat){
574                                     .style = UI_TIP_STYLE_NORMAL,
575                                     .color_id = UI_TIP_LC_NORMAL,
576                                 });
577                         field->text = BLI_strdup(IFACE_("(Shift-Click/Drag to select multiple)"));
578                 }
579
580         }
581         /* Enum field label & tip */
582         if (enum_tip.strinfo) {
583                 uiTooltipField *field = text_field_add(
584                         data, &(uiTooltipFormat){
585                             .style = UI_TIP_STYLE_NORMAL,
586                             .color_id = UI_TIP_LC_VALUE,
587                             .is_pad = true,
588                         });
589                 field->text = BLI_strdup(enum_tip.strinfo);
590         }
591
592         /* Op shortcut */
593         if (op_keymap.strinfo) {
594                 uiTooltipField *field = text_field_add(
595                         data, &(uiTooltipFormat){
596                             .style = UI_TIP_STYLE_NORMAL,
597                             .color_id = UI_TIP_LC_VALUE,
598                             .is_pad = true,
599                         });
600                 field->text = BLI_sprintfN(TIP_("Shortcut: %s"), op_keymap.strinfo);
601         }
602
603         /* Property context-toggle shortcut */
604         if (prop_keymap.strinfo) {
605                 uiTooltipField *field = text_field_add(
606                         data, &(uiTooltipFormat){
607                             .style = UI_TIP_STYLE_NORMAL,
608                             .color_id = UI_TIP_LC_VALUE,
609                             .is_pad = true,
610                         });
611                 field->text = BLI_sprintfN(TIP_("Shortcut: %s"), prop_keymap.strinfo);
612         }
613
614         if (ELEM(but->type, UI_BTYPE_TEXT, UI_BTYPE_SEARCH_MENU)) {
615                 /* better not show the value of a password */
616                 if ((but->rnaprop && (RNA_property_subtype(but->rnaprop) == PROP_PASSWORD)) == 0) {
617                         /* full string */
618                         ui_but_string_get(but, buf, sizeof(buf));
619                         if (buf[0]) {
620                                 uiTooltipField *field = text_field_add(
621                                         data, &(uiTooltipFormat){
622                                             .style = UI_TIP_STYLE_NORMAL,
623                                             .color_id = UI_TIP_LC_VALUE,
624                                             .is_pad = true,
625                                         });
626                                 field->text = BLI_sprintfN(TIP_("Value: %s"), buf);
627                         }
628                 }
629         }
630
631         if (but->rnaprop) {
632                 int unit_type = UI_but_unit_type_get(but);
633
634                 if (unit_type == PROP_UNIT_ROTATION) {
635                         if (RNA_property_type(but->rnaprop) == PROP_FLOAT) {
636                                 float value = RNA_property_array_check(but->rnaprop) ?
637                                                   RNA_property_float_get_index(&but->rnapoin, but->rnaprop, but->rnaindex) :
638                                                   RNA_property_float_get(&but->rnapoin, but->rnaprop);
639
640                                 uiTooltipField *field = text_field_add(
641                                         data, &(uiTooltipFormat){
642                                             .style = UI_TIP_STYLE_NORMAL,
643                                             .color_id = UI_TIP_LC_VALUE,
644                                         });
645                                 field->text = BLI_sprintfN(TIP_("Radians: %f"), value);
646                         }
647                 }
648
649                 if (but->flag & UI_BUT_DRIVEN) {
650                         if (ui_but_anim_expression_get(but, buf, sizeof(buf))) {
651                                 uiTooltipField *field = text_field_add(
652                                         data, &(uiTooltipFormat){
653                                             .style = UI_TIP_STYLE_NORMAL,
654                                             .color_id = UI_TIP_LC_NORMAL,
655                                         });
656                                 field->text = BLI_sprintfN(TIP_("Expression: %s"), buf);
657                         }
658                 }
659
660                 if (but->rnapoin.id.data) {
661                         const ID *id = but->rnapoin.id.data;
662                         if (ID_IS_LINKED(id)) {
663                                 uiTooltipField *field = text_field_add(
664                                         data, &(uiTooltipFormat){
665                                             .style = UI_TIP_STYLE_NORMAL,
666                                             .color_id = UI_TIP_LC_NORMAL,
667                                         });
668                                 field->text = BLI_sprintfN(TIP_("Library: %s"), id->lib->name);
669                         }
670                 }
671         }
672         else if (but->optype) {
673                 PointerRNA *opptr;
674                 char *str;
675                 opptr = UI_but_operator_ptr_get(but); /* allocated when needed, the button owns it */
676
677                 /* so the context is passed to fieldf functions (some py fieldf functions use it) */
678                 WM_operator_properties_sanitize(opptr, false);
679
680                 str = WM_operator_pystring_ex(C, NULL, false, false, but->optype, opptr);
681
682                 /* avoid overly verbose tips (eg, arrays of 20 layers), exact limit is arbitrary */
683                 WM_operator_pystring_abbreviate(str, 32);
684
685                 /* operator info */
686                 if (U.flag & USER_TOOLTIPS_PYTHON) {
687                         uiTooltipField *field = text_field_add(
688                                 data, &(uiTooltipFormat){
689                                     .style = UI_TIP_STYLE_MONO,
690                                     .color_id = UI_TIP_LC_PYTHON,
691                                     .is_pad = true,
692                                 });
693                         field->text = BLI_sprintfN(TIP_("Python: %s"), str);
694                 }
695
696                 MEM_freeN(str);
697         }
698
699         /* button is disabled, we may be able to tell user why */
700         if (but->flag & UI_BUT_DISABLED) {
701                 const char *disabled_msg = NULL;
702
703                 /* if operator poll check failed, it can give pretty precise info why */
704                 if (but->optype) {
705                         CTX_wm_operator_poll_msg_set(C, NULL);
706                         WM_operator_poll_context(C, but->optype, but->opcontext);
707                         disabled_msg = CTX_wm_operator_poll_msg_get(C);
708                 }
709                 /* alternatively, buttons can store some reasoning too */
710                 else if (but->disabled_info) {
711                         disabled_msg = TIP_(but->disabled_info);
712                 }
713
714                 if (disabled_msg && disabled_msg[0]) {
715                         uiTooltipField *field = text_field_add(
716                                 data, &(uiTooltipFormat){
717                                     .style = UI_TIP_STYLE_NORMAL,
718                                     .color_id = UI_TIP_LC_ALERT,
719                                 });
720                         field->text = BLI_sprintfN(TIP_("Disabled: %s"), disabled_msg);
721                 }
722         }
723
724         if ((U.flag & USER_TOOLTIPS_PYTHON) && !but->optype && rna_struct.strinfo) {
725                 {
726                         uiTooltipField *field = text_field_add(
727                                 data, &(uiTooltipFormat){
728                                     .style = UI_TIP_STYLE_MONO,
729                                     .color_id = UI_TIP_LC_PYTHON,
730                                     .is_pad = true,
731                                 });
732                         if (rna_prop.strinfo) {
733                                 /* Struct and prop */
734                                 field->text = BLI_sprintfN(TIP_("Python: %s.%s"), rna_struct.strinfo, rna_prop.strinfo);
735                         }
736                         else {
737                                 /* Only struct (e.g. menus) */
738                                 field->text = BLI_sprintfN(TIP_("Python: %s"), rna_struct.strinfo);
739                         }
740                 }
741
742                 if (but->rnapoin.id.data) {
743                         uiTooltipField *field = text_field_add(
744                                 data, &(uiTooltipFormat){
745                                     .style = UI_TIP_STYLE_MONO,
746                                     .color_id = UI_TIP_LC_PYTHON,
747                                 });
748
749                         /* this could get its own 'BUT_GET_...' type */
750
751                         /* never fails */
752                         /* move ownership (no need for re-alloc) */
753                         if (but->rnaprop) {
754                                 field->text = RNA_path_full_property_py_ex(&but->rnapoin, but->rnaprop, but->rnaindex, true);
755                         }
756                         else {
757                                 field->text = RNA_path_full_struct_py(&but->rnapoin);
758                         }
759
760                 }
761         }
762
763         /* Free strinfo's... */
764         if (but_tip.strinfo)
765                 MEM_freeN(but_tip.strinfo);
766         if (enum_label.strinfo)
767                 MEM_freeN(enum_label.strinfo);
768         if (enum_tip.strinfo)
769                 MEM_freeN(enum_tip.strinfo);
770         if (op_keymap.strinfo)
771                 MEM_freeN(op_keymap.strinfo);
772         if (prop_keymap.strinfo)
773                 MEM_freeN(prop_keymap.strinfo);
774         if (rna_struct.strinfo)
775                 MEM_freeN(rna_struct.strinfo);
776         if (rna_prop.strinfo)
777                 MEM_freeN(rna_prop.strinfo);
778
779         if (data->fields_len == 0) {
780                 MEM_freeN(data);
781                 return NULL;
782         }
783         else {
784                 return data;
785         }
786 }
787
788 static uiTooltipData *ui_tooltip_data_from_gizmo(bContext *C, wmGizmo *gz)
789 {
790         uiTooltipData *data = MEM_callocN(sizeof(uiTooltipData), "uiTooltipData");
791
792         /* TODO(campbell): a way for gizmos to have their own descriptions (low priority). */
793
794         /* Operator Actions */
795         {
796                 bool use_drag = gz->drag_part != -1 && gz->highlight_part != gz->drag_part;
797
798                 const struct {
799                         int part;
800                         const char *prefix;
801                 } mpop_actions[] = {
802                         {
803                                 .part = gz->highlight_part,
804                                 .prefix = use_drag ? TIP_("Click") : NULL,
805                         }, {
806                                 .part = use_drag ? gz->drag_part : -1,
807                                 .prefix = use_drag ? TIP_("Drag") : NULL,
808                         },
809                 };
810
811                 for (int i = 0; i < ARRAY_SIZE(mpop_actions); i++) {
812                         wmGizmoOpElem *mpop = (mpop_actions[i].part != -1) ? WM_gizmo_operator_get(gz, mpop_actions[i].part) : NULL;
813                         if (mpop != NULL) {
814                                 /* Description */
815                                 const char *info = RNA_struct_ui_description(mpop->type->srna);
816                                 if (!(info && info[0])) {
817                                         info  = RNA_struct_ui_name(mpop->type->srna);
818                                 }
819
820                                 if (info && info[0]) {
821                                         char *text = NULL;
822                                         if (mpop_actions[i].prefix != NULL) {
823                                                 text = BLI_sprintfN("%s: %s", mpop_actions[i].prefix, info);
824                                         }
825                                         else {
826                                                 text = BLI_strdup(info);
827                                         }
828
829                                         if (text != NULL) {
830                                                 uiTooltipField *field = text_field_add(
831                                                         data, &(uiTooltipFormat){
832                                                             .style = UI_TIP_STYLE_HEADER,
833                                                             .color_id = UI_TIP_LC_VALUE,
834                                                             .is_pad = true,
835                                                         });
836                                                 field->text = text;
837                                         }
838                                 }
839
840                                 /* Shortcut */
841                                 {
842                                         bool found = false;
843                                         IDProperty *prop = mpop->ptr.data;
844                                         char buf[128];
845                                         if (WM_key_event_operator_string(
846                                                     C, mpop->type->idname, WM_OP_INVOKE_DEFAULT, prop, true,
847                                                     buf, ARRAY_SIZE(buf)))
848                                         {
849                                                 found = true;
850                                         }
851                                         uiTooltipField *field = text_field_add(
852                                                 data, &(uiTooltipFormat){
853                                                     .style = UI_TIP_STYLE_NORMAL,
854                                                     .color_id = UI_TIP_LC_VALUE,
855                                                     .is_pad = true,
856                                                 });
857                                         field->text = BLI_sprintfN(TIP_("Shortcut: %s"), found ? buf : "None");
858                                 }
859                         }
860                 }
861         }
862
863         /* Property Actions */
864         if (gz->type->target_property_defs_len) {
865                 wmGizmoProperty *gz_prop_array = WM_gizmo_target_property_array(gz);
866                 for (int i = 0; i < gz->type->target_property_defs_len; i++) {
867                         /* TODO(campbell): function callback descriptions. */
868                         wmGizmoProperty *gz_prop = &gz_prop_array[i];
869                         if (gz_prop->prop != NULL) {
870                                 const char *info = RNA_property_ui_description(gz_prop->prop);
871                                 if (info && info[0]) {
872                                         uiTooltipField *field = text_field_add(
873                                                 data, &(uiTooltipFormat){
874                                                     .style = UI_TIP_STYLE_NORMAL,
875                                                     .color_id = UI_TIP_LC_VALUE,
876                                                     .is_pad = true,
877                                                 });
878                                         field->text = BLI_strdup(info);
879                                 }
880                         }
881                 }
882         }
883
884         if (data->fields_len == 0) {
885                 MEM_freeN(data);
886                 return NULL;
887         }
888         else {
889                 return data;
890         }
891 }
892
893
894 static ARegion *ui_tooltip_create_with_data(
895         bContext *C, uiTooltipData *data,
896         const float init_position[2],
897         const float aspect)
898 {
899         const float pad_px = UI_TIP_PADDING;
900         wmWindow *win = CTX_wm_window(C);
901         const int winx = WM_window_pixels_x(win);
902         uiStyle *style = UI_style_get();
903         static ARegionType type;
904         ARegion *ar;
905         int fonth, fontw;
906         int h, i;
907         rctf rect_fl;
908         rcti rect_i;
909         int font_flag = 0;
910
911         /* create area region */
912         ar = ui_region_temp_add(CTX_wm_screen(C));
913
914         memset(&type, 0, sizeof(ARegionType));
915         type.draw = ui_tooltip_region_draw_cb;
916         type.free = ui_tooltip_region_free_cb;
917         type.regionid = RGN_TYPE_TEMPORARY;
918         ar->type = &type;
919
920         /* set font, get bb */
921         data->fstyle = style->widget; /* copy struct */
922         ui_fontscale(&data->fstyle.points, aspect);
923
924         UI_fontstyle_set(&data->fstyle);
925
926         data->wrap_width = min_ii(UI_TIP_MAXWIDTH * U.pixelsize / aspect, winx - (UI_TIP_PADDING * 2));
927
928         font_flag |= BLF_WORD_WRAP;
929         if (data->fstyle.kerning == 1) {
930                 font_flag |= BLF_KERNING_DEFAULT;
931         }
932         BLF_enable(data->fstyle.uifont_id, font_flag);
933         BLF_enable(blf_mono_font, font_flag);
934         BLF_wordwrap(data->fstyle.uifont_id, data->wrap_width);
935         BLF_wordwrap(blf_mono_font, data->wrap_width);
936
937         /* these defines tweaked depending on font */
938 #define TIP_BORDER_X (16.0f / aspect)
939 #define TIP_BORDER_Y (6.0f / aspect)
940
941         h = BLF_height_max(data->fstyle.uifont_id);
942
943         for (i = 0, fontw = 0, fonth = 0; i < data->fields_len; i++) {
944                 uiTooltipField *field = &data->fields[i];
945                 uiTooltipField *field_next = (i + 1) != data->fields_len ? &data->fields[i + 1] : NULL;
946
947                 struct ResultBLF info;
948                 int w, x_pos = 0;
949                 int font_id;
950
951                 if (field->format.style == UI_TIP_STYLE_MONO) {
952                         BLF_size(blf_mono_font, data->fstyle.points * U.pixelsize, U.dpi);
953                         font_id = blf_mono_font;
954                 }
955                 else {
956                         BLI_assert(ELEM(field->format.style, UI_TIP_STYLE_NORMAL, UI_TIP_STYLE_HEADER));
957                         font_id = data->fstyle.uifont_id;
958                 }
959                 w = BLF_width_ex(font_id, field->text, BLF_DRAW_STR_DUMMY_MAX, &info);
960
961                 /* check for suffix (enum label) */
962                 if (field->text_suffix && field->text_suffix[0]) {
963                         x_pos = info.width;
964                         w = max_ii(w, x_pos + BLF_width(font_id, field->text_suffix, BLF_DRAW_STR_DUMMY_MAX));
965                 }
966                 fontw = max_ii(fontw, w);
967
968                 fonth += h * info.lines;
969                 if (field_next && field_next->format.is_pad) {
970                         fonth += h * (UI_TIP_PAD_FAC - 1);
971                 }
972
973                 field->geom.lines = info.lines;
974                 field->geom.x_pos = x_pos;
975         }
976
977         //fontw *= aspect;
978
979         BLF_disable(data->fstyle.uifont_id, font_flag);
980         BLF_disable(blf_mono_font, font_flag);
981
982         ar->regiondata = data;
983
984         data->toth = fonth;
985         data->lineh = h;
986
987         /* compute position */
988
989         rect_fl.xmin = init_position[0] - TIP_BORDER_X;
990         rect_fl.xmax = rect_fl.xmin + fontw + pad_px;
991         rect_fl.ymax = init_position[1] - TIP_BORDER_Y;
992         rect_fl.ymin = rect_fl.ymax - fonth  - TIP_BORDER_Y;
993
994         BLI_rcti_rctf_copy(&rect_i, &rect_fl);
995
996 #undef TIP_BORDER_X
997 #undef TIP_BORDER_Y
998
999         /* clip with window boundaries */
1000         if (rect_i.xmax > winx) {
1001                 /* super size */
1002                 if (rect_i.xmax > winx + rect_i.xmin) {
1003                         rect_i.xmax = winx;
1004                         rect_i.xmin = 0;
1005                 }
1006                 else {
1007                         rect_i.xmin -= rect_i.xmax - winx;
1008                         rect_i.xmax = winx;
1009                 }
1010         }
1011         /* ensure at least 5 px above screen bounds
1012          * 25 is just a guess to be above the menu item */
1013         if (rect_i.ymin < 5) {
1014                 rect_i.ymax += (-rect_i.ymin) + 30;
1015                 rect_i.ymin = 30;
1016         }
1017
1018         /* add padding */
1019         BLI_rcti_resize(&rect_i,
1020                         BLI_rcti_size_x(&rect_i) + pad_px,
1021                         BLI_rcti_size_y(&rect_i) + pad_px);
1022
1023         /* widget rect, in region coords */
1024         {
1025                 const int margin = UI_POPUP_MARGIN;
1026
1027                 data->bbox.xmin = margin;
1028                 data->bbox.xmax = BLI_rcti_size_x(&rect_i) - margin;
1029                 data->bbox.ymin = margin;
1030                 data->bbox.ymax = BLI_rcti_size_y(&rect_i);
1031
1032                 /* region bigger for shadow */
1033                 ar->winrct.xmin = rect_i.xmin - margin;
1034                 ar->winrct.xmax = rect_i.xmax + margin;
1035                 ar->winrct.ymin = rect_i.ymin - margin;
1036                 ar->winrct.ymax = rect_i.ymax + margin;
1037         }
1038
1039         /* adds subwindow */
1040         ED_region_init(ar);
1041
1042         /* notify change and redraw */
1043         ED_region_tag_redraw(ar);
1044
1045         return ar;
1046 }
1047
1048 /** \} */
1049
1050 /* -------------------------------------------------------------------- */
1051 /** \name ToolTip Public API
1052  * \{ */
1053
1054
1055 ARegion *UI_tooltip_create_from_button(bContext *C, ARegion *butregion, uiBut *but)
1056 {
1057         wmWindow *win = CTX_wm_window(C);
1058         /* aspect values that shrink text are likely unreadable */
1059         const float aspect = min_ff(1.0f, but->block->aspect);
1060         float init_position[2];
1061
1062         if (but->drawflag & UI_BUT_NO_TOOLTIP) {
1063                 return NULL;
1064         }
1065         uiTooltipData *data = NULL;
1066
1067         if (data == NULL) {
1068                 data = ui_tooltip_data_from_tool(C, but);
1069         }
1070
1071         if (data == NULL) {
1072                 data = ui_tooltip_data_from_button(C, but);
1073         }
1074
1075         if (data == NULL) {
1076                 return NULL;
1077         }
1078
1079         init_position[0] = BLI_rctf_cent_x(&but->rect);
1080         init_position[1] = but->rect.ymin;
1081
1082         if (butregion) {
1083                 ui_block_to_window_fl(butregion, but->block, &init_position[0], &init_position[1]);
1084                 init_position[0] = win->eventstate->x;
1085         }
1086
1087         return ui_tooltip_create_with_data(C, data, init_position, aspect);
1088 }
1089
1090 ARegion *UI_tooltip_create_from_gizmo(bContext *C, wmGizmo *gz)
1091 {
1092         wmWindow *win = CTX_wm_window(C);
1093         const float aspect = 1.0f;
1094         float init_position[2];
1095
1096         uiTooltipData *data = ui_tooltip_data_from_gizmo(C, gz);
1097         if (data == NULL) {
1098                 return NULL;
1099         }
1100
1101         init_position[0] = win->eventstate->x;
1102         init_position[1] = win->eventstate->y;
1103
1104         return ui_tooltip_create_with_data(C, data, init_position, aspect);
1105 }
1106
1107 void UI_tooltip_free(bContext *C, bScreen *sc, ARegion *ar)
1108 {
1109         ui_region_temp_remove(C, sc, ar);
1110 }
1111
1112 /** \} */