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, ®ion->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(®ion->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(®ion->winrct) - margin;
761 data->bbox.ymin = margin;
762 data->bbox.ymax = BLI_rcti_size_y(®ion->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