ncurses 6.2 - patch 20211018
[ncurses.git] / menu / m_driver.c
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 */