xref: /freebsd/contrib/ncurses/menu/m_driver.c (revision 3f0efe05432b1633991114ca4ca330102a561959)
1 /****************************************************************************
2  * Copyright 2020,2021 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.37 2021/03/27 23:46:29 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;
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       int rdiff;
243 
244       if (!((c == REQ_BACK_PATTERN)
245 	    || (c == REQ_NEXT_MATCH) || (c == REQ_PREV_MATCH)))
246 	{
247 	  assert(menu->pattern);
248 	  Reset_Pattern(menu);
249 	}
250 
251       switch (c)
252 	{
253 	case REQ_LEFT_ITEM:
254 	    /*=================*/
255 	  NAVIGATE(left);
256 	  break;
257 
258 	case REQ_RIGHT_ITEM:
259 	    /*==================*/
260 	  NAVIGATE(right);
261 	  break;
262 
263 	case REQ_UP_ITEM:
264 	    /*===============*/
265 	  NAVIGATE(up);
266 	  break;
267 
268 	case REQ_DOWN_ITEM:
269 	    /*=================*/
270 	  NAVIGATE(down);
271 	  break;
272 
273 	case REQ_SCR_ULINE:
274 	    /*=================*/
275 	  if (my_top_row == 0 || !(item->up))
276 	    result = E_REQUEST_DENIED;
277 	  else
278 	    {
279 	      --my_top_row;
280 	      item = item->up;
281 	    }
282 	  break;
283 
284 	case REQ_SCR_DLINE:
285 	    /*=================*/
286 	  if ((my_top_row + menu->arows >= menu->rows) || !(item->down))
287 	    {
288 	      /* only if the menu has less items than rows, we can deny the
289 	         request. Otherwise the epilogue of this routine adjusts the
290 	         top row if necessary */
291 	      result = E_REQUEST_DENIED;
292 	    }
293 	  else
294 	    {
295 	      my_top_row++;
296 	      item = item->down;
297 	    }
298 	  break;
299 
300 	case REQ_SCR_DPAGE:
301 	    /*=================*/
302 	  rdiff = menu->rows - (menu->arows + my_top_row);
303 	  if (rdiff > menu->arows)
304 	    rdiff = menu->arows;
305 	  if (rdiff <= 0)
306 	    result = E_REQUEST_DENIED;
307 	  else
308 	    {
309 	      my_top_row += rdiff;
310 	      while (rdiff-- > 0 && item != 0 && item->down != 0)
311 		item = item->down;
312 	    }
313 	  break;
314 
315 	case REQ_SCR_UPAGE:
316 	    /*=================*/
317 	  rdiff = (menu->arows < my_top_row) ? menu->arows : my_top_row;
318 	  if (rdiff <= 0)
319 	    result = E_REQUEST_DENIED;
320 	  else
321 	    {
322 	      my_top_row -= rdiff;
323 	      while (rdiff-- > 0 && item != 0 && item->up != 0)
324 		item = item->up;
325 	    }
326 	  break;
327 
328 	case REQ_FIRST_ITEM:
329 	    /*==================*/
330 	  item = menu->items[0];
331 	  break;
332 
333 	case REQ_LAST_ITEM:
334 	    /*=================*/
335 	  item = menu->items[menu->nitems - 1];
336 	  break;
337 
338 	case REQ_NEXT_ITEM:
339 	    /*=================*/
340 	  if ((item->index + 1) >= menu->nitems)
341 	    {
342 	      if (menu->opt & O_NONCYCLIC)
343 		result = E_REQUEST_DENIED;
344 	      else
345 		item = menu->items[0];
346 	    }
347 	  else
348 	    item = menu->items[item->index + 1];
349 	  break;
350 
351 	case REQ_PREV_ITEM:
352 	    /*=================*/
353 	  if (item->index <= 0)
354 	    {
355 	      if (menu->opt & O_NONCYCLIC)
356 		result = E_REQUEST_DENIED;
357 	      else
358 		item = menu->items[menu->nitems - 1];
359 	    }
360 	  else
361 	    item = menu->items[item->index - 1];
362 	  break;
363 
364 	case REQ_TOGGLE_ITEM:
365 	    /*===================*/
366 	  if (menu->opt & O_ONEVALUE)
367 	    {
368 	      result = E_REQUEST_DENIED;
369 	    }
370 	  else
371 	    {
372 	      if (menu->curitem->opt & O_SELECTABLE)
373 		{
374 		  menu->curitem->value = !menu->curitem->value;
375 		  Move_And_Post_Item(menu, menu->curitem);
376 		  _nc_Show_Menu(menu);
377 		}
378 	      else
379 		result = E_NOT_SELECTABLE;
380 	    }
381 	  break;
382 
383 	case REQ_CLEAR_PATTERN:
384 	    /*=====================*/
385 	  /* already cleared in prologue */
386 	  break;
387 
388 	case REQ_BACK_PATTERN:
389 	    /*====================*/
390 	  if (menu->pindex > 0)
391 	    {
392 	      assert(menu->pattern);
393 	      Remove_Character_From_Pattern(menu);
394 	      pos_menu_cursor(menu);
395 	    }
396 	  else
397 	    result = E_REQUEST_DENIED;
398 	  break;
399 
400 	case REQ_NEXT_MATCH:
401 	    /*==================*/
402 	  assert(menu->pattern);
403 	  if (menu->pattern[0])
404 	    result = _nc_Match_Next_Character_In_Item_Name(menu, 0, &item);
405 	  else
406 	    {
407 	      if ((item->index + 1) < menu->nitems)
408 		item = menu->items[item->index + 1];
409 	      else
410 		{
411 		  if (menu->opt & O_NONCYCLIC)
412 		    result = E_REQUEST_DENIED;
413 		  else
414 		    item = menu->items[0];
415 		}
416 	    }
417 	  break;
418 
419 	case REQ_PREV_MATCH:
420 	    /*==================*/
421 	  assert(menu->pattern);
422 	  if (menu->pattern[0])
423 	    result = _nc_Match_Next_Character_In_Item_Name(menu, BS, &item);
424 	  else
425 	    {
426 	      if (item->index)
427 		item = menu->items[item->index - 1];
428 	      else
429 		{
430 		  if (menu->opt & O_NONCYCLIC)
431 		    result = E_REQUEST_DENIED;
432 		  else
433 		    item = menu->items[menu->nitems - 1];
434 		}
435 	    }
436 	  break;
437 
438 	default:
439 	    /*======*/
440 	  result = E_UNKNOWN_COMMAND;
441 	  break;
442 	}
443     }
444   else
445     {				/* not a command */
446       if (!(c & ~((int)MAX_REGULAR_CHARACTER)) && isprint(UChar(c)))
447 	result = _nc_Match_Next_Character_In_Item_Name(menu, c, &item);
448 #ifdef NCURSES_MOUSE_VERSION
449       else if (KEY_MOUSE == c)
450 	{
451 	  MEVENT event;
452 	  WINDOW *uwin = Get_Menu_UserWin(menu);
453 
454 	  getmouse(&event);
455 	  if ((event.bstate & (BUTTON1_CLICKED |
456 			       BUTTON1_DOUBLE_CLICKED |
457 			       BUTTON1_TRIPLE_CLICKED))
458 	      && wenclose(uwin, event.y, event.x))
459 	    {			/* we react only if the click was in the userwin, that means
460 				 * inside the menu display area or at the decoration window.
461 				 */
462 	      WINDOW *sub = Get_Menu_Window(menu);
463 	      int ry = event.y, rx = event.x;	/* screen coordinates */
464 
465 	      result = E_REQUEST_DENIED;
466 	      if (mouse_trafo(&ry, &rx, FALSE))
467 		{		/* rx, ry are now "curses" coordinates */
468 		  if (ry < sub->_begy)
469 		    {		/* we clicked above the display region; this is
470 				 * interpreted as "scroll up" request
471 				 */
472 		      if (event.bstate & BUTTON1_CLICKED)
473 			result = menu_driver(menu, REQ_SCR_ULINE);
474 		      else if (event.bstate & BUTTON1_DOUBLE_CLICKED)
475 			result = menu_driver(menu, REQ_SCR_UPAGE);
476 		      else if (event.bstate & BUTTON1_TRIPLE_CLICKED)
477 			result = menu_driver(menu, REQ_FIRST_ITEM);
478 		      RETURN(result);
479 		    }
480 		  else if (ry > sub->_begy + sub->_maxy)
481 		    {		/* we clicked below the display region; this is
482 				 * interpreted as "scroll down" request
483 				 */
484 		      if (event.bstate & BUTTON1_CLICKED)
485 			result = menu_driver(menu, REQ_SCR_DLINE);
486 		      else if (event.bstate & BUTTON1_DOUBLE_CLICKED)
487 			result = menu_driver(menu, REQ_SCR_DPAGE);
488 		      else if (event.bstate & BUTTON1_TRIPLE_CLICKED)
489 			result = menu_driver(menu, REQ_LAST_ITEM);
490 		      RETURN(result);
491 		    }
492 		  else if (wenclose(sub, event.y, event.x))
493 		    {		/* Inside the area we try to find the hit item */
494 		      int x, y;
495 
496 		      ry = event.y;
497 		      rx = event.x;
498 		      if (wmouse_trafo(sub, &ry, &rx, FALSE))
499 			{
500 			  int i;
501 
502 			  for (i = 0; i < menu->nitems; i++)
503 			    {
504 			      int err = _nc_menu_cursor_pos(menu,
505 							    menu->items[i],
506 							    &y, &x);
507 
508 			      if (E_OK == err)
509 				{
510 				  if ((ry == y) &&
511 				      (rx >= x) &&
512 				      (rx < x + menu->itemlen))
513 				    {
514 				      item = menu->items[i];
515 				      result = E_OK;
516 				      break;
517 				    }
518 				}
519 			    }
520 			  if (E_OK == result)
521 			    {	/* We found an item, now we can handle the click.
522 				 * A single click just positions the menu cursor
523 				 * to the clicked item. A double click toggles
524 				 * the item.
525 				 */
526 			      if (event.bstate & BUTTON1_DOUBLE_CLICKED)
527 				{
528 				  _nc_New_TopRow_and_CurrentItem(menu,
529 								 my_top_row,
530 								 item);
531 				  menu_driver(menu, REQ_TOGGLE_ITEM);
532 				  result = E_UNKNOWN_COMMAND;
533 				}
534 			    }
535 			}
536 		    }
537 		}
538 	    }
539 	  else
540 	    {
541 	      if (menu->opt & O_MOUSE_MENU)
542 		ungetmouse(&event);	/* let someone else handle this */
543 	      result = E_REQUEST_DENIED;
544 	    }
545 	}
546 #endif /* NCURSES_MOUSE_VERSION */
547       else
548 	result = E_UNKNOWN_COMMAND;
549     }
550 
551   if (item == 0)
552     {
553       result = E_BAD_STATE;
554     }
555   else if (E_OK == result)
556     {
557       /* Adjust the top row if it turns out that the current item unfortunately
558          doesn't appear in the menu window */
559       if (item->y < my_top_row)
560 	my_top_row = item->y;
561       else if (item->y >= (my_top_row + menu->arows))
562 	my_top_row = item->y - menu->arows + 1;
563 
564       _nc_New_TopRow_and_CurrentItem(menu, my_top_row, item);
565 
566     }
567 
568   RETURN(result);
569 }
570 
571 /* m_driver.c ends here */
572