1 /*
2  * This program is free software; you can redistribute it and/or
3  * modify it under the terms of the GNU General Public License
4  * as published by the Free Software Foundation; either version 2
5  * of the License, or (at your option) any later version.
6  *
7  * This program is distributed in the hope that it will be useful,
8  * but WITHOUT ANY WARRANTY; without even the implied warranty of
9  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
10  * GNU General Public License for more details.
11  *
12  * You should have received a copy of the GNU General Public License
13  * along with this program; if not, write to the Free Software Foundation,
14  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
15  *
16  * The Original Code is Copyright (C) 2008 Blender Foundation.
17  * All rights reserved.
18  */
19 
20 /** \file
21  * \ingroup edinterface
22  *
23  * Search Box Region & Interaction
24  */
25 
26 #include <stdarg.h>
27 #include <stdlib.h>
28 #include <string.h>
29 
30 #include "DNA_ID.h"
31 #include "MEM_guardedalloc.h"
32 
33 #include "DNA_userdef_types.h"
34 
35 #include "BLI_math.h"
36 
37 #include "BLI_listbase.h"
38 #include "BLI_rect.h"
39 #include "BLI_string.h"
40 #include "BLI_utildefines.h"
41 
42 #include "BKE_context.h"
43 #include "BKE_screen.h"
44 
45 #include "WM_api.h"
46 #include "WM_types.h"
47 
48 #include "RNA_access.h"
49 
50 #include "UI_interface.h"
51 #include "UI_interface_icons.h"
52 #include "UI_view2d.h"
53 
54 #include "BLT_translation.h"
55 
56 #include "ED_screen.h"
57 
58 #include "GPU_state.h"
59 #include "interface_intern.h"
60 #include "interface_regions_intern.h"
61 
62 #define MENU_BORDER (int)(0.3f * U.widget_unit)
63 
64 /* -------------------------------------------------------------------- */
65 /** \name Search Box Creation
66  * \{ */
67 
68 struct uiSearchItems {
69   int maxitem, totitem, maxstrlen;
70 
71   int offset, offset_i; /* offset for inserting in array */
72   int more;             /* flag indicating there are more items */
73 
74   char **names;
75   void **pointers;
76   int *icons;
77   int *states;
78   uint8_t *name_prefix_offsets;
79 
80   /** Is there any item with an icon? */
81   bool has_icon;
82 
83   AutoComplete *autocpl;
84   void *active;
85 };
86 
87 typedef struct uiSearchboxData {
88   rcti bbox;
89   uiFontStyle fstyle;
90   uiSearchItems items;
91   /** index in items array */
92   int active;
93   /** when menu opened with enough space for this */
94   bool noback;
95   /** draw thumbnail previews, rather than list */
96   bool preview;
97   /** Use the #UI_SEP_CHAR char for splitting shortcuts (good for operators, bad for data). */
98   bool use_sep;
99   int prv_rows, prv_cols;
100   /**
101    * Show the active icon and text after the last instance of this string.
102    * Used so we can show leading text to menu items less prominently (not related to 'use_sep').
103    */
104   const char *sep_string;
105 } uiSearchboxData;
106 
107 #define SEARCH_ITEMS 10
108 
109 /**
110  * Public function exported for functions that use #UI_BTYPE_SEARCH_MENU.
111  *
112  * \param items: Stores the items.
113  * \param name: Text to display for the item.
114  * \param poin: Opaque pointer (for use by the caller).
115  * \param iconid: The icon, #ICON_NONE for no icon.
116  * \param state: The buttons state flag, compatible with #uiBut.flag,
117  * typically #UI_BUT_DISABLED / #UI_BUT_INACTIVE.
118  * \return false if there is nothing to add.
119  */
UI_search_item_add(uiSearchItems * items,const char * name,void * poin,int iconid,int state,const uint8_t name_prefix_offset)120 bool UI_search_item_add(uiSearchItems *items,
121                         const char *name,
122                         void *poin,
123                         int iconid,
124                         int state,
125                         const uint8_t name_prefix_offset)
126 {
127   /* hijack for autocomplete */
128   if (items->autocpl) {
129     UI_autocomplete_update_name(items->autocpl, name);
130     return true;
131   }
132 
133   if (iconid) {
134     items->has_icon = true;
135   }
136 
137   /* hijack for finding active item */
138   if (items->active) {
139     if (poin == items->active) {
140       items->offset_i = items->totitem;
141     }
142     items->totitem++;
143     return true;
144   }
145 
146   if (items->totitem >= items->maxitem) {
147     items->more = 1;
148     return false;
149   }
150 
151   /* skip first items in list */
152   if (items->offset_i > 0) {
153     items->offset_i--;
154     return true;
155   }
156 
157   if (items->names) {
158     BLI_strncpy(items->names[items->totitem], name, items->maxstrlen);
159   }
160   if (items->pointers) {
161     items->pointers[items->totitem] = poin;
162   }
163   if (items->icons) {
164     items->icons[items->totitem] = iconid;
165   }
166 
167   if (name_prefix_offset != 0) {
168     /* Lazy initialize, as this isn't used often. */
169     if (items->name_prefix_offsets == NULL) {
170       items->name_prefix_offsets = MEM_callocN(
171           items->maxitem * sizeof(*items->name_prefix_offsets), "search name prefix offsets");
172     }
173     items->name_prefix_offsets[items->totitem] = name_prefix_offset;
174   }
175 
176   /* Limit flags that can be set so flags such as 'UI_SELECT' aren't accidentally set
177    * which will cause problems, add others as needed. */
178   BLI_assert(
179       (state & ~(UI_BUT_DISABLED | UI_BUT_INACTIVE | UI_BUT_REDALERT | UI_BUT_HAS_SEP_CHAR)) == 0);
180   if (items->states) {
181     items->states[items->totitem] = state;
182   }
183 
184   items->totitem++;
185 
186   return true;
187 }
188 
UI_searchbox_size_y(void)189 int UI_searchbox_size_y(void)
190 {
191   return SEARCH_ITEMS * UI_UNIT_Y + 2 * UI_POPUP_MENU_TOP;
192 }
193 
UI_searchbox_size_x(void)194 int UI_searchbox_size_x(void)
195 {
196   return 12 * UI_UNIT_X;
197 }
198 
UI_search_items_find_index(uiSearchItems * items,const char * name)199 int UI_search_items_find_index(uiSearchItems *items, const char *name)
200 {
201   if (items->name_prefix_offsets != NULL) {
202     for (int i = 0; i < items->totitem; i++) {
203       if (STREQ(name, items->names[i] + items->name_prefix_offsets[i])) {
204         return i;
205       }
206     }
207   }
208   else {
209     for (int i = 0; i < items->totitem; i++) {
210       if (STREQ(name, items->names[i])) {
211         return i;
212       }
213     }
214   }
215   return -1;
216 }
217 
218 /* region is the search box itself */
ui_searchbox_select(bContext * C,ARegion * region,uiBut * but,int step)219 static void ui_searchbox_select(bContext *C, ARegion *region, uiBut *but, int step)
220 {
221   uiSearchboxData *data = region->regiondata;
222 
223   /* apply step */
224   data->active += step;
225 
226   if (data->items.totitem == 0) {
227     data->active = -1;
228   }
229   else if (data->active >= data->items.totitem) {
230     if (data->items.more) {
231       data->items.offset++;
232       data->active = data->items.totitem - 1;
233       ui_searchbox_update(C, region, but, false);
234     }
235     else {
236       data->active = data->items.totitem - 1;
237     }
238   }
239   else if (data->active < 0) {
240     if (data->items.offset) {
241       data->items.offset--;
242       data->active = 0;
243       ui_searchbox_update(C, region, but, false);
244     }
245     else {
246       /* only let users step into an 'unset' state for unlink buttons */
247       data->active = (but->flag & UI_BUT_VALUE_CLEAR) ? -1 : 0;
248     }
249   }
250 
251   ED_region_tag_redraw(region);
252 }
253 
ui_searchbox_butrect(rcti * r_rect,uiSearchboxData * data,int itemnr)254 static void ui_searchbox_butrect(rcti *r_rect, uiSearchboxData *data, int itemnr)
255 {
256   /* thumbnail preview */
257   if (data->preview) {
258     const int butw = (BLI_rcti_size_x(&data->bbox) - 2 * MENU_BORDER) / data->prv_cols;
259     const int buth = (BLI_rcti_size_y(&data->bbox) - 2 * MENU_BORDER) / data->prv_rows;
260     int row, col;
261 
262     *r_rect = data->bbox;
263 
264     col = itemnr % data->prv_cols;
265     row = itemnr / data->prv_cols;
266 
267     r_rect->xmin += MENU_BORDER + (col * butw);
268     r_rect->xmax = r_rect->xmin + butw;
269 
270     r_rect->ymax -= MENU_BORDER + (row * buth);
271     r_rect->ymin = r_rect->ymax - buth;
272   }
273   /* list view */
274   else {
275     const int buth = (BLI_rcti_size_y(&data->bbox) - 2 * UI_POPUP_MENU_TOP) / SEARCH_ITEMS;
276 
277     *r_rect = data->bbox;
278     r_rect->xmin = data->bbox.xmin + 3.0f;
279     r_rect->xmax = data->bbox.xmax - 3.0f;
280 
281     r_rect->ymax = data->bbox.ymax - UI_POPUP_MENU_TOP - itemnr * buth;
282     r_rect->ymin = r_rect->ymax - buth;
283   }
284 }
285 
ui_searchbox_find_index(ARegion * region,const char * name)286 int ui_searchbox_find_index(ARegion *region, const char *name)
287 {
288   uiSearchboxData *data = region->regiondata;
289   return UI_search_items_find_index(&data->items, name);
290 }
291 
292 /* x and y in screencoords */
ui_searchbox_inside(ARegion * region,int x,int y)293 bool ui_searchbox_inside(ARegion *region, int x, int y)
294 {
295   uiSearchboxData *data = region->regiondata;
296 
297   return BLI_rcti_isect_pt(&data->bbox, x - region->winrct.xmin, y - region->winrct.ymin);
298 }
299 
300 /* string validated to be of correct length (but->hardmax) */
ui_searchbox_apply(uiBut * but,ARegion * region)301 bool ui_searchbox_apply(uiBut *but, ARegion *region)
302 {
303   uiSearchboxData *data = region->regiondata;
304   uiButSearch *search_but = (uiButSearch *)but;
305 
306   BLI_assert(but->type == UI_BTYPE_SEARCH_MENU);
307 
308   search_but->item_active = NULL;
309 
310   if (data->active != -1) {
311     const char *name = data->items.names[data->active] +
312                        /* Never include the prefix in the button. */
313                        (data->items.name_prefix_offsets ?
314                             data->items.name_prefix_offsets[data->active] :
315                             0);
316 
317     const char *name_sep = data->use_sep ? strrchr(name, UI_SEP_CHAR) : NULL;
318 
319     BLI_strncpy(but->editstr, name, name_sep ? (name_sep - name) + 1 : data->items.maxstrlen);
320 
321     search_but->item_active = data->items.pointers[data->active];
322 
323     return true;
324   }
325   if (but->flag & UI_BUT_VALUE_CLEAR) {
326     /* It is valid for _VALUE_CLEAR flavor to have no active element
327      * (it's a valid way to unlink). */
328     but->editstr[0] = '\0';
329 
330     return true;
331   }
332   return false;
333 }
334 
wm_searchbox_tooltip_init(struct bContext * C,struct ARegion * region,int * UNUSED (r_pass),double * UNUSED (pass_delay),bool * r_exit_on_event)335 static struct ARegion *wm_searchbox_tooltip_init(struct bContext *C,
336                                                  struct ARegion *region,
337                                                  int *UNUSED(r_pass),
338                                                  double *UNUSED(pass_delay),
339                                                  bool *r_exit_on_event)
340 {
341   *r_exit_on_event = true;
342 
343   LISTBASE_FOREACH (uiBlock *, block, &region->uiblocks) {
344     LISTBASE_FOREACH (uiBut *, but, &block->buttons) {
345       if (but->type != UI_BTYPE_SEARCH_MENU) {
346         continue;
347       }
348 
349       uiButSearch *search_but = (uiButSearch *)but;
350       if (search_but->item_tooltip_fn) {
351         return search_but->item_tooltip_fn(C, region, search_but->arg, search_but->item_active);
352       }
353     }
354   }
355   return NULL;
356 }
357 
ui_searchbox_event(bContext * C,ARegion * region,uiBut * but,ARegion * butregion,const wmEvent * event)358 bool ui_searchbox_event(
359     bContext *C, ARegion *region, uiBut *but, ARegion *butregion, const wmEvent *event)
360 {
361   uiSearchboxData *data = region->regiondata;
362   uiButSearch *search_but = (uiButSearch *)but;
363   int type = event->type, val = event->val;
364   bool handled = false;
365   bool tooltip_timer_started = false;
366 
367   BLI_assert(but->type == UI_BTYPE_SEARCH_MENU);
368 
369   if (type == MOUSEPAN) {
370     ui_pan_to_scroll(event, &type, &val);
371   }
372 
373   switch (type) {
374     case WHEELUPMOUSE:
375     case EVT_UPARROWKEY:
376       ui_searchbox_select(C, region, but, -1);
377       handled = true;
378       break;
379     case WHEELDOWNMOUSE:
380     case EVT_DOWNARROWKEY:
381       ui_searchbox_select(C, region, but, 1);
382       handled = true;
383       break;
384     case RIGHTMOUSE:
385       if (val) {
386         if (search_but->item_context_menu_fn) {
387           if (data->active != -1) {
388             /* Check the cursor is over the active element
389              * (a little confusing if this isn't the case, although it does work). */
390             rcti rect;
391             ui_searchbox_butrect(&rect, data, data->active);
392             if (BLI_rcti_isect_pt(
393                     &rect, event->x - region->winrct.xmin, event->y - region->winrct.ymin)) {
394 
395               void *active = data->items.pointers[data->active];
396               if (search_but->item_context_menu_fn(C, search_but->arg, active, event)) {
397                 handled = true;
398               }
399             }
400           }
401         }
402       }
403       break;
404     case MOUSEMOVE: {
405       bool is_inside = false;
406 
407       if (BLI_rcti_isect_pt(&region->winrct, event->x, event->y)) {
408         rcti rect;
409         int a;
410 
411         for (a = 0; a < data->items.totitem; a++) {
412           ui_searchbox_butrect(&rect, data, a);
413           if (BLI_rcti_isect_pt(
414                   &rect, event->x - region->winrct.xmin, event->y - region->winrct.ymin)) {
415             is_inside = true;
416             if (data->active != a) {
417               data->active = a;
418               ui_searchbox_select(C, region, but, 0);
419               handled = true;
420               break;
421             }
422           }
423         }
424       }
425 
426       if (U.flag & USER_TOOLTIPS) {
427         if (is_inside) {
428           if (data->active != -1) {
429             ScrArea *area = CTX_wm_area(C);
430             search_but->item_active = data->items.pointers[data->active];
431             WM_tooltip_timer_init(C, CTX_wm_window(C), area, butregion, wm_searchbox_tooltip_init);
432             tooltip_timer_started = true;
433           }
434         }
435       }
436 
437       break;
438     }
439   }
440 
441   if (handled && (tooltip_timer_started == false)) {
442     wmWindow *win = CTX_wm_window(C);
443     WM_tooltip_clear(C, win);
444   }
445 
446   return handled;
447 }
448 
449 /** Wrap #uiButSearchUpdateFn callback. */
ui_searchbox_update_fn(bContext * C,uiButSearch * search_but,const char * str,uiSearchItems * items)450 static void ui_searchbox_update_fn(bContext *C,
451                                    uiButSearch *search_but,
452                                    const char *str,
453                                    uiSearchItems *items)
454 {
455   wmWindow *win = CTX_wm_window(C);
456   WM_tooltip_clear(C, win);
457   search_but->items_update_fn(C, search_but->arg, str, items);
458 }
459 
460 /* region is the search box itself */
ui_searchbox_update(bContext * C,ARegion * region,uiBut * but,const bool reset)461 void ui_searchbox_update(bContext *C, ARegion *region, uiBut *but, const bool reset)
462 {
463   uiButSearch *search_but = (uiButSearch *)but;
464   uiSearchboxData *data = region->regiondata;
465 
466   BLI_assert(but->type == UI_BTYPE_SEARCH_MENU);
467 
468   /* reset vars */
469   data->items.totitem = 0;
470   data->items.more = 0;
471   if (reset == false) {
472     data->items.offset_i = data->items.offset;
473   }
474   else {
475     data->items.offset_i = data->items.offset = 0;
476     data->active = -1;
477 
478     /* handle active */
479     if (search_but->items_update_fn && search_but->item_active) {
480       data->items.active = search_but->item_active;
481       ui_searchbox_update_fn(C, search_but, but->editstr, &data->items);
482       data->items.active = NULL;
483 
484       /* found active item, calculate real offset by centering it */
485       if (data->items.totitem) {
486         /* first case, begin of list */
487         if (data->items.offset_i < data->items.maxitem) {
488           data->active = data->items.offset_i;
489           data->items.offset_i = 0;
490         }
491         else {
492           /* second case, end of list */
493           if (data->items.totitem - data->items.offset_i <= data->items.maxitem) {
494             data->active = data->items.offset_i - data->items.totitem + data->items.maxitem;
495             data->items.offset_i = data->items.totitem - data->items.maxitem;
496           }
497           else {
498             /* center active item */
499             data->items.offset_i -= data->items.maxitem / 2;
500             data->active = data->items.maxitem / 2;
501           }
502         }
503       }
504       data->items.offset = data->items.offset_i;
505       data->items.totitem = 0;
506     }
507   }
508 
509   /* callback */
510   if (search_but->items_update_fn) {
511     ui_searchbox_update_fn(C, search_but, but->editstr, &data->items);
512   }
513 
514   /* handle case where editstr is equal to one of items */
515   if (reset && data->active == -1) {
516     int a;
517 
518     for (a = 0; a < data->items.totitem; a++) {
519       const char *name = data->items.names[a] +
520                          /* Never include the prefix in the button. */
521                          (data->items.name_prefix_offsets ? data->items.name_prefix_offsets[a] :
522                                                             0);
523       const char *name_sep = data->use_sep ? strrchr(name, UI_SEP_CHAR) : NULL;
524       if (STREQLEN(but->editstr, name, name_sep ? (name_sep - name) : data->items.maxstrlen)) {
525         data->active = a;
526         break;
527       }
528     }
529     if (data->items.totitem == 1 && but->editstr[0]) {
530       data->active = 0;
531     }
532   }
533 
534   /* validate selected item */
535   ui_searchbox_select(C, region, but, 0);
536 
537   ED_region_tag_redraw(region);
538 }
539 
ui_searchbox_autocomplete(bContext * C,ARegion * region,uiBut * but,char * str)540 int ui_searchbox_autocomplete(bContext *C, ARegion *region, uiBut *but, char *str)
541 {
542   uiButSearch *search_but = (uiButSearch *)but;
543   uiSearchboxData *data = region->regiondata;
544   int match = AUTOCOMPLETE_NO_MATCH;
545 
546   BLI_assert(but->type == UI_BTYPE_SEARCH_MENU);
547 
548   if (str[0]) {
549     data->items.autocpl = UI_autocomplete_begin(str, ui_but_string_get_max_length(but));
550 
551     ui_searchbox_update_fn(C, search_but, but->editstr, &data->items);
552 
553     match = UI_autocomplete_end(data->items.autocpl, str);
554     data->items.autocpl = NULL;
555   }
556 
557   return match;
558 }
559 
ui_searchbox_region_draw_cb(const bContext * C,ARegion * region)560 static void ui_searchbox_region_draw_cb(const bContext *C, ARegion *region)
561 {
562   uiSearchboxData *data = region->regiondata;
563 
564   /* pixel space */
565   wmOrtho2_region_pixelspace(region);
566 
567   if (data->noback == false) {
568     ui_draw_widget_menu_back(&data->bbox, true);
569   }
570 
571   /* draw text */
572   if (data->items.totitem) {
573     rcti rect;
574     int a;
575 
576     if (data->preview) {
577       /* draw items */
578       for (a = 0; a < data->items.totitem; a++) {
579         const int state = ((a == data->active) ? UI_ACTIVE : 0) | data->items.states[a];
580 
581         /* ensure icon is up-to-date */
582         ui_icon_ensure_deferred(C, data->items.icons[a], data->preview);
583 
584         ui_searchbox_butrect(&rect, data, a);
585 
586         /* widget itself */
587         ui_draw_preview_item(
588             &data->fstyle, &rect, data->items.names[a], data->items.icons[a], state);
589       }
590 
591       /* indicate more */
592       if (data->items.more) {
593         ui_searchbox_butrect(&rect, data, data->items.maxitem - 1);
594         GPU_blend(GPU_BLEND_ALPHA);
595         UI_icon_draw(rect.xmax - 18, rect.ymin - 7, ICON_TRIA_DOWN);
596         GPU_blend(GPU_BLEND_NONE);
597       }
598       if (data->items.offset) {
599         ui_searchbox_butrect(&rect, data, 0);
600         GPU_blend(GPU_BLEND_ALPHA);
601         UI_icon_draw(rect.xmin, rect.ymax - 9, ICON_TRIA_UP);
602         GPU_blend(GPU_BLEND_NONE);
603       }
604     }
605     else {
606       const int search_sep_len = data->sep_string ? strlen(data->sep_string) : 0;
607       /* draw items */
608       for (a = 0; a < data->items.totitem; a++) {
609         const int state = ((a == data->active) ? UI_ACTIVE : 0) | data->items.states[a];
610         char *name = data->items.names[a];
611         int icon = data->items.icons[a];
612         char *name_sep_test = NULL;
613         const bool use_sep_char = data->use_sep || (state & UI_BUT_HAS_SEP_CHAR);
614 
615         ui_searchbox_butrect(&rect, data, a);
616 
617         /* widget itself */
618         if ((search_sep_len == 0) ||
619             !(name_sep_test = strstr(data->items.names[a], data->sep_string))) {
620           if (!icon && data->items.has_icon) {
621             /* If there is any icon item, make sure all items line up. */
622             icon = ICON_BLANK1;
623           }
624 
625           /* Simple menu item. */
626           ui_draw_menu_item(&data->fstyle, &rect, name, icon, state, use_sep_char, NULL);
627         }
628         else {
629           /* Split menu item, faded text before the separator. */
630           char *name_sep = NULL;
631           do {
632             name_sep = name_sep_test;
633             name_sep_test = strstr(name_sep + search_sep_len, data->sep_string);
634           } while (name_sep_test != NULL);
635 
636           name_sep += search_sep_len;
637           const char name_sep_prev = *name_sep;
638           *name_sep = '\0';
639           int name_width = 0;
640           ui_draw_menu_item(
641               &data->fstyle, &rect, name, 0, state | UI_BUT_INACTIVE, false, &name_width);
642           *name_sep = name_sep_prev;
643           rect.xmin += name_width;
644           rect.xmin += UI_UNIT_X / 4;
645 
646           if (icon == ICON_BLANK1) {
647             icon = ICON_NONE;
648             rect.xmin -= UI_DPI_ICON_SIZE / 4;
649           }
650 
651           /* The previous menu item draws the active selection. */
652           ui_draw_menu_item(
653               &data->fstyle, &rect, name_sep, icon, state & ~UI_ACTIVE, use_sep_char, NULL);
654         }
655       }
656       /* indicate more */
657       if (data->items.more) {
658         ui_searchbox_butrect(&rect, data, data->items.maxitem - 1);
659         GPU_blend(GPU_BLEND_ALPHA);
660         UI_icon_draw((BLI_rcti_size_x(&rect)) / 2, rect.ymin - 9, ICON_TRIA_DOWN);
661         GPU_blend(GPU_BLEND_NONE);
662       }
663       if (data->items.offset) {
664         ui_searchbox_butrect(&rect, data, 0);
665         GPU_blend(GPU_BLEND_ALPHA);
666         UI_icon_draw((BLI_rcti_size_x(&rect)) / 2, rect.ymax - 7, ICON_TRIA_UP);
667         GPU_blend(GPU_BLEND_NONE);
668       }
669     }
670   }
671 }
672 
ui_searchbox_region_free_cb(ARegion * region)673 static void ui_searchbox_region_free_cb(ARegion *region)
674 {
675   uiSearchboxData *data = region->regiondata;
676   int a;
677 
678   /* free search data */
679   for (a = 0; a < data->items.maxitem; a++) {
680     MEM_freeN(data->items.names[a]);
681   }
682   MEM_freeN(data->items.names);
683   MEM_freeN(data->items.pointers);
684   MEM_freeN(data->items.icons);
685   MEM_freeN(data->items.states);
686 
687   if (data->items.name_prefix_offsets != NULL) {
688     MEM_freeN(data->items.name_prefix_offsets);
689   }
690 
691   MEM_freeN(data);
692   region->regiondata = NULL;
693 }
694 
ui_searchbox_create_generic(bContext * C,ARegion * butregion,uiButSearch * search_but)695 ARegion *ui_searchbox_create_generic(bContext *C, ARegion *butregion, uiButSearch *search_but)
696 {
697   wmWindow *win = CTX_wm_window(C);
698   const uiStyle *style = UI_style_get();
699   uiBut *but = &search_but->but;
700   static ARegionType type;
701   ARegion *region;
702   uiSearchboxData *data;
703   const float aspect = but->block->aspect;
704   rctf rect_fl;
705   rcti rect_i;
706   const int margin = UI_POPUP_MARGIN;
707   int winx /*, winy */, ofsx, ofsy;
708   int i;
709 
710   /* create area region */
711   region = ui_region_temp_add(CTX_wm_screen(C));
712 
713   memset(&type, 0, sizeof(ARegionType));
714   type.draw = ui_searchbox_region_draw_cb;
715   type.free = ui_searchbox_region_free_cb;
716   type.regionid = RGN_TYPE_TEMPORARY;
717   region->type = &type;
718 
719   /* create searchbox data */
720   data = MEM_callocN(sizeof(uiSearchboxData), "uiSearchboxData");
721 
722   /* set font, get bb */
723   data->fstyle = style->widget; /* copy struct */
724   ui_fontscale(&data->fstyle.points, aspect);
725   UI_fontstyle_set(&data->fstyle);
726 
727   region->regiondata = data;
728 
729   /* special case, hardcoded feature, not draw backdrop when called from menus,
730    * assume for design that popup already added it */
731   if (but->block->flag & UI_BLOCK_SEARCH_MENU) {
732     data->noback = true;
733   }
734 
735   if (but->a1 > 0 && but->a2 > 0) {
736     data->preview = true;
737     data->prv_rows = but->a1;
738     data->prv_cols = but->a2;
739   }
740 
741   /* Only show key shortcuts when needed (checking RNA prop pointer is useless here, a lot of
742    * buttons are about data without having that pointer defined, let's rather try with optype!).
743    * One can also enforce that behavior by setting
744    * UI_BUT_HAS_SHORTCUT drawflag of search button. */
745   if (but->optype != NULL || (but->drawflag & UI_BUT_HAS_SHORTCUT) != 0) {
746     data->use_sep = true;
747   }
748   data->sep_string = search_but->item_sep_string;
749 
750   /* compute position */
751   if (but->block->flag & UI_BLOCK_SEARCH_MENU) {
752     const int search_but_h = BLI_rctf_size_y(&but->rect) + 10;
753     /* this case is search menu inside other menu */
754     /* we copy region size */
755 
756     region->winrct = butregion->winrct;
757 
758     /* widget rect, in region coords */
759     data->bbox.xmin = margin;
760     data->bbox.xmax = BLI_rcti_size_x(&region->winrct) - margin;
761     data->bbox.ymin = margin;
762     data->bbox.ymax = BLI_rcti_size_y(&region->winrct) - margin;
763 
764     /* check if button is lower half */
765     if (but->rect.ymax < BLI_rctf_cent_y(&but->block->rect)) {
766       data->bbox.ymin += search_but_h;
767     }
768     else {
769       data->bbox.ymax -= search_but_h;
770     }
771   }
772   else {
773     const int searchbox_width = UI_searchbox_size_x();
774 
775     rect_fl.xmin = but->rect.xmin - 5; /* align text with button */
776     rect_fl.xmax = but->rect.xmax + 5; /* symmetrical */
777     rect_fl.ymax = but->rect.ymin;
778     rect_fl.ymin = rect_fl.ymax - UI_searchbox_size_y();
779 
780     ofsx = (but->block->panel) ? but->block->panel->ofsx : 0;
781     ofsy = (but->block->panel) ? but->block->panel->ofsy : 0;
782 
783     BLI_rctf_translate(&rect_fl, ofsx, ofsy);
784 
785     /* minimal width */
786     if (BLI_rctf_size_x(&rect_fl) < searchbox_width) {
787       rect_fl.xmax = rect_fl.xmin + searchbox_width;
788     }
789 
790     /* copy to int, gets projected if possible too */
791     BLI_rcti_rctf_copy(&rect_i, &rect_fl);
792 
793     if (butregion->v2d.cur.xmin != butregion->v2d.cur.xmax) {
794       UI_view2d_view_to_region_rcti(&butregion->v2d, &rect_fl, &rect_i);
795     }
796 
797     BLI_rcti_translate(&rect_i, butregion->winrct.xmin, butregion->winrct.ymin);
798 
799     winx = WM_window_pixels_x(win);
800     // winy = WM_window_pixels_y(win);  /* UNUSED */
801     // wm_window_get_size(win, &winx, &winy);
802 
803     if (rect_i.xmax > winx) {
804       /* super size */
805       if (rect_i.xmax > winx + rect_i.xmin) {
806         rect_i.xmax = winx;
807         rect_i.xmin = 0;
808       }
809       else {
810         rect_i.xmin -= rect_i.xmax - winx;
811         rect_i.xmax = winx;
812       }
813     }
814 
815     if (rect_i.ymin < 0) {
816       int newy1 = but->rect.ymax + ofsy;
817 
818       if (butregion->v2d.cur.xmin != butregion->v2d.cur.xmax) {
819         newy1 = UI_view2d_view_to_region_y(&butregion->v2d, newy1);
820       }
821 
822       newy1 += butregion->winrct.ymin;
823 
824       rect_i.ymax = BLI_rcti_size_y(&rect_i) + newy1;
825       rect_i.ymin = newy1;
826     }
827 
828     /* widget rect, in region coords */
829     data->bbox.xmin = margin;
830     data->bbox.xmax = BLI_rcti_size_x(&rect_i) + margin;
831     data->bbox.ymin = margin;
832     data->bbox.ymax = BLI_rcti_size_y(&rect_i) + margin;
833 
834     /* region bigger for shadow */
835     region->winrct.xmin = rect_i.xmin - margin;
836     region->winrct.xmax = rect_i.xmax + margin;
837     region->winrct.ymin = rect_i.ymin - margin;
838     region->winrct.ymax = rect_i.ymax;
839   }
840 
841   /* adds subwindow */
842   ED_region_floating_init(region);
843 
844   /* notify change and redraw */
845   ED_region_tag_redraw(region);
846 
847   /* prepare search data */
848   if (data->preview) {
849     data->items.maxitem = data->prv_rows * data->prv_cols;
850   }
851   else {
852     data->items.maxitem = SEARCH_ITEMS;
853   }
854   data->items.maxstrlen = but->hardmax;
855   data->items.totitem = 0;
856   data->items.names = MEM_callocN(data->items.maxitem * sizeof(void *), "search names");
857   data->items.pointers = MEM_callocN(data->items.maxitem * sizeof(void *), "search pointers");
858   data->items.icons = MEM_callocN(data->items.maxitem * sizeof(int), "search icons");
859   data->items.states = MEM_callocN(data->items.maxitem * sizeof(int), "search flags");
860   data->items.name_prefix_offsets = NULL; /* Lazy initialized as needed. */
861   for (i = 0; i < data->items.maxitem; i++) {
862     data->items.names[i] = MEM_callocN(but->hardmax + 1, "search pointers");
863   }
864 
865   return region;
866 }
867 
868 /**
869  * Similar to Python's `str.title` except...
870  *
871  * - we know words are upper case and ascii only.
872  * - '_' are replaces by spaces.
873  */
str_tolower_titlecaps_ascii(char * str,const size_t len)874 static void str_tolower_titlecaps_ascii(char *str, const size_t len)
875 {
876   size_t i;
877   bool prev_delim = true;
878 
879   for (i = 0; (i < len) && str[i]; i++) {
880     if (str[i] >= 'A' && str[i] <= 'Z') {
881       if (prev_delim == false) {
882         str[i] += 'a' - 'A';
883       }
884     }
885     else if (str[i] == '_') {
886       str[i] = ' ';
887     }
888 
889     prev_delim = ELEM(str[i], ' ') || (str[i] >= '0' && str[i] <= '9');
890   }
891 }
892 
ui_searchbox_region_draw_cb__operator(const bContext * UNUSED (C),ARegion * region)893 static void ui_searchbox_region_draw_cb__operator(const bContext *UNUSED(C), ARegion *region)
894 {
895   uiSearchboxData *data = region->regiondata;
896 
897   /* pixel space */
898   wmOrtho2_region_pixelspace(region);
899 
900   if (data->noback == false) {
901     ui_draw_widget_menu_back(&data->bbox, true);
902   }
903 
904   /* draw text */
905   if (data->items.totitem) {
906     rcti rect;
907     int a;
908 
909     /* draw items */
910     for (a = 0; a < data->items.totitem; a++) {
911       rcti rect_pre, rect_post;
912       ui_searchbox_butrect(&rect, data, a);
913 
914       rect_pre = rect;
915       rect_post = rect;
916 
917       rect_pre.xmax = rect_post.xmin = rect.xmin + ((rect.xmax - rect.xmin) / 4);
918 
919       /* widget itself */
920       /* NOTE: i18n messages extracting tool does the same, please keep it in sync. */
921       {
922         const int state = ((a == data->active) ? UI_ACTIVE : 0) | data->items.states[a];
923 
924         wmOperatorType *ot = data->items.pointers[a];
925         char text_pre[128];
926         char *text_pre_p = strstr(ot->idname, "_OT_");
927         if (text_pre_p == NULL) {
928           text_pre[0] = '\0';
929         }
930         else {
931           int text_pre_len;
932           text_pre_p += 1;
933           text_pre_len = BLI_strncpy_rlen(
934               text_pre, ot->idname, min_ii(sizeof(text_pre), text_pre_p - ot->idname));
935           text_pre[text_pre_len] = ':';
936           text_pre[text_pre_len + 1] = '\0';
937           str_tolower_titlecaps_ascii(text_pre, sizeof(text_pre));
938         }
939 
940         rect_pre.xmax += 4; /* sneaky, avoid showing ugly margin */
941         ui_draw_menu_item(&data->fstyle,
942                           &rect_pre,
943                           CTX_IFACE_(BLT_I18NCONTEXT_OPERATOR_DEFAULT, text_pre),
944                           data->items.icons[a],
945                           state,
946                           false,
947                           NULL);
948         ui_draw_menu_item(
949             &data->fstyle, &rect_post, data->items.names[a], 0, state, data->use_sep, NULL);
950       }
951     }
952     /* indicate more */
953     if (data->items.more) {
954       ui_searchbox_butrect(&rect, data, data->items.maxitem - 1);
955       GPU_blend(GPU_BLEND_ALPHA);
956       UI_icon_draw((BLI_rcti_size_x(&rect)) / 2, rect.ymin - 9, ICON_TRIA_DOWN);
957       GPU_blend(GPU_BLEND_NONE);
958     }
959     if (data->items.offset) {
960       ui_searchbox_butrect(&rect, data, 0);
961       GPU_blend(GPU_BLEND_ALPHA);
962       UI_icon_draw((BLI_rcti_size_x(&rect)) / 2, rect.ymax - 7, ICON_TRIA_UP);
963       GPU_blend(GPU_BLEND_NONE);
964     }
965   }
966 }
967 
ui_searchbox_create_operator(bContext * C,ARegion * butregion,uiButSearch * search_but)968 ARegion *ui_searchbox_create_operator(bContext *C, ARegion *butregion, uiButSearch *search_but)
969 {
970   ARegion *region;
971 
972   UI_but_drawflag_enable(&search_but->but, UI_BUT_HAS_SHORTCUT);
973   region = ui_searchbox_create_generic(C, butregion, search_but);
974 
975   region->type->draw = ui_searchbox_region_draw_cb__operator;
976 
977   return region;
978 }
979 
ui_searchbox_free(bContext * C,ARegion * region)980 void ui_searchbox_free(bContext *C, ARegion *region)
981 {
982   ui_region_temp_remove(C, CTX_wm_screen(C), region);
983 }
984 
ui_searchbox_region_draw_cb__menu(const bContext * UNUSED (C),ARegion * UNUSED (region))985 static void ui_searchbox_region_draw_cb__menu(const bContext *UNUSED(C), ARegion *UNUSED(region))
986 {
987   /* Currently unused. */
988 }
989 
ui_searchbox_create_menu(bContext * C,ARegion * butregion,uiButSearch * search_but)990 ARegion *ui_searchbox_create_menu(bContext *C, ARegion *butregion, uiButSearch *search_but)
991 {
992   ARegion *region;
993 
994   UI_but_drawflag_enable(&search_but->but, UI_BUT_HAS_SHORTCUT);
995   region = ui_searchbox_create_generic(C, butregion, search_but);
996 
997   if (false) {
998     region->type->draw = ui_searchbox_region_draw_cb__menu;
999   }
1000 
1001   return region;
1002 }
1003 
1004 /* sets red alert if button holds a string it can't find */
1005 /* XXX weak: search_func adds all partial matches... */
ui_but_search_refresh(uiButSearch * search_but)1006 void ui_but_search_refresh(uiButSearch *search_but)
1007 {
1008   uiBut *but = &search_but->but;
1009   uiSearchItems *items;
1010   int x1;
1011 
1012   /* possibly very large lists (such as ID datablocks) only
1013    * only validate string RNA buts (not pointers) */
1014   if (but->rnaprop && RNA_property_type(but->rnaprop) != PROP_STRING) {
1015     return;
1016   }
1017 
1018   items = MEM_callocN(sizeof(uiSearchItems), "search items");
1019 
1020   /* setup search struct */
1021   items->maxitem = 10;
1022   items->maxstrlen = 256;
1023   items->names = MEM_callocN(items->maxitem * sizeof(void *), "search names");
1024   for (x1 = 0; x1 < items->maxitem; x1++) {
1025     items->names[x1] = MEM_callocN(but->hardmax + 1, "search names");
1026   }
1027 
1028   ui_searchbox_update_fn(but->block->evil_C, search_but, but->drawstr, items);
1029 
1030   /* only redalert when we are sure of it, this can miss cases when >10 matches */
1031   if (items->totitem == 0) {
1032     UI_but_flag_enable(but, UI_BUT_REDALERT);
1033   }
1034   else if (items->more == 0) {
1035     if (UI_search_items_find_index(items, but->drawstr) == -1) {
1036       UI_but_flag_enable(but, UI_BUT_REDALERT);
1037     }
1038   }
1039 
1040   for (x1 = 0; x1 < items->maxitem; x1++) {
1041     MEM_freeN(items->names[x1]);
1042   }
1043   MEM_freeN(items->names);
1044   MEM_freeN(items);
1045 }
1046 
1047 /** \} */
1048