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