6c9fe4eca7bc48974d104d1debcaefe345c804d3
[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 /* Check for menu event */
527 void
528 Menu::event(SDL_Event& event)
529 {
530   SDLKey key;
531   switch(event.type)
532     {
533     case SDL_KEYDOWN:
534       key = event.key.keysym.sym;
535       SDLMod keymod;
536       char ch[2];
537       keymod = SDL_GetModState();
538       int x,y;
539
540       /* If the current unicode character is an ASCII character,
541          assign it to ch. */
542       if ( (event.key.keysym.unicode & 0xFF80) == 0 )
543         {
544           ch[0] = event.key.keysym.unicode & 0x7F;
545           ch[1] = '\0';
546         }
547       else
548         {
549           /* An International Character. */
550         }
551
552       switch(key)
553         {
554         case SDLK_UP:           /* Menu Up */
555           menuaction = MENU_ACTION_UP;
556           break;
557         case SDLK_DOWN:         /* Menu Down */
558           menuaction = MENU_ACTION_DOWN;
559           break;
560         case SDLK_LEFT:         /* Menu Up */
561           menuaction = MENU_ACTION_LEFT;
562           break;
563         case SDLK_RIGHT:                /* Menu Down */
564           menuaction = MENU_ACTION_RIGHT;
565           break;
566         case SDLK_SPACE:
567           if(item[active_item].kind == MN_TEXTFIELD)
568             {
569               menuaction = MENU_ACTION_INPUT;
570               mn_input_char = ' ';
571               break;
572             }
573         case SDLK_RETURN: /* Menu Hit */
574           menuaction = MENU_ACTION_HIT;
575           break;
576         case SDLK_DELETE:
577         case SDLK_BACKSPACE:
578           menuaction = MENU_ACTION_REMOVE;
579           delete_character++;
580           break;
581         case SDLK_ESCAPE:
582           if(Menu::current())
583             {
584               if (has_backitem == true && last_menu != NULL)
585                 Menu::set_current(last_menu);
586               else
587                 Menu::set_current(0);
588             }
589         default:
590           if( (key >= SDLK_0 && key <= SDLK_9) || (key >= SDLK_a && key <= SDLK_z) || (key >= SDLK_SPACE && key <= SDLK_SLASH))
591             {
592               menuaction = MENU_ACTION_INPUT;
593               mn_input_char = *ch;
594             }
595           else
596             {
597               mn_input_char = '\0';
598             }
599           break;
600         }
601       break;
602     case  SDL_JOYAXISMOTION:
603       if(event.jaxis.axis == JOY_Y)
604         {
605           if (event.jaxis.value > 1024)
606             menuaction = MENU_ACTION_DOWN;
607           else if (event.jaxis.value < -1024)
608             menuaction = MENU_ACTION_UP;
609         }
610       break;
611     case  SDL_JOYBUTTONDOWN:
612       menuaction = MENU_ACTION_HIT;
613       break;
614     case SDL_MOUSEBUTTONDOWN:
615       x = event.motion.x;
616       y = event.motion.y;
617       if(x > pos_x - width()/2 &&
618          x < pos_x + width()/2 &&
619          y > pos_y - height()/2 &&
620          y < pos_y + height()/2)
621         {
622           menuaction = MENU_ACTION_HIT;
623         }
624       break;
625     case SDL_MOUSEMOTION:
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           active_item = (y - (pos_y - height()/2)) / 24;
634           mouse_cursor->set_state(MC_LINK);
635         }
636       else
637         {
638           mouse_cursor->set_state(MC_NORMAL);
639         }
640       break;
641     default:
642       break;
643     }
644 }
645
646
647 // EOF //