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