1/*
2 *  $Id: editbox.c,v 1.70 2018/06/19 22:57:01 tom Exp $
3 *
4 *  editbox.c -- implements the edit box
5 *
6 *  Copyright 2007-2016,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 *
11 *  This program is distributed in the hope that it will be useful, but
12 *  WITHOUT ANY WARRANTY; without even the implied warranty of
13 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14 *  Lesser General Public License for more details.
15 *
16 *  You should have received a copy of the GNU Lesser General Public
17 *  License along with this program; if not, write to
18 *	Free Software Foundation, Inc.
19 *	51 Franklin St., Fifth Floor
20 *	Boston, MA 02110, USA.
21 */
22
23#include <dialog.h>
24#include <dlg_keys.h>
25
26#include <sys/stat.h>
27
28#define sTEXT -1
29
30static void
31fail_list(void)
32{
33    dlg_exiterr("File too large");
34}
35
36static void
37grow_list(char ***list, int *have, int want)
38{
39    if (want > *have) {
40	size_t last = (size_t) *have;
41	size_t need = (size_t) (want | 31) + 3;
42	*have = (int) need;
43	(*list) = dlg_realloc(char *, need, *list);
44	if ((*list) == 0) {
45	    fail_list();
46	} else {
47	    while (++last < need) {
48		(*list)[last] = 0;
49	    }
50	}
51    }
52}
53
54static void
55load_list(const char *file, char ***list, int *rows)
56{
57    FILE *fp;
58    char *blob = 0;
59    struct stat sb;
60    unsigned n, pass;
61    unsigned need;
62    size_t size;
63
64    *list = 0;
65    *rows = 0;
66
67    if (stat(file, &sb) < 0 ||
68	(sb.st_mode & S_IFMT) != S_IFREG)
69	dlg_exiterr("Not a file: %s", file);
70
71    size = (size_t) sb.st_size;
72    if ((blob = dlg_malloc(char, size + 2)) == 0) {
73	fail_list();
74    } else {
75	blob[size] = '\0';
76
77	if ((fp = fopen(file, "r")) == 0)
78	    dlg_exiterr("Cannot open: %s", file);
79	size = fread(blob, sizeof(char), size, fp);
80	fclose(fp);
81
82	/*
83	 * If the file is not empty, ensure that it ends with a newline.
84	 */
85	if (size != 0 && blob[size - 1] != '\n') {
86	    blob[++size - 1] = '\n';
87	    blob[size] = '\0';
88	}
89
90	for (pass = 0; pass < 2; ++pass) {
91	    int first = TRUE;
92	    need = 0;
93	    for (n = 0; n < size; ++n) {
94		if (first && pass) {
95		    (*list)[need] = blob + n;
96		    first = FALSE;
97		}
98		if (blob[n] == '\n') {
99		    first = TRUE;
100		    ++need;
101		    if (pass)
102			blob[n] = '\0';
103		}
104	    }
105	    if (pass) {
106		if (need == 0) {
107		    (*list)[0] = dlg_strclone("");
108		    (*list)[1] = 0;
109		} else {
110		    for (n = 0; n < need; ++n) {
111			(*list)[n] = dlg_strclone((*list)[n]);
112		    }
113		    (*list)[need] = 0;
114		}
115	    } else {
116		grow_list(list, rows, (int) need + 1);
117	    }
118	}
119	free(blob);
120    }
121}
122
123static void
124free_list(char ***list, int *rows)
125{
126    if (*list != 0) {
127	int n;
128	for (n = 0; n < (*rows); ++n) {
129	    if ((*list)[n] != 0)
130		free((*list)[n]);
131	}
132	free(*list);
133	*list = 0;
134    }
135    *rows = 0;
136}
137
138/*
139 * Display a single row in the editing window:
140 * thisrow is the actual row number that's being displayed.
141 * show_row is the row number that's highlighted for edit.
142 * base_row is the first row number in the window
143 */
144static bool
145display_one(WINDOW *win,
146	    char *text,
147	    int thisrow,
148	    int show_row,
149	    int base_row,
150	    int chr_offset)
151{
152    bool result;
153
154    if (text != 0) {
155	dlg_show_string(win,
156			text,
157			chr_offset,
158			((thisrow == show_row)
159			 ? form_active_text_attr
160			 : form_text_attr),
161			thisrow - base_row,
162			0,
163			getmaxx(win),
164			FALSE,
165			FALSE);
166	result = TRUE;
167    } else {
168	result = FALSE;
169    }
170    return result;
171}
172
173static void
174display_all(WINDOW *win,
175	    char **list,
176	    int show_row,
177	    int firstrow,
178	    int lastrow,
179	    int chr_offset)
180{
181    int limit = getmaxy(win);
182    int row;
183
184    dlg_attr_clear(win, getmaxy(win), getmaxx(win), dialog_attr);
185    if (lastrow - firstrow >= limit)
186	lastrow = firstrow + limit;
187    for (row = firstrow; row < lastrow; ++row) {
188	if (!display_one(win, list[row],
189			 row, show_row, firstrow,
190			 (row == show_row) ? chr_offset : 0))
191	    break;
192    }
193}
194
195static int
196size_list(char **list)
197{
198    int result = 0;
199
200    if (list != 0) {
201	while (*list++ != 0) {
202	    ++result;
203	}
204    }
205    return result;
206}
207
208static bool
209scroll_to(int pagesize, int rows, int *base_row, int *this_row, int target)
210{
211    bool result = FALSE;
212
213    if (target < *base_row) {
214	if (target < 0) {
215	    if (*base_row == 0 && *this_row == 0) {
216		beep();
217	    } else {
218		*this_row = 0;
219		*base_row = 0;
220		result = TRUE;
221	    }
222	} else {
223	    *this_row = target;
224	    *base_row = target;
225	    result = TRUE;
226	}
227    } else if (target >= rows) {
228	if (*this_row < rows - 1) {
229	    *this_row = rows - 1;
230	    *base_row = rows - 1;
231	    result = TRUE;
232	} else {
233	    beep();
234	}
235    } else if (target >= *base_row + pagesize) {
236	*this_row = target;
237	*base_row = target;
238	result = TRUE;
239    } else {
240	*this_row = target;
241	result = FALSE;
242    }
243    if (pagesize < rows) {
244	if (*base_row + pagesize >= rows) {
245	    *base_row = rows - pagesize;
246	}
247    } else {
248	*base_row = 0;
249    }
250    return result;
251}
252
253static int
254col_to_chr_offset(const char *text, int col)
255{
256    const int *cols = dlg_index_columns(text);
257    const int *indx = dlg_index_wchars(text);
258    bool found = FALSE;
259    int result = 0;
260    unsigned n;
261    unsigned len = (unsigned) dlg_count_wchars(text);
262
263    for (n = 0; n < len; ++n) {
264	if (cols[n] <= col && cols[n + 1] > col) {
265	    result = indx[n];
266	    found = TRUE;
267	    break;
268	}
269    }
270    if (!found && len && cols[len] == col) {
271	result = indx[len];
272    }
273    return result;
274}
275
276#define SCROLL_TO(target) show_all = scroll_to(pagesize, listsize, &base_row, &thisrow, target)
277
278#define PREV_ROW (*list)[thisrow - 1]
279#define THIS_ROW (*list)[thisrow]
280#define NEXT_ROW (*list)[thisrow + 1]
281
282#define UPDATE_COL(input) col_offset = dlg_edit_offset(input, chr_offset, box_width)
283
284static int
285widest_line(char **list)
286{
287    int result = MAX_LEN;
288    char *value;
289
290    if (list != 0) {
291	while ((value = *list++) != 0) {
292	    int check = (int) strlen(value);
293	    if (check > result)
294		result = check;
295	}
296    }
297    return result;
298}
299
300#define NAVIGATE_BINDINGS \
301	DLG_KEYS_DATA( DLGK_GRID_DOWN,	KEY_DOWN ), \
302	DLG_KEYS_DATA( DLGK_GRID_RIGHT,	KEY_RIGHT ), \
303	DLG_KEYS_DATA( DLGK_GRID_LEFT,	KEY_LEFT ), \
304	DLG_KEYS_DATA( DLGK_GRID_UP,	KEY_UP ), \
305	DLG_KEYS_DATA( DLGK_FIELD_NEXT,	TAB ), \
306	DLG_KEYS_DATA( DLGK_FIELD_PREV,	KEY_BTAB ), \
307	DLG_KEYS_DATA( DLGK_PAGE_FIRST,	KEY_HOME ), \
308	DLG_KEYS_DATA( DLGK_PAGE_LAST,	KEY_END ), \
309	DLG_KEYS_DATA( DLGK_PAGE_LAST,	KEY_LL ), \
310	DLG_KEYS_DATA( DLGK_PAGE_NEXT,	KEY_NPAGE ), \
311	DLG_KEYS_DATA( DLGK_PAGE_NEXT,	DLGK_MOUSE(KEY_NPAGE) ), \
312	DLG_KEYS_DATA( DLGK_PAGE_PREV,	KEY_PPAGE ), \
313	DLG_KEYS_DATA( DLGK_PAGE_PREV,	DLGK_MOUSE(KEY_PPAGE) )
314/*
315 * Display a dialog box for editing a copy of a file
316 */
317int
318dlg_editbox(const char *title,
319	    char ***list,
320	    int *rows,
321	    int height,
322	    int width)
323{
324    /* *INDENT-OFF* */
325    static DLG_KEYS_BINDING binding[] = {
326	HELPKEY_BINDINGS,
327	ENTERKEY_BINDINGS,
328	NAVIGATE_BINDINGS,
329	TOGGLEKEY_BINDINGS,
330	END_KEYS_BINDING
331    };
332    static DLG_KEYS_BINDING binding2[] = {
333	INPUTSTR_BINDINGS,
334	HELPKEY_BINDINGS,
335	ENTERKEY_BINDINGS,
336	NAVIGATE_BINDINGS,
337	/* no TOGGLEKEY_BINDINGS, since that includes space... */
338	END_KEYS_BINDING
339    };
340    /* *INDENT-ON* */
341
342#ifdef KEY_RESIZE
343    int old_height = height;
344    int old_width = width;
345#endif
346    int x, y, box_y, box_x, box_height, box_width;
347    int show_buttons;
348    int thisrow, base_row, lastrow;
349    int goal_col = -1;
350    int col_offset = 0;
351    int chr_offset = 0;
352    int key, fkey, code;
353    int pagesize;
354    int listsize = size_list(*list);
355    int result = DLG_EXIT_UNKNOWN;
356    int state;
357    size_t max_len = (size_t) dlg_max_input(widest_line(*list));
358    char *input, *buffer;
359    bool show_all, show_one, was_mouse;
360    bool first_trace = TRUE;
361    WINDOW *dialog;
362    WINDOW *editing;
363    DIALOG_VARS save_vars;
364    const char **buttons = dlg_ok_labels();
365    int mincols = (3 * COLS / 4);
366
367    DLG_TRACE(("# editbox args:\n"));
368    DLG_TRACE2S("title", title);
369    /* FIXME dump the rows & list */
370    DLG_TRACE2N("height", height);
371    DLG_TRACE2N("width", width);
372
373    dlg_save_vars(&save_vars);
374    dialog_vars.separate_output = TRUE;
375
376    dlg_does_output();
377
378    buffer = dlg_malloc(char, max_len + 1);
379    assert_ptr(buffer, "dlg_editbox");
380
381    thisrow = base_row = lastrow = 0;
382
383#ifdef KEY_RESIZE
384  retry:
385#endif
386    show_buttons = TRUE;
387    state = dialog_vars.default_button >= 0 ? dlg_default_button() : sTEXT;
388    fkey = 0;
389
390    dlg_button_layout(buttons, &mincols);
391    dlg_auto_size(title, "", &height, &width, 3 * LINES / 4, mincols);
392    dlg_print_size(height, width);
393    dlg_ctl_size(height, width);
394
395    x = dlg_box_x_ordinate(width);
396    y = dlg_box_y_ordinate(height);
397
398    dialog = dlg_new_window(height, width, y, x);
399    dlg_register_window(dialog, "editbox", binding);
400    dlg_register_buttons(dialog, "editbox", buttons);
401
402    dlg_mouse_setbase(x, y);
403
404    dlg_draw_box2(dialog, 0, 0, height, width, dialog_attr, border_attr, border2_attr);
405    dlg_draw_bottom_box2(dialog, border_attr, border2_attr, dialog_attr);
406    dlg_draw_title(dialog, title);
407
408    dlg_attrset(dialog, dialog_attr);
409
410    /* Draw the editing field in a box */
411    box_y = MARGIN + 0;
412    box_x = MARGIN + 1;
413    box_width = width - 2 - (2 * MARGIN);
414    box_height = height - (4 * MARGIN);
415
416    dlg_draw_box(dialog,
417		 box_y,
418		 box_x,
419		 box_height,
420		 box_width,
421		 border_attr, border2_attr);
422    dlg_mouse_mkbigregion(box_y + MARGIN,
423			  box_x + MARGIN,
424			  box_height - (2 * MARGIN),
425			  box_width - (2 * MARGIN),
426			  KEY_MAX, 1, 1, 3);
427    editing = dlg_sub_window(dialog,
428			     box_height - (2 * MARGIN),
429			     box_width - (2 * MARGIN),
430			     getbegy(dialog) + box_y + 1,
431			     getbegx(dialog) + box_x + 1);
432    dlg_register_window(editing, "editbox2", binding2);
433
434    show_all = TRUE;
435    show_one = FALSE;
436    pagesize = getmaxy(editing);
437
438    while (result == DLG_EXIT_UNKNOWN) {
439	int edit = 0;
440
441	if (show_all) {
442	    display_all(editing, *list, thisrow, base_row, listsize, chr_offset);
443	    display_one(editing, THIS_ROW,
444			thisrow, thisrow, base_row, chr_offset);
445	    show_all = FALSE;
446	    show_one = TRUE;
447	} else {
448	    if (thisrow != lastrow) {
449		display_one(editing, (*list)[lastrow],
450			    lastrow, thisrow, base_row, 0);
451		show_one = TRUE;
452	    }
453	}
454	if (show_one) {
455	    display_one(editing, THIS_ROW,
456			thisrow, thisrow, base_row, chr_offset);
457	    getyx(editing, y, x);
458	    dlg_draw_scrollbar(dialog,
459			       base_row,
460			       base_row,
461			       base_row + pagesize,
462			       listsize,
463			       box_x,
464			       box_x + getmaxx(editing),
465			       box_y + 0,
466			       box_y + getmaxy(editing) + 1,
467			       border2_attr,
468			       border_attr);
469	    wmove(editing, y, x);
470	    show_one = FALSE;
471	}
472	lastrow = thisrow;
473	input = THIS_ROW;
474
475	/*
476	 * The last field drawn determines where the cursor is shown:
477	 */
478	if (show_buttons) {
479	    show_buttons = FALSE;
480	    UPDATE_COL(input);
481	    if (state != sTEXT) {
482		display_one(editing, input, thisrow,
483			    -1, base_row, 0);
484		wrefresh(editing);
485	    }
486	    dlg_draw_buttons(dialog,
487			     height - 2,
488			     0,
489			     buttons,
490			     (state != sTEXT) ? state : 99,
491			     FALSE,
492			     width);
493	    if (state == sTEXT) {
494		display_one(editing, input, thisrow,
495			    thisrow, base_row, chr_offset);
496	    }
497	}
498
499	if (first_trace) {
500	    first_trace = FALSE;
501	    dlg_trace_win(dialog);
502	}
503
504	key = dlg_mouse_wgetch((state == sTEXT) ? editing : dialog, &fkey);
505	if (key == ERR) {
506	    result = DLG_EXIT_ERROR;
507	    break;
508	} else if (key == ESC) {
509	    result = DLG_EXIT_ESC;
510	    break;
511	}
512	if (state != sTEXT) {
513	    if (dlg_result_key(key, fkey, &result))
514		break;
515	}
516
517	was_mouse = (fkey && is_DLGK_MOUSE(key));
518	if (was_mouse)
519	    key -= M_EVENT;
520
521	/*
522	 * Handle mouse clicks first, since we want to know if this is a
523	 * button, or something that dlg_edit_string() should handle.
524	 */
525	if (fkey
526	    && was_mouse
527	    && (code = dlg_ok_buttoncode(key)) >= 0) {
528	    result = code;
529	    continue;
530	}
531
532	if (was_mouse
533	    && (key >= KEY_MAX)) {
534	    int wide = getmaxx(editing);
535	    int cell = key - KEY_MAX;
536	    int check = (cell / wide) + base_row;
537	    if (check < listsize) {
538		thisrow = check;
539		col_offset = (cell % wide);
540		chr_offset = col_to_chr_offset(THIS_ROW, col_offset);
541		show_one = TRUE;
542		if (state != sTEXT) {
543		    state = sTEXT;
544		    show_buttons = TRUE;
545		}
546	    } else {
547		beep();
548	    }
549	    continue;
550	} else if (was_mouse && key >= KEY_MIN) {
551	    key = dlg_lookup_key(dialog, key, &fkey);
552	}
553
554	if (state == sTEXT) {	/* editing box selected */
555	    /*
556	     * Intercept scrolling keys that dlg_edit_string() does not
557	     * understand.
558	     */
559	    if (fkey) {
560		bool moved = TRUE;
561
562		switch (key) {
563		case DLGK_GRID_UP:
564		    SCROLL_TO(thisrow - 1);
565		    break;
566		case DLGK_GRID_DOWN:
567		    SCROLL_TO(thisrow + 1);
568		    break;
569		case DLGK_PAGE_FIRST:
570		    SCROLL_TO(0);
571		    break;
572		case DLGK_PAGE_LAST:
573		    SCROLL_TO(listsize);
574		    break;
575		case DLGK_PAGE_NEXT:
576		    SCROLL_TO(base_row + pagesize);
577		    break;
578		case DLGK_PAGE_PREV:
579		    if (thisrow > base_row) {
580			SCROLL_TO(base_row);
581		    } else {
582			SCROLL_TO(base_row - pagesize);
583		    }
584		    break;
585		case DLGK_DELETE_LEFT:
586		    if (chr_offset == 0) {
587			if (thisrow == 0) {
588			    beep();
589			} else {
590			    size_t len = (strlen(THIS_ROW) +
591					  strlen(PREV_ROW) + 1);
592			    char *tmp = dlg_malloc(char, len);
593
594			    assert_ptr(tmp, "dlg_editbox");
595
596			    chr_offset = dlg_count_wchars(PREV_ROW);
597			    UPDATE_COL(PREV_ROW);
598			    goal_col = col_offset;
599
600			    sprintf(tmp, "%s%s", PREV_ROW, THIS_ROW);
601			    if (len > max_len)
602				tmp[max_len] = '\0';
603
604			    free(PREV_ROW);
605			    PREV_ROW = tmp;
606			    for (y = thisrow; y < listsize; ++y) {
607				(*list)[y] = (*list)[y + 1];
608			    }
609			    --listsize;
610			    --thisrow;
611			    SCROLL_TO(thisrow);
612
613			    show_all = TRUE;
614			}
615		    } else {
616			/* dlg_edit_string() can handle this case */
617			moved = FALSE;
618		    }
619		    break;
620		default:
621		    moved = FALSE;
622		    break;
623		}
624		if (moved) {
625		    if (thisrow != lastrow) {
626			if (goal_col < 0)
627			    goal_col = col_offset;
628			chr_offset = col_to_chr_offset(THIS_ROW, goal_col);
629		    } else {
630			UPDATE_COL(THIS_ROW);
631		    }
632		    continue;
633		}
634	    }
635	    strncpy(buffer, input, max_len - 1)[max_len - 1] = '\0';
636	    edit = dlg_edit_string(buffer, &chr_offset, key, fkey, FALSE);
637
638	    if (edit) {
639		goal_col = UPDATE_COL(input);
640		if (strcmp(input, buffer)) {
641		    free(input);
642		    THIS_ROW = dlg_strclone(buffer);
643		    input = THIS_ROW;
644		}
645		display_one(editing, input, thisrow,
646			    thisrow, base_row, chr_offset);
647		continue;
648	    }
649	}
650
651	/* handle non-functionkeys */
652	if (!fkey && (code = dlg_char_to_button(key, buttons)) >= 0) {
653	    dlg_del_window(dialog);
654	    result = dlg_ok_buttoncode(code);
655	    continue;
656	}
657
658	/* handle functionkeys */
659	if (fkey) {
660	    switch (key) {
661	    case DLGK_GRID_UP:
662	    case DLGK_GRID_LEFT:
663	    case DLGK_FIELD_PREV:
664		show_buttons = TRUE;
665		state = dlg_prev_ok_buttonindex(state, sTEXT);
666		break;
667	    case DLGK_GRID_RIGHT:
668	    case DLGK_GRID_DOWN:
669	    case DLGK_FIELD_NEXT:
670		show_buttons = TRUE;
671		state = dlg_next_ok_buttonindex(state, sTEXT);
672		break;
673	    case DLGK_ENTER:
674		if (state == sTEXT) {
675		    const int *indx = dlg_index_wchars(THIS_ROW);
676		    int split = indx[chr_offset];
677		    char *tmp = dlg_strclone(THIS_ROW + split);
678
679		    assert_ptr(tmp, "dlg_editbox");
680		    grow_list(list, rows, listsize + 1);
681		    ++listsize;
682		    for (y = listsize; y > thisrow; --y) {
683			(*list)[y] = (*list)[y - 1];
684		    }
685		    THIS_ROW[split] = '\0';
686		    ++thisrow;
687		    chr_offset = 0;
688		    col_offset = 0;
689		    THIS_ROW = tmp;
690		    SCROLL_TO(thisrow);
691		    show_all = TRUE;
692		} else {
693		    result = dlg_ok_buttoncode(state);
694		}
695		break;
696#ifdef KEY_RESIZE
697	    case KEY_RESIZE:
698		dlg_will_resize(dialog);
699		/* reset data */
700		height = old_height;
701		width = old_width;
702		dlg_clear();
703		dlg_unregister_window(editing);
704		dlg_del_window(editing);
705		dlg_del_window(dialog);
706		dlg_mouse_free_regions();
707		/* repaint */
708		goto retry;
709#endif
710	    case DLGK_TOGGLE:
711		if (state != sTEXT) {
712		    result = dlg_ok_buttoncode(state);
713		} else {
714		    beep();
715		}
716		break;
717	    default:
718		beep();
719		break;
720	    }
721	} else {
722	    beep();
723	}
724    }
725
726    dlg_unregister_window(editing);
727    dlg_del_window(editing);
728    dlg_del_window(dialog);
729    dlg_mouse_free_regions();
730
731    /*
732     * The caller's copy of the (*list)[] array has been updated, but for
733     * consistency with the other widgets, we put the "real" result in
734     * the output buffer.
735     */
736    if (result == DLG_EXIT_OK) {
737	int n;
738	for (n = 0; n < listsize; ++n) {
739	    dlg_add_result((*list)[n]);
740	    dlg_add_separator();
741	}
742	dlg_add_last_key(-1);
743    }
744    free(buffer);
745    dlg_restore_vars(&save_vars);
746    return result;
747}
748
749int
750dialog_editbox(const char *title, const char *file, int height, int width)
751{
752    int result;
753    char **list;
754    int rows;
755
756    load_list(file, &list, &rows);
757    result = dlg_editbox(title, &list, &rows, height, width);
758    free_list(&list, &rows);
759    return result;
760}
761