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