Fix T69931: Materials with keyframes duplicated by 'make single user' are linked.
[blender.git] / source / blender / editors / interface / interface_utils.c
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) 2009 Blender Foundation.
17  * All rights reserved.
18  */
19
20 /** \file
21  * \ingroup edinterface
22  */
23
24 #include <stdio.h>
25 #include <stdlib.h>
26 #include <string.h>
27 #include <assert.h>
28
29 #include "DNA_object_types.h"
30 #include "DNA_screen_types.h"
31
32 #include "BLI_utildefines.h"
33 #include "BLI_math.h"
34 #include "BLI_string.h"
35 #include "BLI_listbase.h"
36
37 #include "BLT_translation.h"
38
39 #include "BKE_report.h"
40
41 #include "MEM_guardedalloc.h"
42
43 #include "RNA_access.h"
44
45 #include "UI_interface.h"
46 #include "UI_resources.h"
47
48 #include "WM_api.h"
49 #include "WM_types.h"
50
51 #include "interface_intern.h"
52
53 /*************************** RNA Utilities ******************************/
54
55 uiBut *uiDefAutoButR(uiBlock *block,
56                      PointerRNA *ptr,
57                      PropertyRNA *prop,
58                      int index,
59                      const char *name,
60                      int icon,
61                      int x1,
62                      int y1,
63                      int x2,
64                      int y2)
65 {
66   uiBut *but = NULL;
67
68   switch (RNA_property_type(prop)) {
69     case PROP_BOOLEAN: {
70       if (RNA_property_array_check(prop) && index == -1) {
71         return NULL;
72       }
73
74       if (icon && name && name[0] == '\0') {
75         but = uiDefIconButR_prop(block,
76                                  UI_BTYPE_ICON_TOGGLE,
77                                  0,
78                                  icon,
79                                  x1,
80                                  y1,
81                                  x2,
82                                  y2,
83                                  ptr,
84                                  prop,
85                                  index,
86                                  0,
87                                  0,
88                                  -1,
89                                  -1,
90                                  NULL);
91       }
92       else if (icon) {
93         but = uiDefIconTextButR_prop(block,
94                                      UI_BTYPE_ICON_TOGGLE,
95                                      0,
96                                      icon,
97                                      name,
98                                      x1,
99                                      y1,
100                                      x2,
101                                      y2,
102                                      ptr,
103                                      prop,
104                                      index,
105                                      0,
106                                      0,
107                                      -1,
108                                      -1,
109                                      NULL);
110       }
111       else {
112         but = uiDefButR_prop(block,
113                              UI_BTYPE_CHECKBOX,
114                              0,
115                              name,
116                              x1,
117                              y1,
118                              x2,
119                              y2,
120                              ptr,
121                              prop,
122                              index,
123                              0,
124                              0,
125                              -1,
126                              -1,
127                              NULL);
128       }
129       break;
130     }
131     case PROP_INT:
132     case PROP_FLOAT: {
133       if (RNA_property_array_check(prop) && index == -1) {
134         if (ELEM(RNA_property_subtype(prop), PROP_COLOR, PROP_COLOR_GAMMA)) {
135           but = uiDefButR_prop(
136               block, UI_BTYPE_COLOR, 0, name, x1, y1, x2, y2, ptr, prop, -1, 0, 0, -1, -1, NULL);
137         }
138         else {
139           return NULL;
140         }
141       }
142       else if (RNA_property_subtype(prop) == PROP_PERCENTAGE ||
143                RNA_property_subtype(prop) == PROP_FACTOR) {
144         but = uiDefButR_prop(block,
145                              UI_BTYPE_NUM_SLIDER,
146                              0,
147                              name,
148                              x1,
149                              y1,
150                              x2,
151                              y2,
152                              ptr,
153                              prop,
154                              index,
155                              0,
156                              0,
157                              -1,
158                              -1,
159                              NULL);
160       }
161       else {
162         but = uiDefButR_prop(
163             block, UI_BTYPE_NUM, 0, name, x1, y1, x2, y2, ptr, prop, index, 0, 0, -1, -1, NULL);
164       }
165
166       if (RNA_property_flag(prop) & PROP_TEXTEDIT_UPDATE) {
167         UI_but_flag_enable(but, UI_BUT_TEXTEDIT_UPDATE);
168       }
169       break;
170     }
171     case PROP_ENUM:
172       if (icon && name && name[0] == '\0') {
173         but = uiDefIconButR_prop(
174             block, UI_BTYPE_MENU, 0, icon, x1, y1, x2, y2, ptr, prop, index, 0, 0, -1, -1, NULL);
175       }
176       else if (icon) {
177         but = uiDefIconTextButR_prop(block,
178                                      UI_BTYPE_MENU,
179                                      0,
180                                      icon,
181                                      NULL,
182                                      x1,
183                                      y1,
184                                      x2,
185                                      y2,
186                                      ptr,
187                                      prop,
188                                      index,
189                                      0,
190                                      0,
191                                      -1,
192                                      -1,
193                                      NULL);
194       }
195       else {
196         but = uiDefButR_prop(
197             block, UI_BTYPE_MENU, 0, name, x1, y1, x2, y2, ptr, prop, index, 0, 0, -1, -1, NULL);
198       }
199       break;
200     case PROP_STRING:
201       if (icon && name && name[0] == '\0') {
202         but = uiDefIconButR_prop(
203             block, UI_BTYPE_TEXT, 0, icon, x1, y1, x2, y2, ptr, prop, index, 0, 0, -1, -1, NULL);
204       }
205       else if (icon) {
206         but = uiDefIconTextButR_prop(block,
207                                      UI_BTYPE_TEXT,
208                                      0,
209                                      icon,
210                                      name,
211                                      x1,
212                                      y1,
213                                      x2,
214                                      y2,
215                                      ptr,
216                                      prop,
217                                      index,
218                                      0,
219                                      0,
220                                      -1,
221                                      -1,
222                                      NULL);
223       }
224       else {
225         but = uiDefButR_prop(
226             block, UI_BTYPE_TEXT, 0, name, x1, y1, x2, y2, ptr, prop, index, 0, 0, -1, -1, NULL);
227       }
228
229       if (RNA_property_flag(prop) & PROP_TEXTEDIT_UPDATE) {
230         /* TEXTEDIT_UPDATE is usually used for search buttons. For these we also want
231          * the 'x' icon to clear search string, so setting VALUE_CLEAR flag, too. */
232         UI_but_flag_enable(but, UI_BUT_TEXTEDIT_UPDATE | UI_BUT_VALUE_CLEAR);
233       }
234       break;
235     case PROP_POINTER: {
236       if (icon == 0) {
237         PointerRNA pptr = RNA_property_pointer_get(ptr, prop);
238         icon = RNA_struct_ui_icon(pptr.type ? pptr.type : RNA_property_pointer_type(ptr, prop));
239       }
240       if (icon == ICON_DOT) {
241         icon = 0;
242       }
243
244       but = uiDefIconTextButR_prop(block,
245                                    UI_BTYPE_SEARCH_MENU,
246                                    0,
247                                    icon,
248                                    name,
249                                    x1,
250                                    y1,
251                                    x2,
252                                    y2,
253                                    ptr,
254                                    prop,
255                                    index,
256                                    0,
257                                    0,
258                                    -1,
259                                    -1,
260                                    NULL);
261       break;
262     }
263     case PROP_COLLECTION: {
264       char text[256];
265       BLI_snprintf(
266           text, sizeof(text), IFACE_("%d items"), RNA_property_collection_length(ptr, prop));
267       but = uiDefBut(block, UI_BTYPE_LABEL, 0, text, x1, y1, x2, y2, NULL, 0, 0, 0, 0, NULL);
268       UI_but_flag_enable(but, UI_BUT_DISABLED);
269       break;
270     }
271     default:
272       but = NULL;
273       break;
274   }
275
276   return but;
277 }
278
279 /**
280  * \a check_prop callback filters functions to avoid drawing certain properties,
281  * in cases where PROP_HIDDEN flag can't be used for a property.
282  *
283  * \param prop_activate_init: Property to activate on initial popup (#UI_BUT_ACTIVATE_ON_INIT).
284  */
285 eAutoPropButsReturn uiDefAutoButsRNA(uiLayout *layout,
286                                      PointerRNA *ptr,
287                                      bool (*check_prop)(PointerRNA *ptr,
288                                                         PropertyRNA *prop,
289                                                         void *user_data),
290                                      void *user_data,
291                                      PropertyRNA *prop_activate_init,
292                                      const eButLabelAlign label_align,
293                                      const bool compact)
294 {
295   eAutoPropButsReturn return_info = UI_PROP_BUTS_NONE_ADDED;
296   uiLayout *split, *col;
297   const char *name;
298
299   RNA_STRUCT_BEGIN (ptr, prop) {
300     const int flag = RNA_property_flag(prop);
301
302     if (flag & PROP_HIDDEN) {
303       continue;
304     }
305     if (check_prop && check_prop(ptr, prop, user_data) == 0) {
306       return_info |= UI_PROP_BUTS_ANY_FAILED_CHECK;
307       continue;
308     }
309
310     const PropertyType type = RNA_property_type(prop);
311     switch (label_align) {
312       case UI_BUT_LABEL_ALIGN_COLUMN:
313       case UI_BUT_LABEL_ALIGN_SPLIT_COLUMN: {
314         const bool is_boolean = (type == PROP_BOOLEAN && !RNA_property_array_check(prop));
315
316         name = RNA_property_ui_name(prop);
317
318         if (label_align == UI_BUT_LABEL_ALIGN_COLUMN) {
319           col = uiLayoutColumn(layout, true);
320
321           if (!is_boolean) {
322             uiItemL(col, name, ICON_NONE);
323           }
324         }
325         else {
326           BLI_assert(label_align == UI_BUT_LABEL_ALIGN_SPLIT_COLUMN);
327           split = uiLayoutSplit(layout, 0.5f, false);
328
329           col = uiLayoutColumn(split, false);
330           uiItemL(col, (is_boolean) ? "" : name, ICON_NONE);
331           col = uiLayoutColumn(split, false);
332         }
333
334         /* May need to add more cases here.
335          * don't override enum flag names */
336
337         /* name is shown above, empty name for button below */
338         name = (flag & PROP_ENUM_FLAG || is_boolean) ? NULL : "";
339
340         break;
341       }
342       case UI_BUT_LABEL_ALIGN_NONE:
343       default:
344         col = layout;
345         name = NULL; /* no smart label alignment, show default name with button */
346         break;
347     }
348
349     /* Only buttons that can be edited as text. */
350     const bool use_activate_init = ((prop == prop_activate_init) &&
351                                     (ELEM(type, PROP_STRING, PROP_INT, PROP_FLOAT)));
352
353     if (use_activate_init) {
354       uiLayoutSetActivateInit(col, true);
355     }
356
357     uiItemFullR(col, ptr, prop, -1, 0, compact ? UI_ITEM_R_COMPACT : 0, name, ICON_NONE);
358     return_info &= ~UI_PROP_BUTS_NONE_ADDED;
359
360     if (use_activate_init) {
361       uiLayoutSetActivateInit(col, false);
362     }
363   }
364   RNA_STRUCT_END;
365
366   return return_info;
367 }
368
369 /* *** RNA collection search menu *** */
370
371 typedef struct CollItemSearch {
372   struct CollItemSearch *next, *prev;
373   void *data;
374   char *name;
375   int index;
376   int iconid;
377 } CollItemSearch;
378
379 static int sort_search_items_list(const void *a, const void *b)
380 {
381   const CollItemSearch *cis1 = a;
382   const CollItemSearch *cis2 = b;
383
384   if (BLI_strcasecmp(cis1->name, cis2->name) > 0) {
385     return 1;
386   }
387   else {
388     return 0;
389   }
390 }
391
392 void ui_rna_collection_search_cb(const struct bContext *C,
393                                  void *arg,
394                                  const char *str,
395                                  uiSearchItems *items)
396 {
397   uiRNACollectionSearch *data = arg;
398   char *name;
399   int i = 0, iconid = 0, flag = RNA_property_flag(data->target_prop);
400   ListBase *items_list = MEM_callocN(sizeof(ListBase), "items_list");
401   CollItemSearch *cis;
402   const bool skip_filter = (data->but_changed && !(*data->but_changed));
403
404   /* build a temporary list of relevant items first */
405   RNA_PROP_BEGIN (&data->search_ptr, itemptr, data->search_prop) {
406
407     if (flag & PROP_ID_SELF_CHECK) {
408       if (itemptr.data == data->target_ptr.owner_id) {
409         continue;
410       }
411     }
412
413     /* use filter */
414     if (RNA_property_type(data->target_prop) == PROP_POINTER) {
415       if (RNA_property_pointer_poll(&data->target_ptr, data->target_prop, &itemptr) == 0) {
416         continue;
417       }
418     }
419
420     /* Could use the string length here. */
421     name = RNA_struct_name_get_alloc(&itemptr, NULL, 0, NULL);
422
423     iconid = 0;
424     if (itemptr.type && RNA_struct_is_ID(itemptr.type)) {
425       iconid = ui_id_icon_get(C, itemptr.data, false);
426     }
427
428     if (name) {
429       if (skip_filter || BLI_strcasestr(name, str)) {
430         cis = MEM_callocN(sizeof(CollItemSearch), "CollectionItemSearch");
431         cis->data = itemptr.data;
432         cis->name = MEM_dupallocN(name);
433         cis->index = i;
434         cis->iconid = iconid;
435         BLI_addtail(items_list, cis);
436       }
437       MEM_freeN(name);
438     }
439
440     i++;
441   }
442   RNA_PROP_END;
443
444   BLI_listbase_sort(items_list, sort_search_items_list);
445
446   /* add search items from temporary list */
447   for (cis = items_list->first; cis; cis = cis->next) {
448     if (UI_search_item_add(items, cis->name, cis->data, cis->iconid) == false) {
449       break;
450     }
451   }
452
453   for (cis = items_list->first; cis; cis = cis->next) {
454     MEM_freeN(cis->name);
455   }
456   BLI_freelistN(items_list);
457   MEM_freeN(items_list);
458 }
459
460 /***************************** ID Utilities *******************************/
461 int UI_icon_from_id(ID *id)
462 {
463   Object *ob;
464   PointerRNA ptr;
465   short idcode;
466
467   if (id == NULL) {
468     return ICON_NONE;
469   }
470
471   idcode = GS(id->name);
472
473   /* exception for objects */
474   if (idcode == ID_OB) {
475     ob = (Object *)id;
476
477     if (ob->type == OB_EMPTY) {
478       return ICON_EMPTY_DATA;
479     }
480     else {
481       return UI_icon_from_id(ob->data);
482     }
483   }
484
485   /* otherwise get it through RNA, creating the pointer
486    * will set the right type, also with subclassing */
487   RNA_id_pointer_create(id, &ptr);
488
489   return (ptr.type) ? RNA_struct_ui_icon(ptr.type) : ICON_NONE;
490 }
491
492 /* see: report_type_str */
493 int UI_icon_from_report_type(int type)
494 {
495   if (type & RPT_ERROR_ALL) {
496     return ICON_ERROR;
497   }
498   else if (type & RPT_WARNING_ALL) {
499     return ICON_ERROR;
500   }
501   else if (type & RPT_INFO_ALL) {
502     return ICON_INFO;
503   }
504   else {
505     return ICON_NONE;
506   }
507 }
508
509 /********************************** Misc **************************************/
510
511 /**
512  * Returns the best "UI" precision for given floating value,
513  * so that e.g. 10.000001 rather gets drawn as '10'...
514  */
515 int UI_calc_float_precision(int prec, double value)
516 {
517   static const double pow10_neg[UI_PRECISION_FLOAT_MAX + 1] = {
518       1e0, 1e-1, 1e-2, 1e-3, 1e-4, 1e-5, 1e-6};
519   static const double max_pow = 10000000.0; /* pow(10, UI_PRECISION_FLOAT_MAX) */
520
521   BLI_assert(prec <= UI_PRECISION_FLOAT_MAX);
522   BLI_assert(fabs(pow10_neg[prec] - pow(10, -prec)) < 1e-16);
523
524   /* Check on the number of decimal places need to display the number,
525    * this is so 0.00001 is not displayed as 0.00,
526    * _but_, this is only for small values si 10.0001 will not get the same treatment.
527    */
528   value = ABS(value);
529   if ((value < pow10_neg[prec]) && (value > (1.0 / max_pow))) {
530     int value_i = (int)((value * max_pow) + 0.5);
531     if (value_i != 0) {
532       const int prec_span = 3; /* show: 0.01001, 5 would allow 0.0100001 for eg. */
533       int test_prec;
534       int prec_min = -1;
535       int dec_flag = 0;
536       int i = UI_PRECISION_FLOAT_MAX;
537       while (i && value_i) {
538         if (value_i % 10) {
539           dec_flag |= 1 << i;
540           prec_min = i;
541         }
542         value_i /= 10;
543         i--;
544       }
545
546       /* even though its a small value, if the second last digit is not 0, use it */
547       test_prec = prec_min;
548
549       dec_flag = (dec_flag >> (prec_min + 1)) & ((1 << prec_span) - 1);
550
551       while (dec_flag) {
552         test_prec++;
553         dec_flag = dec_flag >> 1;
554       }
555
556       if (test_prec > prec) {
557         prec = test_prec;
558       }
559     }
560   }
561
562   CLAMP(prec, 0, UI_PRECISION_FLOAT_MAX);
563
564   return prec;
565 }
566
567 bool UI_but_online_manual_id(const uiBut *but, char *r_str, size_t maxlength)
568 {
569   if (but->rnapoin.owner_id && but->rnapoin.data && but->rnaprop) {
570     BLI_snprintf(r_str,
571                  maxlength,
572                  "%s.%s",
573                  RNA_struct_identifier(but->rnapoin.type),
574                  RNA_property_identifier(but->rnaprop));
575     return true;
576   }
577   else if (but->optype) {
578     WM_operator_py_idname(r_str, but->optype->idname);
579     return true;
580   }
581
582   *r_str = '\0';
583   return false;
584 }
585
586 bool UI_but_online_manual_id_from_active(const struct bContext *C, char *r_str, size_t maxlength)
587 {
588   uiBut *but = UI_context_active_but_get(C);
589
590   if (but) {
591     return UI_but_online_manual_id(but, r_str, maxlength);
592   }
593
594   *r_str = '\0';
595   return false;
596 }
597
598 /* -------------------------------------------------------------------- */
599 /* Modal Button Store API */
600
601 /** \name Button Store
602  *
603  * Store for modal operators & handlers to register button pointers
604  * which are maintained while drawing or NULL when removed.
605  *
606  * This is needed since button pointers are continuously freed and re-allocated.
607  *
608  * \{ */
609
610 struct uiButStore {
611   struct uiButStore *next, *prev;
612   uiBlock *block;
613   ListBase items;
614 };
615
616 struct uiButStoreElem {
617   struct uiButStoreElem *next, *prev;
618   uiBut **but_p;
619 };
620
621 /**
622  * Create a new button store, the caller must manage and run #UI_butstore_free
623  */
624 uiButStore *UI_butstore_create(uiBlock *block)
625 {
626   uiButStore *bs_handle = MEM_callocN(sizeof(uiButStore), __func__);
627
628   bs_handle->block = block;
629   BLI_addtail(&block->butstore, bs_handle);
630
631   return bs_handle;
632 }
633
634 void UI_butstore_free(uiBlock *block, uiButStore *bs_handle)
635 {
636   /* Workaround for button store being moved into new block,
637    * which then can't use the previous buttons state
638    * ('ui_but_update_from_old_block' fails to find a match),
639    * keeping the active button in the old block holding a reference
640    * to the button-state in the new block: see T49034.
641    *
642    * Ideally we would manage moving the 'uiButStore', keeping a correct state.
643    * All things considered this is the most straightforward fix - Campbell.
644    */
645   if (block != bs_handle->block && bs_handle->block != NULL) {
646     block = bs_handle->block;
647   }
648
649   BLI_freelistN(&bs_handle->items);
650   BLI_remlink(&block->butstore, bs_handle);
651
652   MEM_freeN(bs_handle);
653 }
654
655 bool UI_butstore_is_valid(uiButStore *bs)
656 {
657   return (bs->block != NULL);
658 }
659
660 bool UI_butstore_is_registered(uiBlock *block, uiBut *but)
661 {
662   uiButStore *bs_handle;
663
664   for (bs_handle = block->butstore.first; bs_handle; bs_handle = bs_handle->next) {
665     uiButStoreElem *bs_elem;
666
667     for (bs_elem = bs_handle->items.first; bs_elem; bs_elem = bs_elem->next) {
668       if (*bs_elem->but_p == but) {
669         return true;
670       }
671     }
672   }
673
674   return false;
675 }
676
677 void UI_butstore_register(uiButStore *bs_handle, uiBut **but_p)
678 {
679   uiButStoreElem *bs_elem = MEM_callocN(sizeof(uiButStoreElem), __func__);
680   BLI_assert(*but_p);
681   bs_elem->but_p = but_p;
682
683   BLI_addtail(&bs_handle->items, bs_elem);
684 }
685
686 void UI_butstore_unregister(uiButStore *bs_handle, uiBut **but_p)
687 {
688   uiButStoreElem *bs_elem, *bs_elem_next;
689
690   for (bs_elem = bs_handle->items.first; bs_elem; bs_elem = bs_elem_next) {
691     bs_elem_next = bs_elem->next;
692     if (bs_elem->but_p == but_p) {
693       BLI_remlink(&bs_handle->items, bs_elem);
694       MEM_freeN(bs_elem);
695     }
696   }
697
698   BLI_assert(0);
699 }
700
701 /**
702  * Update the pointer for a registered button.
703  */
704 bool UI_butstore_register_update(uiBlock *block, uiBut *but_dst, const uiBut *but_src)
705 {
706   uiButStore *bs_handle;
707   bool found = false;
708
709   for (bs_handle = block->butstore.first; bs_handle; bs_handle = bs_handle->next) {
710     uiButStoreElem *bs_elem;
711     for (bs_elem = bs_handle->items.first; bs_elem; bs_elem = bs_elem->next) {
712       if (*bs_elem->but_p == but_src) {
713         *bs_elem->but_p = but_dst;
714         found = true;
715       }
716     }
717   }
718
719   return found;
720 }
721
722 /**
723  * NULL all pointers, don't free since the owner needs to be able to inspect.
724  */
725 void UI_butstore_clear(uiBlock *block)
726 {
727   uiButStore *bs_handle;
728
729   for (bs_handle = block->butstore.first; bs_handle; bs_handle = bs_handle->next) {
730     uiButStoreElem *bs_elem;
731
732     bs_handle->block = NULL;
733
734     for (bs_elem = bs_handle->items.first; bs_elem; bs_elem = bs_elem->next) {
735       *bs_elem->but_p = NULL;
736     }
737   }
738 }
739
740 /**
741  * Map freed buttons from the old block and update pointers.
742  */
743 void UI_butstore_update(uiBlock *block)
744 {
745   uiButStore *bs_handle;
746
747   /* move this list to the new block */
748   if (block->oldblock) {
749     if (block->oldblock->butstore.first) {
750       block->butstore = block->oldblock->butstore;
751       BLI_listbase_clear(&block->oldblock->butstore);
752     }
753   }
754
755   if (LIKELY(block->butstore.first == NULL)) {
756     return;
757   }
758
759   /* warning, loop-in-loop, in practice we only store <10 buttons at a time,
760    * so this isn't going to be a problem, if that changes old-new mapping can be cached first */
761   for (bs_handle = block->butstore.first; bs_handle; bs_handle = bs_handle->next) {
762
763     BLI_assert((bs_handle->block == NULL) || (bs_handle->block == block) ||
764                (block->oldblock && block->oldblock == bs_handle->block));
765
766     if (bs_handle->block == block->oldblock) {
767       uiButStoreElem *bs_elem;
768
769       bs_handle->block = block;
770
771       for (bs_elem = bs_handle->items.first; bs_elem; bs_elem = bs_elem->next) {
772         if (*bs_elem->but_p) {
773           uiBut *but_new = ui_but_find_new(block, *bs_elem->but_p);
774
775           /* can be NULL if the buttons removed,
776            * note: we could allow passing in a callback when buttons are removed
777            * so the caller can cleanup */
778           *bs_elem->but_p = but_new;
779         }
780       }
781     }
782   }
783 }
784
785 /** \} */