- removed a few menu variables
[supertux.git] / src / menu.cpp
1 /*
2   menu.c
3   
4   Super Tux - Menu
5   
6   by Tobias Glaesser
7   tobi.web@gmx.de
8   http://www.newbreedsoftware.com/supertux/
9   
10   December 20, 2003 - March 15, 2004
11 */
12
13 #ifndef WIN32
14 #include <sys/types.h>
15 #include <ctype.h>
16 #endif
17
18 #include <stdlib.h>
19 #include <stdio.h>
20 #include <string.h>
21
22 #include "defines.h"
23 #include "globals.h"
24 #include "menu.h"
25 #include "screen.h"
26 #include "setup.h"
27 #include "sound.h"
28 #include "scene.h"
29 #include "leveleditor.h"
30 #include "timer.h"
31 #include "high_scores.h"
32
33 Surface* checkbox;
34 Surface* checkbox_checked;
35 Surface* back;
36 Surface* arrow_left;
37 Surface* arrow_right;
38
39 Menu* main_menu      = 0;
40 Menu* game_menu      = 0;
41 Menu* worldmap_menu  = 0;
42 Menu* options_menu   = 0;
43 Menu* options_controls_menu   = 0;
44 Menu* highscore_menu = 0;
45 Menu* load_game_menu = 0;
46 Menu* save_game_menu = 0;
47 Menu* contrib_menu   = 0;
48 Menu* contrib_subset_menu   = 0;
49
50 Menu* Menu::current_ = 0;
51
52 void
53 Menu::set_current(Menu* menu)
54 {
55   if (menu)
56     {
57       menu->last_menu = current_;
58       menu->effect.start(500);
59     }
60   
61   current_ = menu;
62 }
63
64 /* Return a pointer to a new menu item */
65 MenuItem*
66 MenuItem::create(MenuItemKind kind_, const char *text_, int init_toggle_, Menu* target_menu_)
67 {
68   MenuItem *pnew_item = new MenuItem;
69   
70   pnew_item->kind = kind_;
71   pnew_item->text = (char*) malloc(sizeof(char) * (strlen(text_) + 1));
72   strcpy(pnew_item->text, text_);
73
74   if(kind_ == MN_TOGGLE)
75     pnew_item->toggled = init_toggle_;
76   else
77     pnew_item->toggled = false;
78
79   pnew_item->target_menu = target_menu_;
80   pnew_item->input = (char*) malloc(sizeof(char));
81   pnew_item->input[0] = '\0';
82
83   if(kind_ == MN_STRINGSELECT)
84     {
85       pnew_item->list = (string_list_type*) malloc(sizeof(string_list_type));
86       string_list_init(pnew_item->list);
87     }
88   else
89     pnew_item->list = NULL;
90   return pnew_item;
91 }
92
93 void
94 MenuItem::change_text(const  char *text_)
95 {
96   if (text_)
97     {
98       free(text);
99       text = (char*) malloc(sizeof(char )*(strlen(text_)+1));
100       strcpy(text, text_);
101     }
102 }
103
104 void
105 MenuItem::change_input(const  char *text_)
106 {
107   if(text)
108     {
109       free(input);
110       input = (char*) malloc(sizeof(char )*(strlen(text_)+1));
111       strcpy(input, text_);
112     }
113 }
114
115 /* Free a menu and all its items */
116 Menu::~Menu()
117 {
118   if(item.size() != 0)
119     {
120       for(unsigned int i = 0; i < item.size(); ++i)
121         {
122           free(item[i].text);
123           free(item[i].input);
124           string_list_free(item[i].list);
125         }
126     }
127 }
128
129
130 Menu::Menu()
131 {
132   menuaction = MENU_ACTION_NONE;
133   delete_character = 0;
134   mn_input_char = '\0';
135   
136   pos_x        = screen->w/2;
137   pos_y        = screen->h/2;
138   has_backitem = false;
139   last_menu    = 0;
140   arrange_left = 0;
141   active_item  = 0;
142   last_menu    = 0;
143   effect.init(false);
144 }
145
146 void Menu::set_pos(int x, int y, float rw, float rh)
147 {
148   pos_x = x + (int)((float)width() * rw);
149   pos_y = y + (int)((float)height() * rh);
150 }
151
152 void
153 Menu::additem(MenuItemKind kind_, const std::string& text_, int toggle_, Menu* menu_)
154 {
155   if(kind_ == MN_BACK)
156     has_backitem = true;
157
158   additem(MenuItem::create(kind_, text_.c_str(), toggle_, menu_));
159 }
160
161 /* Add an item to a menu */
162 void
163 Menu::additem(MenuItem* pmenu_item)
164 {
165   if(pmenu_item->kind == MN_BACK)
166     has_backitem = true;
167
168   item.push_back(*pmenu_item);
169   delete pmenu_item;
170 }
171
172 void
173 Menu::clear()
174 {
175   item.clear();
176 }
177
178 /* Process actions done on the menu */
179 void
180 Menu::action()
181 {
182   if(item.size() != 0)
183     {
184       switch(menuaction)
185         {
186         case MENU_ACTION_UP:
187           if (active_item > 0)
188             --active_item;
189           else
190             active_item = int(item.size())-1;
191           break;
192
193         case MENU_ACTION_DOWN:
194           if(active_item < int(item.size())-1)
195             ++active_item;
196           else
197             active_item = 0;
198           break;
199
200         case MENU_ACTION_LEFT:
201           if(item[active_item].kind == MN_STRINGSELECT
202               && item[active_item].list->num_items != 0)
203             {
204               if(item[active_item].list->active_item > 0)
205                 --item[active_item].list->active_item;
206               else
207                 item[active_item].list->active_item = item[active_item].list->num_items-1;
208             }
209           break;
210
211         case MENU_ACTION_RIGHT:
212           if(item[active_item].kind == MN_STRINGSELECT
213               && item[active_item].list->num_items != 0)
214             {
215               if(item[active_item].list->active_item < item[active_item].list->num_items-1)
216                 ++item[active_item].list->active_item;
217               else
218                 item[active_item].list->active_item = 0;
219             }
220           break;
221
222         case MENU_ACTION_HIT:
223           {
224             switch (item[active_item].kind)
225               {
226               case MN_GOTO:
227                 if (item[active_item].target_menu != NULL)
228                   Menu::set_current(item[active_item].target_menu);
229                 else
230                   puts("NULLL");
231                 break;
232
233               case MN_TOGGLE:
234                 item[active_item].toggled = !item[active_item].toggled;
235                 break;
236
237               case MN_ACTION:
238               case MN_TEXTFIELD:
239               case MN_NUMFIELD:
240               case MN_CONTROLFIELD:
241                 item[active_item].toggled = true;
242                 break;
243
244               case MN_BACK:
245                 if(last_menu != NULL)
246                   Menu::set_current(last_menu);
247                 break;
248               default:
249                 break;
250               }
251           }
252           break;
253
254         case MENU_ACTION_REMOVE:
255           if(item[active_item].kind == MN_TEXTFIELD
256               || item[active_item].kind == MN_NUMFIELD)
257             {
258               if(item[active_item].input != NULL)
259                 {
260                   int i = strlen(item[active_item].input);
261
262                   while(delete_character > 0)   /* remove charactes */
263                     {
264                       item[active_item].input[i-1] = '\0';
265                       delete_character--;
266                     }
267                 }
268             }
269           break;
270
271         case MENU_ACTION_INPUT:
272           if(item[active_item].kind == MN_TEXTFIELD
273               || (item[active_item].kind == MN_NUMFIELD && mn_input_char >= '0' && mn_input_char <= '9'))
274             {
275               if(item[active_item].input != NULL)
276                 {
277                   int i = strlen(item[active_item].input);
278                   item[active_item].input = (char*) realloc(item[active_item].input,sizeof(char)*(i + 2));
279                   item[active_item].input[i] = mn_input_char;
280                   item[active_item].input[i+1] = '\0';
281                 }
282               else
283                 {
284                   item[active_item].input = (char*) malloc(2*sizeof(char));
285                   item[active_item].input[0] = mn_input_char;
286                   item[active_item].input[1] = '\0';
287                 }
288             }
289           break;
290
291         case MENU_ACTION_NONE:
292           break;
293         }
294     }
295
296   MenuItem& new_item = item[active_item];
297   if(new_item.kind == MN_DEACTIVE
298       || new_item.kind == MN_LABEL
299       || new_item.kind == MN_HL)
300     {
301       // Skip the horzontal line item
302       if (menuaction != MENU_ACTION_UP && menuaction != MENU_ACTION_DOWN)
303         menuaction = MENU_ACTION_DOWN;
304
305       if (item.size() > 1)
306         action();
307     }
308
309   menuaction = MENU_ACTION_NONE;
310 }
311
312 int
313 Menu::check()
314 {
315   if (item.size() != 0)
316     {
317       if((item[active_item].kind == MN_ACTION
318           || item[active_item].kind == MN_TEXTFIELD
319           || item[active_item].kind == MN_NUMFIELD)
320           && item[active_item].toggled)
321         {
322           item[active_item].toggled = false;
323           Menu::set_current(0);
324           return active_item;
325         }
326       else if(item[active_item].kind == MN_TOGGLE || item[active_item].kind == MN_GOTO)
327         {
328           return active_item;
329         }
330       else
331         return -1;
332     }
333   else
334     return -1;
335 }
336
337 void
338 Menu::draw_item(int index, // Position of the current item in the menu
339                 int menu_width,
340                 int menu_height)
341 {
342   const MenuItem& pitem = item[index];
343
344   int font_width  = 16;
345   int effect_offset = 0;
346   {
347     int effect_time = 0;
348
349     if(effect.check())
350       effect_time = effect.get_left() / 4;
351
352     effect_offset = (index % 2) ? effect_time : -effect_time;
353   }
354
355   int x_pos       = pos_x;
356   int y_pos       = pos_y + 24*index - menu_height/2 + 12 + effect_offset;
357   int shadow_size = 2;
358   int text_width  = strlen(pitem.text) * font_width;
359   int input_width = strlen(pitem.input) * font_width;
360   int list_width  = strlen(string_list_active(pitem.list)) * font_width;
361   Text* text_font = white_text;
362
363   if (arrange_left)
364     x_pos += 24 - menu_width/2 + (text_width + input_width + list_width)/2;
365
366   if(index == active_item)
367     {
368       shadow_size = 3;
369       text_font = blue_text;
370     }
371
372   switch (pitem.kind)
373     {
374     case MN_DEACTIVE:
375       {
376         black_text->draw_align(pitem.text,
377                                x_pos, y_pos,
378                                A_HMIDDLE, A_VMIDDLE, 2);
379         break;
380       }
381
382     case MN_HL:
383       {
384         int x = pos_x - menu_width/2;
385         int y = y_pos - 12 - effect_offset;
386         /* Draw a horizontal line with a little 3d effect */
387         fillrect(x, y + 6,
388                  menu_width, 4,
389                  150,200,255,225);
390         fillrect(x, y + 6,
391                  menu_width, 2,
392                  255,255,255,255);
393         break;
394       }
395     case MN_LABEL:
396       {
397         white_big_text->draw_align(pitem.text,
398                                    x_pos, y_pos,
399                                    A_HMIDDLE, A_VMIDDLE, 2);
400         break;
401       }
402     case MN_TEXTFIELD:
403     case MN_NUMFIELD:
404     case MN_CONTROLFIELD:
405       {
406         int input_pos = input_width/2;
407         int text_pos  = (text_width + font_width)/2;
408
409         fillrect(x_pos - input_pos + text_pos - 1, y_pos - 10,
410                  input_width + font_width + 2, 20,
411                  255,255,255,255);
412         fillrect(x_pos - input_pos + text_pos, y_pos - 9,
413                  input_width + font_width, 18,
414                  0,0,0,128);
415
416         gold_text->draw_align(pitem.input,
417                               x_pos + text_pos, y_pos,
418                               A_HMIDDLE, A_VMIDDLE, 2);
419
420         text_font->draw_align(pitem.text,
421                               x_pos - (input_width + font_width)/2, y_pos,
422                               A_HMIDDLE, A_VMIDDLE, shadow_size);
423         break;
424       }
425     case MN_STRINGSELECT:
426       {
427         int list_pos_2 = list_width + font_width;
428         int list_pos   = list_width/2;
429         int text_pos   = (text_width + font_width)/2;
430
431         /* Draw arrows */
432         arrow_left->draw(  x_pos - list_pos + text_pos - 17, y_pos - 8);
433         arrow_right->draw( x_pos - list_pos + text_pos - 1 + list_pos_2, y_pos - 8);
434
435         /* Draw input background */
436         fillrect(x_pos - list_pos + text_pos - 1, y_pos - 10,
437                  list_pos_2 + 2, 20,
438                  255,255,255,255);
439         fillrect(x_pos - list_pos + text_pos, y_pos - 9,
440                  list_pos_2, 18,
441                  0,0,0,128);
442
443         gold_text->draw_align(string_list_active(pitem.list),
444                         x_pos + text_pos, y_pos,
445                         A_HMIDDLE, A_VMIDDLE,2);
446
447         text_font->draw_align(pitem.text,
448                         x_pos - list_pos_2/2, y_pos,
449                         A_HMIDDLE, A_VMIDDLE, shadow_size);
450         break;
451       }
452     case MN_BACK:
453       {
454         text_font->draw_align(pitem.text, x_pos, y_pos, A_HMIDDLE, A_VMIDDLE, shadow_size);
455         back->draw( x_pos + text_width/2  + font_width, y_pos - 8);
456         break;
457       }
458
459     case MN_TOGGLE:
460       {
461         text_font->draw_align(pitem.text, x_pos, y_pos, A_HMIDDLE, A_VMIDDLE, shadow_size);
462
463         if(pitem.toggled)
464           checkbox_checked->draw(
465                        x_pos + (text_width+font_width)/2,
466                        y_pos - 8);
467         else
468           checkbox->draw(
469                        x_pos + (text_width+font_width)/2,
470                        y_pos - 8);
471         break;
472       }
473     case MN_ACTION:
474       text_font->draw_align(pitem.text, x_pos, y_pos, A_HMIDDLE, A_VMIDDLE, shadow_size);
475       break;
476
477     case MN_GOTO:
478       text_font->draw_align(pitem.text, x_pos, y_pos, A_HMIDDLE, A_VMIDDLE, shadow_size);
479       break;
480     }
481 }
482
483 int Menu::width()
484 {
485   /* The width of the menu has to be more than the width of the text
486      with the most characters */
487   int menu_width = 0;
488   for(unsigned int i = 0; i < item.size(); ++i)
489     {
490       int w = strlen(item[i].text) + (item[i].input ? strlen(item[i].input) + 1 : 0) + strlen(string_list_active(item[i].list));
491       if( w > menu_width )
492         {
493           menu_width = w;
494           if( item[i].kind == MN_TOGGLE)
495             menu_width += 2;
496         }
497     }
498
499   return (menu_width * 16 + 24);
500 }
501
502 int Menu::height()
503 {
504   return item.size() * 24;
505 }
506
507 /* Draw the current menu. */
508 void
509 Menu::draw()
510 {
511   int menu_height = height();
512   int menu_width = width();
513
514   /* Draw a transparent background */
515   fillrect(pos_x - menu_width/2,
516            pos_y - 24*item.size()/2 - 10,
517            menu_width,menu_height + 20,
518            150,180,200,125);
519
520   for(unsigned int i = 0; i < item.size(); ++i)
521     {
522       draw_item(i, menu_width, menu_height);
523     }
524 }
525
526 /* --- MENU --- */
527 /* Draw the current menu and execute the (menu)events */
528 void menu_process_current()
529 {
530   if(Menu::current())
531     {
532       Menu::current()->action();
533       Menu::current()->draw();
534     }
535 }
536
537 /* Check for menu event */
538 void
539 Menu::event(SDL_Event& event)
540 {
541   SDLKey key;
542   switch(event.type)
543     {
544     case SDL_KEYDOWN:
545       key = event.key.keysym.sym;
546       SDLMod keymod;
547       char ch[2];
548       keymod = SDL_GetModState();
549       int x,y;
550
551       /* If the current unicode character is an ASCII character,
552          assign it to ch. */
553       if ( (event.key.keysym.unicode & 0xFF80) == 0 )
554         {
555           ch[0] = event.key.keysym.unicode & 0x7F;
556           ch[1] = '\0';
557         }
558       else
559         {
560           /* An International Character. */
561         }
562
563       switch(key)
564         {
565         case SDLK_UP:           /* Menu Up */
566           menuaction = MENU_ACTION_UP;
567           break;
568         case SDLK_DOWN:         /* Menu Down */
569           menuaction = MENU_ACTION_DOWN;
570           break;
571         case SDLK_LEFT:         /* Menu Up */
572           menuaction = MENU_ACTION_LEFT;
573           break;
574         case SDLK_RIGHT:                /* Menu Down */
575           menuaction = MENU_ACTION_RIGHT;
576           break;
577         case SDLK_SPACE:
578           if(item[active_item].kind == MN_TEXTFIELD)
579             {
580               menuaction = MENU_ACTION_INPUT;
581               mn_input_char = ' ';
582               break;
583             }
584         case SDLK_RETURN: /* Menu Hit */
585           menuaction = MENU_ACTION_HIT;
586           break;
587         case SDLK_DELETE:
588         case SDLK_BACKSPACE:
589           menuaction = MENU_ACTION_REMOVE;
590           delete_character++;
591           break;
592         case SDLK_ESCAPE:
593           if(Menu::current())
594             {
595               if (has_backitem == true && last_menu != NULL)
596                 Menu::set_current(last_menu);
597               else
598                 Menu::set_current(0);
599             }
600         default:
601           if( (key >= SDLK_0 && key <= SDLK_9) || (key >= SDLK_a && key <= SDLK_z) || (key >= SDLK_SPACE && key <= SDLK_SLASH))
602             {
603               menuaction = MENU_ACTION_INPUT;
604               mn_input_char = *ch;
605             }
606           else
607             {
608               mn_input_char = '\0';
609             }
610           break;
611         }
612       break;
613     case  SDL_JOYAXISMOTION:
614       if(event.jaxis.axis == JOY_Y)
615         {
616           if (event.jaxis.value > 1024)
617             menuaction = MENU_ACTION_DOWN;
618           else if (event.jaxis.value < -1024)
619             menuaction = MENU_ACTION_UP;
620         }
621       break;
622     case  SDL_JOYBUTTONDOWN:
623       menuaction = MENU_ACTION_HIT;
624       break;
625     case SDL_MOUSEBUTTONDOWN:
626       x = event.motion.x;
627       y = event.motion.y;
628       if(x > pos_x - width()/2 &&
629          x < pos_x + width()/2 &&
630          y > pos_y - height()/2 &&
631          y < pos_y + height()/2)
632         {
633           menuaction = MENU_ACTION_HIT;
634         }
635       break;
636     case SDL_MOUSEMOTION:
637       x = event.motion.x;
638       y = event.motion.y;
639       if(x > pos_x - width()/2 &&
640          x < pos_x + width()/2 &&
641          y > pos_y - height()/2 &&
642          y < pos_y + height()/2)
643         {
644           active_item = (y - (pos_y - height()/2)) / 24;
645           mouse_cursor->set_state(MC_LINK);
646         }
647       else
648         {
649           mouse_cursor->set_state(MC_NORMAL);
650         }
651       break;
652     default:
653       break;
654     }
655 }
656
657
658 // EOF //