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