2 * $Id: menubox.c,v 1.149 2015/01/25 23:53:43 tom Exp $
4 * menubox.c -- implements the menu box
6 * Copyright 2000-2013,2015 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)
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
);
72 int climit
= (data
->item_x
- data
->tag_x
- GUTTER
);
73 int my_width
= data
->menu_width
;
74 int my_x
= data
->item_x
;
75 int my_y
= ItemToRow(choice
);
76 bool both
= (!dialog_vars
.no_tags
&& !dialog_vars
.no_items
);
79 const char *show
= (dialog_vars
.no_items
89 bordchar
= item_selected_attr
;
92 bordchar
= dialog_attr
;
96 /* Clear 'residue' of last item and mark current current item */
98 (void) wattrset(win
, (selected
!= Unselected
) ? item_selected_attr
: item_attr
);
99 for (n
= my_y
- 1; n
< my_y
+ INPUT_ROWS
- 1; n
++) {
101 wprintw(win
, "%*s", my_width
, " ");
104 (void) wattrset(win
, menubox_attr
);
106 wprintw(win
, "%*s", my_width
, " ");
109 /* highlight first char of the tag to be special */
111 (void) wmove(win
, my_y
, data
->tag_x
);
112 dlg_print_listitem(win
, item
->name
, climit
, first
, selected
);
116 /* Draw the input field box (only for inputmenu) */
117 (void) wmove(win
, my_y
, my_x
);
120 dlg_draw_box(win
, my_y
- 1, my_x
, INPUT_ROWS
, my_width
- my_x
- data
->tag_x
,
127 /* print actual item */
128 wmove(win
, my_y
, my_x
);
129 dlg_print_listitem(win
, show
, my_width
- my_x
, first
, selected
);
132 dlg_item_help(item
->help
);
134 (void) wattrset(win
, save
);
138 * Allow the user to edit the text of a menu entry.
141 input_menu_edit(ALL_DATA
* data
,
142 DIALOG_LISTITEM
* items
,
146 chtype save
= dlg_get_attrs(data
->menu
);
149 int key
= 0, fkey
= 0;
152 bool is_inputmenu
= TRUE
;
153 int y
= ItemToRow(choice
);
155 int max_len
= dlg_max_input(MAX((int) strlen(items
->text
) + 1, MAX_LEN
));
157 result
= dlg_malloc(char, (size_t) max_len
);
158 assert_ptr(result
, "input_menu_edit");
160 /* original item is used to initialize the input string. */
162 strcpy(result
, items
->text
);
164 print_item(data
, data
->menu
, items
, choice
, Editing
, TRUE
);
166 /* taken out of inputbox.c - but somewhat modified */
169 key
= dlg_mouse_wgetch(data
->menu
, &fkey
);
170 if (dlg_edit_string(result
, &offset
, key
, fkey
, first
)) {
171 dlg_show_string(data
->menu
, result
, offset
, inputbox_attr
,
174 data
->menu_width
- data
->item_x
- 3,
177 } else if (key
== ESC
|| key
== TAB
) {
184 print_item(data
, data
->menu
, items
, choice
, Selected
, TRUE
);
185 (void) wattrset(data
->menu
, save
);
192 handle_button(int code
, DIALOG_LISTITEM
* items
, int choice
)
197 case DLG_EXIT_OK
: /* FALLTHRU */
199 dlg_add_string(items
[choice
].name
);
202 dlg_add_help_listitem(&code
, &help_result
, &items
[choice
]);
203 dlg_add_string(help_result
);
210 dlg_renamed_menutext(DIALOG_LISTITEM
* items
, int current
, char *newtext
)
212 if (dialog_vars
.input_result
)
213 dialog_vars
.input_result
[0] = '\0';
214 dlg_add_result("RENAMED ");
215 dlg_add_string(items
[current
].name
);
217 dlg_add_string(newtext
);
218 return DLG_EXIT_EXTRA
;
222 dlg_dummy_menutext(DIALOG_LISTITEM
* items
, int current
, char *newtext
)
227 return DLG_EXIT_ERROR
;
231 print_menu(ALL_DATA
* data
, int choice
, int scrollamt
, int max_choice
, bool is_inputmenu
)
235 for (i
= 0; i
< max_choice
; i
++) {
238 &data
->items
[i
+ scrollamt
],
240 (i
== choice
) ? Selected
: Unselected
,
244 /* Clean bottom lines */
246 int spare_lines
, x_count
;
247 spare_lines
= data
->menu_height
% INPUT_ROWS
;
248 (void) wattrset(data
->menu
, menubox_attr
);
249 for (; spare_lines
; spare_lines
--) {
250 wmove(data
->menu
, data
->menu_height
- spare_lines
, 0);
251 for (x_count
= 0; x_count
< data
->menu_width
;
253 waddch(data
->menu
, ' ');
258 (void) wnoutrefresh(data
->menu
);
260 dlg_draw_scrollbar(data
->dialog
,
263 scrollamt
+ max_choice
,
266 data
->box_x
+ data
->menu_width
,
268 data
->box_y
+ data
->menu_height
+ 1,
269 menubox_border2_attr
,
270 menubox_border_attr
);
274 check_hotkey(DIALOG_LISTITEM
* items
, int choice
)
278 if (dlg_match_char(dlg_last_getc(),
281 : items
[choice
].name
))) {
288 * This is an alternate interface to 'menu' which allows the application
289 * to read the list item states back directly without putting them in the
293 dlg_menu(const char *title
,
299 DIALOG_LISTITEM
* items
,
301 DIALOG_INPUTMENU rename_menutext
)
304 static DLG_KEYS_BINDING binding
[] = {
307 DLG_KEYS_DATA( DLGK_FIELD_NEXT
, ' ' ),
308 DLG_KEYS_DATA( DLGK_FIELD_NEXT
, KEY_RIGHT
),
309 DLG_KEYS_DATA( DLGK_FIELD_NEXT
, TAB
),
310 DLG_KEYS_DATA( DLGK_FIELD_PREV
, KEY_BTAB
),
311 DLG_KEYS_DATA( DLGK_FIELD_PREV
, KEY_LEFT
),
312 DLG_KEYS_DATA( DLGK_ITEM_NEXT
, '+' ),
313 DLG_KEYS_DATA( DLGK_ITEM_NEXT
, KEY_DOWN
),
314 DLG_KEYS_DATA( DLGK_ITEM_NEXT
, CHR_NEXT
),
315 DLG_KEYS_DATA( DLGK_ITEM_PREV
, '-' ),
316 DLG_KEYS_DATA( DLGK_ITEM_PREV
, KEY_UP
),
317 DLG_KEYS_DATA( DLGK_ITEM_PREV
, CHR_PREVIOUS
),
318 DLG_KEYS_DATA( DLGK_PAGE_FIRST
, KEY_HOME
),
319 DLG_KEYS_DATA( DLGK_PAGE_LAST
, KEY_END
),
320 DLG_KEYS_DATA( DLGK_PAGE_LAST
, KEY_LL
),
321 DLG_KEYS_DATA( DLGK_PAGE_NEXT
, KEY_NPAGE
),
322 DLG_KEYS_DATA( DLGK_PAGE_PREV
, KEY_PPAGE
),
325 static DLG_KEYS_BINDING binding2
[] = {
334 int old_height
= height
;
335 int old_width
= width
;
338 int i
, j
, x
, y
, cur_x
, cur_y
;
340 int button
= dialog_state
.visit_items
? -1 : dlg_default_button();
341 int choice
= dlg_default_listitem(items
);
342 int result
= DLG_EXIT_UNKNOWN
;
346 int use_width
, name_width
, text_width
, list_width
;
347 WINDOW
*dialog
, *menu
;
348 char *prompt
= dlg_strclone(cprompt
);
349 const char **buttons
= dlg_ok_labels();
350 bool is_inputmenu
= ((rename_menutext
!= 0)
351 && (rename_menutext
!= dlg_dummy_menutext
));
353 dialog_state
.plain_buttons
= TRUE
;
356 all
.item_no
= item_no
;
359 dlg_tab_correct_str(prompt
);
365 all
.menu_height
= menu_height
;
366 use_width
= dlg_calc_list_width(item_no
, items
) + 10;
367 use_width
= MAX(26, use_width
);
368 if (all
.menu_height
== 0) {
369 /* calculate height without items (4) */
370 dlg_auto_size(title
, prompt
, &height
, &width
, MIN_HIGH
, use_width
);
371 dlg_calc_listh(&height
, &all
.menu_height
, item_no
);
373 dlg_auto_size(title
, prompt
,
375 MIN_HIGH
+ all
.menu_height
, use_width
);
377 dlg_button_layout(buttons
, &width
);
378 dlg_print_size(height
, width
);
379 dlg_ctl_size(height
, width
);
381 x
= dlg_box_x_ordinate(width
);
382 y
= dlg_box_y_ordinate(height
);
384 dialog
= dlg_new_window(height
, width
, y
, x
);
387 dlg_register_window(dialog
, "menubox", binding
);
388 dlg_register_buttons(dialog
, "menubox", buttons
);
390 dlg_mouse_setbase(x
, y
);
392 dlg_draw_box2(dialog
, 0, 0, height
, width
, dialog_attr
, border_attr
, border2_attr
);
393 dlg_draw_bottom_box2(dialog
, border_attr
, border2_attr
, dialog_attr
);
394 dlg_draw_title(dialog
, title
);
396 (void) wattrset(dialog
, dialog_attr
);
397 dlg_print_autowrap(dialog
, prompt
, height
, width
);
399 all
.menu_width
= width
- 6;
400 getyx(dialog
, cur_y
, cur_x
);
401 all
.box_y
= cur_y
+ 1;
402 all
.box_x
= (width
- all
.menu_width
) / 2 - 1;
405 * After displaying the prompt, we know how much space we really have.
406 * Limit the list to avoid overwriting the ok-button.
408 if (all
.menu_height
+ MIN_HIGH
> height
- cur_y
)
409 all
.menu_height
= height
- MIN_HIGH
- cur_y
;
410 if (all
.menu_height
<= 0)
413 /* Find out maximal number of displayable items at once. */
414 max_choice
= MIN(all
.menu_height
,
417 max_choice
/= INPUT_ROWS
;
419 /* create new window for the menu */
420 menu
= dlg_sub_window(dialog
, all
.menu_height
, all
.menu_width
,
425 dlg_register_window(menu
, "menu", binding2
);
426 dlg_register_buttons(menu
, "menu", buttons
);
428 /* draw a box around the menu items */
430 all
.box_y
, all
.box_x
,
431 all
.menu_height
+ 2, all
.menu_width
+ 2,
432 menubox_border_attr
, menubox_border2_attr
);
437 /* Find length of longest item to center menu *
438 * only if --menu was given, using --inputmenu *
439 * won't be centered. */
440 for (i
= 0; i
< item_no
; i
++) {
441 name_width
= MAX(name_width
, dlg_count_columns(items
[i
].name
));
442 text_width
= MAX(text_width
, dlg_count_columns(items
[i
].text
));
445 /* If the name+text is wider than the list is allowed, then truncate
446 * one or both of them. If the name is no wider than 30% of the list,
449 * FIXME: the gutter width and name/list ratio should be configurable.
451 use_width
= (all
.menu_width
- GUTTER
);
452 if (dialog_vars
.no_tags
) {
453 list_width
= MIN(use_width
, text_width
);
454 } else if (dialog_vars
.no_items
) {
455 list_width
= MIN(use_width
, name_width
);
460 && text_width
+ name_width
> use_width
) {
461 int need
= (int) (0.30 * use_width
);
462 if (name_width
> need
) {
463 int want
= (int) (use_width
464 * ((double) name_width
)
465 / (text_width
+ name_width
));
466 name_width
= (want
> need
) ? want
: need
;
468 text_width
= use_width
- name_width
;
470 list_width
= (text_width
+ name_width
);
473 all
.tag_x
= (is_inputmenu
475 : (use_width
- list_width
) / 2);
476 all
.item_x
= ((dialog_vars
.no_tags
478 : (dialog_vars
.no_items
480 : (GUTTER
+ name_width
)))
483 if (choice
- scrollamt
>= max_choice
) {
484 scrollamt
= choice
- (max_choice
- 1);
485 choice
= max_choice
- 1;
488 print_menu(&all
, choice
, scrollamt
, max_choice
, is_inputmenu
);
490 /* register the new window, along with its borders */
491 dlg_mouse_mkbigregion(all
.box_y
+ 1, all
.box_x
,
492 all
.menu_height
+ 2, all
.menu_width
+ 2,
493 KEY_MAX
, 1, 1, 1 /* by lines */ );
495 dlg_draw_buttons(dialog
, height
- 2, 0, buttons
, button
, FALSE
, width
);
497 dlg_trace_win(dialog
);
498 while (result
== DLG_EXIT_UNKNOWN
) {
499 if (button
< 0) /* --visit-items */
501 all
.box_y
+ ItemToRow(choice
) + 1,
502 all
.box_x
+ all
.tag_x
+ 1);
504 key
= dlg_mouse_wgetch(dialog
, &fkey
);
505 if (dlg_result_key(key
, fkey
, &result
))
511 * Allow a mouse-click on a box to switch selection to that box.
512 * Handling a button click is a little more complicated, since we
513 * push a KEY_ENTER back onto the input stream so we'll put the
514 * cursor at the right place before handling the "keypress".
516 if (key
>= DLGK_MOUSE(KEY_MAX
)) {
517 key
-= DLGK_MOUSE(KEY_MAX
);
519 if (i
< max_choice
) {
525 } else if (is_DLGK_MOUSE(key
)
526 && dlg_ok_buttoncode(key
- M_EVENT
) >= 0) {
527 button
= (key
- M_EVENT
);
533 * Check if key pressed matches first character of any item tag in
534 * list. If there is more than one match, we will cycle through
535 * each one as the same key is pressed repeatedly.
537 if (button
< 0 || !dialog_state
.visit_items
) {
538 for (j
= scrollamt
+ choice
+ 1; j
< item_no
; j
++) {
539 if (check_hotkey(items
, j
)) {
546 for (j
= 0; j
<= scrollamt
+ choice
; j
++) {
547 if (check_hotkey(items
, j
)) {
556 } else if ((j
= dlg_char_to_button(key
, buttons
)) >= 0) {
563 * A single digit (1-9) positions the selection to that line in the
569 && (key
- '1' < max_choice
)) {
575 if (!found
&& fkey
) {
578 case DLGK_PAGE_FIRST
:
582 i
= item_no
- 1 - scrollamt
;
584 case DLGK_MOUSE(KEY_PPAGE
):
588 else if (scrollamt
!= 0)
589 i
= -MIN(scrollamt
, max_choice
);
593 case DLGK_MOUSE(KEY_NPAGE
):
595 i
= MIN(choice
+ max_choice
, item_no
- scrollamt
- 1);
599 if (choice
== 0 && scrollamt
== 0)
604 if (scrollamt
+ choice
>= item_no
- 1)
615 getyx(dialog
, cur_y
, cur_x
);
616 if (i
< 0 || i
>= max_choice
) {
621 choice
= max_choice
- 1;
622 scrollamt
+= (i
- max_choice
+ 1);
624 print_menu(&all
, choice
, scrollamt
, max_choice
, is_inputmenu
);
627 print_menu(&all
, choice
, scrollamt
, max_choice
, is_inputmenu
);
628 (void) wmove(dialog
, cur_y
, cur_x
);
632 continue; /* wait for another key press */
637 case DLGK_FIELD_PREV
:
638 button
= dlg_prev_button(buttons
, button
);
639 dlg_draw_buttons(dialog
, height
- 2, 0, buttons
, button
,
642 case DLGK_FIELD_NEXT
:
643 button
= dlg_next_button(buttons
, button
);
644 dlg_draw_buttons(dialog
, height
- 2, 0, buttons
, button
,
649 result
= dlg_ok_buttoncode(button
);
651 result
= dlg_enter_buttoncode(button
);
654 * If dlg_menu() is called from dialog_menu(), we want to
655 * capture the results into dialog_vars.input_result.
657 if (result
== DLG_EXIT_ERROR
) {
658 result
= DLG_EXIT_UNKNOWN
;
659 } else if (is_inputmenu
660 || rename_menutext
== dlg_dummy_menutext
) {
661 result
= handle_button(result
,
667 * If we have a rename_menutext function, interpret the Extra
668 * button as a request to rename the menu's text. If that
669 * function doesn't return "Unknown", we will exit from this
670 * function. Usually that is done for dialog_menu(), so the
671 * shell script can use the updated value. If it does return
672 * "Unknown", update the list item only. A direct caller of
673 * dlg_menu() can free the renamed value - we cannot.
675 if (is_inputmenu
&& result
== DLG_EXIT_EXTRA
) {
678 if (input_menu_edit(&all
,
679 &items
[scrollamt
+ choice
],
682 result
= rename_menutext(items
, scrollamt
+ choice
, tmp
);
683 if (result
== DLG_EXIT_UNKNOWN
) {
684 items
[scrollamt
+ choice
].text
= tmp
;
689 result
= DLG_EXIT_UNKNOWN
;
692 &items
[scrollamt
+ choice
],
696 (void) wnoutrefresh(menu
);
700 if (result
== DLG_EXIT_UNKNOWN
) {
701 dlg_draw_buttons(dialog
, height
- 2, 0,
702 buttons
, button
, FALSE
, width
);
713 dlg_del_window(dialog
);
715 dlg_mouse_free_regions();
725 dlg_mouse_free_regions();
726 dlg_unregister_window(menu
);
727 dlg_del_window(dialog
);
730 *current_item
= scrollamt
+ choice
;
735 * Display a menu for choosing among a number of options
738 dialog_menu(const char *title
,
749 DIALOG_LISTITEM
*listitems
;
751 listitems
= dlg_calloc(DIALOG_LISTITEM
, (size_t) item_no
+ 1);
752 assert_ptr(listitems
, "dialog_menu");
754 for (i
= j
= 0; i
< item_no
; ++i
) {
755 listitems
[i
].name
= items
[j
++];
756 listitems
[i
].text
= (dialog_vars
.no_items
759 listitems
[i
].help
= ((dialog_vars
.item_help
)
763 dlg_align_columns(&listitems
[0].text
, sizeof(DIALOG_LISTITEM
), item_no
);
765 result
= dlg_menu(title
,
773 (dialog_vars
.input_menu
774 ? dlg_renamed_menutext
775 : dlg_dummy_menutext
));
777 dlg_free_columns(&listitems
[0].text
, sizeof(DIALOG_LISTITEM
), item_no
);