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