/* Dialog box implementation. */ #ifdef HAVE_CONFIG_H #include "config.h" #endif #include #include #include "elinks.h" #include "bfu/dialog.h" #include "config/kbdbind.h" #include "config/options.h" #include "intl/charsets.h" #include "intl/libintl.h" #include "terminal/draw.h" #include "main/timer.h" #include "terminal/kbd.h" #include "terminal/terminal.h" #include "terminal/window.h" #include "util/color.h" #include "util/conv.h" #include "util/error.h" #include "util/memlist.h" #include "util/memory.h" #include "util/string.h" static window_handler_T dialog_func; struct dialog_data * do_dialog(struct terminal *term, struct dialog *dlg, struct memory_list *ml) { struct dialog_data *dlg_data; dlg_data = (struct dialog_data *)mem_calloc(1, sizeof(*dlg_data) + sizeof(struct widget_data) * dlg->number_of_widgets); if (!dlg_data) { /* Worry not: freeml() checks whether its argument is NULL. */ freeml(ml); return NULL; } dlg_data->dlg = dlg; dlg_data->number_of_widgets = dlg->number_of_widgets; dlg_data->ml = ml; add_window(term, dialog_func, dlg_data); return dlg_data; } static void cycle_widget_focus(struct dialog_data *dlg_data, int direction); #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Warray-bounds" static void update_all_widgets(struct dialog_data *dlg_data) { struct widget_data *widget_data; /* Iterate backwards rather than forwards so that listboxes are drawn * last, which means that they can grab the cursor. Yes, 'tis hacky. */ foreach_widget_back(dlg_data, widget_data) { display_widget(dlg_data, widget_data); } } #pragma GCC diagnostic pop void redraw_dialog(struct dialog_data *dlg_data, int layout) { struct terminal *term = dlg_data->win->term; struct color_pair *title_color; if (layout) { dlg_data->dlg->layouter(dlg_data); /* This might not be the best place. We need to be able * to make focusability of widgets dynamic so widgets * like scrollable text don't receive focus when there * is nothing to scroll. */ if (!widget_is_focusable(selected_widget(dlg_data))) cycle_widget_focus(dlg_data, 1); } if (!dlg_data->dlg->layout.only_widgets) { set_box(&dlg_data->real_box, dlg_data->box.x + (DIALOG_LEFT_BORDER + 1), dlg_data->box.y + (DIALOG_TOP_BORDER + 1), dlg_data->box.width - 2 * (DIALOG_LEFT_BORDER + 1), dlg_data->box.height - 2 * (DIALOG_TOP_BORDER + 1)); draw_border(term, &dlg_data->real_box, get_bfu_color(term, "dialog.frame"), DIALOG_FRAME); assert(dlg_data->dlg->title); title_color = get_bfu_color(term, "dialog.title"); if (title_color && dlg_data->real_box.width > 2) { char *title = dlg_data->dlg->title; int titlelen = strlen(title); int titlecells = titlelen; int x, y; #ifdef CONFIG_UTF8 if (term->utf8_cp) titlecells = utf8_ptr2cells(title, &title[titlelen]); #endif /* CONFIG_UTF8 */ titlecells = int_min(dlg_data->real_box.width - 2, titlecells); #ifdef CONFIG_UTF8 if (term->utf8_cp) titlelen = utf8_cells2bytes(title, titlecells, NULL); #endif /* CONFIG_UTF8 */ x = (dlg_data->real_box.width - titlecells) / 2 + dlg_data->real_box.x; y = dlg_data->real_box.y - 1; draw_text(term, x - 1, y, " ", 1, 0, title_color); draw_text(term, x, y, title, titlelen, 0, title_color); draw_text(term, x + titlecells, y, " ", 1, 0, title_color); } } update_all_widgets(dlg_data); redraw_windows(REDRAW_IN_FRONT_OF_WINDOW, dlg_data->win); } static void select_dlg_item(struct dialog_data *dlg_data, struct widget_data *widget_data) { select_widget(dlg_data, widget_data); if (widget_data->widget->ops->select) widget_data->widget->ops->select(dlg_data, widget_data); } static const struct widget_ops *const widget_type_to_ops[] = { &checkbox_ops, &field_ops, &field_pass_ops, &button_ops, &listbox_ops, &text_ops, }; static struct widget_data * init_widget(struct dialog_data *dlg_data, int i) { struct widget_data *widget_data = &dlg_data->widgets_data[i]; memset(widget_data, 0, sizeof(*widget_data)); widget_data->widget = &dlg_data->dlg->widgets[i]; if (widget_data->widget->datalen) { widget_data->cdata = (char *)mem_alloc(widget_data->widget->datalen); if (!widget_data->cdata) { return NULL; } memcpy(widget_data->cdata, widget_data->widget->data, widget_data->widget->datalen); } widget_data->widget->ops = widget_type_to_ops[widget_data->widget->type]; if (widget_has_history(widget_data)) { init_list(widget_data->info.field.history); widget_data->info.field.cur_hist = (struct input_history_entry *) &widget_data->info.field.history; } if (widget_data->widget->ops->init) widget_data->widget->ops->init(dlg_data, widget_data); return widget_data; } static int check_range(struct dialog_data *dlg_data, struct widget_data *widget_data) { if (!dlg_data->dlg->layout.only_widgets) { struct el_box *box = &widget_data->box; struct el_box *dlgbox = &dlg_data->real_box; int y = box->y - dlgbox->y; if ((y < dlg_data->y) || (y >= dlg_data->y + dlgbox->height)) { /* This calculates the offset of the window's top. */ dlg_data->y = (y / dlgbox->height) * dlgbox->height; return 1; } } return 0; } void select_widget(struct dialog_data *dlg_data, struct widget_data *widget_data) { struct widget_data *previously_selected_widget; previously_selected_widget = selected_widget(dlg_data); dlg_data->selected_widget_id = widget_data - dlg_data->widgets_data; if (check_range(dlg_data, widget_data)) { redraw_windows(REDRAW_WINDOW_AND_FRONT, dlg_data->win); return; } display_widget(dlg_data, previously_selected_widget); display_widget(dlg_data, widget_data); } struct widget_data * select_widget_by_id(struct dialog_data *dlg_data, int i) { struct widget_data *widget_data; if (i >= dlg_data->number_of_widgets) return NULL; widget_data = &dlg_data->widgets_data[i]; select_widget(dlg_data, widget_data); return widget_data; } static void cycle_widget_focus(struct dialog_data *dlg_data, int direction) { int prev_selected = dlg_data->selected_widget_id; struct widget_data *previously_selected_widget; previously_selected_widget = selected_widget(dlg_data); do { dlg_data->selected_widget_id += direction; if (dlg_data->selected_widget_id >= dlg_data->number_of_widgets) dlg_data->selected_widget_id = 0; else if (dlg_data->selected_widget_id < 0) dlg_data->selected_widget_id = dlg_data->number_of_widgets - 1; } while (!widget_is_focusable(selected_widget(dlg_data)) && dlg_data->selected_widget_id != prev_selected); if (check_range(dlg_data, selected_widget(dlg_data))) { redraw_windows(REDRAW_WINDOW_AND_FRONT, dlg_data->win); return; } display_widget(dlg_data, previously_selected_widget); display_widget(dlg_data, selected_widget(dlg_data)); redraw_windows(REDRAW_IN_FRONT_OF_WINDOW, dlg_data->win); } static void dialog_ev_init(struct dialog_data *dlg_data) { int i; dlg_data->y = 0; /* TODO: foreachback_widget() */ for (i = dlg_data->number_of_widgets - 1; i >= 0; i--) { struct widget_data *widget_data; widget_data = init_widget(dlg_data, i); /* Make sure the selected widget is focusable */ if (widget_data && widget_is_focusable(widget_data)) dlg_data->selected_widget_id = i; } } #ifdef CONFIG_MOUSE static void dialog_ev_mouse(struct dialog_data *dlg_data) { struct widget_data *widget_data; foreach_widget(dlg_data, widget_data) { if (widget_data->widget->ops->mouse && widget_data->widget->ops->mouse(dlg_data, widget_data) == EVENT_PROCESSED) break; } } #endif /* CONFIG_MOUSE */ /* Look up for a button with matching flag. */ static void select_button_by_flag(struct dialog_data *dlg_data, int flag) { struct widget_data *widget_data; foreach_widget(dlg_data, widget_data) { if (widget_data->widget->type == WIDGET_BUTTON && widget_data->widget->info.button.flags & flag) { select_dlg_item(dlg_data, widget_data); break; } } } /* Look up for a button with matching starting letter. */ static void select_button_by_key(struct dialog_data *dlg_data) { term_event_char_T key; #ifdef CONFIG_UTF8 int codepage; #endif struct widget_data *widget_data; struct term_event *ev = dlg_data->term_event; if (!check_kbd_label_key(ev)) return; #ifdef CONFIG_UTF8 key = unicode_fold_label_case(get_kbd_key(ev)); codepage = get_terminal_codepage(dlg_data->win->term); #else key = toupper(get_kbd_key(ev)); #endif foreach_widget(dlg_data, widget_data) { int hk_pos; char *hk_ptr; term_event_char_T hk_char; if (widget_data->widget->type != WIDGET_BUTTON) continue; hk_ptr = widget_data->widget->text; if (!*hk_ptr) continue; /* We first try to match marked hotkey if there is * one else we fallback to first character in button * name. */ hk_pos = widget_data->widget->info.button.hotkey_pos; if (hk_pos >= 0) hk_ptr += hk_pos + 1; #ifdef CONFIG_UTF8 hk_char = cp_to_unicode(codepage, &hk_ptr, strchr(hk_ptr, '\0')); /* hk_char can be UCS_NO_CHAR only if the text of the * widget is not in the expected codepage. */ assert(hk_char != UCS_NO_CHAR); if_assert_failed continue; hk_char = unicode_fold_label_case(hk_char); #else hk_char = toupper(*hk_ptr); #endif if (hk_char == key) { select_dlg_item(dlg_data, widget_data); break; } } } static void dialog_ev_kbd(struct dialog_data *dlg_data) { struct widget_data *widget_data = selected_widget(dlg_data); const struct widget_ops *ops = widget_data->widget->ops; /* XXX: KEYMAP_EDIT ? --pasky */ action_id_T action_id; struct term_event *ev = dlg_data->term_event; /* First let the widget try out. */ if (ops->kbd && ops->kbd(dlg_data, widget_data) == EVENT_PROCESSED) return; action_id = kbd_action(KEYMAP_MENU, ev, NULL); switch (action_id) { case ACT_MENU_SELECT: /* Can we select? */ if (ops->select) { ops->select(dlg_data, widget_data); } break; case ACT_MENU_ENTER: /* Submit button. */ if (ops->select) { ops->select(dlg_data, widget_data); break; } if (widget_is_textfield(widget_data) || check_kbd_modifier(ev, KBD_MOD_CTRL) || check_kbd_modifier(ev, KBD_MOD_ALT)) { select_button_by_flag(dlg_data, B_ENTER); } break; case ACT_MENU_CANCEL: /* Cancel button. */ select_button_by_flag(dlg_data, B_ESC); break; case ACT_MENU_NEXT_ITEM: case ACT_MENU_DOWN: case ACT_MENU_RIGHT: /* Cycle focus. */ cycle_widget_focus(dlg_data, 1); break; case ACT_MENU_PREVIOUS_ITEM: case ACT_MENU_UP: case ACT_MENU_LEFT: /* Cycle focus (reverse). */ cycle_widget_focus(dlg_data, -1); break; case ACT_MENU_REDRAW: redraw_terminal_cls(dlg_data->win->term); break; default: select_button_by_key(dlg_data); break; } } static void dialog_ev_abort(struct dialog_data *dlg_data) { struct widget_data *widget_data; if (dlg_data->dlg->refresh) { struct dialog_refresh *refresh = dlg_data->dlg->refresh; kill_timer(&refresh->timer); mem_free(refresh); } if (dlg_data->dlg->abort) dlg_data->dlg->abort(dlg_data); foreach_widget(dlg_data, widget_data) { mem_free_if(widget_data->cdata); if (widget_has_history(widget_data)) free_list(widget_data->info.field.history); } freeml(dlg_data->ml); dlg_data->y = 0; } /* TODO: use EVENT_PROCESSED/EVENT_NOT_PROCESSED. */ static void dialog_func(struct window *win, struct term_event *ev) { struct dialog_data *dlg_data = (struct dialog_data *)win->data; dlg_data->win = win; dlg_data->term_event = ev; /* Look whether user event handlers can help us.. */ if (dlg_data->dlg->handle_event && (dlg_data->dlg->handle_event(dlg_data) == EVENT_PROCESSED)) { return; } switch (ev->ev) { case EVENT_INIT: dialog_ev_init(dlg_data); /* fallback */ case EVENT_RESIZE: case EVENT_REDRAW: redraw_dialog(dlg_data, 1); #ifdef CONFIG_LIBSIXEL win->term->sixel = 0; #endif break; case EVENT_MOUSE: #ifdef CONFIG_MOUSE dialog_ev_mouse(dlg_data); #endif break; case EVENT_KBD: dialog_ev_kbd(dlg_data); break; case EVENT_ABORT: dialog_ev_abort(dlg_data); #ifdef CONFIG_LIBSIXEL win->term->sixel = 1; #endif break; } } int check_dialog(struct dialog_data *dlg_data) { struct widget_data *widget_data; foreach_widget(dlg_data, widget_data) { if (widget_data->widget->type != WIDGET_CHECKBOX && !widget_is_textfield(widget_data)) continue; if (widget_data->widget->handler && widget_data->widget->handler(dlg_data, widget_data) == EVENT_NOT_PROCESSED) { select_widget(dlg_data, widget_data); redraw_dialog(dlg_data, 0); return 1; } } return 0; } widget_handler_status_T cancel_dialog(struct dialog_data *dlg_data, struct widget_data *xxx) { delete_window(dlg_data->win); return EVENT_PROCESSED; } int update_dialog_data(struct dialog_data *dlg_data) { struct widget_data *widget_data; foreach_widget(dlg_data, widget_data) { if (!widget_data->widget->datalen) continue; memcpy(widget_data->widget->data, widget_data->cdata, widget_data->widget->datalen); } return 0; } widget_handler_status_T ok_dialog(struct dialog_data *dlg_data, struct widget_data *widget_data) { done_handler_T *done = widget_data->widget->info.button.done; void *done_data = widget_data->widget->info.button.done_data; if (check_dialog(dlg_data)) return EVENT_NOT_PROCESSED; update_dialog_data(dlg_data); if (done) done(done_data); return cancel_dialog(dlg_data, widget_data); } /* Clear dialog fields (if widget has clear callback). */ widget_handler_status_T clear_dialog(struct dialog_data *dlg_data, struct widget_data *xxx) { struct widget_data *widget_data; foreach_widget(dlg_data, widget_data) { if (widget_data->widget->ops->clear) widget_data->widget->ops->clear(dlg_data, widget_data); } /* Move focus to the first widget. It helps with bookmark search dialog * (and others). */ select_widget_by_id(dlg_data, 0); redraw_dialog(dlg_data, 0); return EVENT_PROCESSED; } static void format_widgets(struct terminal *term, struct dialog_data *dlg_data, int x, int *y, int w, int h, int *rw, int format_only) { struct widget_data *wdata = dlg_data->widgets_data; int widgets = dlg_data->number_of_widgets; /* TODO: Do something if (*y) gets > height. */ for (; widgets > 0; widgets--, wdata++, (*y)++) { switch (wdata->widget->type) { case WIDGET_FIELD_PASS: case WIDGET_FIELD: dlg_format_field(dlg_data, wdata, x, y, w, rw, ALIGN_LEFT, format_only); break; case WIDGET_LISTBOX: dlg_format_listbox(dlg_data, wdata, x, y, w, h, rw, ALIGN_LEFT, format_only); break; case WIDGET_TEXT: dlg_format_text(dlg_data, wdata, x, y, w, rw, h, format_only); break; case WIDGET_CHECKBOX: { int group = widget_has_group(wdata); if (group > 0 && dlg_data->dlg->layout.float_groups) { int size; /* Find group size */ for (size = 1; widgets > 0; size++, widgets--) { struct widget_data *next = &wdata[size]; if (group != widget_has_group(next)) break; } dlg_format_group(dlg_data, wdata, size, x, y, w, rw, format_only); wdata += size - 1; } else { /* No horizontal space between checkboxes belonging to * the same group. */ dlg_format_checkbox(dlg_data, wdata, x, y, w, rw, ALIGN_LEFT, format_only); if (widgets > 1 && group == widget_has_group(&wdata[1])) (*y)--; } } break; /* We assume that the buttons are all stuffed at the very end * of the dialog. */ case WIDGET_BUTTON: dlg_format_buttons(dlg_data, wdata, widgets, x, y, w, rw, ALIGN_CENTER, format_only); return; } } } void generic_dialog_layouter(struct dialog_data *dlg_data) { struct terminal *term = dlg_data->win->term; int w = dialog_max_width(term); int height = dialog_max_height(term); int x = 0, y, rw; #ifdef CONFIG_UTF8 if (term->utf8_cp) rw = int_min(w, utf8_ptr2cells(dlg_data->dlg->title, NULL)); else #endif /* CONFIG_UTF8 */ rw = int_min(w, strlen(dlg_data->dlg->title)); y = dlg_data->dlg->layout.padding_top ? 0 : -1; format_widgets(term, dlg_data, x, &y, w, height, &rw, 1); /* Update the width to respond to the required minimum width */ if (dlg_data->dlg->layout.fit_datalen) { int_lower_bound(&rw, dlg_data->dlg->widgets->datalen); int_upper_bound(&w, rw); } else if (!dlg_data->dlg->layout.maximize_width) { w = rw; } draw_dialog(dlg_data, w, y); y = dlg_data->box.y + DIALOG_TB + dlg_data->dlg->layout.padding_top; x = dlg_data->box.x + DIALOG_LB; format_widgets(term, dlg_data, x, &y, w, height, NULL, 0); } void draw_dialog(struct dialog_data *dlg_data, int width, int height) { struct terminal *term = dlg_data->win->term; int dlg_width = int_min(term->width, width + 2 * DIALOG_LB); int dlg_height = int_min(term->height, height + 2 * DIALOG_TB); set_box(&dlg_data->box, (term->width - dlg_width) / 2, (term->height - dlg_height) / 2, dlg_width, dlg_height); draw_box(term, &dlg_data->box, ' ', 0, get_bfu_color(term, "dialog.generic")); if (get_opt_bool("ui.dialogs.shadows", NULL)) { /* Draw shadow */ draw_shadow(term, &dlg_data->box, get_bfu_color(term, "dialog.shadow"), 2, 1); #ifdef CONFIG_UTF8 if (term->utf8_cp) fix_dwchar_around_box(term, &dlg_data->box, 0, 2, 1); #endif /* CONFIG_UTF8 */ } #ifdef CONFIG_UTF8 else if (term->utf8_cp) fix_dwchar_around_box(term, &dlg_data->box, 0, 0, 0); #endif /* CONFIG_UTF8 */ } /* Timer callback for @refresh->timer. As explained in @install_timer, * this function must erase the expired timer ID from all variables. */ static void do_refresh_dialog(struct dialog_data *dlg_data) { struct dialog_refresh *refresh = dlg_data->dlg->refresh; enum dlg_refresh_code refresh_code; assert(refresh && refresh->handler); refresh_code = refresh->handler(dlg_data, refresh->data); if (refresh_code == REFRESH_CANCEL || refresh_code == REFRESH_STOP) { refresh->timer = TIMER_ID_UNDEF; if (refresh_code == REFRESH_CANCEL) cancel_dialog(dlg_data, NULL); return; } /* We want dialog_has_refresh() to be true while drawing * so we can not set the timer to -1. */ if (refresh_code == REFRESH_DIALOG) { redraw_dialog(dlg_data, 1); } install_timer(&refresh->timer, RESOURCE_INFO_REFRESH, (void (*)(void *)) do_refresh_dialog, dlg_data); /* The expired timer ID has now been erased. */ } void refresh_dialog(struct dialog_data *dlg_data, dialog_refresh_handler_T handler, void *data) { struct dialog_refresh *refresh = dlg_data->dlg->refresh; if (!refresh) { refresh = (struct dialog_refresh *)mem_calloc(1, sizeof(*refresh)); if (!refresh) return; dlg_data->dlg->refresh = refresh; } else { kill_timer(&refresh->timer); } refresh->handler = handler; refresh->data = data; install_timer(&refresh->timer, RESOURCE_INFO_REFRESH, (void (*)(void *)) do_refresh_dialog, dlg_data); }