2 * $Id: checklist.c,v 1.170 2022/04/04 22:25:11 tom Exp $
4 * checklist.c -- implements the checklist box
6 * Copyright 2000-2020,2022 Thomas E. Dickey
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.
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.
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.
23 * An earlier version of this program lists as authors:
24 * Savio Lam (lam836@cs.cuhk.hk)
25 * Stuart Herbert - S.Herbert@sheffield.ac.uk: radiolist extension
26 * Alessandro Rubini - rubini@ipvvis.unipv.it: merged the two
29 #include <dlg_internals.h>
32 #define MIN_HIGH (1 + (5 * MARGIN))
35 /* the outer-window */
44 /* the inner-window */
46 DIALOG_LISTITEM
*items
;
52 * Print list item. The 'selected' parameter is true if 'choice' is the
53 * current item. That one is colored differently from the other items.
56 print_item(ALL_DATA
* data
,
58 DIALOG_LISTITEM
* item
,
63 chtype save
= dlg_get_attrs(win
);
65 bool both
= (!dialog_vars
.no_tags
&& !dialog_vars
.no_items
);
67 int climit
= (getmaxx(win
) - data
->check_x
+ 1);
68 const char *show
= (dialog_vars
.no_items
72 /* Clear 'residue' of last item */
73 dlg_attrset(win
, menubox_attr
);
74 (void) wmove(win
, choice
, 0);
75 for (i
= 0; i
< data
->use_width
; i
++)
76 (void) waddch(win
, ' ');
78 (void) wmove(win
, choice
, data
->check_x
);
79 dlg_attrset(win
, selected
? check_selected_attr
: check_attr
);
81 (data
->checkflag
== FLAG_CHECK
) ? "[%c]" : "(%c)",
83 dlg_attrset(win
, menubox_attr
);
84 (void) waddch(win
, ' ');
87 dlg_print_listitem(win
, item
->name
, climit
, first
, selected
);
91 (void) wmove(win
, choice
, data
->item_x
);
92 dlg_print_listitem(win
, show
, climit
, first
, selected
);
95 dlg_item_help(item
->help
);
97 dlg_attrset(win
, save
);
101 print_list(ALL_DATA
* data
, int choice
, int scrollamt
, int max_choice
, int max_items
)
106 getyx(data
->dialog
, cur_y
, cur_x
);
107 for (i
= 0; i
< max_choice
; i
++) {
108 int ii
= i
+ scrollamt
;
116 (void) wnoutrefresh(data
->list
);
118 dlg_draw_scrollbar(data
->dialog
,
121 (long) (scrollamt
+ max_choice
),
122 (long) (data
->item_no
),
123 data
->box_x
+ data
->check_x
,
124 data
->box_x
+ data
->use_width
,
126 data
->box_y
+ data
->use_height
+ 1,
127 menubox_border2_attr
,
128 menubox_border_attr
);
130 (void) wmove(data
->dialog
, cur_y
, cur_x
);
134 check_hotkey(DIALOG_LISTITEM
* items
, int choice
)
138 if (dlg_match_char(dlg_last_getc(),
141 : items
[choice
].name
))) {
148 * This is an alternate interface to 'checklist' which allows the application
149 * to read the list item states back directly without putting them in the
150 * output buffer. It also provides for more than two states over which the
151 * check/radio box can display.
154 dlg_checklist(const char *title
,
160 DIALOG_LISTITEM
* items
,
166 static DLG_KEYS_BINDING binding
[] = {
169 DLG_KEYS_DATA( DLGK_FIELD_NEXT
, KEY_RIGHT
),
170 DLG_KEYS_DATA( DLGK_FIELD_NEXT
, TAB
),
171 DLG_KEYS_DATA( DLGK_FIELD_PREV
, KEY_BTAB
),
172 DLG_KEYS_DATA( DLGK_FIELD_PREV
, KEY_LEFT
),
173 DLG_KEYS_DATA( DLGK_ITEM_FIRST
, KEY_HOME
),
174 DLG_KEYS_DATA( DLGK_ITEM_LAST
, KEY_END
),
175 DLG_KEYS_DATA( DLGK_ITEM_LAST
, KEY_LL
),
176 DLG_KEYS_DATA( DLGK_ITEM_NEXT
, '+' ),
177 DLG_KEYS_DATA( DLGK_ITEM_NEXT
, KEY_DOWN
),
178 DLG_KEYS_DATA( DLGK_ITEM_NEXT
, CHR_NEXT
),
179 DLG_KEYS_DATA( DLGK_ITEM_PREV
, '-' ),
180 DLG_KEYS_DATA( DLGK_ITEM_PREV
, KEY_UP
),
181 DLG_KEYS_DATA( DLGK_ITEM_PREV
, CHR_PREVIOUS
),
182 DLG_KEYS_DATA( DLGK_PAGE_NEXT
, KEY_NPAGE
),
183 DLG_KEYS_DATA( DLGK_PAGE_NEXT
, DLGK_MOUSE(KEY_NPAGE
) ),
184 DLG_KEYS_DATA( DLGK_PAGE_PREV
, KEY_PPAGE
),
185 DLG_KEYS_DATA( DLGK_PAGE_PREV
, DLGK_MOUSE(KEY_PPAGE
) ),
192 int old_height
= height
;
193 int old_width
= width
;
196 int i
, j
, key2
, found
, x
, y
, cur_x
, cur_y
;
198 int button
= dialog_state
.visit_items
? -1 : dlg_default_button();
199 int choice
= dlg_default_listitem(items
);
202 int use_width
, list_width
, name_width
, text_width
;
203 int result
= DLG_EXIT_UNKNOWN
;
207 const char **buttons
= dlg_ok_labels();
208 const char *widget_name
;
210 DLG_TRACE(("# %s args:\n", flag
? "checklist" : "radiolist"));
211 DLG_TRACE2S("title", title
);
212 DLG_TRACE2S("message", cprompt
);
213 DLG_TRACE2N("height", height
);
214 DLG_TRACE2N("width", width
);
215 DLG_TRACE2N("lheight", list_height
);
216 DLG_TRACE2N("llength", item_no
);
217 /* FIXME dump the items[][] too */
218 DLG_TRACE2S("states", states
);
219 DLG_TRACE2N("flag", flag
);
220 DLG_TRACE2N("current", *current_item
);
222 dialog_state
.plain_buttons
= TRUE
;
224 memset(&all
, 0, sizeof(all
));
226 all
.item_no
= item_no
;
231 * If this is a radiobutton list, ensure that no more than one item is
232 * selected initially. Allow none to be selected, since some users may
233 * wish to provide this flavor.
235 if (flag
== FLAG_RADIO
) {
238 for (i
= 0; i
< item_no
; i
++) {
239 if (items
[i
].state
) {
247 widget_name
= "radiolist";
249 widget_name
= "checklist";
255 prompt
= dlg_strclone(cprompt
);
256 dlg_tab_correct_str(prompt
);
258 all
.use_height
= list_height
;
259 use_width
= dlg_calc_list_width(item_no
, items
) + 10;
260 use_width
= MAX(26, use_width
);
261 if (all
.use_height
== 0) {
262 /* calculate height without items (4) */
263 dlg_auto_size(title
, prompt
, &height
, &width
, MIN_HIGH
, use_width
);
264 dlg_calc_listh(&height
, &all
.use_height
, item_no
);
266 dlg_auto_size(title
, prompt
,
268 MIN_HIGH
+ all
.use_height
, use_width
);
270 dlg_button_layout(buttons
, &width
);
271 dlg_print_size(height
, width
);
272 dlg_ctl_size(height
, width
);
274 /* we need at least two states */
275 if (states
== 0 || strlen(states
) < 2)
277 num_states
= (int) strlen(states
);
280 all
.checkflag
= flag
;
282 x
= dlg_box_x_ordinate(width
);
283 y
= dlg_box_y_ordinate(height
);
285 dialog
= dlg_new_window(height
, width
, y
, x
);
287 dlg_register_window(dialog
, widget_name
, binding
);
288 dlg_register_buttons(dialog
, widget_name
, buttons
);
290 dlg_mouse_setbase(x
, y
);
292 dlg_draw_box2(dialog
, 0, 0, height
, width
, dialog_attr
, border_attr
, border2_attr
);
293 dlg_draw_bottom_box2(dialog
, border_attr
, border2_attr
, dialog_attr
);
294 dlg_draw_title(dialog
, title
);
296 dlg_attrset(dialog
, dialog_attr
);
297 dlg_print_autowrap(dialog
, prompt
, height
, width
);
299 all
.use_width
= width
- 6;
300 getyx(dialog
, cur_y
, cur_x
);
301 all
.box_y
= cur_y
+ 1;
302 all
.box_x
= (width
- all
.use_width
) / 2 - 1;
305 * After displaying the prompt, we know how much space we really have.
306 * Limit the list to avoid overwriting the ok-button.
308 all
.use_height
= height
- MIN_HIGH
- cur_y
;
309 if (all
.use_height
<= 0)
312 max_choice
= MIN(all
.use_height
, item_no
);
313 max_choice
= MAX(max_choice
, 1);
315 /* create new window for the list */
316 all
.list
= dlg_sub_window(dialog
, all
.use_height
, all
.use_width
,
317 y
+ all
.box_y
+ 1, x
+ all
.box_x
+ 1);
319 /* draw a box around the list items */
320 dlg_draw_box(dialog
, all
.box_y
, all
.box_x
,
321 all
.use_height
+ 2 * MARGIN
,
322 all
.use_width
+ 2 * MARGIN
,
323 menubox_border_attr
, menubox_border2_attr
);
327 /* Find length of longest item to center checklist */
328 for (i
= 0; i
< item_no
; i
++) {
329 text_width
= MAX(text_width
, dlg_count_columns(items
[i
].text
));
330 name_width
= MAX(name_width
, dlg_count_columns(items
[i
].name
));
333 /* If the name+text is wider than the list is allowed, then truncate
334 * one or both of them. If the name is no wider than 1/4 of the list,
337 use_width
= (all
.use_width
- 6);
338 if (dialog_vars
.no_tags
) {
339 list_width
= MIN(all
.use_width
, text_width
);
340 } else if (dialog_vars
.no_items
) {
341 list_width
= MIN(all
.use_width
, name_width
);
346 && text_width
+ name_width
> use_width
) {
347 int need
= (int) (0.25 * use_width
);
348 if (name_width
> need
) {
349 int want
= (int) (use_width
* ((double) name_width
) /
350 (text_width
+ name_width
));
351 name_width
= (want
> need
) ? want
: need
;
353 text_width
= use_width
- name_width
;
355 list_width
= (text_width
+ name_width
);
358 all
.check_x
= (use_width
- list_width
) / 2;
359 all
.item_x
= ((dialog_vars
.no_tags
361 : (dialog_vars
.no_items
366 /* ensure we are scrolled to show the current choice */
367 scrollamt
= MIN(scrollamt
, max_choice
+ item_no
- 1);
368 if (choice
>= (max_choice
+ scrollamt
- 1)) {
369 scrollamt
= MAX(0, choice
- max_choice
+ 1);
370 choice
= max_choice
- 1;
372 print_list(&all
, choice
, scrollamt
, max_choice
, item_no
);
374 /* register the new window, along with its borders */
375 dlg_mouse_mkbigregion(all
.box_y
+ 1, all
.box_x
,
376 all
.use_height
, all
.use_width
+ 2,
377 KEY_MAX
, 1, 1, 1 /* by lines */ );
379 dlg_draw_buttons(dialog
, height
- 2, 0, buttons
, button
, FALSE
, width
);
381 dlg_trace_win(dialog
);
383 while (result
== DLG_EXIT_UNKNOWN
) {
386 if (button
< 0) /* --visit-items */
387 wmove(dialog
, all
.box_y
+ choice
+ 1, all
.box_x
+ all
.check_x
+ 2);
389 key
= dlg_mouse_wgetch(dialog
, &fkey
);
390 if (dlg_result_key(key
, fkey
, &result
)) {
391 if (!dlg_button_key(result
, &button
, &key
, &fkey
))
395 was_mouse
= (fkey
&& is_DLGK_MOUSE(key
));
399 if (was_mouse
&& (key
>= KEY_MAX
)) {
400 getyx(dialog
, cur_y
, cur_x
);
402 if (i
< max_choice
) {
403 choice
= (key
- KEY_MAX
);
404 print_list(&all
, choice
, scrollamt
, max_choice
, item_no
);
406 key
= DLGK_TOGGLE
; /* force the selected item to toggle */
412 } else if (was_mouse
&& key
>= KEY_MIN
) {
413 key
= dlg_lookup_key(dialog
, key
, &fkey
);
417 * A space toggles the item status. We handle either a checklist
418 * (any number of items can be selected) or radio list (zero or one
419 * items can be selected).
421 if (key
== DLGK_TOGGLE
) {
422 int current
= scrollamt
+ choice
;
423 int next
= items
[current
].state
+ 1;
425 if (next
>= num_states
)
428 if (flag
== FLAG_CHECK
) { /* checklist? */
429 getyx(dialog
, cur_y
, cur_x
);
430 items
[current
].state
= next
;
431 print_item(&all
, all
.list
,
432 &items
[scrollamt
+ choice
],
435 (void) wnoutrefresh(all
.list
);
436 (void) wmove(dialog
, cur_y
, cur_x
);
437 } else { /* radiolist */
438 for (i
= 0; i
< item_no
; i
++) {
443 if (items
[current
].state
) {
444 getyx(dialog
, cur_y
, cur_x
);
445 items
[current
].state
= next
? next
: 1;
446 print_item(&all
, all
.list
,
450 (void) wnoutrefresh(all
.list
);
451 (void) wmove(dialog
, cur_y
, cur_x
);
453 items
[current
].state
= 1;
454 print_list(&all
, choice
, scrollamt
, max_choice
, item_no
);
457 continue; /* wait for another key press */
461 * Check if key pressed matches first character of any item tag in
462 * list. If there is more than one match, we will cycle through
463 * each one as the same key is pressed repeatedly.
467 if (button
< 0 || !dialog_state
.visit_items
) {
468 for (j
= scrollamt
+ choice
+ 1; j
< item_no
; j
++) {
469 if (check_hotkey(items
, j
)) {
476 for (j
= 0; j
<= scrollamt
+ choice
; j
++) {
477 if (check_hotkey(items
, j
)) {
486 } else if ((j
= dlg_char_to_button(key
, buttons
)) >= 0) {
494 * A single digit (1-9) positions the selection to that line in the
500 && (key
- '1' < max_choice
)) {
509 case DLGK_ITEM_FIRST
:
513 i
= item_no
- 1 - scrollamt
;
518 else if (scrollamt
!= 0)
519 i
= -MIN(scrollamt
, max_choice
);
524 i
= MIN(choice
+ max_choice
, item_no
- scrollamt
- 1);
528 if (choice
== 0 && scrollamt
== 0)
533 if (scrollamt
+ choice
>= item_no
- 1)
545 getyx(dialog
, cur_y
, cur_x
);
546 if (i
< 0 || i
>= max_choice
) {
551 choice
= max_choice
- 1;
552 scrollamt
+= (i
- max_choice
+ 1);
554 print_list(&all
, choice
, scrollamt
, max_choice
, item_no
);
557 print_list(&all
, choice
, scrollamt
, max_choice
, item_no
);
560 continue; /* wait for another key press */
566 result
= dlg_enter_buttoncode(button
);
569 result
= dlg_ok_buttoncode(button
);
571 case DLGK_FIELD_PREV
:
572 button
= dlg_prev_button(buttons
, button
);
573 dlg_draw_buttons(dialog
, height
- 2, 0, buttons
, button
,
576 case DLGK_FIELD_NEXT
:
577 button
= dlg_next_button(buttons
, button
);
578 dlg_draw_buttons(dialog
, height
- 2, 0, buttons
, button
,
583 dlg_will_resize(dialog
);
588 _dlg_resize_cleanup(dialog
);
597 if ((key2
= dlg_ok_buttoncode(key
)) >= 0) {
604 } else if (key
> 0) {
609 dlg_del_window(dialog
);
610 dlg_mouse_free_regions();
612 *current_item
= (scrollamt
+ choice
);
617 * Display a dialog box with a list of options that can be turned on or off
618 * The `flag' parameter is used to select between radiolist and checklist.
621 dialog_checklist(const char *title
,
632 DIALOG_LISTITEM
*listitems
;
633 bool separate_output
= ((flag
== FLAG_CHECK
)
634 && (dialog_vars
.separate_output
));
635 bool show_status
= FALSE
;
639 listitems
= dlg_calloc(DIALOG_LISTITEM
, (size_t) item_no
+ 1);
640 assert_ptr(listitems
, "dialog_checklist");
642 for (i
= j
= 0; i
< item_no
; ++i
) {
643 listitems
[i
].name
= items
[j
++];
644 listitems
[i
].text
= (dialog_vars
.no_items
647 listitems
[i
].state
= !dlg_strcmp(items
[j
++], "on");
648 listitems
[i
].help
= ((dialog_vars
.item_help
)
652 dlg_align_columns(&listitems
[0].text
, (int) sizeof(DIALOG_LISTITEM
), item_no
);
654 result
= dlg_checklist(title
,
666 case DLG_EXIT_OK
: /* FALLTHRU */
671 dlg_add_help_listitem(&result
, &help_result
, &listitems
[current
]);
672 if ((show_status
= dialog_vars
.help_status
)) {
673 if (separate_output
) {
674 dlg_add_string(help_result
);
677 dlg_add_quoted(help_result
);
680 dlg_add_string(help_result
);
686 for (i
= 0; i
< item_no
; i
++) {
687 if (listitems
[i
].state
) {
688 if (separate_output
) {
689 dlg_add_string(listitems
[i
].name
);
692 if (dlg_need_separator())
694 if (flag
== FLAG_CHECK
)
695 dlg_add_quoted(listitems
[i
].name
);
697 dlg_add_string(listitems
[i
].name
);
701 dlg_add_last_key(separate_output
);
704 dlg_free_columns(&listitems
[0].text
, (int) sizeof(DIALOG_LISTITEM
), item_no
);