xref: /freebsd/contrib/ncurses/menu/m_driver.c (revision e6bfd18d21b225af6a0ed67ceeaf1293b7b9eba5)
1 /****************************************************************************
2  * Copyright 2020 Thomas E. Dickey                                          *
3  * Copyright 1998-2012,2016 Free Software Foundation, Inc.                  *
4  *                                                                          *
5  * Permission is hereby granted, free of charge, to any person obtaining a  *
6  * copy of this software and associated documentation files (the            *
7  * "Software"), to deal in the Software without restriction, including      *
8  * without limitation the rights to use, copy, modify, merge, publish,      *
9  * distribute, distribute with modifications, sublicense, and/or sell       *
10  * copies of the Software, and to permit persons to whom the Software is    *
11  * furnished to do so, subject to the following conditions:                 *
12  *                                                                          *
13  * The above copyright notice and this permission notice shall be included  *
14  * in all copies or substantial portions of the Software.                   *
15  *                                                                          *
16  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS  *
17  * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF               *
18  * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.   *
19  * IN NO EVENT SHALL THE ABOVE COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,   *
20  * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR    *
21  * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR    *
22  * THE USE OR OTHER DEALINGS IN THE SOFTWARE.                               *
23  *                                                                          *
24  * Except as contained in this notice, the name(s) of the above copyright   *
25  * holders shall not be used in advertising or otherwise to promote the     *
26  * sale, use or other dealings in this Software without prior written       *
27  * authorization.                                                           *
28  ****************************************************************************/
29 
30 /****************************************************************************
31  *   Author:  Juergen Pfeifer, 1995,1997                                    *
32  ****************************************************************************/
33 
34 /***************************************************************************
35 * Module m_driver                                                          *
36 * Central dispatching routine                                              *
37 ***************************************************************************/
38 
39 #include "menu.priv.h"
40 
41 MODULE_ID("$Id: m_driver.c,v 1.36 2020/12/12 00:38:02 tom Exp $")
42 
43 /* Macros */
44 
45 /* Remove the last character from the match pattern buffer */
46 #define Remove_Character_From_Pattern(menu) \
47   (menu)->pattern[--((menu)->pindex)] = '\0'
48 
49 /* Add a new character to the match pattern buffer */
50 #define Add_Character_To_Pattern(menu,ch) \
51   { (menu)->pattern[((menu)->pindex)++] = (char) (ch);\
52     (menu)->pattern[(menu)->pindex] = '\0'; }
53 
54 /*---------------------------------------------------------------------------
55 |   Facility      :  libnmenu
56 |   Function      :  static bool Is_Sub_String(
57 |                           bool IgnoreCaseFlag,
58 |                           const char *part,
59 |                           const char *string)
60 |
61 |   Description   :  Checks whether or not part is a substring of string.
62 |
63 |   Return Values :  TRUE   - if it is a substring
64 |                    FALSE  - if it is not a substring
65 +--------------------------------------------------------------------------*/
66 static bool
67 Is_Sub_String(
68 	       bool IgnoreCaseFlag,
69 	       const char *part,
70 	       const char *string
71 )
72 {
73   assert(part && string);
74   if (IgnoreCaseFlag)
75     {
76       while (*string && *part)
77 	{
78 	  if (toupper(UChar(*string++)) != toupper(UChar(*part)))
79 	    break;
80 	  part++;
81 	}
82     }
83   else
84     {
85       while (*string && *part)
86 	if (*part != *string++)
87 	  break;
88       part++;
89     }
90   return ((*part) ? FALSE : TRUE);
91 }
92 
93 /*---------------------------------------------------------------------------
94 |   Facility      :  libnmenu
95 |   Function      :  int _nc_Match_Next_Character_In_Item_Name(
96 |                           MENU *menu,
97 |                           int  ch,
98 |                           ITEM **item)
99 |
100 |   Description   :  This internal routine is called for a menu positioned
101 |                    at an item with three different classes of characters:
102 |                       - a printable character; the character is added to
103 |                         the current pattern and the next item matching
104 |                         this pattern is searched.
105 |                       - NUL; the pattern stays as it is and the next item
106 |                         matching the pattern is searched
107 |                       - BS; the pattern stays as it is and the previous
108 |                         item matching the pattern is searched
109 |
110 |                       The item parameter contains on call a pointer to
111 |                       the item where the search starts. On return - if
112 |                       a match was found - it contains a pointer to the
113 |                       matching item.
114 |
115 |   Return Values :  E_OK        - an item matching the pattern was found
116 |                    E_NO_MATCH  - nothing found
117 +--------------------------------------------------------------------------*/
118 MENU_EXPORT(int)
119 _nc_Match_Next_Character_In_Item_Name
120 (MENU *menu, int ch, ITEM **item)
121 {
122   bool found = FALSE, passed = FALSE;
123   int idx, last;
124 
125   T((T_CALLED("_nc_Match_Next_Character(%p,%d,%p)"),
126      (void *)menu, ch, (void *)item));
127 
128   assert(menu && item && *item);
129   idx = (*item)->index;
130 
131   if (ch && ch != BS)
132     {
133       /* if we become to long, we need no further checking : there can't be
134          a match ! */
135       if ((menu->pindex + 1) > menu->namelen)
136 	RETURN(E_NO_MATCH);
137 
138       Add_Character_To_Pattern(menu, ch);
139       /* we artificially position one item back, because in the do...while
140          loop we start with the next item. This means, that with a new
141          pattern search we always start the scan with the actual item. If
142          we do a NEXT_PATTERN or PREV_PATTERN search, we start with the
143          one after or before the actual item. */
144       if (--idx < 0)
145 	idx = menu->nitems - 1;
146     }
147 
148   last = idx;			/* this closes the cycle */
149 
150   do
151     {
152       if (ch == BS)
153 	{			/* we have to go backward */
154 	  if (--idx < 0)
155 	    idx = menu->nitems - 1;
156 	}
157       else
158 	{			/* otherwise we always go forward */
159 	  if (++idx >= menu->nitems)
160 	    idx = 0;
161 	}
162       if (Is_Sub_String((bool)((menu->opt & O_IGNORECASE) != 0),
163 			menu->pattern,
164 			menu->items[idx]->name.str)
165 	)
166 	found = TRUE;
167       else
168 	passed = TRUE;
169     }
170   while (!found && (idx != last));
171 
172   if (found)
173     {
174       if (!((idx == (*item)->index) && passed))
175 	{
176 	  *item = menu->items[idx];
177 	  RETURN(E_OK);
178 	}
179       /* This point is reached, if we fully cycled through the item list
180          and the only match we found is the starting item. With a NEXT_PATTERN
181          or PREV_PATTERN scan this means, that there was no additional match.
182          If we searched with an expanded new pattern, we should never reach
183          this point, because if the expanded pattern matches also the actual
184          item we will find it in the first attempt (passed==FALSE) and we
185          will never cycle through the whole item array.
186        */
187       assert(ch == 0 || ch == BS);
188     }
189   else
190     {
191       if (ch && ch != BS && menu->pindex > 0)
192 	{
193 	  /* if we had no match with a new pattern, we have to restore it */
194 	  Remove_Character_From_Pattern(menu);
195 	}
196     }
197   RETURN(E_NO_MATCH);
198 }
199 
200 /*---------------------------------------------------------------------------
201 |   Facility      :  libnmenu
202 |   Function      :  int menu_driver(MENU* menu, int c)
203 |
204 |   Description   :  Central dispatcher for the menu. Translates the logical
205 |                    request 'c' into a menu action.
206 |
207 |   Return Values :  E_OK            - success
208 |                    E_BAD_ARGUMENT  - invalid menu pointer
209 |                    E_BAD_STATE     - menu is in user hook routine
210 |                    E_NOT_POSTED    - menu is not posted
211 +--------------------------------------------------------------------------*/
212 MENU_EXPORT(int)
213 menu_driver(MENU *menu, int c)
214 {
215 #define NAVIGATE(dir) \
216   if (!item->dir)\
217      result = E_REQUEST_DENIED;\
218   else\
219      item = item->dir
220 
221   int result = E_OK;
222   ITEM *item;
223   int my_top_row, rdiff;
224 
225   T((T_CALLED("menu_driver(%p,%d)"), (void *)menu, c));
226 
227   if (!menu)
228     RETURN(E_BAD_ARGUMENT);
229 
230   if (menu->status & _IN_DRIVER)
231     RETURN(E_BAD_STATE);
232   if (!(menu->status & _POSTED))
233     RETURN(E_NOT_POSTED);
234 
235   item = menu->curitem;
236 
237   my_top_row = menu->toprow;
238   assert(item);
239 
240   if ((c > KEY_MAX) && (c <= MAX_MENU_COMMAND))
241     {
242       if (!((c == REQ_BACK_PATTERN)
243 	    || (c == REQ_NEXT_MATCH) || (c == REQ_PREV_MATCH)))
244 	{
245 	  assert(menu->pattern);
246 	  Reset_Pattern(menu);
247 	}
248 
249       switch (c)
250 	{
251 	case REQ_LEFT_ITEM:
252 	    /*=================*/
253 	  NAVIGATE(left);
254 	  break;
255 
256 	case REQ_RIGHT_ITEM:
257 	    /*==================*/
258 	  NAVIGATE(right);
259 	  break;
260 
261 	case REQ_UP_ITEM:
262 	    /*===============*/
263 	  NAVIGATE(up);
264 	  break;
265 
266 	case REQ_DOWN_ITEM:
267 	    /*=================*/
268 	  NAVIGATE(down);
269 	  break;
270 
271 	case REQ_SCR_ULINE:
272 	    /*=================*/
273 	  if (my_top_row == 0 || !(item->up))
274 	    result = E_REQUEST_DENIED;
275 	  else
276 	    {
277 	      --my_top_row;
278 	      item = item->up;
279 	    }
280 	  break;
281 
282 	case REQ_SCR_DLINE:
283 	    /*=================*/
284 	  if ((my_top_row + menu->arows >= menu->rows) || !(item->down))
285 	    {
286 	      /* only if the menu has less items than rows, we can deny the
287 	         request. Otherwise the epilogue of this routine adjusts the
288 	         top row if necessary */
289 	      result = E_REQUEST_DENIED;
290 	    }
291 	  else
292 	    {
293 	      my_top_row++;
294 	      item = item->down;
295 	    }
296 	  break;
297 
298 	case REQ_SCR_DPAGE:
299 	    /*=================*/
300 	  rdiff = menu->rows - (menu->arows + my_top_row);
301 	  if (rdiff > menu->arows)
302 	    rdiff = menu->arows;
303 	  if (rdiff <= 0)
304 	    result = E_REQUEST_DENIED;
305 	  else
306 	    {
307 	      my_top_row += rdiff;
308 	      while (rdiff-- > 0 && item != 0 && item->down != 0)
309 		item = item->down;
310 	    }
311 	  break;
312 
313 	case REQ_SCR_UPAGE:
314 	    /*=================*/
315 	  rdiff = (menu->arows < my_top_row) ? menu->arows : my_top_row;
316 	  if (rdiff <= 0)
317 	    result = E_REQUEST_DENIED;
318 	  else
319 	    {
320 	      my_top_row -= rdiff;
321 	      while (rdiff-- > 0 && item != 0 && item->up != 0)
322 		item = item->up;
323 	    }
324 	  break;
325 
326 	case REQ_FIRST_ITEM:
327 	    /*==================*/
328 	  item = menu->items[0];
329 	  break;
330 
331 	case REQ_LAST_ITEM:
332 	    /*=================*/
333 	  item = menu->items[menu->nitems - 1];
334 	  break;
335 
336 	case REQ_NEXT_ITEM:
337 	    /*=================*/
338 	  if ((item->index + 1) >= menu->nitems)
339 	    {
340 	      if (menu->opt & O_NONCYCLIC)
341 		result = E_REQUEST_DENIED;
342 	      else
343 		item = menu->items[0];
344 	    }
345 	  else
346 	    item = menu->items[item->index + 1];
347 	  break;
348 
349 	case REQ_PREV_ITEM:
350 	    /*=================*/
351 	  if (item->index <= 0)
352 	    {
353 	      if (menu->opt & O_NONCYCLIC)
354 		result = E_REQUEST_DENIED;
355 	      else
356 		item = menu->items[menu->nitems - 1];
357 	    }
358 	  else
359 	    item = menu->items[item->index - 1];
360 	  break;
361 
362 	case REQ_TOGGLE_ITEM:
363 	    /*===================*/
364 	  if (menu->opt & O_ONEVALUE)
365 	    {
366 	      result = E_REQUEST_DENIED;
367 	    }
368 	  else
369 	    {
370 	      if (menu->curitem->opt & O_SELECTABLE)
371 		{
372 		  menu->curitem->value = !menu->curitem->value;
373 		  Move_And_Post_Item(menu, menu->curitem);
374 		  _nc_Show_Menu(menu);
375 		}
376 	      else
377 		result = E_NOT_SELECTABLE;
378 	    }
379 	  break;
380 
381 	case REQ_CLEAR_PATTERN:
382 	    /*=====================*/
383 	  /* already cleared in prologue */
384 	  break;
385 
386 	case REQ_BACK_PATTERN:
387 	    /*====================*/
388 	  if (menu->pindex > 0)
389 	    {
390 	      assert(menu->pattern);
391 	      Remove_Character_From_Pattern(menu);
392 	      pos_menu_cursor(menu);
393 	    }
394 	  else
395 	    result = E_REQUEST_DENIED;
396 	  break;
397 
398 	case REQ_NEXT_MATCH:
399 	    /*==================*/
400 	  assert(menu->pattern);
401 	  if (menu->pattern[0])
402 	    result = _nc_Match_Next_Character_In_Item_Name(menu, 0, &item);
403 	  else
404 	    {
405 	      if ((item->index + 1) < menu->nitems)
406 		item = menu->items[item->index + 1];
407 	      else
408 		{
409 		  if (menu->opt & O_NONCYCLIC)
410 		    result = E_REQUEST_DENIED;
411 		  else
412 		    item = menu->items[0];
413 		}
414 	    }
415 	  break;
416 
417 	case REQ_PREV_MATCH:
418 	    /*==================*/
419 	  assert(menu->pattern);
420 	  if (menu->pattern[0])
421 	    result = _nc_Match_Next_Character_In_Item_Name(menu, BS, &item);
422 	  else
423 	    {
424 	      if (item->index)
425 		item = menu->items[item->index - 1];
426 	      else
427 		{
428 		  if (menu->opt & O_NONCYCLIC)
429 		    result = E_REQUEST_DENIED;
430 		  else
431 		    item = menu->items[menu->nitems - 1];
432 		}
433 	    }
434 	  break;
435 
436 	default:
437 	    /*======*/
438 	  result = E_UNKNOWN_COMMAND;
439 	  break;
440 	}
441     }
442   else
443     {				/* not a command */
444       if (!(c & ~((int)MAX_REGULAR_CHARACTER)) && isprint(UChar(c)))
445 	result = _nc_Match_Next_Character_In_Item_Name(menu, c, &item);
446 #ifdef NCURSES_MOUSE_VERSION
447       else if (KEY_MOUSE == c)
448 	{
449 	  MEVENT event;
450 	  WINDOW *uwin = Get_Menu_UserWin(menu);
451 
452 	  getmouse(&event);
453 	  if ((event.bstate & (BUTTON1_CLICKED |
454 			       BUTTON1_DOUBLE_CLICKED |
455 			       BUTTON1_TRIPLE_CLICKED))
456 	      && wenclose(uwin, event.y, event.x))
457 	    {			/* we react only if the click was in the userwin, that means
458 				 * inside the menu display area or at the decoration window.
459 				 */
460 	      WINDOW *sub = Get_Menu_Window(menu);
461 	      int ry = event.y, rx = event.x;	/* screen coordinates */
462 
463 	      result = E_REQUEST_DENIED;
464 	      if (mouse_trafo(&ry, &rx, FALSE))
465 		{		/* rx, ry are now "curses" coordinates */
466 		  if (ry < sub->_begy)
467 		    {		/* we clicked above the display region; this is
468 				 * interpreted as "scroll up" request
469 				 */
470 		      if (event.bstate & BUTTON1_CLICKED)
471 			result = menu_driver(menu, REQ_SCR_ULINE);
472 		      else if (event.bstate & BUTTON1_DOUBLE_CLICKED)
473 			result = menu_driver(menu, REQ_SCR_UPAGE);
474 		      else if (event.bstate & BUTTON1_TRIPLE_CLICKED)
475 			result = menu_driver(menu, REQ_FIRST_ITEM);
476 		      RETURN(result);
477 		    }
478 		  else if (ry > sub->_begy + sub->_maxy)
479 		    {		/* we clicked below the display region; this is
480 				 * interpreted as "scroll down" request
481 				 */
482 		      if (event.bstate & BUTTON1_CLICKED)
483 			result = menu_driver(menu, REQ_SCR_DLINE);
484 		      else if (event.bstate & BUTTON1_DOUBLE_CLICKED)
485 			result = menu_driver(menu, REQ_SCR_DPAGE);
486 		      else if (event.bstate & BUTTON1_TRIPLE_CLICKED)
487 			result = menu_driver(menu, REQ_LAST_ITEM);
488 		      RETURN(result);
489 		    }
490 		  else if (wenclose(sub, event.y, event.x))
491 		    {		/* Inside the area we try to find the hit item */
492 		      int i, x, y, err;
493 
494 		      ry = event.y;
495 		      rx = event.x;
496 		      if (wmouse_trafo(sub, &ry, &rx, FALSE))
497 			{
498 			  for (i = 0; i < menu->nitems; i++)
499 			    {
500 			      err = _nc_menu_cursor_pos(menu, menu->items[i],
501 							&y, &x);
502 			      if (E_OK == err)
503 				{
504 				  if ((ry == y) &&
505 				      (rx >= x) &&
506 				      (rx < x + menu->itemlen))
507 				    {
508 				      item = menu->items[i];
509 				      result = E_OK;
510 				      break;
511 				    }
512 				}
513 			    }
514 			  if (E_OK == result)
515 			    {	/* We found an item, now we can handle the click.
516 				 * A single click just positions the menu cursor
517 				 * to the clicked item. A double click toggles
518 				 * the item.
519 				 */
520 			      if (event.bstate & BUTTON1_DOUBLE_CLICKED)
521 				{
522 				  _nc_New_TopRow_and_CurrentItem(menu,
523 								 my_top_row,
524 								 item);
525 				  menu_driver(menu, REQ_TOGGLE_ITEM);
526 				  result = E_UNKNOWN_COMMAND;
527 				}
528 			    }
529 			}
530 		    }
531 		}
532 	    }
533 	  else
534 	    {
535 	      if (menu->opt & O_MOUSE_MENU)
536 		ungetmouse(&event);	/* let someone else handle this */
537 	      result = E_REQUEST_DENIED;
538 	    }
539 	}
540 #endif /* NCURSES_MOUSE_VERSION */
541       else
542 	result = E_UNKNOWN_COMMAND;
543     }
544 
545   if (item == 0)
546     {
547       result = E_BAD_STATE;
548     }
549   else if (E_OK == result)
550     {
551       /* Adjust the top row if it turns out that the current item unfortunately
552          doesn't appear in the menu window */
553       if (item->y < my_top_row)
554 	my_top_row = item->y;
555       else if (item->y >= (my_top_row + menu->arows))
556 	my_top_row = item->y - menu->arows + 1;
557 
558       _nc_New_TopRow_and_CurrentItem(menu, my_top_row, item);
559 
560     }
561 
562   RETURN(result);
563 }
564 
565 /* m_driver.c ends here */
566