- increased Tux's safe time to 1250ms
[supertux.git] / src / menu.cpp
1 //  $Id$
2 //
3 //  SuperTux
4 //  Copyright (C) 2004 Tobias Glaesser <tobi.web@gmx.de>
5 //
6 //  This program is free software; you can redistribute it and/or
7 //  modify it under the terms of the GNU General Public License
8 //  as published by the Free Software Foundation; either version 2
9 //  of the License, or (at your option) any later version.
10 //
11 //  This program is distributed in the hope that it will be useful,
12 //  but WITHOUT ANY WARRANTY; without even the implied warranty of
13 //  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14 //  GNU General Public License for more details.
15 //
16 //  You should have received a copy of the GNU General Public License
17 //  along with this program; if not, write to the Free Software
18 //  Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
19
20 #ifndef WIN32
21 #include <sys/types.h>
22 #include <ctype.h>
23 #endif
24
25 #include <iostream>
26 #include <stdlib.h>
27 #include <stdio.h>
28 #include <string.h>
29 #include <assert.h>
30
31 #include "defines.h"
32 #include "globals.h"
33 #include "menu.h"
34 #include "screen.h"
35 #include "setup.h"
36 #include "sound.h"
37 #include "scene.h"
38 #include "leveleditor.h"
39 #include "timer.h"
40 #include "high_scores.h"
41
42 #define FLICK_CURSOR_TIME 500
43
44 Surface* checkbox;
45 Surface* checkbox_checked;
46 Surface* back;
47 Surface* arrow_left;
48 Surface* arrow_right;
49
50 Menu* main_menu      = 0;
51 Menu* game_menu      = 0;
52 Menu* worldmap_menu  = 0;
53 Menu* options_menu   = 0;
54 Menu* options_keys_menu     = 0;
55 Menu* options_joystick_menu = 0;
56 Menu* highscore_menu = 0;
57 Menu* load_game_menu = 0;
58 Menu* save_game_menu = 0;
59 Menu* contrib_menu   = 0;
60 Menu* contrib_subset_menu   = 0;
61
62 std::vector<Menu*> Menu::last_menus;
63 Menu* Menu::current_ = 0;
64
65 /* just displays a Yes/No text that can be used to confirm stuff */
66 bool confirm_dialog(std::string text)
67 {
68   Surface* cap_screen = Surface::CaptureScreen();
69   
70   Menu* dialog = new Menu;
71   dialog->additem(MN_DEACTIVE, text,0,0);
72   dialog->additem(MN_HL,"",0,0);
73   dialog->additem(MN_ACTION,"Yes",0,0,true);
74   dialog->additem(MN_ACTION,"No",0,0,false);
75   dialog->additem(MN_HL,"",0,0);
76
77   Menu::set_current(dialog);
78
79   while(true)
80   {
81     SDL_Event event;
82
83     while (SDL_PollEvent(&event))
84     {
85       dialog->event(event);
86     }
87
88     cap_screen->draw(0,0);
89
90     dialog->draw();
91     dialog->action();
92
93     switch (dialog->check())
94     {
95     case true:
96       delete cap_screen;
97       Menu::set_current(0);
98       delete dialog;
99       return true;
100       break;
101     case false:
102       delete cap_screen;
103       Menu::set_current(0);
104       delete dialog;
105       return false;
106       break;
107     default:
108       break;
109     }
110
111     mouse_cursor->draw();
112     flipscreen();
113     SDL_Delay(25);
114   }
115
116
117 }
118
119 void
120 Menu::push_current(Menu* pmenu)
121 {
122   if (current_)
123     last_menus.push_back(current_);
124
125   current_ = pmenu;
126   current_->effect.start(500);
127 }
128
129 void
130 Menu::pop_current()
131 {
132   if (!last_menus.empty())
133   {
134     current_ = last_menus.back();
135     current_->effect.start(500);
136
137     last_menus.pop_back();
138   }
139   else
140   {
141     current_ = 0;
142   }
143 }
144
145 void
146 Menu::set_current(Menu* menu)
147 {
148   last_menus.clear();
149
150   if (menu)
151     menu->effect.start(500);
152
153   current_ = menu;
154 }
155
156 /* Return a pointer to a new menu item */
157 MenuItem*
158 MenuItem::create(MenuItemKind kind_, const char *text_, int init_toggle_, Menu* target_menu_, int id, int* int_p_)
159 {
160   MenuItem *pnew_item = new MenuItem;
161
162   pnew_item->kind = kind_;
163   pnew_item->text = (char*) malloc(sizeof(char) * (strlen(text_) + 1));
164   strcpy(pnew_item->text, text_);
165
166   if(kind_ == MN_TOGGLE)
167     pnew_item->toggled = init_toggle_;
168   else
169     pnew_item->toggled = false;
170
171   pnew_item->target_menu = target_menu_;
172   pnew_item->input = (char*) malloc(sizeof(char));
173   pnew_item->input[0] = '\0';
174
175   if(kind_ == MN_STRINGSELECT)
176   {
177     pnew_item->list = (string_list_type*) malloc(sizeof(string_list_type));
178     string_list_init(pnew_item->list);
179   }
180   else
181     pnew_item->list = NULL;
182
183   pnew_item->id = id;
184   pnew_item->int_p = int_p_;
185
186   pnew_item->input_flickering = false;
187   pnew_item->input_flickering_timer.init(true);
188   pnew_item->input_flickering_timer.start(FLICK_CURSOR_TIME);
189
190   return pnew_item;
191 }
192
193 void
194 MenuItem::change_text(const  char *text_)
195 {
196   if (text_)
197   {
198     free(text);
199     text = (char*) malloc(sizeof(char )*(strlen(text_)+1));
200     strcpy(text, text_);
201   }
202 }
203
204 void
205 MenuItem::change_input(const  char *text_)
206 {
207   if(text)
208   {
209     free(input);
210     input = (char*) malloc(sizeof(char )*(strlen(text_)+1));
211     strcpy(input, text_);
212   }
213 }
214
215 std::string MenuItem::get_input_with_symbol(bool active_item)
216 {
217   if(!active_item)
218     input_flickering = true;
219   else
220   {
221     if(input_flickering_timer.get_left() < 0)
222     {
223       if(input_flickering)
224         input_flickering = false;
225       else
226         input_flickering = true;
227       input_flickering_timer.start(FLICK_CURSOR_TIME);
228     }
229   }
230
231   char str[1024];
232   if(input_flickering)
233     sprintf(str,"%s_",input);
234   else
235     sprintf(str,"%s ",input);
236
237   std::string string = str;
238
239   return string;
240 }
241
242 /* Set ControlField a key */
243 void Menu::get_controlfield_key_into_input(MenuItem *item)
244 {
245   switch(*item->int_p)
246   {
247   case SDLK_UP:
248     item->change_input("Up cursor");
249     break;
250   case SDLK_DOWN:
251     item->change_input("Down cursor");
252     break;
253   case SDLK_LEFT:
254     item->change_input("Left cursor");
255     break;
256   case SDLK_RIGHT:
257     item->change_input("Right cursor");
258     break;
259   case SDLK_RETURN:
260     item->change_input("Return");
261     break;
262   case SDLK_SPACE:
263     item->change_input("Space");
264     break;
265   case SDLK_RSHIFT:
266     item->change_input("Right Shift");
267     break;
268   case SDLK_LSHIFT:
269     item->change_input("Left Shift");
270     break;
271   case SDLK_RCTRL:
272     item->change_input("Right Control");
273     break;
274   case SDLK_LCTRL:
275     item->change_input("Left Control");
276     break;
277   case SDLK_RALT:
278     item->change_input("Right Alt");
279     break;
280   case SDLK_LALT:
281     item->change_input("Left Alt");
282     break;
283   default:
284     {
285       char tmp[64];
286       snprintf(tmp, 64, "%d", *item->int_p);
287       item->change_input(tmp);
288     }
289     break;
290   }
291 }
292
293 /* Free a menu and all its items */
294 Menu::~Menu()
295 {
296   if(item.size() != 0)
297   {
298     for(unsigned int i = 0; i < item.size(); ++i)
299     {
300       free(item[i].text);
301       free(item[i].input);
302       string_list_free(item[i].list);
303     }
304   }
305 }
306
307
308 Menu::Menu()
309 {
310   hit_item = -1;
311   menuaction = MENU_ACTION_NONE;
312   delete_character = 0;
313   mn_input_char = '\0';
314
315   pos_x        = screen->w/2;
316   pos_y        = screen->h/2;
317   arrange_left = 0;
318   active_item  = 0;
319   effect.init(false);
320 }
321
322 void Menu::set_pos(int x, int y, float rw, float rh)
323 {
324   pos_x = x + (int)((float)get_width() * rw);
325   pos_y = y + (int)((float)get_height() * rh);
326 }
327
328 void
329 Menu::additem(MenuItemKind kind_, const std::string& text_, int toggle_, Menu* menu_, int id, int* int_p)
330 {
331   additem(MenuItem::create(kind_, text_.c_str(), toggle_, menu_, id, int_p));
332 }
333
334 /* Add an item to a menu */
335 void
336 Menu::additem(MenuItem* pmenu_item)
337 {
338   item.push_back(*pmenu_item);
339   delete pmenu_item;
340 }
341
342 void
343 Menu::clear()
344 {
345   item.clear();
346 }
347
348 /* Process actions done on the menu */
349 void
350 Menu::action()
351 {
352   hit_item = -1;
353   if(item.size() != 0)
354   {
355     switch(menuaction)
356     {
357     case MENU_ACTION_UP:
358       if (active_item > 0)
359         --active_item;
360       else
361         active_item = int(item.size())-1;
362       break;
363
364     case MENU_ACTION_DOWN:
365       if(active_item < int(item.size())-1)
366         ++active_item;
367       else
368         active_item = 0;
369       break;
370
371     case MENU_ACTION_LEFT:
372       if(item[active_item].kind == MN_STRINGSELECT
373           && item[active_item].list->num_items != 0)
374       {
375         if(item[active_item].list->active_item > 0)
376           --item[active_item].list->active_item;
377         else
378           item[active_item].list->active_item = item[active_item].list->num_items-1;
379       }
380       break;
381
382     case MENU_ACTION_RIGHT:
383       if(item[active_item].kind == MN_STRINGSELECT
384           && item[active_item].list->num_items != 0)
385       {
386         if(item[active_item].list->active_item < item[active_item].list->num_items-1)
387           ++item[active_item].list->active_item;
388         else
389           item[active_item].list->active_item = 0;
390       }
391       break;
392
393     case MENU_ACTION_HIT:
394       {
395         hit_item = active_item;
396         switch (item[active_item].kind)
397         {
398         case MN_GOTO:
399           if (item[active_item].target_menu != NULL)
400             Menu::push_current(item[active_item].target_menu);
401           else
402             puts("NULLL");
403           break;
404
405         case MN_TOGGLE:
406           item[active_item].toggled = !item[active_item].toggled;
407           break;
408
409         case MN_ACTION:
410           Menu::set_current(0);
411           item[active_item].toggled = true;
412           break;
413         case MN_TEXTFIELD:
414         case MN_NUMFIELD:
415           menuaction = MENU_ACTION_DOWN;
416           action();
417           break;
418
419         case MN_BACK:
420           Menu::pop_current();
421           break;
422         default:
423           break;
424         }
425       }
426       break;
427
428     case MENU_ACTION_REMOVE:
429       if(item[active_item].kind == MN_TEXTFIELD
430           || item[active_item].kind == MN_NUMFIELD)
431       {
432         if(item[active_item].input != NULL)
433         {
434           int i = strlen(item[active_item].input);
435
436           while(delete_character > 0)   /* remove charactes */
437           {
438             item[active_item].input[i-1] = '\0';
439             delete_character--;
440           }
441         }
442       }
443       break;
444
445     case MENU_ACTION_INPUT:
446       if(item[active_item].kind == MN_TEXTFIELD
447           || (item[active_item].kind == MN_NUMFIELD && mn_input_char >= '0' && mn_input_char <= '9'))
448       {
449         if(item[active_item].input != NULL)
450         {
451           int i = strlen(item[active_item].input);
452           item[active_item].input = (char*) realloc(item[active_item].input,sizeof(char)*(i + 2));
453           item[active_item].input[i] = mn_input_char;
454           item[active_item].input[i+1] = '\0';
455         }
456         else
457         {
458           item[active_item].input = (char*) malloc(2*sizeof(char));
459           item[active_item].input[0] = mn_input_char;
460           item[active_item].input[1] = '\0';
461         }
462       }
463
464     case MENU_ACTION_NONE:
465       break;
466     }
467   }
468
469   MenuItem& new_item = item[active_item];
470   if(new_item.kind == MN_DEACTIVE
471       || new_item.kind == MN_LABEL
472       || new_item.kind == MN_HL)
473   {
474     // Skip the horzontal line item
475     if (menuaction != MENU_ACTION_UP && menuaction != MENU_ACTION_DOWN)
476       menuaction = MENU_ACTION_DOWN;
477
478     if (item.size() > 1)
479       action();
480   }
481
482   menuaction = MENU_ACTION_NONE;
483 }
484
485 int
486 Menu::check()
487 {
488   if (hit_item != -1)
489     return item[hit_item].id;
490   else
491     return -1;
492 }
493
494 void
495 Menu::draw_item(int index, // Position of the current item in the menu
496                 int menu_width,
497                 int menu_height)
498 {
499   MenuItem& pitem = item[index];
500
501   int font_width  = 16;
502   int effect_offset = 0;
503   {
504     int effect_time = 0;
505
506     if(effect.check())
507       effect_time = effect.get_left() / 4;
508
509     effect_offset = (index % 2) ? effect_time : -effect_time;
510   }
511
512   int x_pos       = pos_x;
513   int y_pos       = pos_y + 24*index - menu_height/2 + 12 + effect_offset;
514   int shadow_size = 2;
515   int text_width  = strlen(pitem.text) * font_width;
516   int input_width = (strlen(pitem.input)+ 1) * font_width;
517   int list_width  = strlen(string_list_active(pitem.list)) * font_width;
518   Text* text_font = white_text;
519
520   if (arrange_left)
521     x_pos += 24 - menu_width/2 + (text_width + input_width + list_width)/2;
522
523   if(index == active_item)
524   {
525     shadow_size = 3;
526     text_font = blue_text;
527   }
528
529   switch (pitem.kind)
530   {
531   case MN_DEACTIVE:
532     {
533       black_text->draw_align(pitem.text,
534                              x_pos, y_pos,
535                              A_HMIDDLE, A_VMIDDLE, 2);
536       break;
537     }
538
539   case MN_HL:
540     {
541       int x = pos_x - menu_width/2;
542       int y = y_pos - 12 - effect_offset;
543       /* Draw a horizontal line with a little 3d effect */
544       fillrect(x, y + 6,
545                menu_width, 4,
546                150,200,255,225);
547       fillrect(x, y + 6,
548                menu_width, 2,
549                255,255,255,255);
550       break;
551     }
552   case MN_LABEL:
553     {
554       white_big_text->draw_align(pitem.text,
555                                  x_pos, y_pos,
556                                  A_HMIDDLE, A_VMIDDLE, 2);
557       break;
558     }
559   case MN_TEXTFIELD:
560   case MN_NUMFIELD:
561   case MN_CONTROLFIELD:
562     {
563       int input_pos = input_width/2;
564       int text_pos  = (text_width + font_width)/2;
565
566       fillrect(x_pos - input_pos + text_pos - 1, y_pos - 10,
567                input_width + font_width + 2, 20,
568                255,255,255,255);
569       fillrect(x_pos - input_pos + text_pos, y_pos - 9,
570                input_width + font_width, 18,
571                0,0,0,128);
572
573       if(pitem.kind == MN_CONTROLFIELD)
574         get_controlfield_key_into_input(&pitem);
575
576       if(pitem.kind == MN_TEXTFIELD || pitem.kind == MN_NUMFIELD)
577       {
578         if(active_item == index)
579           gold_text->draw_align((pitem.get_input_with_symbol(true)).c_str(), x_pos + text_pos, y_pos, A_HMIDDLE, A_VMIDDLE, 2);
580         else
581           gold_text->draw_align((pitem.get_input_with_symbol(false)).c_str(), x_pos + text_pos, y_pos, A_HMIDDLE, A_VMIDDLE, 2);
582       }
583       else
584         gold_text->draw_align(pitem.input,
585                               x_pos + text_pos, y_pos,
586                               A_HMIDDLE, A_VMIDDLE, 2);
587
588       text_font->draw_align(pitem.text,
589                             x_pos - (input_width + font_width)/2, y_pos,
590                             A_HMIDDLE, A_VMIDDLE, shadow_size);
591       break;
592     }
593   case MN_STRINGSELECT:
594     {
595       int list_pos_2 = list_width + font_width;
596       int list_pos   = list_width/2;
597       int text_pos   = (text_width + font_width)/2;
598
599       /* Draw arrows */
600       arrow_left->draw(  x_pos - list_pos + text_pos - 17, y_pos - 8);
601       arrow_right->draw( x_pos - list_pos + text_pos - 1 + list_pos_2, y_pos - 8);
602
603       /* Draw input background */
604       fillrect(x_pos - list_pos + text_pos - 1, y_pos - 10,
605                list_pos_2 + 2, 20,
606                255,255,255,255);
607       fillrect(x_pos - list_pos + text_pos, y_pos - 9,
608                list_pos_2, 18,
609                0,0,0,128);
610
611       gold_text->draw_align(string_list_active(pitem.list),
612                             x_pos + text_pos, y_pos,
613                             A_HMIDDLE, A_VMIDDLE,2);
614
615       text_font->draw_align(pitem.text,
616                             x_pos - list_pos_2/2, y_pos,
617                             A_HMIDDLE, A_VMIDDLE, shadow_size);
618       break;
619     }
620   case MN_BACK:
621     {
622       text_font->draw_align(pitem.text, x_pos, y_pos, A_HMIDDLE, A_VMIDDLE, shadow_size);
623       back->draw( x_pos + text_width/2  + font_width, y_pos - 8);
624       break;
625     }
626
627   case MN_TOGGLE:
628     {
629       text_font->draw_align(pitem.text, x_pos, y_pos, A_HMIDDLE, A_VMIDDLE, shadow_size);
630
631       if(pitem.toggled)
632         checkbox_checked->draw(
633           x_pos + (text_width+font_width)/2,
634           y_pos - 8);
635       else
636         checkbox->draw(
637           x_pos + (text_width+font_width)/2,
638           y_pos - 8);
639       break;
640     }
641   case MN_ACTION:
642     text_font->draw_align(pitem.text, x_pos, y_pos, A_HMIDDLE, A_VMIDDLE, shadow_size);
643     break;
644
645   case MN_GOTO:
646     text_font->draw_align(pitem.text, x_pos, y_pos, A_HMIDDLE, A_VMIDDLE, shadow_size);
647     break;
648   }
649 }
650
651 int Menu::get_width() const
652 {
653   /* The width of the menu has to be more than the width of the text
654      with the most characters */
655   int menu_width = 0;
656   for(unsigned int i = 0; i < item.size(); ++i)
657   {
658     int w = strlen(item[i].text) + (item[i].input ? strlen(item[i].input) + 1 : 0) + strlen(string_list_active(item[i].list));
659     if( w > menu_width )
660     {
661       menu_width = w;
662       if( item[i].kind == MN_TOGGLE)
663         menu_width += 2;
664     }
665   }
666
667   return (menu_width * 16 + 24);
668 }
669
670 int Menu::get_height() const
671 {
672   return item.size() * 24;
673 }
674
675 /* Draw the current menu. */
676 void
677 Menu::draw()
678 {
679   int menu_height = get_height();
680   int menu_width  = get_width();
681
682   /* Draw a transparent background */
683   fillrect(pos_x - menu_width/2,
684            pos_y - 24*item.size()/2 - 10,
685            menu_width,menu_height + 20,
686            150,180,200,125);
687
688   for(unsigned int i = 0; i < item.size(); ++i)
689   {
690     draw_item(i, menu_width, menu_height);
691   }
692 }
693
694 MenuItem&
695 Menu::get_item_by_id(int id)
696 {
697   for(std::vector<MenuItem>::iterator i = item.begin(); i != item.end(); ++i)
698   {
699     if(i->id == id)
700       return *i;
701   }
702
703   assert(false);
704   static MenuItem dummyitem;
705   return dummyitem;
706 }
707
708 int Menu::get_active_item_id()
709 {
710   return item[active_item].id;
711 }
712
713 bool
714 Menu::isToggled(int id)
715 {
716   return get_item_by_id(id).toggled;
717 }
718
719 /* Check for menu event */
720 void
721 Menu::event(SDL_Event& event)
722 {
723   SDLKey key;
724   switch(event.type)
725   {
726   case SDL_KEYDOWN:
727     key = event.key.keysym.sym;
728     SDLMod keymod;
729     char ch[2];
730     keymod = SDL_GetModState();
731     int x,y;
732
733     /* If the current unicode character is an ASCII character,
734        assign it to ch. */
735     if ( (event.key.keysym.unicode & 0xFF80) == 0 )
736     {
737       ch[0] = event.key.keysym.unicode & 0x7F;
738       ch[1] = '\0';
739     }
740     else
741     {
742       /* An International Character. */
743     }
744
745     if(item[active_item].kind == MN_CONTROLFIELD)
746     {
747       if(key == SDLK_ESCAPE)
748       {
749         Menu::pop_current();
750         return;
751       }
752       *item[active_item].int_p = key;
753       menuaction = MENU_ACTION_DOWN;
754       return;
755     }
756
757
758     switch(key)
759     {
760     case SDLK_UP:               /* Menu Up */
761       menuaction = MENU_ACTION_UP;
762       break;
763     case SDLK_DOWN:             /* Menu Down */
764       menuaction = MENU_ACTION_DOWN;
765       break;
766     case SDLK_LEFT:             /* Menu Up */
767       menuaction = MENU_ACTION_LEFT;
768       break;
769     case SDLK_RIGHT:            /* Menu Down */
770       menuaction = MENU_ACTION_RIGHT;
771       break;
772     case SDLK_SPACE:
773       if(item[active_item].kind == MN_TEXTFIELD)
774       {
775         menuaction = MENU_ACTION_INPUT;
776         mn_input_char = ' ';
777         break;
778       }
779     case SDLK_RETURN: /* Menu Hit */
780       menuaction = MENU_ACTION_HIT;
781       break;
782     case SDLK_DELETE:
783     case SDLK_BACKSPACE:
784       menuaction = MENU_ACTION_REMOVE;
785       delete_character++;
786       break;
787     case SDLK_ESCAPE:
788       Menu::pop_current();
789       break;
790     default:
791       if( (key >= SDLK_0 && key <= SDLK_9) || (key >= SDLK_a && key <= SDLK_z) || (key >= SDLK_SPACE && key <= SDLK_SLASH))
792       {
793         menuaction = MENU_ACTION_INPUT;
794         mn_input_char = *ch;
795       }
796       else
797       {
798         mn_input_char = '\0';
799       }
800       break;
801     }
802     break;
803   case  SDL_JOYAXISMOTION:
804     if(event.jaxis.axis == joystick_keymap.y_axis)
805     {
806       if (event.jaxis.value > 1024)
807         menuaction = MENU_ACTION_DOWN;
808       else if (event.jaxis.value < -1024)
809         menuaction = MENU_ACTION_UP;
810     }
811     break;
812   case  SDL_JOYBUTTONDOWN:
813     menuaction = MENU_ACTION_HIT;
814     break;
815   case SDL_MOUSEBUTTONDOWN:
816     x = event.motion.x;
817     y = event.motion.y;
818     if(x > pos_x - get_width()/2 &&
819         x < pos_x + get_width()/2 &&
820         y > pos_y - get_height()/2 &&
821         y < pos_y + get_height()/2)
822     {
823       menuaction = MENU_ACTION_HIT;
824     }
825     break;
826   case SDL_MOUSEMOTION:
827     x = event.motion.x;
828     y = event.motion.y;
829     if(x > pos_x - get_width()/2 &&
830         x < pos_x + get_width()/2 &&
831         y > pos_y - get_height()/2 &&
832         y < pos_y + get_height()/2)
833     {
834       active_item = (y - (pos_y - get_height()/2)) / 24;
835       mouse_cursor->set_state(MC_LINK);
836     }
837     else
838     {
839       mouse_cursor->set_state(MC_NORMAL);
840     }
841     break;
842   default:
843     break;
844   }
845 }
846
847
848 // EOF //