2 * $Id: menubox.c,v 1.175 2022/04/05 00:14:56 tom Exp $
4 * menubox.c -- implements the menu box
6 * Copyright 2000-2021,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 Licens, version 2.1e
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)
27 #include <dlg_internals.h>
37 /* the outer-window */
45 /* the inner-window */
47 DIALOG_LISTITEM
*items
;
51 #define MIN_HIGH (1 + (5 * MARGIN))
53 #define INPUT_ROWS 3 /* rows per inputmenu entry */
55 #define RowHeight(i) (is_inputmenu ? ((i) * INPUT_ROWS) : ((i) * 1))
56 #define ItemToRow(i) (is_inputmenu ? ((i) * INPUT_ROWS + 1) : (i))
57 #define RowToItem(i) (is_inputmenu ? ((i) / INPUT_ROWS + 0) : (i))
63 print_item(ALL_DATA
* data
,
65 DIALOG_LISTITEM
* item
,
70 chtype save
= dlg_get_attrs(win
);
71 int climit
= (data
->item_x
- data
->tag_x
- GUTTER
);
72 int my_width
= data
->menu_width
;
73 int my_x
= data
->item_x
;
74 int my_y
= ItemToRow(choice
);
75 bool both
= (!dialog_vars
.no_tags
&& !dialog_vars
.no_items
);
78 const char *show
= (dialog_vars
.no_items
88 bordchar
= item_selected_attr
;
91 bordchar
= dialog_attr
;
95 /* Clear 'residue' of last item and mark current current item */
99 dlg_attrset(win
, (selected
!= Unselected
) ? item_selected_attr
: item_attr
);
100 for (n
= my_y
- 1; n
< my_y
+ INPUT_ROWS
- 1; n
++) {
102 wprintw(win
, "%*s", my_width
, " ");
105 dlg_attrset(win
, menubox_attr
);
107 wprintw(win
, "%*s", my_width
, " ");
110 /* highlight first char of the tag to be special */
112 (void) wmove(win
, my_y
, data
->tag_x
);
113 dlg_print_listitem(win
, item
->name
, climit
, first
, selected
);
117 /* Draw the input field box (only for inputmenu) */
118 (void) wmove(win
, my_y
, my_x
);
121 dlg_draw_box(win
, my_y
- 1, my_x
, INPUT_ROWS
, my_width
- my_x
- data
->tag_x
,
128 /* print actual item */
129 wmove(win
, my_y
, my_x
);
130 dlg_print_listitem(win
, show
, my_width
- my_x
, first
, selected
);
133 dlg_item_help(item
->help
);
135 dlg_attrset(win
, save
);
139 * Allow the user to edit the text of a menu entry.
142 input_menu_edit(ALL_DATA
* data
,
143 DIALOG_LISTITEM
* items
,
147 chtype save
= dlg_get_attrs(data
->menu
);
150 int key
= 0, fkey
= 0;
153 bool is_inputmenu
= TRUE
;
154 int y
= ItemToRow(choice
);
156 int max_len
= dlg_max_input(MAX((int) strlen(items
->text
) + 1, MAX_LEN
));
158 result
= dlg_malloc(char, (size_t) max_len
);
159 assert_ptr(result
, "input_menu_edit");
161 /* original item is used to initialize the input string. */
163 strcpy(result
, items
->text
);
165 print_item(data
, data
->menu
, items
, choice
, Editing
, TRUE
);
167 /* taken out of inputbox.c - but somewhat modified */
170 int check
= DLG_EXIT_UNKNOWN
;
171 key
= dlg_mouse_wgetch(data
->menu
, &fkey
);
172 if (dlg_result_key(key
, fkey
, &check
)) {
173 if (check
== DLG_EXIT_CANCEL
) {
181 if (dlg_edit_string(result
, &offset
, key
, fkey
, first
)) {
182 dlg_show_string(data
->menu
, result
, offset
, inputbox_attr
,
185 data
->menu_width
- data
->item_x
- 3,
188 } else if (key
== ESC
|| key
== TAB
) {
195 print_item(data
, data
->menu
, items
, choice
, Selected
, TRUE
);
196 dlg_attrset(data
->menu
, save
);
203 handle_button(int code
, DIALOG_LISTITEM
* items
, int choice
)
208 case DLG_EXIT_OK
: /* FALLTHRU */
210 dlg_add_string(items
[choice
].name
);
213 dlg_add_help_listitem(&code
, &help_result
, &items
[choice
]);
214 dlg_add_string(help_result
);
222 dlg_renamed_menutext(DIALOG_LISTITEM
* items
, int current
, char *newtext
)
224 if (dialog_vars
.input_result
)
225 dialog_vars
.input_result
[0] = '\0';
226 dlg_add_result("RENAMED ");
227 dlg_add_string(items
[current
].name
);
229 dlg_add_string(newtext
);
231 return DLG_EXIT_EXTRA
;
235 dlg_dummy_menutext(DIALOG_LISTITEM
* items
, int current
, char *newtext
)
240 return DLG_EXIT_ERROR
;
244 print_menu(ALL_DATA
* data
,
253 for (i
= 0; i
< max_choice
; i
++) {
254 int ii
= i
+ scrollamt
;
260 (i
== choice
) ? Selected
: Unselected
,
264 /* Clean bottom lines */
266 int spare_lines
, x_count
;
267 spare_lines
= data
->menu_height
% INPUT_ROWS
;
268 dlg_attrset(data
->menu
, menubox_attr
);
269 for (; spare_lines
; spare_lines
--) {
270 wmove(data
->menu
, data
->menu_height
- spare_lines
, 0);
271 for (x_count
= 0; x_count
< data
->menu_width
;
273 waddch(data
->menu
, ' ');
278 (void) wnoutrefresh(data
->menu
);
280 dlg_draw_scrollbar(data
->dialog
,
283 scrollamt
+ max_choice
,
286 data
->box_x
+ data
->menu_width
,
288 data
->box_y
+ data
->menu_height
+ 1,
289 menubox_border2_attr
,
290 menubox_border_attr
);
294 check_hotkey(DIALOG_LISTITEM
* items
, int choice
)
298 if (dlg_match_char(dlg_last_getc(),
301 : items
[choice
].name
))) {
308 * This is an alternate interface to 'menu' which allows the application
309 * to read the list item states back directly without putting them in the
313 dlg_menu(const char *title
,
319 DIALOG_LISTITEM
* items
,
321 DIALOG_INPUTMENU rename_menutext
)
324 static DLG_KEYS_BINDING binding
[] = {
328 DLG_KEYS_DATA( DLGK_FIELD_NEXT
, KEY_RIGHT
),
329 DLG_KEYS_DATA( DLGK_FIELD_NEXT
, TAB
),
330 DLG_KEYS_DATA( DLGK_FIELD_PREV
, KEY_BTAB
),
331 DLG_KEYS_DATA( DLGK_FIELD_PREV
, KEY_LEFT
),
332 DLG_KEYS_DATA( DLGK_ITEM_NEXT
, '+' ),
333 DLG_KEYS_DATA( DLGK_ITEM_NEXT
, KEY_DOWN
),
334 DLG_KEYS_DATA( DLGK_ITEM_NEXT
, CHR_NEXT
),
335 DLG_KEYS_DATA( DLGK_ITEM_PREV
, '-' ),
336 DLG_KEYS_DATA( DLGK_ITEM_PREV
, KEY_UP
),
337 DLG_KEYS_DATA( DLGK_ITEM_PREV
, CHR_PREVIOUS
),
338 DLG_KEYS_DATA( DLGK_PAGE_FIRST
, KEY_HOME
),
339 DLG_KEYS_DATA( DLGK_PAGE_LAST
, KEY_END
),
340 DLG_KEYS_DATA( DLGK_PAGE_LAST
, KEY_LL
),
341 DLG_KEYS_DATA( DLGK_PAGE_NEXT
, KEY_NPAGE
),
342 DLG_KEYS_DATA( DLGK_PAGE_PREV
, KEY_PPAGE
),
345 static DLG_KEYS_BINDING binding2
[] = {
354 int old_LINES
= LINES
;
356 int old_height
= height
;
357 int old_width
= width
;
360 int i
, j
, x
, y
, cur_x
, cur_y
;
362 int button
= dialog_state
.visit_items
? -1 : dlg_default_button();
363 int choice
= dlg_default_listitem(items
);
364 int result
= DLG_EXIT_UNKNOWN
;
367 int use_width
, name_width
, text_width
, list_width
;
368 WINDOW
*dialog
, *menu
;
370 const char **buttons
= dlg_ok_labels();
371 bool is_inputmenu
= ((rename_menutext
!= 0)
372 && (rename_menutext
!= dlg_dummy_menutext
));
374 DLG_TRACE(("# menubox args:\n"));
375 DLG_TRACE2S("title", title
);
376 DLG_TRACE2S("message", cprompt
);
377 DLG_TRACE2N("height", height
);
378 DLG_TRACE2N("width", width
);
379 DLG_TRACE2N("lheight", menu_height
);
380 DLG_TRACE2N("llength", item_no
);
381 /* FIXME dump the items[][] too */
382 DLG_TRACE2N("rename", rename_menutext
!= 0);
384 dialog_state
.plain_buttons
= TRUE
;
387 all
.item_no
= item_no
;
395 prompt
= dlg_strclone(cprompt
);
396 dlg_tab_correct_str(prompt
);
398 all
.menu_height
= menu_height
;
399 use_width
= dlg_calc_list_width(item_no
, items
) + 10;
400 use_width
= MAX(26, use_width
);
401 if (all
.menu_height
== 0) {
402 /* calculate height without items (4) */
403 dlg_auto_size(title
, prompt
, &height
, &width
, MIN_HIGH
, use_width
);
404 dlg_calc_listh(&height
, &all
.menu_height
, item_no
);
406 dlg_auto_size(title
, prompt
,
408 MIN_HIGH
+ all
.menu_height
, use_width
);
410 dlg_button_layout(buttons
, &width
);
411 dlg_print_size(height
, width
);
412 dlg_ctl_size(height
, width
);
414 x
= dlg_box_x_ordinate(width
);
415 y
= dlg_box_y_ordinate(height
);
417 dialog
= dlg_new_window(height
, width
, y
, x
);
420 dlg_register_window(dialog
, "menubox", binding
);
421 dlg_register_buttons(dialog
, "menubox", buttons
);
423 dlg_mouse_setbase(x
, y
);
425 dlg_draw_box2(dialog
, 0, 0, height
, width
, dialog_attr
, border_attr
, border2_attr
);
426 dlg_draw_bottom_box2(dialog
, border_attr
, border2_attr
, dialog_attr
);
427 dlg_draw_title(dialog
, title
);
429 dlg_attrset(dialog
, dialog_attr
);
430 dlg_print_autowrap(dialog
, prompt
, height
, width
);
432 all
.menu_width
= width
- 6;
433 getyx(dialog
, cur_y
, cur_x
);
434 all
.box_y
= cur_y
+ 1;
435 all
.box_x
= (width
- all
.menu_width
) / 2 - 1;
438 * After displaying the prompt, we know how much space we really have.
439 * Limit the list to avoid overwriting the ok-button.
441 all
.menu_height
= height
- MIN_HIGH
- cur_y
;
442 if (all
.menu_height
<= 0)
445 /* Find out maximal number of displayable items at once. */
446 max_choice
= MIN(all
.menu_height
,
449 max_choice
/= INPUT_ROWS
;
451 /* create new window for the menu */
452 menu
= dlg_sub_window(dialog
, all
.menu_height
, all
.menu_width
,
457 dlg_register_window(menu
, "menu", binding2
);
458 dlg_register_buttons(menu
, "menu", buttons
);
460 /* draw a box around the menu items */
462 all
.box_y
, all
.box_x
,
463 all
.menu_height
+ 2, all
.menu_width
+ 2,
464 menubox_border_attr
, menubox_border2_attr
);
469 /* Find length of longest item to center menu *
470 * only if --menu was given, using --inputmenu *
471 * won't be centered. */
472 for (i
= 0; i
< item_no
; i
++) {
473 name_width
= MAX(name_width
, dlg_count_columns(items
[i
].name
));
474 text_width
= MAX(text_width
, dlg_count_columns(items
[i
].text
));
477 /* If the name+text is wider than the list is allowed, then truncate
478 * one or both of them. If the name is no wider than 30% of the list,
481 * FIXME: the gutter width and name/list ratio should be configurable.
483 use_width
= (all
.menu_width
- GUTTER
);
484 if (dialog_vars
.no_tags
) {
485 list_width
= MIN(use_width
, text_width
);
486 } else if (dialog_vars
.no_items
) {
487 list_width
= MIN(use_width
, name_width
);
492 && text_width
+ name_width
> use_width
) {
493 int need
= (int) (0.30 * use_width
);
494 if (name_width
> need
) {
495 int want
= (int) (use_width
496 * ((double) name_width
)
497 / (text_width
+ name_width
));
498 name_width
= (want
> need
) ? want
: need
;
500 text_width
= use_width
- name_width
;
502 list_width
= (text_width
+ name_width
);
505 all
.tag_x
= (is_inputmenu
507 : (use_width
- list_width
) / 2);
508 all
.item_x
= ((dialog_vars
.no_tags
510 : (dialog_vars
.no_items
512 : (GUTTER
+ name_width
)))
515 if (choice
- scrollamt
>= max_choice
) {
516 scrollamt
= choice
- (max_choice
- 1);
517 choice
= max_choice
- 1;
519 #define PrintMenu() \
520 print_menu(&all, choice, scrollamt, max_choice, item_no, is_inputmenu)
523 /* register the new window, along with its borders */
524 dlg_mouse_mkbigregion(all
.box_y
+ 1, all
.box_x
,
525 all
.menu_height
+ 2, all
.menu_width
+ 2,
526 KEY_MAX
, 1, 1, 1 /* by lines */ );
528 dlg_draw_buttons(dialog
, height
- 2, 0, buttons
, button
, FALSE
, width
);
530 dlg_trace_win(dialog
);
532 while (result
== DLG_EXIT_UNKNOWN
) {
535 if (button
< 0) /* --visit-items */
537 all
.box_y
+ ItemToRow(choice
) + 1,
538 all
.box_x
+ all
.tag_x
+ 1);
540 key
= dlg_mouse_wgetch(dialog
, &fkey
);
541 if (dlg_result_key(key
, fkey
, &result
)) {
542 if (!dlg_button_key(result
, &button
, &key
, &fkey
))
549 * Allow a mouse-click on a box to switch selection to that box.
550 * Handling a button click is a little more complicated, since we
551 * push a KEY_ENTER back onto the input stream so we'll put the
552 * cursor at the right place before handling the "keypress".
554 if (key
>= DLGK_MOUSE(KEY_MAX
)) {
555 key
-= DLGK_MOUSE(KEY_MAX
);
557 if (i
< max_choice
) {
563 } else if (is_DLGK_MOUSE(key
)
564 && dlg_ok_buttoncode(key
- M_EVENT
) >= 0) {
565 button
= (key
- M_EVENT
);
571 * Check if key pressed matches first character of any item tag in
572 * list. If there is more than one match, we will cycle through
573 * each one as the same key is pressed repeatedly.
575 if (button
< 0 || !dialog_state
.visit_items
) {
576 for (j
= scrollamt
+ choice
+ 1; j
< item_no
; j
++) {
577 if (check_hotkey(items
, j
)) {
584 for (j
= 0; j
<= scrollamt
+ choice
; j
++) {
585 if (check_hotkey(items
, j
)) {
594 } else if ((j
= dlg_char_to_button(key
, buttons
)) >= 0) {
601 * A single digit (1-9) positions the selection to that line in the
607 && (key
- '1' < max_choice
)) {
613 if (!found
&& fkey
) {
616 case DLGK_PAGE_FIRST
:
620 i
= item_no
- 1 - scrollamt
;
622 case DLGK_MOUSE(KEY_PPAGE
):
626 else if (scrollamt
!= 0)
627 i
= -MIN(scrollamt
, max_choice
);
631 case DLGK_MOUSE(KEY_NPAGE
):
633 i
= MIN(choice
+ max_choice
, item_no
- scrollamt
- 1);
637 if (choice
== 0 && scrollamt
== 0)
642 if (scrollamt
+ choice
>= item_no
- 1)
653 getyx(dialog
, cur_y
, cur_x
);
654 if (i
< 0 || i
>= max_choice
) {
659 choice
= max_choice
- 1;
660 scrollamt
+= (i
- max_choice
+ 1);
666 (void) wmove(dialog
, cur_y
, cur_x
);
670 continue; /* wait for another key press */
675 case DLGK_FIELD_PREV
:
676 button
= dlg_prev_button(buttons
, button
);
677 dlg_draw_buttons(dialog
, height
- 2, 0, buttons
, button
,
681 case DLGK_FIELD_NEXT
:
682 button
= dlg_next_button(buttons
, button
);
683 dlg_draw_buttons(dialog
, height
- 2, 0, buttons
, button
,
690 result
= ((key
== DLGK_LEAVE
)
691 ? dlg_ok_buttoncode(button
)
692 : dlg_enter_buttoncode(button
));
695 * If dlg_menu() is called from dialog_menu(), we want to
696 * capture the results into dialog_vars.input_result.
698 if (result
== DLG_EXIT_ERROR
) {
699 result
= DLG_EXIT_UNKNOWN
;
700 } else if (is_inputmenu
701 || rename_menutext
== dlg_dummy_menutext
) {
702 result
= handle_button(result
,
708 * If we have a rename_menutext function, interpret the Extra
709 * button as a request to rename the menu's text. If that
710 * function doesn't return "Unknown", we will exit from this
711 * function. Usually that is done for dialog_menu(), so the
712 * shell script can use the updated value. If it does return
713 * "Unknown", update the list item only. A direct caller of
714 * dlg_menu() can free the renamed value - we cannot.
716 if (is_inputmenu
&& result
== DLG_EXIT_EXTRA
) {
719 if (input_menu_edit(&all
,
720 &items
[scrollamt
+ choice
],
723 result
= rename_menutext(items
, scrollamt
+ choice
, tmp
);
724 if (result
== DLG_EXIT_UNKNOWN
) {
725 items
[scrollamt
+ choice
].text
= tmp
;
730 result
= DLG_EXIT_UNKNOWN
;
733 &items
[scrollamt
+ choice
],
737 (void) wnoutrefresh(menu
);
741 if (result
== DLG_EXIT_UNKNOWN
) {
742 dlg_draw_buttons(dialog
, height
- 2, 0,
743 buttons
, button
, FALSE
, width
);
749 dlg_will_resize(dialog
);
751 resizeit(height
, LINES
);
752 resizeit(width
, COLS
);
754 _dlg_resize_cleanup(dialog
);
755 dlg_unregister_window(menu
);
756 dlg_unregister_window(dialog
);
770 dlg_mouse_free_regions();
771 dlg_unregister_window(menu
);
772 dlg_del_window(dialog
);
775 *current_item
= scrollamt
+ choice
;
777 DLG_TRACE2N("current", *current_item
);
782 * Display a menu for choosing among a number of options
785 dialog_menu(const char *title
,
796 DIALOG_LISTITEM
*listitems
;
798 listitems
= dlg_calloc(DIALOG_LISTITEM
, (size_t) item_no
+ 1);
799 assert_ptr(listitems
, "dialog_menu");
801 for (i
= j
= 0; i
< item_no
; ++i
) {
802 listitems
[i
].name
= items
[j
++];
803 listitems
[i
].text
= (dialog_vars
.no_items
806 listitems
[i
].help
= ((dialog_vars
.item_help
)
810 dlg_align_columns(&listitems
[0].text
, sizeof(DIALOG_LISTITEM
), item_no
);
812 result
= dlg_menu(title
,
820 (dialog_vars
.input_menu
821 ? dlg_renamed_menutext
822 : dlg_dummy_menutext
));
824 dlg_free_columns(&listitems
[0].text
, sizeof(DIALOG_LISTITEM
), item_no
);