Client - Allowed menu to cursor onto expensive items (not selectable)
[tpg/opendispense2.git] / src / client / menu.c
1 /*
2  * OpenDispense 2 
3  * UCC (University [of WA] Computer Club) Electronic Accounting System
4  * - Dispense Client
5  *
6  * menu.c - ncurses dispense menu
7  *
8  * This file is licenced under the 3-clause BSD Licence. See the file
9  * COPYING for full details.
10  */
11 #include <stdlib.h>
12 #include <ncurses.h>
13 #include <pwd.h>        // getpwuids
14 #include <unistd.h>     // getuid
15 #include "common.h"
16
17 // === CONSTANTS ===
18 #define COLOURPAIR_DEFAULT      0
19 #define COLOURPAIR_CANTBUY      1
20 #define COLOURPAIR_SELECTED     2
21
22 // === PROTOTYPES ===
23  int    ShowItemAt(int Row, int Col, int Width, int Index, int bHilighted);
24 void    PrintAlign(int Row, int Col, int Width, const char *Left, char Pad1, const char *Mid, char Pad2, const char *Right, ...);
25
26 // -------------------
27 // --- NCurses GUI ---
28 // -------------------
29 /**
30  * \brief Render the NCurses UI
31  */
32 int ShowNCursesUI(void)
33 {
34          int    ch;
35          int    i, times;
36          int    xBase, yBase;
37         const int       displayMinWidth = 50;
38         const char      *titleString = "Dispense";
39          int    items_in_view;
40          int    maxItemIndex;
41          int    itemBase = 0;
42          int    currentItem;
43          int    ret = -2;       // -2: Used for marking "no return yet"
44         
45         char    balance_str[5+1+2+1];   // If $9999.99 is too little, something's wrong
46         char    *username;
47         struct passwd *pwd;
48          
49          int    height, width;
50         
51         void _ItemDown(void)
52         {
53                 currentItem ++;
54                 // Skip over spacers
55                 while( ShowItemAt(0, 0, 0, currentItem, 0) == -1 )
56                         currentItem ++;
57                 
58                 if( currentItem >= maxItemIndex ) {
59                         currentItem = 0;
60                         // Skip over spacers
61                         while( ShowItemAt(0, 0, 0, currentItem, 0) == -1 )
62                                 currentItem ++;
63                 }
64         }
65         
66         void _ItemUp(void)
67         {
68                 currentItem --;
69                 // Skip over spacers
70                 while( ShowItemAt(0, 0, 0, currentItem, 0) == -1 )
71                         currentItem --;
72                 
73                 if( currentItem < 0 ) {
74                         currentItem = maxItemIndex - 1;
75                         // Skip over spacers
76                         while( ShowItemAt(0, 0, 0, currentItem, 0) == -1 )
77                                 currentItem --;
78                 }
79         }
80
81         // Get Username
82         if( gsEffectiveUser )
83                 username = gsEffectiveUser;
84         else {
85                 pwd = getpwuid( getuid() );
86                 username = pwd->pw_name;
87         }
88         // Get balance
89         snprintf(balance_str, sizeof(balance_str), "$%i.%02i", giUserBalance/100, abs(giUserBalance)%100);
90         
91         // Enter curses mode
92         initscr();
93         start_color();
94         use_default_colors();
95         init_pair(COLOURPAIR_DEFAULT,  -1,  -1);        // Not avaliable
96         init_pair(COLOURPAIR_CANTBUY,  COLOR_BLACK,  -1);       // Not avaliable
97         init_pair(COLOURPAIR_SELECTED, COLOR_GREEN,  -1);       // Selected
98         cbreak(); noecho();
99         
100         // Get max index
101         maxItemIndex = ShowItemAt(0, 0, 0, -1, 0);
102         // Get item count per screen
103         // - 6: randomly chosen (Need at least 3)
104         items_in_view = LINES - 6;
105         if( items_in_view > maxItemIndex )
106                 items_in_view = maxItemIndex;
107         // Get first index
108         currentItem = 0;
109         while( ShowItemAt(0, 0, 0, currentItem, 0) == -1 )
110                 currentItem ++;
111         
112         
113         // Get dimensions
114         height = items_in_view + 3;
115         width = displayMinWidth;
116         
117         // Get positions
118         xBase = COLS/2 - width/2;
119         yBase = LINES/2 - height/2;
120         
121         for( ;; )
122         {
123                 // Header
124                 PrintAlign(yBase, xBase, width, "/", '-', titleString, '-', "\\");
125                 
126                 // Items
127                 for( i = 0; i < items_in_view; i ++ )
128                 {
129                          int    pos = 0;
130                         
131                         move( yBase + 1 + i, xBase );
132                         printw("| ");
133                         
134                         pos += 2;
135                         
136                         // Check for the '...' row
137                         // - Oh god, magic numbers!
138                         if( (i == 0 && itemBase > 0)
139                          || (i == items_in_view - 1 && itemBase < maxItemIndex - items_in_view) )
140                         {
141                                 printw("     ...");     pos += 8;
142                                 times = (width - pos) - 1;
143                                 while(times--)  addch(' ');
144                         }
145                         // Show an item
146                         else {
147                                 ShowItemAt(
148                                         yBase + 1 + i, xBase + pos,     // Position
149                                         (width - pos) - 3,      // Width
150                                         itemBase + i,   // Index
151                                         !!(currentItem == itemBase + i) // Hilighted
152                                         );
153                                 printw("  ");
154                         }
155                         
156                         // Scrollbar (if needed)
157                         if( maxItemIndex > items_in_view ) {
158                                 if( i == 0 ) {
159                                         addch('A');
160                                 }
161                                 else if( i == items_in_view - 1 ) {
162                                         addch('V');
163                                 }
164                                 else {
165                                          int    percentage = itemBase * 100 / (maxItemIndex-items_in_view);
166                                         if( i-1 == percentage*(items_in_view-3)/100 ) {
167                                                 addch('#');
168                                         }
169                                         else {
170                                                 addch('|');
171                                         }
172                                 }
173                         }
174                         else {
175                                 addch('|');
176                         }
177                 }
178                 
179                 // Footer
180                 PrintAlign(yBase+height-2, xBase, width, "\\", '-', "", '-', "/");
181                 
182                 // User line
183                 // - Username, balance, flags
184                 PrintAlign(yBase+height-1, xBase+1, width-2,
185                         username, ' ', balance_str, ' ', gsUserFlags);
186                 PrintAlign(yBase+height, xBase+1, width-2,
187                         "q: Quit", ' ', "Arrows: Select", ' ', "Enter: Buy");
188                 
189                 
190                 // Get input
191                 ch = getch();
192                 
193                 if( ch == '\x1B' ) {
194                         ch = getch();
195                         if( ch == '[' ) {
196                                 ch = getch();
197                                 
198                                 switch(ch)
199                                 {
200                                 case 'B':       _ItemDown();    break;
201                                 case 'A':       _ItemUp();      break;
202                                 }
203                         }
204                         else if( ch == ERR || ch == '\x1b' ) {
205                                 ret = -1;
206                                 break;
207                         }
208                         else {
209                                 fprintf(stderr, "Unknown character 0x%x\n", ch);
210                         }
211                 }
212                 else {
213                         switch(ch)
214                         {
215                         case '\n':
216                                 ret = ShowItemAt(0, 0, 0, currentItem, 0);
217                                 break;
218                         case 'h':       break;
219                         case 'j':       _ItemDown();    break;
220                         case 'k':       _ItemUp();      break;
221                         case 'l':       break;
222                         case 'q':
223                                 ret = -1;       // -1: Return with no dispense
224                                 break;
225                         }
226                         
227                         // Check if the return value was changed
228                         if( ret != -2 ) break;
229                 }
230                 
231                 // Scroll only if needed
232                 if( items_in_view < maxItemIndex )
233                 {
234                         // - If the current item is above the second item shown, and we're not at the top
235                         if( currentItem < itemBase + 2 && itemBase > 0 ) {
236                                 itemBase = currentItem - 2;
237                                 if(itemBase < 0)        itemBase = 0;
238                         }
239                         // - If the current item is below the second item show, and we're not at the bottom
240                         if( currentItem > itemBase + items_in_view - 2 && itemBase + items_in_view < maxItemIndex ) {
241                                 itemBase = currentItem - items_in_view + 2;
242                                 if( itemBase > maxItemIndex - items_in_view )
243                                         itemBase = maxItemIndex - items_in_view;
244                         }
245                 }
246         }
247         
248         
249         // Leave
250         endwin();
251         return ret;
252 }
253
254 /**
255  * \brief Show item \a Index at (\a Col, \a Row)
256  * \return Dispense index of item
257  * \note Part of the NCurses UI
258  */
259 int ShowItemAt(int Row, int Col, int Width, int Index, int bHilighted)
260 {
261         char    *name = NULL;
262          int    price = 0;
263          int    status = -1;
264         
265         switch(giUIMode)
266         {
267         // Standard UI
268         // - This assumes that 
269         case UI_MODE_STANDARD:
270                 // Bounds check
271                 // Index = -1, request limit
272                 if( Index < 0 || Index >= giNumItems+2 )
273                         return giNumItems+2;
274                 // Drink label
275                 if( Index == 0 )
276                 {
277                         price = 0;
278                         name = "Coke Machine";
279                         Index = -1;     // -1 indicates a label
280                         break;
281                 }
282                 Index --;
283                 // Drinks 0 - 6
284                 if( Index <= 6 )
285                 {
286                         name = gaItems[Index].Desc;
287                         price = gaItems[Index].Price;
288                         status = gaItems[Index].Status;
289                         break;
290                 }
291                 Index -= 7;
292                 // EPS label
293                 if( Index == 0 )
294                 {
295                         price = 0;
296                         name = "Electronic Payment System";
297                         Index = -1;     // -1 indicates a label
298                         break;
299                 }
300                 Index --;
301                 Index += 7;
302                 name = gaItems[Index].Desc;
303                 price = gaItems[Index].Price;
304                 status = gaItems[Index].Status;
305                 break;
306         default:
307                 return -1;
308         }
309         
310         // Width = 0, don't print
311         if( Width > 0 )
312         {
313                 // 4 preceding, 5 price
314                 int nameWidth = Width - 4 - snprintf(NULL, 0, " %4i", price);
315                 move( Row, Col );
316                 
317                 if( Index >= 0 )
318                 {
319                         // Show hilight and status
320                         switch( status )
321                         {
322                         case 0:
323                                 if( bHilighted ) {
324                                         color_set( COLOURPAIR_SELECTED, NULL );
325                                         printw("->  ");
326                                 }
327                                 else if( price > giUserBalance ) {
328                                         attrset(A_BOLD);
329                                         color_set( COLOURPAIR_CANTBUY, NULL );
330                                         printw("    ");
331                                 }
332                                 else {
333                                         color_set( 0, NULL );
334                                         printw("    ");
335                                 }
336                                 break;
337                         case 1:
338                                 attrset(A_BOLD);
339                                 color_set( COLOURPAIR_CANTBUY, NULL );
340                                 printw("SLD ");
341                                 break;
342                         
343                         default:
344                         case -1:
345                                 color_set( COLOURPAIR_CANTBUY, NULL );
346                                 printw("ERR ");
347                                 break;
348                         }
349                         
350                         printw("%-*.*s", nameWidth, nameWidth, name);
351                 
352                         printw(" %4i", price);
353                         color_set(0, NULL);
354                         attrset(A_NORMAL);
355                 }
356                 else
357                 {
358                         printw("-- %-*.*s ", Width-4, Width-4, name);
359                 }
360         }
361         
362         // If the item isn't availiable for sale, return -1 (so it's skipped)
363         if( status > 0 || (price > giUserBalance && gbDisallowSelectWithoutBalance) )
364                 Index = -2;
365         
366         return Index;
367 }
368
369 /**
370  * \brief Print a three-part string at the specified position (formatted)
371  * \note NCurses UI Helper
372  * 
373  * Prints \a Left on the left of the area, \a Right on the righthand side
374  * and \a Mid in the middle of the area. These are padded with \a Pad1
375  * between \a Left and \a Mid, and \a Pad2 between \a Mid and \a Right.
376  * 
377  * ::printf style format codes are allowed in \a Left, \a Mid and \a Right,
378  * and the arguments to these are read in that order.
379  */
380 void PrintAlign(int Row, int Col, int Width, const char *Left, char Pad1,
381         const char *Mid, char Pad2, const char *Right, ...)
382 {
383          int    lLen, mLen, rLen;
384          int    times;
385         
386         va_list args;
387         
388         // Get the length of the strings
389         va_start(args, Right);
390         lLen = vsnprintf(NULL, 0, Left, args);
391         mLen = vsnprintf(NULL, 0, Mid, args);
392         rLen = vsnprintf(NULL, 0, Right, args);
393         va_end(args);
394         
395         // Sanity check
396         if( lLen + mLen/2 > Width/2 || mLen/2 + rLen > Width/2 ) {
397                 return ;        // TODO: What to do?
398         }
399         
400         move(Row, Col);
401         
402         // Render strings
403         va_start(args, Right);
404         // - Left
405         {
406                 char    tmp[lLen+1];
407                 vsnprintf(tmp, lLen+1, Left, args);
408                 addstr(tmp);
409         }
410         // - Left padding
411         times = (Width - mLen)/2 - lLen;
412         while(times--)  addch(Pad1);
413         // - Middle
414         {
415                 char    tmp[mLen+1];
416                 vsnprintf(tmp, mLen+1, Mid, args);
417                 addstr(tmp);
418         }
419         // - Right Padding
420         times = (Width - mLen)/2 - rLen;
421         if( (Width - mLen) % 2 )        times ++;
422         while(times--)  addch(Pad2);
423         // - Right
424         {
425                 char    tmp[rLen+1];
426                 vsnprintf(tmp, rLen+1, Right, args);
427                 addstr(tmp);
428         }
429 }
430

UCC git Repository :: git.ucc.asn.au