xref: /dragonfly/contrib/dialog/buttons.c (revision b2dabe2e739bd72461a68ac543307c2dedfb048c)
1 /*
2  *  $Id: buttons.c,v 1.109 2022/04/05 23:45:54 tom Exp $
3  *
4  *  buttons.c -- draw buttons, e.g., OK/Cancel
5  *
6  *  Copyright 2000-2021,2022  Thomas E. Dickey
7  *
8  *  This program is free software; you can redistribute it and/or modify
9  *  it under the terms of the GNU Lesser General Public License, version 2.1
10  *  as published by the Free Software Foundation.
11  *
12  *  This program is distributed in the hope that it will be useful, but
13  *  WITHOUT ANY WARRANTY; without even the implied warranty of
14  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
15  *  Lesser General Public License for more details.
16  *
17  *  You should have received a copy of the GNU Lesser General Public
18  *  License along with this program; if not, write to
19  *        Free Software Foundation, Inc.
20  *        51 Franklin St., Fifth Floor
21  *        Boston, MA 02110, USA.
22  */
23 
24 #include <dlg_internals.h>
25 #include <dlg_keys.h>
26 
27 #define MIN_BUTTON (-dialog_state.visit_cols)
28 #define CHR_BUTTON (!dialog_state.plain_buttons)
29 
30 static void
center_label(char * buffer,int longest,const char * label)31 center_label(char *buffer, int longest, const char *label)
32 {
33     int len = dlg_count_columns(label);
34     int right = 0;
35 
36     *buffer = 0;
37     if (len < longest) {
38           int left = (longest - len) / 2;
39           right = (longest - len - left);
40           if (left > 0)
41               sprintf(buffer, "%*s", left, " ");
42     }
43     strcat(buffer, label);
44     if (right > 0)
45           sprintf(buffer + strlen(buffer), "%*s", right, " ");
46 }
47 
48 /*
49  * Parse a multibyte character out of the string, set it past the parsed
50  * character.
51  */
52 static int
string_to_char(const char ** stringp)53 string_to_char(const char **stringp)
54 {
55     int result;
56 #ifdef USE_WIDE_CURSES
57     const char *string = *stringp;
58     size_t have = strlen(string);
59     size_t len;
60     wchar_t cmp2[2];
61     mbstate_t state;
62 
63     memset(&state, 0, sizeof(state));
64     len = mbrlen(string, have, &state);
65 
66     if ((int) len > 0 && len <= have) {
67           size_t check;
68 
69           memset(&state, 0, sizeof(state));
70           memset(cmp2, 0, sizeof(cmp2));
71           check = mbrtowc(cmp2, string, len, &state);
72           if ((int) check <= 0)
73               cmp2[0] = 0;
74           *stringp += len;
75     } else {
76           cmp2[0] = UCH(*string);
77           *stringp += 1;
78     }
79     result = cmp2[0];
80 #else
81     const char *string = *stringp;
82     result = UCH(*string);
83     *stringp += 1;
84 #endif
85     return result;
86 }
87 
88 static size_t
count_labels(const char ** labels)89 count_labels(const char **labels)
90 {
91     size_t result = 0;
92     if (labels != 0) {
93           while (*labels++ != 0) {
94               ++result;
95           }
96     }
97     return result;
98 }
99 
100 /*
101  * Check if the latest key should be added to the hotkey list.
102  */
103 static int
was_hotkey(int this_key,int * used_keys,size_t next)104 was_hotkey(int this_key, int *used_keys, size_t next)
105 {
106     int result = FALSE;
107 
108     if (next != 0) {
109           size_t n;
110           for (n = 0; n < next; ++n) {
111               if (used_keys[n] == this_key) {
112                     result = TRUE;
113                     break;
114               }
115           }
116     }
117     return result;
118 }
119 
120 /*
121  * Determine the hot-keys for a set of button-labels.  Normally these are
122  * the first uppercase character in each label.  However, if more than one
123  * button has the same first-uppercase, then we will (attempt to) look for
124  * an alternate.
125  *
126  * This allocates data which must be freed by the caller.
127  */
128 static int *
get_hotkeys(const char ** labels)129 get_hotkeys(const char **labels)
130 {
131     int *result = 0;
132     size_t count = count_labels(labels);
133 
134     if ((result = dlg_calloc(int, count + 1)) != 0) {
135           size_t n;
136 
137           for (n = 0; n < count; ++n) {
138               const char *label = labels[n];
139               const int *indx = dlg_index_wchars(label);
140               int limit = dlg_count_wchars(label);
141               int i;
142 
143               for (i = 0; i < limit; ++i) {
144                     int first = indx[i];
145                     int check = UCH(label[first]);
146 #ifdef USE_WIDE_CURSES
147                     int last = indx[i + 1];
148                     if ((last - first) != 1) {
149                         const char *temp = (label + first);
150                         check = string_to_char(&temp);
151                     }
152 #endif
153                     if (dlg_isupper(check) && !was_hotkey(check, result, n)) {
154                         result[n] = check;
155                         break;
156                     }
157               }
158           }
159     }
160     return result;
161 }
162 
163 typedef enum {
164     sFIND_KEY = 0
165     ,sHAVE_KEY = 1
166     ,sHAD_KEY = 2
167 } HOTKEY;
168 
169 /*
170  * Print a button
171  */
172 static void
print_button(WINDOW * win,char * label,int hotkey,int y,int x,int selected)173 print_button(WINDOW *win, char *label, int hotkey, int y, int x, int selected)
174 {
175     int i;
176     HOTKEY state = sFIND_KEY;
177     const int *indx = dlg_index_wchars(label);
178     int limit = dlg_count_wchars(label);
179     chtype key_attr = (selected
180                            ? button_key_active_attr
181                            : button_key_inactive_attr);
182     chtype label_attr = (selected
183                                ? button_label_active_attr
184                                : button_label_inactive_attr);
185 
186     (void) wmove(win, y, x);
187     dlg_attrset(win, selected
188                     ? button_active_attr
189                     : button_inactive_attr);
190     (void) waddstr(win, "<");
191     dlg_attrset(win, label_attr);
192     for (i = 0; i < limit; ++i) {
193           int check;
194           int first = indx[i];
195           int last = indx[i + 1];
196 
197           switch (state) {
198           case sFIND_KEY:
199               check = UCH(label[first]);
200 #ifdef USE_WIDE_CURSES
201               if ((last - first) != 1) {
202                     const char *temp = (label + first);
203                     check = string_to_char(&temp);
204               }
205 #endif
206               if (check == hotkey) {
207                     dlg_attrset(win, key_attr);
208                     state = sHAVE_KEY;
209               }
210               break;
211           case sHAVE_KEY:
212               dlg_attrset(win, label_attr);
213               state = sHAD_KEY;
214               break;
215           default:
216               break;
217           }
218           waddnstr(win, label + first, last - first);
219     }
220     dlg_attrset(win, selected
221                     ? button_active_attr
222                     : button_inactive_attr);
223     (void) waddstr(win, ">");
224     if (!dialog_vars.cursor_off_label) {
225           (void) wmove(win, y, x + ((int) (strspn) (label, " ")) + 1);
226     }
227 }
228 
229 /*
230  * Count the buttons in the list.
231  */
232 int
dlg_button_count(const char ** labels)233 dlg_button_count(const char **labels)
234 {
235     int result = 0;
236     while (*labels++ != 0)
237           ++result;
238     return result;
239 }
240 
241 /*
242  * Compute the size of the button array in columns.  Return the total number of
243  * columns in *length, and the longest button's columns in *longest
244  */
245 void
dlg_button_sizes(const char ** labels,int vertical,int * longest,int * length)246 dlg_button_sizes(const char **labels,
247                      int vertical,
248                      int *longest,
249                      int *length)
250 {
251     int n;
252 
253     *length = 0;
254     *longest = 0;
255     for (n = 0; labels[n] != 0; n++) {
256           if (vertical) {
257               *length += 1;
258               *longest = 1;
259           } else {
260               int len = dlg_count_columns(labels[n]);
261               if (len > *longest)
262                     *longest = len;
263               *length += len;
264           }
265     }
266     /*
267      * If we can, make all of the buttons the same size.  This is only optional
268      * for buttons laid out horizontally.
269      */
270     if (*longest < 6 - (*longest & 1))
271           *longest = 6 - (*longest & 1);
272     if (!vertical)
273           *length = *longest * n;
274 }
275 
276 /*
277  * Compute the size of the button array.
278  */
279 int
dlg_button_x_step(const char ** labels,int limit,int * gap,int * margin,int * step)280 dlg_button_x_step(const char **labels, int limit, int *gap, int *margin, int *step)
281 {
282     int count = dlg_button_count(labels);
283     int longest;
284     int length;
285     int result;
286 
287     *margin = 0;
288     if (count != 0) {
289           int unused;
290           int used;
291 
292           dlg_button_sizes(labels, FALSE, &longest, &length);
293           used = (length + (count * 2));
294           unused = limit - used;
295 
296           if ((*gap = unused / (count + 3)) <= 0) {
297               if ((*gap = unused / (count + 1)) <= 0)
298                     *gap = 1;
299               *margin = *gap;
300           } else {
301               *margin = *gap * 2;
302           }
303           *step = *gap + (used + count - 1) / count;
304           result = (*gap > 0) && (unused >= 0);
305     } else {
306           result = 0;
307     }
308     return result;
309 }
310 
311 /*
312  * Make sure there is enough space for the buttons
313  */
314 void
dlg_button_layout(const char ** labels,int * limit)315 dlg_button_layout(const char **labels, int *limit)
316 {
317     int gap, margin, step;
318 
319     if (labels != 0 && dlg_button_count(labels)) {
320           int width = 1;
321 
322           while (!dlg_button_x_step(labels, width, &gap, &margin, &step))
323               ++width;
324           width += (4 * MARGIN);
325           if (width > COLS)
326               width = COLS;
327           if (width > *limit)
328               *limit = width;
329     }
330 }
331 
332 /*
333  * Print a list of buttons at the given position.
334  */
335 void
dlg_draw_buttons(WINDOW * win,int y,int x,const char ** labels,int selected,int vertical,int limit)336 dlg_draw_buttons(WINDOW *win,
337                      int y, int x,
338                      const char **labels,
339                      int selected,
340                      int vertical,
341                      int limit)
342 {
343     chtype save = dlg_get_attrs(win);
344     int step = 0;
345     int length;
346     int longest;
347     int final_x;
348     int final_y;
349     int gap;
350     int margin;
351     size_t need;
352 
353     dlg_mouse_setbase(getbegx(win), getbegy(win));
354 
355     getyx(win, final_y, final_x);
356 
357     dlg_button_sizes(labels, vertical, &longest, &length);
358 
359     if (vertical) {
360           y += 1;
361           step = 1;
362     } else {
363           dlg_button_x_step(labels, limit, &gap, &margin, &step);
364           x += margin;
365     }
366 
367     /*
368      * Allocate a buffer big enough for any label.
369      */
370     need = (size_t) longest;
371     if (need != 0) {
372           char *buffer;
373           int n;
374           int *hotkeys = get_hotkeys(labels);
375 
376           assert_ptr(hotkeys, "dlg_draw_buttons");
377 
378           for (n = 0; labels[n] != 0; ++n) {
379               need += strlen(labels[n]) + 1;
380           }
381           buffer = dlg_malloc(char, need);
382           assert_ptr(buffer, "dlg_draw_buttons");
383 
384           /*
385            * Draw the labels.
386            */
387           for (n = 0; labels[n] != 0; n++) {
388               center_label(buffer, longest, labels[n]);
389               mouse_mkbutton(y, x, dlg_count_columns(buffer), n);
390               print_button(win, buffer,
391                                CHR_BUTTON ? hotkeys[n] : -1,
392                                y, x,
393                                (selected == n) || (n == 0 && selected < 0));
394               if (selected == n)
395                     getyx(win, final_y, final_x);
396 
397               if (vertical) {
398                     if ((y += step) > limit)
399                         break;
400               } else {
401                     if ((x += step) > limit)
402                         break;
403               }
404           }
405           (void) wmove(win, final_y, final_x);
406           wrefresh(win);
407           dlg_attrset(win, save);
408           free(buffer);
409           free(hotkeys);
410     }
411 }
412 
413 /*
414  * Match a given character against the beginning of the string, ignoring case
415  * of the given character.  The matching string must begin with an uppercase
416  * character.
417  */
418 int
dlg_match_char(int ch,const char * string)419 dlg_match_char(int ch, const char *string)
420 {
421     if (!dialog_vars.no_hot_list) {
422           if (string != 0) {
423               int cmp2 = string_to_char(&string);
424 #ifdef USE_WIDE_CURSES
425               wint_t cmp1 = dlg_toupper(ch);
426               if (cmp2 != 0 && (wchar_t) cmp1 == (wchar_t) dlg_toupper(cmp2)) {
427                     return TRUE;
428               }
429 #else
430               if (ch > 0 && ch < 256) {
431                     if (dlg_toupper(ch) == dlg_toupper(cmp2))
432                         return TRUE;
433               }
434 #endif
435           }
436     }
437     return FALSE;
438 }
439 
440 /*
441  * Find the first uppercase character in the label, which we may use for an
442  * abbreviation.
443  */
444 int
dlg_button_to_char(const char * label)445 dlg_button_to_char(const char *label)
446 {
447     int cmp = -1;
448 
449     while (*label != 0) {
450           int ch = string_to_char(&label);
451           if (dlg_isupper(ch)) {
452               cmp = ch;
453               break;
454           }
455     }
456     return cmp;
457 }
458 
459 /*
460  * Given a list of button labels, and a character which may be the abbreviation
461  * for one, find it, if it exists.  An abbreviation will be the first character
462  * which happens to be capitalized in the label.
463  */
464 int
dlg_char_to_button(int ch,const char ** labels)465 dlg_char_to_button(int ch, const char **labels)
466 {
467     int result = DLG_EXIT_UNKNOWN;
468 
469     if (labels != 0) {
470           int *hotkeys = get_hotkeys(labels);
471 
472           ch = (int) dlg_toupper(dlg_last_getc());
473 
474           if (hotkeys != 0) {
475               int j;
476 
477               for (j = 0; labels[j] != 0; ++j) {
478                     if (ch == hotkeys[j]) {
479                         dlg_flush_getc();
480                         result = j;
481                         break;
482                     }
483               }
484               free(hotkeys);
485           }
486     }
487 
488     return result;
489 }
490 
491 static const char *
my_yes_label(void)492 my_yes_label(void)
493 {
494     return (dialog_vars.yes_label != NULL)
495           ? dialog_vars.yes_label
496           : _("Yes");
497 }
498 
499 static const char *
my_no_label(void)500 my_no_label(void)
501 {
502     return (dialog_vars.no_label != NULL)
503           ? dialog_vars.no_label
504           : _("No");
505 }
506 
507 static const char *
my_ok_label(void)508 my_ok_label(void)
509 {
510     return (dialog_vars.ok_label != NULL)
511           ? dialog_vars.ok_label
512           : _("OK");
513 }
514 
515 static const char *
my_cancel_label(void)516 my_cancel_label(void)
517 {
518     return (dialog_vars.cancel_label != NULL)
519           ? dialog_vars.cancel_label
520           : _("Cancel");
521 }
522 
523 static const char *
my_exit_label(void)524 my_exit_label(void)
525 {
526     return (dialog_vars.exit_label != NULL)
527           ? dialog_vars.exit_label
528           : _("EXIT");
529 }
530 
531 static const char *
my_extra_label(void)532 my_extra_label(void)
533 {
534     return (dialog_vars.extra_label != NULL)
535           ? dialog_vars.extra_label
536           : _("Extra");
537 }
538 
539 static const char *
my_help_label(void)540 my_help_label(void)
541 {
542     return (dialog_vars.help_label != NULL)
543           ? dialog_vars.help_label
544           : _("Help");
545 }
546 
547 /*
548  * Return a list of button labels.
549  */
550 const char **
dlg_exit_label(void)551 dlg_exit_label(void)
552 {
553     const char **result;
554     DIALOG_VARS save;
555 
556     if (dialog_vars.extra_button) {
557           dlg_save_vars(&save);
558           dialog_vars.nocancel = TRUE;
559           result = dlg_ok_labels();
560           dlg_restore_vars(&save);
561     } else {
562           static const char *labels[3];
563           int n = 0;
564 
565           if (!dialog_vars.nook)
566               labels[n++] = my_exit_label();
567           if (dialog_vars.help_button)
568               labels[n++] = my_help_label();
569           if (n == 0)
570               labels[n++] = my_exit_label();
571           labels[n] = 0;
572 
573           result = labels;
574     }
575     return result;
576 }
577 
578 /*
579  * Map the given button index for dlg_exit_label() into our exit-code.
580  */
581 int
dlg_exit_buttoncode(int button)582 dlg_exit_buttoncode(int button)
583 {
584     int result;
585     DIALOG_VARS save;
586 
587     dlg_save_vars(&save);
588     dialog_vars.nocancel = TRUE;
589 
590     result = dlg_ok_buttoncode(button);
591 
592     dlg_restore_vars(&save);
593 
594     return result;
595 }
596 
597 static const char **
finish_ok_label(const char ** labels,int n)598 finish_ok_label(const char **labels, int n)
599 {
600     if (n == 0) {
601           labels[n++] = my_ok_label();
602           dialog_vars.nook = FALSE;
603           DLG_TRACE(("# ignore --nook, since at least one button is needed\n"));
604     }
605 
606     labels[n] = NULL;
607     return labels;
608 }
609 
610 /*
611  * Return a list of button labels for the OK (no Cancel) group, used in msgbox
612  * and progressbox.
613  */
614 const char **
dlg_ok_label(void)615 dlg_ok_label(void)
616 {
617     static const char *labels[4];
618     int n = 0;
619 
620     if (!dialog_vars.nook)
621           labels[n++] = my_ok_label();
622     if (dialog_vars.extra_button)
623           labels[n++] = my_extra_label();
624     if (dialog_vars.help_button)
625           labels[n++] = my_help_label();
626 
627     return finish_ok_label(labels, n);
628 }
629 
630 /*
631  * Return a list of button labels for the OK/Cancel group, used in most widgets
632  * that select an option or data.
633  */
634 const char **
dlg_ok_labels(void)635 dlg_ok_labels(void)
636 {
637     static const char *labels[5];
638     int n = 0;
639 
640     if (!dialog_vars.nook)
641           labels[n++] = my_ok_label();
642     if (dialog_vars.extra_button)
643           labels[n++] = my_extra_label();
644     if (!dialog_vars.nocancel)
645           labels[n++] = my_cancel_label();
646     if (dialog_vars.help_button)
647           labels[n++] = my_help_label();
648 
649     return finish_ok_label(labels, n);
650 }
651 
652 /*
653  * Map the given button index for dlg_ok_labels() into our exit-code
654  */
655 int
dlg_ok_buttoncode(int button)656 dlg_ok_buttoncode(int button)
657 {
658     int result = DLG_EXIT_ERROR;
659     int n = !dialog_vars.nook;
660 
661     if (!dialog_vars.nook && (button <= 0)) {
662           result = DLG_EXIT_OK;
663     } else if (dialog_vars.extra_button && (button == n++)) {
664           result = DLG_EXIT_EXTRA;
665     } else if (!dialog_vars.nocancel && (button == n++)) {
666           result = DLG_EXIT_CANCEL;
667     } else if (dialog_vars.help_button && (button == n)) {
668           result = DLG_EXIT_HELP;
669     }
670     DLG_TRACE(("# dlg_ok_buttoncode(%d) = %d:%s\n",
671                  button, result, dlg_exitcode2s(result)));
672     return result;
673 }
674 
675 /*
676  * Given that we're using dlg_ok_labels() to list buttons, find the next index
677  * in the list of buttons.  The 'extra' parameter if negative provides a way to
678  * enumerate extra active areas on the widget.
679  */
680 int
dlg_next_ok_buttonindex(int current,int extra)681 dlg_next_ok_buttonindex(int current, int extra)
682 {
683     int result = current + 1;
684 
685     if (current >= 0
686           && dlg_ok_buttoncode(result) < 0)
687           result = extra;
688     return result;
689 }
690 
691 /*
692  * Similarly, find the previous button index.
693  */
694 int
dlg_prev_ok_buttonindex(int current,int extra)695 dlg_prev_ok_buttonindex(int current, int extra)
696 {
697     int result = current - 1;
698 
699     if (result < extra) {
700           for (result = 0; dlg_ok_buttoncode(result + 1) >= 0; ++result) {
701               ;
702           }
703     }
704     return result;
705 }
706 
707 /*
708  * Find the button-index for the "OK" or "Cancel" button, according to
709  * whether --defaultno is given.  If --nocancel was given, we always return
710  * the index for the first button (usually "OK" unless --nook was used).
711  */
712 int
dlg_defaultno_button(void)713 dlg_defaultno_button(void)
714 {
715     int result = 0;
716 
717     if (dialog_vars.defaultno && !dialog_vars.nocancel) {
718           while (dlg_ok_buttoncode(result) != DLG_EXIT_CANCEL)
719               ++result;
720     }
721     DLG_TRACE(("# dlg_defaultno_button() = %d\n", result));
722     return result;
723 }
724 
725 /*
726  * Find the button-index for a button named with --default-button. If the
727  * option was not specified, or if the selected button does not exist, return
728  * the index of the first button (usually "OK" unless --nook was used).
729  */
730 int
dlg_default_button(void)731 dlg_default_button(void)
732 {
733     int result = 0;
734 
735     if (dialog_vars.default_button >= 0) {
736           int i, n;
737 
738           for (i = 0; (n = dlg_ok_buttoncode(i)) >= 0; i++) {
739               if (n == dialog_vars.default_button) {
740                     result = i;
741                     break;
742               }
743           }
744     }
745     DLG_TRACE(("# dlg_default_button() = %d\n", result));
746     return result;
747 }
748 
749 /*
750  * Return a list of buttons for Yes/No labels.
751  */
752 const char **
dlg_yes_labels(void)753 dlg_yes_labels(void)
754 {
755     static const char *labels[5];
756     int n = 0;
757     const char **result;
758 
759     labels[n++] = my_yes_label();
760     if (dialog_vars.extra_button)
761           labels[n++] = my_extra_label();
762     labels[n++] = my_no_label();
763     if (dialog_vars.help_button)
764           labels[n++] = my_help_label();
765     labels[n] = NULL;
766 
767     result = labels;
768 
769     return result;
770 }
771 
772 /*
773  * Map the given button index for dlg_yes_labels() into our exit-code.
774  */
775 int
dlg_yes_buttoncode(int button)776 dlg_yes_buttoncode(int button)
777 {
778     int result = DLG_EXIT_ERROR;
779 
780     if (dialog_vars.extra_button) {
781           result = dlg_ok_buttoncode(button);
782     } else if (button == 0) {
783           result = DLG_EXIT_OK;
784     } else if (button == 1) {
785           result = DLG_EXIT_CANCEL;
786     } else if (button == 2 && dialog_vars.help_button) {
787           result = DLG_EXIT_HELP;
788     }
789 
790     return result;
791 }
792 
793 /*
794  * Return the next index in labels[];
795  */
796 int
dlg_next_button(const char ** labels,int button)797 dlg_next_button(const char **labels, int button)
798 {
799     if (button < -1)
800           button = -1;
801 
802     if (labels[button + 1] != 0) {
803           ++button;
804     } else {
805           button = MIN_BUTTON;
806     }
807     return button;
808 }
809 
810 /*
811  * Return the previous index in labels[];
812  */
813 int
dlg_prev_button(const char ** labels,int button)814 dlg_prev_button(const char **labels, int button)
815 {
816     if (button > MIN_BUTTON) {
817           --button;
818     } else {
819           if (button < -1)
820               button = -1;
821 
822           while (labels[button + 1] != 0)
823               ++button;
824     }
825     return button;
826 }
827