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
43MODULE_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+--------------------------------------------------------------------------*/
68static bool
69Is_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+--------------------------------------------------------------------------*/
120MENU_EXPORT(int)
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+--------------------------------------------------------------------------*/
214MENU_EXPORT(int)
215menu_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