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

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