- fixed a warning
[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   if (active_item >= int(item.size()))
485     active_item = int(item.size()) - 1;
486 }
487
488 int
489 Menu::check()
490 {
491   if (hit_item != -1)
492     return item[hit_item].id;
493   else
494     return -1;
495 }
496
497 void
498 Menu::draw_item(int index, // Position of the current item in the menu
499                 int menu_width,
500                 int menu_height)
501 {
502   MenuItem& pitem = item[index];
503
504   int font_width  = 16;
505   int effect_offset = 0;
506   {
507     int effect_time = 0;
508
509     if(effect.check())
510       effect_time = effect.get_left() / 4;
511
512     effect_offset = (index % 2) ? effect_time : -effect_time;
513   }
514
515   int x_pos       = pos_x;
516   int y_pos       = pos_y + 24*index - menu_height/2 + 12 + effect_offset;
517   int shadow_size = 2;
518   int text_width  = strlen(pitem.text) * font_width;
519   int input_width = (strlen(pitem.input)+ 1) * font_width;
520   int list_width  = strlen(string_list_active(pitem.list)) * font_width;
521   Text* text_font = white_text;
522
523   if (arrange_left)
524     x_pos += 24 - menu_width/2 + (text_width + input_width + list_width)/2;
525
526   if(index == active_item)
527   {
528     shadow_size = 3;
529     text_font = blue_text;
530   }
531
532   switch (pitem.kind)
533   {
534   case MN_DEACTIVE:
535     {
536       black_text->draw_align(pitem.text,
537                              x_pos, y_pos,
538                              A_HMIDDLE, A_VMIDDLE, 2);
539       break;
540     }
541
542   case MN_HL:
543     {
544       int x = pos_x - menu_width/2;
545       int y = y_pos - 12 - effect_offset;
546       /* Draw a horizontal line with a little 3d effect */
547       fillrect(x, y + 6,
548                menu_width, 4,
549                150,200,255,225);
550       fillrect(x, y + 6,
551                menu_width, 2,
552                255,255,255,255);
553       break;
554     }
555   case MN_LABEL:
556     {
557       white_big_text->draw_align(pitem.text,
558                                  x_pos, y_pos,
559                                  A_HMIDDLE, A_VMIDDLE, 2);
560       break;
561     }
562   case MN_TEXTFIELD:
563   case MN_NUMFIELD:
564   case MN_CONTROLFIELD:
565     {
566       int input_pos = input_width/2;
567       int text_pos  = (text_width + font_width)/2;
568
569       fillrect(x_pos - input_pos + text_pos - 1, y_pos - 10,
570                input_width + font_width + 2, 20,
571                255,255,255,255);
572       fillrect(x_pos - input_pos + text_pos, y_pos - 9,
573                input_width + font_width, 18,
574                0,0,0,128);
575
576       if(pitem.kind == MN_CONTROLFIELD)
577         get_controlfield_key_into_input(&pitem);
578
579       if(pitem.kind == MN_TEXTFIELD || pitem.kind == MN_NUMFIELD)
580       {
581         if(active_item == index)
582           gold_text->draw_align((pitem.get_input_with_symbol(true)).c_str(), x_pos + text_pos, y_pos, A_HMIDDLE, A_VMIDDLE, 2);
583         else
584           gold_text->draw_align((pitem.get_input_with_symbol(false)).c_str(), x_pos + text_pos, y_pos, A_HMIDDLE, A_VMIDDLE, 2);
585       }
586       else
587         gold_text->draw_align(pitem.input,
588                               x_pos + text_pos, y_pos,
589                               A_HMIDDLE, A_VMIDDLE, 2);
590
591       text_font->draw_align(pitem.text,
592                             x_pos - (input_width + font_width)/2, y_pos,
593                             A_HMIDDLE, A_VMIDDLE, shadow_size);
594       break;
595     }
596   case MN_STRINGSELECT:
597     {
598       int list_pos_2 = list_width + font_width;
599       int list_pos   = list_width/2;
600       int text_pos   = (text_width + font_width)/2;
601
602       /* Draw arrows */
603       arrow_left->draw(  x_pos - list_pos + text_pos - 17, y_pos - 8);
604       arrow_right->draw( x_pos - list_pos + text_pos - 1 + list_pos_2, y_pos - 8);
605
606       /* Draw input background */
607       fillrect(x_pos - list_pos + text_pos - 1, y_pos - 10,
608                list_pos_2 + 2, 20,
609                255,255,255,255);
610       fillrect(x_pos - list_pos + text_pos, y_pos - 9,
611                list_pos_2, 18,
612                0,0,0,128);
613
614       gold_text->draw_align(string_list_active(pitem.list),
615                             x_pos + text_pos, y_pos,
616                             A_HMIDDLE, A_VMIDDLE,2);
617
618       text_font->draw_align(pitem.text,
619                             x_pos - list_pos_2/2, y_pos,
620                             A_HMIDDLE, A_VMIDDLE, shadow_size);
621       break;
622     }
623   case MN_BACK:
624     {
625       text_font->draw_align(pitem.text, x_pos, y_pos, A_HMIDDLE, A_VMIDDLE, shadow_size);
626       back->draw( x_pos + text_width/2  + font_width, y_pos - 8);
627       break;
628     }
629
630   case MN_TOGGLE:
631     {
632       text_font->draw_align(pitem.text, x_pos, y_pos, A_HMIDDLE, A_VMIDDLE, shadow_size);
633
634       if(pitem.toggled)
635         checkbox_checked->draw(
636           x_pos + (text_width+font_width)/2,
637           y_pos - 8);
638       else
639         checkbox->draw(
640           x_pos + (text_width+font_width)/2,
641           y_pos - 8);
642       break;
643     }
644   case MN_ACTION:
645     text_font->draw_align(pitem.text, x_pos, y_pos, A_HMIDDLE, A_VMIDDLE, shadow_size);
646     break;
647
648   case MN_GOTO:
649     text_font->draw_align(pitem.text, x_pos, y_pos, A_HMIDDLE, A_VMIDDLE, shadow_size);
650     break;
651   }
652 }
653
654 int Menu::get_width() const
655 {
656   /* The width of the menu has to be more than the width of the text
657      with the most characters */
658   int menu_width = 0;
659   for(unsigned int i = 0; i < item.size(); ++i)
660   {
661     int w = strlen(item[i].text) + (item[i].input ? strlen(item[i].input) + 1 : 0) + strlen(string_list_active(item[i].list));
662     if( w > menu_width )
663     {
664       menu_width = w;
665       if( item[i].kind == MN_TOGGLE)
666         menu_width += 2;
667     }
668   }
669
670   return (menu_width * 16 + 24);
671 }
672
673 int Menu::get_height() const
674 {
675   return item.size() * 24;
676 }
677
678 /* Draw the current menu. */
679 void
680 Menu::draw()
681 {
682   int menu_height = get_height();
683   int menu_width  = get_width();
684
685   /* Draw a transparent background */
686   fillrect(pos_x - menu_width/2,
687            pos_y - 24*item.size()/2 - 10,
688            menu_width,menu_height + 20,
689            150,180,200,125);
690
691   for(unsigned int i = 0; i < item.size(); ++i)
692   {
693     draw_item(i, menu_width, menu_height);
694   }
695 }
696
697 MenuItem&
698 Menu::get_item_by_id(int id)
699 {
700   for(std::vector<MenuItem>::iterator i = item.begin(); i != item.end(); ++i)
701   {
702     if(i->id == id)
703       return *i;
704   }
705
706   assert(false);
707   static MenuItem dummyitem;
708   return dummyitem;
709 }
710
711 int Menu::get_active_item_id()
712 {
713   return item[active_item].id;
714 }
715
716 bool
717 Menu::isToggled(int id)
718 {
719   return get_item_by_id(id).toggled;
720 }
721
722 /* Check for menu event */
723 void
724 Menu::event(SDL_Event& event)
725 {
726   SDLKey key;
727   switch(event.type)
728   {
729   case SDL_KEYDOWN:
730     key = event.key.keysym.sym;
731     SDLMod keymod;
732     char ch[2];
733     keymod = SDL_GetModState();
734     int x,y;
735
736     /* If the current unicode character is an ASCII character,
737        assign it to ch. */
738     if ( (event.key.keysym.unicode & 0xFF80) == 0 )
739     {
740       ch[0] = event.key.keysym.unicode & 0x7F;
741       ch[1] = '\0';
742     }
743     else
744     {
745       /* An International Character. */
746     }
747
748     if(item[active_item].kind == MN_CONTROLFIELD)
749     {
750       if(key == SDLK_ESCAPE)
751       {
752         Menu::pop_current();
753         return;
754       }
755       *item[active_item].int_p = key;
756       menuaction = MENU_ACTION_DOWN;
757       return;
758     }
759
760
761     switch(key)
762     {
763     case SDLK_UP:               /* Menu Up */
764       menuaction = MENU_ACTION_UP;
765       break;
766     case SDLK_DOWN:             /* Menu Down */
767       menuaction = MENU_ACTION_DOWN;
768       break;
769     case SDLK_LEFT:             /* Menu Up */
770       menuaction = MENU_ACTION_LEFT;
771       break;
772     case SDLK_RIGHT:            /* Menu Down */
773       menuaction = MENU_ACTION_RIGHT;
774       break;
775     case SDLK_SPACE:
776       if(item[active_item].kind == MN_TEXTFIELD)
777       {
778         menuaction = MENU_ACTION_INPUT;
779         mn_input_char = ' ';
780         break;
781       }
782     case SDLK_RETURN: /* Menu Hit */
783       menuaction = MENU_ACTION_HIT;
784       break;
785     case SDLK_DELETE:
786     case SDLK_BACKSPACE:
787       menuaction = MENU_ACTION_REMOVE;
788       delete_character++;
789       break;
790     case SDLK_ESCAPE:
791       Menu::pop_current();
792       break;
793     default:
794       if( (key >= SDLK_0 && key <= SDLK_9) || (key >= SDLK_a && key <= SDLK_z) || (key >= SDLK_SPACE && key <= SDLK_SLASH))
795       {
796         menuaction = MENU_ACTION_INPUT;
797         mn_input_char = *ch;
798       }
799       else
800       {
801         mn_input_char = '\0';
802       }
803       break;
804     }
805     break;
806   case  SDL_JOYAXISMOTION:
807     if(event.jaxis.axis == joystick_keymap.y_axis)
808     {
809       if (event.jaxis.value > 1024)
810         menuaction = MENU_ACTION_DOWN;
811       else if (event.jaxis.value < -1024)
812         menuaction = MENU_ACTION_UP;
813     }
814     break;
815   case  SDL_JOYBUTTONDOWN:
816     menuaction = MENU_ACTION_HIT;
817     break;
818   case SDL_MOUSEBUTTONDOWN:
819     x = event.motion.x;
820     y = event.motion.y;
821     if(x > pos_x - get_width()/2 &&
822         x < pos_x + get_width()/2 &&
823         y > pos_y - get_height()/2 &&
824         y < pos_y + get_height()/2)
825     {
826       menuaction = MENU_ACTION_HIT;
827     }
828     break;
829   case SDL_MOUSEMOTION:
830     x = event.motion.x;
831     y = event.motion.y;
832     if(x > pos_x - get_width()/2 &&
833         x < pos_x + get_width()/2 &&
834         y > pos_y - get_height()/2 &&
835         y < pos_y + get_height()/2)
836     {
837       active_item = (y - (pos_y - get_height()/2)) / 24;
838       mouse_cursor->set_state(MC_LINK);
839     }
840     else
841     {
842       mouse_cursor->set_state(MC_NORMAL);
843     }
844     break;
845   default:
846     break;
847   }
848 }
849
850
851 // EOF //