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