xref: /netbsd-src/lib/libmenu/internals.c (revision aaf4ece63a859a04e37cf3a7229b5fab0157cc06)
1 /*	$NetBSD: internals.c,v 1.12 2003/03/09 01:08:48 lukem Exp $	*/
2 
3 /*-
4  * Copyright (c) 1998-1999 Brett Lymn (blymn@baea.com.au, brett_lymn@yahoo.com.au)
5  * All rights reserved.
6  *
7  * Redistribution and use in source and binary forms, with or without
8  * modification, are permitted provided that the following conditions
9  * are met:
10  * 1. Redistributions of source code must retain the above copyright
11  *    notice, this list of conditions and the following disclaimer.
12  * 2. The name of the author may not be used to endorse or promote products
13  *    derived from this software without specific prior written permission
14  *
15  * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
16  * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
17  * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
18  * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
19  * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
20  * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
21  * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
22  * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
23  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
24  * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
25  *
26  *
27  */
28 
29 #include <sys/cdefs.h>
30 __RCSID("$NetBSD: internals.c,v 1.12 2003/03/09 01:08:48 lukem Exp $");
31 
32 #include <menu.h>
33 #include <ctype.h>
34 #include <stdlib.h>
35 #include <string.h>
36 #include "internals.h"
37 
38 /* internal function prototypes */
39 static void
40 _menui_calc_neighbours(MENU *menu, int item_no, int cycle, int item_rows,
41 		       int item_cols, ITEM **next, ITEM **prev,
42 			ITEM **major_next, ITEM **major_prev);
43 static void _menui_redraw_menu(MENU *menu, int old_top_row, int old_cur_item);
44 
45   /*
46    * Link all the menu items together to speed up navigation.  We need
47    * to calculate the widest item entry, then work out how many columns
48    * of items the window will accomodate and then how many rows there will
49    * be.  Once the layout is determined the neighbours of each item is
50    * calculated and the item structures updated.
51    */
52 int
53 _menui_stitch_items(MENU *menu)
54 {
55 	int i, cycle, row_major;
56 
57 	cycle = ((menu->opts & O_NONCYCLIC) != O_NONCYCLIC);
58 	row_major = ((menu->opts & O_ROWMAJOR) == O_ROWMAJOR);
59 
60 	if (menu->posted == 1)
61 		return E_POSTED;
62 	if (menu->items == NULL)
63 		return E_BAD_ARGUMENT;
64 
65 	if (row_major) {
66 		menu->item_rows = menu->item_count / menu->cols;
67 		menu->item_cols = menu->cols;
68 		if (menu->item_count > (menu->item_rows * menu->item_cols))
69 			menu->item_rows += 1;
70 	} else {
71 		menu->item_cols = menu->item_count / menu->rows;
72 		menu->item_rows = menu->rows;
73 		if (menu->item_count > (menu->item_rows * menu->item_cols))
74 			menu->item_cols += 1;
75 	}
76 
77 
78 	_menui_max_item_size(menu);
79 
80 	for (i = 0; i < menu->item_count; i++) {
81 		  /* Calculate the neighbours.  The ugliness here deals with
82 		   * the differing menu layout styles.  The layout affects
83 		   * the neighbour calculation so we change the arguments
84 		   * around depending on the layout style.
85 		   */
86 		_menui_calc_neighbours(menu, i, cycle,
87 					(row_major) ? menu->item_rows
88 					: menu->item_cols,
89 					(row_major) ? menu->item_cols
90 					: menu->item_rows,
91 					(row_major) ? &menu->items[i]->right
92 					: &menu->items[i]->down,
93 					(row_major) ? &menu->items[i]->left
94 					: &menu->items[i]->up,
95 					(row_major) ? &menu->items[i]->down
96 					: &menu->items[i]->right,
97 					(row_major) ? &menu->items[i]->up
98 					: &menu->items[i]->left);
99 
100 		  /* fill in the row and column value of the item */
101 		if (row_major) {
102 			menu->items[i]->row = i / menu->item_cols;
103 			menu->items[i]->col = i % menu->item_cols;
104 		} else {
105 			menu->items[i]->row = i % menu->item_rows;
106 			menu->items[i]->col = i / menu->item_rows;
107 		}
108 	}
109 
110 	return E_OK;
111 }
112 
113   /*
114    * Calculate the neighbours for an item in menu.  This routine deliberately
115    * does not refer to up/down/left/right as these concepts depend on the menu
116    * layout style (row major or not).  By arranging the arguments in the right
117    * order the caller can generate the neighbours for either menu layout style.
118    */
119 static void
120 _menui_calc_neighbours(MENU *menu, int item_no, int cycle, int item_rows,
121 		       int item_cols, ITEM **next, ITEM **prev,
122 		       ITEM **major_next, ITEM **major_prev)
123 {
124 	int neighbour;
125 
126 	if (item_rows < 2) {
127 		if (cycle) {
128 			*major_next = menu->items[item_no];
129 			*major_prev = menu->items[item_no];
130 		} else {
131 			*major_next = NULL;
132 			*major_prev = NULL;
133 		}
134 	} else {
135 		neighbour = item_no + item_cols;
136 		if (neighbour >= menu->item_count) {
137 			if (cycle) {
138 				if (item_rows == 2) {
139 					neighbour = item_no - item_cols;
140 					if (neighbour < 0)
141 						neighbour = item_no;
142 					*major_next = menu->items[neighbour];
143 				} else {
144 					*major_next =
145 						menu->items[item_no % item_cols];
146 				}
147 			} else
148 				*major_next = NULL;
149 		} else
150 			*major_next = menu->items[neighbour];
151 
152 
153 		neighbour = item_no - item_cols;
154 		if (neighbour < 0) {
155 			if (cycle) {
156 				if (item_rows == 2) {
157 					neighbour = item_no + item_cols;
158 					if (neighbour >= menu->item_count)
159 						neighbour = item_no;
160 					*major_prev = menu->items[neighbour];
161 				} else {
162 					neighbour = item_no +
163 						(item_rows - 1) * item_cols;
164 
165 					if (neighbour >= menu->item_count)
166 						neighbour = item_no +
167 							(item_rows - 2)
168 							* item_cols;
169 
170 					*major_prev = menu->items[neighbour];
171 				}
172 			} else
173 				*major_prev = NULL;
174 		} else
175 			*major_prev = menu->items[neighbour];
176 	}
177 
178 	if ((item_no % item_cols) == 0) {
179 		if (cycle) {
180 			if (item_cols  < 2) {
181 				*prev = menu->items[item_no];
182 			} else {
183 				neighbour = item_no + item_cols - 1;
184 				if (neighbour >= menu->item_count) {
185 					if (item_cols == 2) {
186 						*prev = menu->items[item_no];
187 					} else {
188 						*prev = menu->items[menu->item_count - 1];
189 					}
190 				} else
191 					*prev = menu->items[neighbour];
192 			}
193 		} else
194 			*prev = NULL;
195 	} else
196 		*prev = menu->items[item_no - 1];
197 
198 	if ((item_no % item_cols) == (item_cols - 1)) {
199 		if (cycle) {
200 			if (item_cols  < 2) {
201 				*next = menu->items[item_no];
202 			} else {
203 				neighbour = item_no - item_cols + 1;
204 				if (neighbour >= menu->item_count) {
205 					if (item_cols == 2) {
206 						*next = menu->items[item_no];
207 					} else {
208 						neighbour = item_cols * item_no / item_cols;
209 
210 						*next = menu->items[neighbour];
211 					}
212 				} else
213 					*next = menu->items[neighbour];
214 			}
215 		} else
216 			*next = NULL;
217 	} else {
218 		neighbour = item_no + 1;
219 		if (neighbour >= menu->item_count) {
220 			if (cycle) {
221 				neighbour = item_cols * (item_rows - 1);
222 				*next = menu->items[neighbour];
223 			} else
224 				*next = NULL;
225 		} else
226 			*next = menu->items[neighbour];
227 	}
228 }
229 
230 /*
231  * Goto the item pointed to by item and adjust the menu structure
232  * accordingly.  Call the term and init functions if required.
233  */
234 int
235 _menui_goto_item(MENU *menu, ITEM *item, int new_top_row)
236 {
237 	int old_top_row = menu->top_row, old_cur_item = menu->cur_item;
238 
239 	  /* If we get a null then the menu is not cyclic so deny request */
240 	if (item == NULL)
241 		return E_REQUEST_DENIED;
242 
243 	menu->in_init = 1;
244 	if (menu->top_row != new_top_row) {
245 		if ((menu->posted == 1) && (menu->menu_term != NULL))
246 			menu->menu_term(menu);
247 		menu->top_row = new_top_row;
248 
249 		if ((menu->posted == 1) && (menu->menu_init != NULL))
250 			menu->menu_init(menu);
251 	}
252 
253 	  /* this looks like wasted effort but it can happen.... */
254 	if (menu->cur_item != item->index) {
255 
256 		if ((menu->posted == 1) && (menu->item_term != NULL))
257 			menu->item_term(menu);
258 
259 		menu->cur_item = item->index;
260 		menu->cur_row = item->row;
261 		menu->cur_col = item->col;
262 
263 		if (menu->posted == 1)
264 			_menui_redraw_menu(menu, old_top_row, old_cur_item);
265 
266 		if ((menu->posted == 1) && (menu->item_init != NULL))
267 			menu->item_init(menu);
268 
269 	}
270 
271 	menu->in_init = 0;
272 	return E_OK;
273 }
274 
275 /*
276  * Attempt to match items with the pattern buffer in the direction given
277  * by iterating over the menu items.  If a match is found return E_OK
278  * otherwise return E_NO_MATCH
279  */
280 int
281 _menui_match_items(MENU *menu, int direction, int *item_matched)
282 {
283 	int i, caseless;
284 
285 	caseless = ((menu->opts & O_IGNORECASE) == O_IGNORECASE);
286 
287 	i = menu->cur_item;
288 	if (direction == MATCH_NEXT_FORWARD) {
289 		if (++i >= menu->item_count) i = 0;
290 	} else if (direction == MATCH_NEXT_REVERSE) {
291 		if (--i < 0) i = menu->item_count - 1;
292 	}
293 
294 
295 	do {
296 		if (menu->items[i]->name.length >= menu->plen) {
297 			  /* no chance if pattern is longer */
298 			if (caseless) {
299 				if (strncasecmp(menu->items[i]->name.string,
300 						menu->pattern,
301 						(size_t) menu->plen) == 0) {
302 					*item_matched = i;
303 					menu->match_len = menu->plen;
304 					return E_OK;
305 				}
306 			} else {
307 				if (strncmp(menu->items[i]->name.string,
308 					    menu->pattern,
309 					    (size_t) menu->plen) == 0) {
310 					*item_matched = i;
311 					menu->match_len = menu->plen;
312 					return E_OK;
313 				}
314 			}
315 		}
316 
317 		if ((direction == MATCH_FORWARD) ||
318 		    (direction == MATCH_NEXT_FORWARD)) {
319 			if (++i >= menu->item_count) i = 0;
320 		} else {
321 			if (--i <= 0) i = menu->item_count - 1;
322 		}
323 	} while (i != menu->cur_item);
324 
325 	menu->match_len = 0; /* match did not succeed - kill the match len. */
326 	return E_NO_MATCH;
327 }
328 
329 /*
330  * Attempt to match the pattern buffer against the items.  If c is a
331  * printable character then add it to the pattern buffer prior to
332  * performing the match.  Direction determines the direction of matching.
333  * If the match is successful update the item_matched variable with the
334  * index of the item that matched the pattern.
335  */
336 int
337 _menui_match_pattern(MENU *menu, int c, int direction, int *item_matched)
338 {
339 	if (menu == NULL)
340 		return E_BAD_ARGUMENT;
341 	if (menu->items == NULL)
342 		return E_BAD_ARGUMENT;
343 	if (*menu->items == NULL)
344 		return E_BAD_ARGUMENT;
345 
346 	if (isprint(c)) {
347 		  /* add char to buffer - first allocate room for it */
348 		if ((menu->pattern = (char *)
349 		     realloc(menu->pattern,
350 			     menu->plen + sizeof(char) +
351 			     ((menu->plen > 0)? 0 : 1)))
352 		    == NULL)
353 			return E_SYSTEM_ERROR;
354 		menu->pattern[menu->plen] = c;
355 		menu->pattern[++menu->plen] = '\0';
356 
357 		  /* there is no chance of a match if pattern is longer
358 		     than all the items */
359 		if (menu->plen >= menu->max_item_width) {
360 			menu->pattern[--menu->plen] = '\0';
361 			return E_NO_MATCH;
362 		}
363 
364 		if (_menui_match_items(menu, direction,
365 					item_matched) == E_NO_MATCH) {
366 			menu->pattern[--menu->plen] = '\0';
367 			return E_NO_MATCH;
368 		} else
369 			return E_OK;
370 	} else {
371 		if (_menui_match_items(menu, direction,
372 					item_matched) == E_OK) {
373 			return E_OK;
374 		} else {
375 			return E_NO_MATCH;
376 		}
377 	}
378 }
379 
380 /*
381  * Draw an item in the subwindow complete with appropriate highlighting.
382  */
383 void
384 _menui_draw_item(MENU *menu, int item)
385 {
386 	int j, pad_len, mark_len;
387 
388 	mark_len = max(menu->mark.length, menu->unmark.length);
389 
390 	wmove(menu->scrwin,
391 	      menu->items[item]->row - menu->top_row,
392 	      menu->items[item]->col * (menu->col_width + 1));
393 
394 	if (menu->cur_item == item)
395 		wattrset(menu->scrwin, menu->fore);
396 	if ((menu->items[item]->opts & O_SELECTABLE) != O_SELECTABLE)
397 		wattron(menu->scrwin, menu->grey);
398 
399 	  /* deal with the menu mark, if  one is set.
400 	   * We mark the selected items and write blanks for
401 	   * all others unless the menu unmark string is set in which
402 	   * case the unmark string is written.
403 	   */
404 	if (menu->items[item]->selected == 1) {
405 		if (menu->mark.string != NULL) {
406 			for (j = 0; j < menu->mark.length; j++) {
407 				waddch(menu->scrwin,
408 				       menu->mark.string[j]);
409 			}
410 		}
411 		  /* blank any length difference between mark & unmark */
412 		for (j = menu->mark.length; j < mark_len; j++)
413 			waddch(menu->scrwin, ' ');
414 	} else {
415 		if (menu->unmark.string != NULL) {
416 			for (j = 0; j < menu->unmark.length; j++) {
417 				waddch(menu->scrwin,
418 				       menu->unmark.string[j]);
419 			}
420 		}
421 		  /* blank any length difference between mark & unmark */
422 		for (j = menu->unmark.length; j < mark_len; j++)
423 			waddch(menu->scrwin, ' ');
424 	}
425 
426 	  /* add the menu name */
427 	for (j=0; j < menu->items[item]->name.length; j++)
428 		waddch(menu->scrwin,
429 		       menu->items[item]->name.string[j]);
430 
431 	pad_len = menu->col_width - menu->items[item]->name.length
432 		- mark_len - 1;
433 	if ((menu->opts & O_SHOWDESC) == O_SHOWDESC) {
434 		pad_len -= menu->items[item]->description.length - 1;
435 		for (j = 0; j < pad_len; j++)
436 			waddch(menu->scrwin, menu->pad);
437 		for (j = 0; j < menu->items[item]->description.length; j++) {
438 			waddch(menu->scrwin,
439 			       menu->items[item]->description.string[j]);
440 		}
441 	} else {
442 		for (j = 0; j < pad_len; j++)
443 			waddch(menu->scrwin, ' ');
444 	}
445 	menu->items[item]->visible = 1;
446 
447 	  /* kill any special attributes... */
448 	wattrset(menu->scrwin, menu->back);
449 
450 	  /*
451 	   * Fill in the spacing between items, annoying but it looks
452 	   * odd if the menu items are inverse because the spacings do not
453 	   * have the same attributes as the items.
454 	   */
455 	if ((menu->items[item]->col > 0) &&
456 	    (menu->items[item]->col < (menu->item_cols - 1))) {
457 		wmove(menu->scrwin,
458 		      menu->items[item]->row - menu->top_row,
459 		      menu->items[item]->col * (menu->col_width + 1) - 1);
460 		waddch(menu->scrwin, ' ');
461 	}
462 
463 	  /* and position the cursor nicely */
464 	pos_menu_cursor(menu);
465 }
466 
467 /*
468  * Draw the menu in the subwindow provided.
469  */
470 int
471 _menui_draw_menu(MENU *menu)
472 {
473 	int rowmajor, i, j, max_items, last_item, row = -1, col = -1;
474 
475 	rowmajor = ((menu->opts & O_ROWMAJOR) == O_ROWMAJOR);
476 
477 	for (i = 0;  i < menu->item_count; i++) {
478 		if (menu->items[i]->row == menu->top_row)
479 			break;
480 		menu->items[i]->visible = 0;
481 	}
482 
483 	wmove(menu->scrwin, 0, 0);
484 
485 	menu->col_width = getmaxx(menu->scrwin) / menu->cols;
486 
487 	max_items = menu->rows * menu->cols;
488 	last_item = ((max_items + i) > menu->item_count) ? menu->item_count :
489 		(max_items + i);
490 
491 	for (; i < last_item; i++) {
492 		if (i > menu->item_count) {
493 			  /* no more items to draw, write background blanks */
494 			wattrset(menu->scrwin, menu->back);
495 			if (row < 0) {
496 				row = menu->items[menu->item_count - 1]->row;
497 				col = menu->items[menu->item_count - 1]->col;
498 			}
499 
500 			if (rowmajor) {
501 				col++;
502 				if (col > menu->cols) {
503 					col = 0;
504 					row++;
505 				}
506 			} else {
507 				row++;
508 				if (row > menu->rows) {
509 					row = 0;
510 					col++;
511 				}
512 			}
513 			wmove(menu->scrwin, row,
514 			      col * (menu->col_width + 1));
515 			for (j = 0; j < menu->col_width; j++)
516 				waddch(menu->scrwin, ' ');
517 		} else {
518 			_menui_draw_item(menu, i);
519 
520 		}
521 
522 	}
523 
524 	if (last_item < menu->item_count) {
525 		for (j = last_item; j < menu->item_count; j++)
526 			menu->items[j]->visible = 0;
527 	}
528 
529 	return E_OK;
530 }
531 
532 
533 /*
534  * Calculate the widest menu item and stash it in the menu struct.
535  *
536  */
537 void
538 _menui_max_item_size(MENU *menu)
539 {
540 	int i, with_desc, width;
541 
542 	with_desc = ((menu->opts & O_SHOWDESC) == O_SHOWDESC);
543 
544 	for (i = 0; i < menu->item_count; i++) {
545 		width = menu->items[i]->name.length
546 			+ max(menu->mark.length, menu->unmark.length);
547 		if (with_desc)
548 			width += menu->items[i]->description.length + 1;
549 
550 		menu->max_item_width = max(menu->max_item_width, width);
551 	}
552 }
553 
554 
555 /*
556  * Redraw the menu on the screen.  If the current item has changed then
557  * unhighlight the old item and highlight the new one.
558  */
559 static void
560 _menui_redraw_menu(MENU *menu, int old_top_row, int old_cur_item)
561 {
562 
563 	if (menu->top_row != old_top_row) {
564 		  /* top row changed - redo the whole menu
565 		   * XXXX this could be improved if we had wscrl implemented.
566 
567 		   * XXXX we could scroll the window and just fill in the
568 		   * XXXX changed lines.
569 		   */
570 		wclear(menu->scrwin);
571 		_menui_draw_menu(menu);
572 	} else {
573 		if (menu->cur_item != old_cur_item) {
574 			  /* redo the old item as a normal one. */
575 			_menui_draw_item(menu, old_cur_item);
576 		}
577 		  /* and then redraw the current item */
578 		_menui_draw_item(menu, menu->cur_item);
579 	}
580 }
581