1 /*
2  * GNU Typist  - interactive typing tutor program for UNIX systems
3  *
4  * Copyright (C) 2003, 2008  GNU Typist Development Team <bug-gtypist@gnu.org>
5  *
6  * This program is free software: you can redistribute it and/or modify
7  * it under the terms of the GNU General Public License as published by
8  * the Free Software Foundation, either version 3 of the License, or
9  * (at your option) any later version.
10  *
11  * This program is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  * GNU General Public License for more details.
15  *
16  * You should have received a copy of the GNU General Public License
17  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
18  */
19 
20 #include "config.h"
21 #include "cursmenu.h"
22 #include "script.h"
23 
24 #ifdef HAVE_PDCURSES
25 #include <curses.h>
26 #else
27 #include <ncurses.h>
28 #endif
29 
30 #include "error.h"
31 #include "gtypist.h"
32 
33 #include <stdlib.h>
34 #include <string.h>
35 #include <ctype.h>
36 
37 #include "gettext.h"
38 #define _(String) gettext (String)
39 
40 extern int isUTF8Locale;
41 
42 #define max(x,y) (((x)>(y)) ? (x) : (y))
43 #define min(x,y) (((x)<(y)) ? (x) : (y))
44 
45 // This history list is needed in order to get the entire menu navigation
46 // working
47 typedef struct _MenuNode
48 {
49    char *label;
50    int line;
51    struct _MenuNode *next;
52 }
53 MenuNode;
54 
55 // They are kept non-NULL as soon as the main menu is visited.
56 static MenuNode *start_node = NULL, *last_node = NULL;
57 
node_new()58 static MenuNode *node_new ()
59 {
60    MenuNode *mn = (MenuNode *) calloc (1, sizeof (MenuNode));
61    if (!mn)
62    {
63       perror ("calloc");
64       fatal_error ("internal error: calloc", NULL);
65    }
66 
67    return mn;
68 }
69 
node_delete(MenuNode * mn)70 static void node_delete (MenuNode *mn)
71 {
72    if (mn -> label)
73       free (mn -> label);
74 
75    free (mn);
76 }
77 
append_menu_history(const char * label)78 static void append_menu_history (const char *label)
79 {
80    if (!label)
81       label = "";
82 
83    // First check if we've already been here
84    if (start_node)
85    {
86       MenuNode *mn = start_node;
87       do
88       {
89 	 if (global_line_counter == (mn -> line))
90 	 {
91 	    if (mn == last_node)
92 	       return;
93 
94 	    // Go to the (same) old place
95 	    last_node = mn;
96 
97 	    mn = (mn -> next);
98 	    while (mn)
99 	    {
100 	       MenuNode *t = (mn -> next);
101 	       node_delete (mn);
102 	       mn = t;
103 	    }
104 
105 	    (last_node -> next) = NULL;
106 
107 	    return;
108 	 }
109 
110 	 mn = (mn -> next);
111       }
112       while (mn);
113    }
114 
115    // Ok, append it to the history
116    if (!last_node)
117       start_node = last_node = node_new ();
118    else
119    {
120       (last_node -> next) = node_new ();
121       last_node = (last_node -> next);
122    }
123 
124    (last_node -> label) = strdup (label);
125    if (!(last_node -> label))
126    {
127       perror ("strdup");
128       fatal_error ("internal error: strdup", NULL);
129    }
130 
131    (last_node -> line) = global_line_counter;
132 }
133 
134 // Set the position of the script to the preceeding to the last_label,
135 // remove the last position of the history.
prepare_to_go_back(FILE * script)136 static void prepare_to_go_back (FILE *script)
137 {
138    MenuNode *mn = start_node;
139 
140    if (!start_node)
141       do_exit (script);	// No way back
142 
143    if (!(start_node -> next))
144       do_exit (script);	// No way back too
145 
146    // Get the previous node
147    while ((mn -> next) != last_node)
148       mn = (mn -> next);
149 
150    if (!*(start_node -> next -> label))
151       do_exit (script);
152 
153    node_delete (last_node);
154    (mn -> next) = NULL;
155    last_node = mn;
156 
157    if (!strcmp ((mn -> label), ""))
158    {
159       global_line_counter = 0;
160       rewind (script);
161    }
162    else
163       seek_label (script, (mn -> label), NULL);
164 
165    if (mn == start_node)
166    {
167       node_delete (start_node);
168       start_node = last_node = NULL;
169    }
170 }
171 
172 /* TODO: check terminal setup/reset */
do_menu(FILE * script,char * line)173 char *do_menu (FILE *script, char *line)
174 {
175   int num_items;
176   char *data, *up, *title, **labels, **descriptions;
177   int ch, i, j, k, idx;
178   int cur_choice = 0, max_width, start_y, columns;
179   int start_idx, end_idx; /* visible menu-items */
180   int items_first_column, items_per_page, real_items_per_column, spacing;
181   int has_up_label = 0;
182 
183   const int MENU_HEIGHT_MAX = LINES - 6;
184 
185   append_menu_history (__last_label);
186 
187   // Bind our former F12 key to the current menu
188   bind_F12 (__last_label);
189 
190   data = buffer_command (script, line);
191 
192   /* data has a trailing '\n' => num_items = num_newlines - 1
193      (plus one item for UP or EXIT) */
194   i = 0; j = 0;
195   while (data[i] != '\0')
196   {
197     if (data[i++] == '\n')
198       j++;
199   }
200 //  num_items = j;
201   num_items = j - 1;
202 
203   i = 0;
204   /* get UP-label if present */
205   up = NULL; /* up=NULL means top-level menu (exit-option in menu) */
206   while (isspace(data[i]))
207     i++;
208   if (strncmp (data + i, "up=", 3) == 0 ||
209       strncmp (data + i, "UP=", 3) == 0)
210   { /* I expect to see <up=LABEL> */
211     i += 3; /* start of up-label */
212     up = data + i;
213     while (data[i] != ' ')
214       i++;
215     data[i] = 0;
216     if (strcmp (up, "_exit") == 0 ||
217 	strcmp (up, "_EXIT") == 0)
218       up = NULL;
219 
220     has_up_label = 1;
221   }
222 
223   /* get title */
224   while (data[i] != '"') /* find opening " */
225     i++;
226   i++;
227   title = data + i;
228   /* find closing ": the title may contain ", so
229      we have to find the _last_ " */
230   while (data[i] != '\n')
231     i++;
232   while (data[i] != '"')
233     i--;
234   data[i] = 0; /* terminate title-string */
235 
236   /* get menu-items */
237   labels = (char**)malloc (sizeof (char*) * num_items);
238   descriptions = (char**)malloc (sizeof (char*) * num_items);
239   /* iterate through [0;num_items - 2] (the last item is for up/exit) */
240   for (k = 0; k < num_items/* - 1*/; k++)
241   {
242     while (data[i] != '\n')
243       i++;
244     /* skip '\n' and other whitespace */
245     while (isspace (data[i]))
246       i++;
247     /* get label, which ends when the description (enclosed in
248        quotes) starts */
249     labels[k] = data + i;
250     while (data[i] != '"')
251       i++;
252     j = i + 1; /* remember this position: start of description */
253     i--;
254     while (isspace (data[i]))
255       i--;
256     data[i + 1] = 0; /* terminate label-string */
257     /* get description (enclosed in double quotes) */
258     i = j;
259     descriptions[k] = data + i;
260     /* look for closing quote: the description may contain "
261        so we have to find the _last_ " */
262     while (data[i] != '\n')
263       i++;
264     while (data[i] != '"')
265       i--;
266     data[i] = 0; /* terminate description */
267   }
268 
269   /* get the longest description */
270   max_width = 0;
271   for (i = 0; i < num_items; i++)
272     max_width = max (max_width, utf8len (descriptions[i]));
273 
274   /* compute the number of columns */
275   columns = COLS / (max_width + 2); /* maximum number of columns possible */
276   while (columns > 1 && num_items / columns <= 3)
277     /* it doesn't make sense to have i.e. 4 cols each having just one item! */
278     columns--;
279 
280   /* how many item-rows are in the 1st column ? */
281   items_first_column = num_items / columns;
282   if (num_items % columns != 0)
283     items_first_column++;
284 
285   /* compute start_y */
286   if (items_first_column > MENU_HEIGHT_MAX)
287     start_y = 4;
288   else
289     start_y = (LINES - items_first_column) / 2;
290 
291   /* compute spacing: space between columns and left-right end
292      think about it: for columns=1: COLS = spacing + max_width + spacing
293      => COLS = (columns+1) * spacing + columns * max_width <=> */
294   spacing = (COLS - columns * max_width) / (columns + 1);
295 
296   /* compute items/page (for scrolling) */
297   items_per_page = min (num_items, columns *
298 			min (MENU_HEIGHT_MAX, items_first_column));
299 
300   /* find # of visible items in column */
301   real_items_per_column = items_per_page / columns;
302   if (items_per_page % columns != 0)
303     real_items_per_column++;
304 
305   /* the "viewport" (visible menu-items when scrolling)  */
306   start_idx = 0;
307   end_idx = items_per_page - 1;
308 
309   /* do clrscr only once */
310   // Preserve the top banner.
311   move (1, 0);
312   clrtobot ();
313 
314   // The menu title
315   wattron (stdscr, A_BOLD);
316   attron (COLOR_PAIR (C_MENU_TITLE));
317   mvwideaddstr (2, (80 - utf8len (title)) / 2, title);
318   attron (COLOR_PAIR (C_NORMAL));
319   wattroff (stdscr, A_BOLD);
320 
321   // The prompt at the bottom of the screen
322   mvwideaddstr (LINES - 1, 0,
323 		_(
324 "Use arrowed keys to move around, "
325 "SPACE or RETURN to select and ESCAPE to go back")
326 		);
327 
328   do
329     {
330       /* (re)display the menu */
331       for (i = 0; i < columns; i++)
332 	{
333 	  /* write 1 column */
334 	  for (j = 0; j < real_items_per_column &&
335 		 (idx = i * real_items_per_column + j + start_idx)
336 		 <= end_idx;
337 	       j++)
338 	    {
339 	      if (idx == cur_choice)
340 		wattrset (stdscr, A_REVERSE);
341 	      else
342 		wattroff (stdscr, A_REVERSE);
343 	      /* the formula for start_x:
344 		 i=0: 1*spacing + 0*max_width
345 		 i=1: 2*spacing + 1*max_width
346 		 i=2: 3*spacing + 2*max_width
347 		 i=3: 4*spacing + 3*max_width
348 		 => (i+1)*spacing + i*max_width */
349 	      mvwideaddstr (start_y + j,
350 			 (i + 1) * spacing + i * max_width,
351 			 descriptions[idx]);
352 	      for (k = max_width - utf8len (descriptions[idx]); k > 0; k--)
353 		waddch (stdscr, ' ');
354 	    }
355 	}
356 
357       wattroff (stdscr, A_REVERSE);
358 
359       get_widech( &ch );
360       switch (ch)
361 	{
362 	case KEY_UP:
363 	case 'K':
364 	case 'k':
365 	  cur_choice = max (0, cur_choice - 1);
366 	  if (cur_choice < start_idx) {
367 	    start_idx--; end_idx--;
368 	  }
369 	  break;
370 	case KEY_DOWN:
371 	case 'J':
372 	case 'j':
373 	  cur_choice = min (cur_choice + 1, num_items - 1);
374 	  if (cur_choice > end_idx) {
375 	    start_idx++; end_idx++;
376 	  }
377 	  break;
378 
379 	case KEY_PPAGE:
380 	  k = start_idx;
381 	  start_idx = max (0, start_idx - items_per_page);
382 	  end_idx += start_idx - k;
383 	  cur_choice += start_idx - k;
384 	  break;
385 	case KEY_NPAGE:
386 	  k = end_idx;
387 	  end_idx = min (end_idx + items_per_page, num_items - 1);
388 	  start_idx += end_idx - k;
389 	  cur_choice += end_idx - k;
390 	  break;
391 
392 	case KEY_RIGHT:
393 	case 'l':
394 	case 'L':
395 	  if (cur_choice + real_items_per_column < end_idx)
396 	     cur_choice += real_items_per_column;
397 	  else
398 	  {
399 	     k = end_idx;
400 	     end_idx = min (end_idx + items_per_page, num_items - 1);
401 	     start_idx += end_idx - k;
402 	     if (end_idx - k)
403 	        cur_choice += end_idx - k;
404 	     else
405 		cur_choice = num_items - 1;
406 	  }
407 
408 	  break;
409 
410 	case ASCII_NL:
411 	case ASCII_SPACE:
412 	  ch = KEY_ENTER;
413 	case KEY_ENTER:
414 	  break;
415 
416 	case KEY_LEFT:
417 	case 'h':
418 	case 'H':
419 	  if (cur_choice - real_items_per_column >= start_idx)
420 	     cur_choice -= real_items_per_column;
421 	  else
422 	  {
423 	     k = start_idx;
424 	     start_idx = max (0, start_idx - items_per_page);
425 	     end_idx += start_idx - k;
426 	     if (start_idx - k)
427 	        cur_choice += start_idx - k;
428 	     else
429 		cur_choice = 0;
430 	  }
431 	  break;
432 
433 	case KEY_CANCEL: // anyone knows where is this key on a PC keyboard?
434 	case ASCII_ESC:
435 	case 'q':
436 	case 'Q':
437 	  if (has_up_label)
438              seek_label (script, up, NULL);
439 	  else
440 	     prepare_to_go_back (script);
441 	  goto cleanup;
442 
443 	default:
444 	  // printf ("libncurses think that it's key \\%o\n", ch);
445 	  break;
446 	}
447 
448     } while (ch != KEY_ENTER);
449 
450   wattroff (stdscr, A_REVERSE);
451   if (labels[cur_choice] != NULL)
452   {
453     seek_label (script, labels[cur_choice], NULL);
454     get_script_line( script, line );
455   }
456   else
457     do_exit (script);
458 
459 cleanup:
460   free (labels);
461   free (descriptions);
462   free (data);
463 
464   return NULL;
465 }
466 
467 /*
468   Local Variables:
469   tab-width: 8
470   End:
471 */
472