Cleaned up MenuManager some more, some ownership issues remain, so things will crash...
[supertux.git] / src / gui / menu.cpp
1 //  SuperTux
2 //  Copyright (C) 2006 Matthias Braun <matze@braunis.de>
3 //
4 //  This program is free software: you can redistribute it and/or modify
5 //  it under the terms of the GNU General Public License as published by
6 //  the Free Software Foundation, either version 3 of the License, or
7 //  (at your option) any later version.
8 //
9 //  This program is distributed in the hope that it will be useful,
10 //  but WITHOUT ANY WARRANTY; without even the implied warranty of
11 //  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 //  GNU General Public License for more details.
13 //
14 //  You should have received a copy of the GNU General Public License
15 //  along with this program.  If not, see <http://www.gnu.org/licenses/>.
16
17 #include "gui/menu.hpp"
18
19 #include <math.h>
20 #include <stdexcept>
21
22 #include "control/input_manager.hpp"
23 #include "gui/menu_item.hpp"
24 #include "gui/menu_manager.hpp"
25 #include "gui/mousecursor.hpp"
26 #include "supertux/globals.hpp"
27 #include "supertux/resources.hpp"
28 #include "supertux/screen_manager.hpp"
29 #include "supertux/timer.hpp"
30 #include "util/gettext.hpp"
31 #include "video/drawing_context.hpp"
32 #include "video/font.hpp"
33 #include "video/renderer.hpp"
34
35 static const float MENU_REPEAT_INITIAL = 0.4f;
36 static const float MENU_REPEAT_RATE    = 0.1f;
37
38 Menu::Menu() :
39   hit_item(),
40   pos(),
41   menuaction(),
42   delete_character(),
43   mn_input_char(),
44   menu_repeat_time(),
45   items(),
46   arrange_left(),
47   active_item()
48 {
49   hit_item = -1;
50   menuaction = MENU_ACTION_NONE;
51   delete_character = 0;
52   mn_input_char = '\0';
53
54   pos.x        = SCREEN_WIDTH/2;
55   pos.y        = SCREEN_HEIGHT/2;
56   arrange_left = 0;
57   active_item  = -1;
58 }
59
60 Menu::~Menu()
61 {
62 }
63
64 void
65 Menu::set_pos(float x, float y)
66 {
67   pos.x = x;
68   pos.y = y;
69 }
70
71 /* Add an item to a menu */
72 MenuItem*
73 Menu::add_item(std::unique_ptr<MenuItem> new_item)
74 {
75   items.push_back(std::move(new_item));
76   MenuItem* item = items.back().get();
77
78   /* If a new menu is being built, the active item shouldn't be set to
79    * something that isn't selectable. Set the active_item to the first
80    * selectable item added.
81    */
82   if (active_item == -1
83       && item->kind != MN_HL
84       && item->kind != MN_LABEL
85       && item->kind != MN_INACTIVE)
86   {
87     active_item = items.size() - 1;
88   }
89
90   return item;
91 }
92
93 MenuItem*
94 Menu::add_hl()
95 {
96   std::unique_ptr<MenuItem> item(new MenuItem(MN_HL));
97   return add_item(std::move(item));
98 }
99
100 MenuItem*
101 Menu::add_label(const std::string& text)
102 {
103   std::unique_ptr<MenuItem> item(new MenuItem(MN_LABEL));
104   item->text = text;
105   return add_item(std::move(item));
106 }
107
108 MenuItem*
109 Menu::add_controlfield(int id, const std::string& text,
110                        const std::string& mapping)
111 {
112   std::unique_ptr<MenuItem> item(new MenuItem(MN_CONTROLFIELD, id));
113   item->change_text(text);
114   item->change_input(mapping);
115   return add_item(std::move(item));
116 }
117
118 MenuItem*
119 Menu::add_entry(int id, const std::string& text)
120 {
121   std::unique_ptr<MenuItem> item(new MenuItem(MN_ACTION, id));
122   item->text = text;
123   return add_item(std::move(item));
124 }
125
126 MenuItem*
127 Menu::add_inactive(int id, const std::string& text)
128 {
129   std::unique_ptr<MenuItem> item(new MenuItem(MN_INACTIVE, id));
130   item->text = text;
131   return add_item(std::move(item));
132 }
133
134 MenuItem*
135 Menu::add_toggle(int id, const std::string& text, bool toogled)
136 {
137   std::unique_ptr<MenuItem> item(new MenuItem(MN_TOGGLE, id));
138   item->text = text;
139   item->toggled = toogled;
140   return add_item(std::move(item));
141 }
142
143 MenuItem*
144 Menu::add_string_select(int id, const std::string& text)
145 {
146   std::unique_ptr<MenuItem> item(new MenuItem(MN_STRINGSELECT, id));
147   item->text = text;
148   return add_item(std::move(item));
149 }
150
151 MenuItem*
152 Menu::add_back(const std::string& text)
153 {
154   std::unique_ptr<MenuItem> item(new MenuItem(MN_BACK));
155   item->text = text;
156   return add_item(std::move(item));
157 }
158
159 MenuItem*
160 Menu::add_submenu(const std::string& text, int submenu)
161 {
162   std::unique_ptr<MenuItem> item(new MenuItem(MN_GOTO));
163   item->text = text;
164   item->target_menu = submenu;
165   return add_item(std::move(item));
166 }
167
168 void
169 Menu::clear()
170 {
171   items.clear();
172   active_item = -1;
173 }
174
175 /* Process actions done on the menu */
176 void
177 Menu::update()
178 {
179   int menu_height = (int) get_height();
180   if (menu_height > SCREEN_HEIGHT)
181   { // Scrolling
182     int scroll_offset = (menu_height - SCREEN_HEIGHT) / 2 + 32;
183     pos.y = SCREEN_HEIGHT/2 - scroll_offset * ((float(active_item) / (items.size()-1)) - 0.5f) * 2.0f;
184   }
185
186   Controller* controller = g_input_manager->get_controller();
187   /** check main input controller... */
188   if(controller->pressed(Controller::UP)) {
189     menuaction = MENU_ACTION_UP;
190     menu_repeat_time = real_time + MENU_REPEAT_INITIAL;
191   }
192   if(controller->hold(Controller::UP) &&
193      menu_repeat_time != 0 && real_time > menu_repeat_time) {
194     menuaction = MENU_ACTION_UP;
195     menu_repeat_time = real_time + MENU_REPEAT_RATE;
196   }
197
198   if(controller->pressed(Controller::DOWN)) {
199     menuaction = MENU_ACTION_DOWN;
200     menu_repeat_time = real_time + MENU_REPEAT_INITIAL;
201   }
202   if(controller->hold(Controller::DOWN) &&
203      menu_repeat_time != 0 && real_time > menu_repeat_time) {
204     menuaction = MENU_ACTION_DOWN;
205     menu_repeat_time = real_time + MENU_REPEAT_RATE;
206   }
207
208   if(controller->pressed(Controller::LEFT)) {
209     menuaction = MENU_ACTION_LEFT;
210     menu_repeat_time = real_time + MENU_REPEAT_INITIAL;
211   }
212   if(controller->hold(Controller::LEFT) &&
213      menu_repeat_time != 0 && real_time > menu_repeat_time) {
214     menuaction = MENU_ACTION_LEFT;
215     menu_repeat_time = real_time + MENU_REPEAT_RATE;
216   }
217
218   if(controller->pressed(Controller::RIGHT)) {
219     menuaction = MENU_ACTION_RIGHT;
220     menu_repeat_time = real_time + MENU_REPEAT_INITIAL;
221   }
222   if(controller->hold(Controller::RIGHT) &&
223      menu_repeat_time != 0 && real_time > menu_repeat_time) {
224     menuaction = MENU_ACTION_RIGHT;
225     menu_repeat_time = real_time + MENU_REPEAT_RATE;
226   }
227
228   if(controller->pressed(Controller::ACTION)
229      || controller->pressed(Controller::MENU_SELECT)) {
230     menuaction = MENU_ACTION_HIT;
231   }
232   if(controller->pressed(Controller::PAUSE_MENU)
233     || controller->pressed(Controller::MENU_BACK)) {
234     menuaction = MENU_ACTION_BACK;
235   }
236
237   hit_item = -1;
238   if(items.size() == 0)
239     return;
240
241   int last_active_item = active_item;
242   switch(menuaction) {
243     case MENU_ACTION_UP:
244       do {
245         if (active_item > 0)
246           --active_item;
247         else
248           active_item = int(items.size())-1;
249       } while ((items[active_item]->kind == MN_HL
250                 || items[active_item]->kind == MN_LABEL
251                 || items[active_item]->kind == MN_INACTIVE)
252                && (active_item != last_active_item));
253
254       break;
255
256     case MENU_ACTION_DOWN:
257       do {
258         if(active_item < int(items.size())-1 )
259           ++active_item;
260         else
261           active_item = 0;
262       } while ((items[active_item]->kind == MN_HL
263                 || items[active_item]->kind == MN_LABEL
264                 || items[active_item]->kind == MN_INACTIVE)
265                && (active_item != last_active_item));
266
267       break;
268
269     case MENU_ACTION_LEFT:
270       if(items[active_item]->kind == MN_STRINGSELECT) {
271         if(items[active_item]->selected > 0)
272           items[active_item]->selected--;
273         else
274           items[active_item]->selected = items[active_item]->list.size()-1;
275
276         menu_action(items[active_item].get());
277       }
278       break;
279
280     case MENU_ACTION_RIGHT:
281       if(items[active_item]->kind == MN_STRINGSELECT) {
282         if(items[active_item]->selected+1 < items[active_item]->list.size())
283           items[active_item]->selected++;
284         else
285           items[active_item]->selected = 0;
286
287         menu_action(items[active_item].get());
288       }
289       break;
290
291     case MENU_ACTION_HIT: {
292       hit_item = active_item;
293       switch (items[active_item]->kind) {
294         case MN_GOTO:
295           assert(items[active_item]->target_menu != 0);
296           MenuManager::instance().push_menu(items[active_item]->target_menu);
297           break;
298
299         case MN_TOGGLE:
300           items[active_item]->toggled = !items[active_item]->toggled;
301           menu_action(items[active_item].get());
302           break;
303
304         case MN_CONTROLFIELD:
305           menu_action(items[active_item].get());
306           break;
307
308         case MN_ACTION:
309           menu_action(items[active_item].get());
310           break;
311
312         case MN_STRINGSELECT:
313           if(items[active_item]->selected+1 < items[active_item]->list.size())
314             items[active_item]->selected++;
315           else
316             items[active_item]->selected = 0;
317
318           menu_action(items[active_item].get());
319           break;
320
321         case MN_TEXTFIELD:
322         case MN_NUMFIELD:
323           menuaction = MENU_ACTION_DOWN;
324           update();
325           break;
326
327         case MN_BACK:
328           MenuManager::instance().pop_menu();
329           break;
330         default:
331           break;
332       }
333       break;
334     }
335
336     case MENU_ACTION_REMOVE:
337       if(items[active_item]->kind == MN_TEXTFIELD
338          || items[active_item]->kind == MN_NUMFIELD)
339       {
340         if(!items[active_item]->input.empty())
341         {
342           int i = items[active_item]->input.size();
343
344           while(delete_character > 0)        /* remove characters */
345           {
346             items[active_item]->input.resize(i-1);
347             delete_character--;
348           }
349         }
350       }
351       break;
352
353     case MENU_ACTION_INPUT:
354       if(items[active_item]->kind == MN_TEXTFIELD
355          || (items[active_item]->kind == MN_NUMFIELD
356              && mn_input_char >= '0' && mn_input_char <= '9'))
357       {
358         items[active_item]->input.push_back(mn_input_char);
359       }
360       break;
361
362     case MENU_ACTION_BACK:
363       MenuManager::instance().pop_menu();
364       break;
365
366     case MENU_ACTION_NONE:
367       break;
368   }
369   menuaction = MENU_ACTION_NONE;
370
371   assert(active_item < int(items.size()));
372 }
373
374 int
375 Menu::check()
376 {
377   if (hit_item != -1)
378   {
379     int id = items[hit_item]->id;
380     // Clear event when checked out.. (we would end up in a loop when we try to leave "fake" submenu like Addons or Contrib)
381     hit_item = -1;
382     return id;
383   }
384   else
385     return -1;
386 }
387
388 void
389 Menu::menu_action(MenuItem* )
390 {}
391
392 void
393 Menu::draw_item(DrawingContext& context, int index)
394 {
395   float menu_height = get_height();
396   float menu_width  = get_width();
397
398   MenuItem& pitem = *(items[index]);
399
400   Color text_color = default_color;
401   float x_pos       = pos.x;
402   float y_pos       = pos.y + 24*index - menu_height/2 + 12;
403   int text_width  = int(Resources::normal_font->get_text_width(pitem.text));
404   int input_width = int(Resources::normal_font->get_text_width(pitem.input) + 10);
405   int list_width = 0;
406
407   float left  = pos.x - menu_width/2 + 16;
408   float right = pos.x + menu_width/2 - 16;
409
410   if(pitem.list.size() > 0) {
411     list_width = (int) Resources::normal_font->get_text_width(pitem.list[pitem.selected]);
412   }
413
414   if (arrange_left)
415     x_pos += 24 - menu_width/2 + (text_width + input_width + list_width)/2;
416
417   if(index == active_item)
418   {
419     text_color = active_color;
420   }
421
422   if(active_item == index)
423   {
424     float blink = (sinf(real_time * M_PI * 1.0f)/2.0f + 0.5f) * 0.5f + 0.25f;
425     context.draw_filled_rect(Rectf(Vector(pos.x - menu_width/2 + 10 - 2, y_pos - 12 - 2),
426                                    Vector(pos.x + menu_width/2 - 10 + 2, y_pos + 12 + 2)),
427                              Color(1.0f, 1.0f, 1.0f, blink),
428                              14.0f,
429                              LAYER_GUI-10);
430     context.draw_filled_rect(Rectf(Vector(pos.x - menu_width/2 + 10, y_pos - 12),
431                                    Vector(pos.x + menu_width/2 - 10, y_pos + 12)),
432                              Color(1.0f, 1.0f, 1.0f, 0.5f),
433                              12.0f,
434                              LAYER_GUI-10);
435   }
436
437   switch (pitem.kind)
438   {
439     case MN_INACTIVE:
440     {
441       context.draw_text(Resources::normal_font, pitem.text,
442                         Vector(pos.x, y_pos - int(Resources::normal_font->get_height()/2)),
443                         ALIGN_CENTER, LAYER_GUI, inactive_color);
444       break;
445     }
446
447     case MN_HL:
448     {
449       // TODO
450       float x = pos.x - menu_width/2;
451       float y = y_pos - 12;
452       /* Draw a horizontal line with a little 3d effect */
453       context.draw_filled_rect(Vector(x, y + 6),
454                                Vector(menu_width, 4),
455                                Color(0.6f, 0.7f, 1.0f, 1.0f), LAYER_GUI);
456       context.draw_filled_rect(Vector(x, y + 6),
457                                Vector(menu_width, 2),
458                                Color(1.0f, 1.0f, 1.0f, 1.0f), LAYER_GUI);
459       break;
460     }
461     case MN_LABEL:
462     {
463       context.draw_text(Resources::big_font, pitem.text,
464                         Vector(pos.x, y_pos - int(Resources::big_font->get_height()/2)),
465                         ALIGN_CENTER, LAYER_GUI, label_color);
466       break;
467     }
468     case MN_TEXTFIELD:
469     case MN_NUMFIELD:
470     case MN_CONTROLFIELD:
471     {
472       if(pitem.kind == MN_TEXTFIELD || pitem.kind == MN_NUMFIELD)
473       {
474         if(active_item == index)
475           context.draw_text(Resources::normal_font,
476                             pitem.get_input_with_symbol(true),
477                             Vector(right, y_pos - int(Resources::normal_font->get_height()/2)),
478                             ALIGN_RIGHT, LAYER_GUI, field_color);
479         else
480           context.draw_text(Resources::normal_font,
481                             pitem.get_input_with_symbol(false),
482                             Vector(right, y_pos - int(Resources::normal_font->get_height()/2)),
483                             ALIGN_RIGHT, LAYER_GUI, field_color);
484       }
485       else
486         context.draw_text(Resources::normal_font, pitem.input,
487                           Vector(right, y_pos - int(Resources::normal_font->get_height()/2)),
488                           ALIGN_RIGHT, LAYER_GUI, field_color);
489
490       context.draw_text(Resources::normal_font, pitem.text,
491                         Vector(left, y_pos - int(Resources::normal_font->get_height()/2)),
492                         ALIGN_LEFT, LAYER_GUI, text_color);
493       break;
494     }
495     case MN_STRINGSELECT:
496     {
497       float roff = Resources::arrow_left->get_width();
498       // Draw left side
499       context.draw_text(Resources::normal_font, pitem.text,
500                         Vector(left, y_pos - int(Resources::normal_font->get_height()/2)),
501                         ALIGN_LEFT, LAYER_GUI, text_color);
502
503       // Draw right side
504       context.draw_surface(Resources::arrow_left,
505                            Vector(right - list_width - roff - roff, y_pos - 8),
506                            LAYER_GUI);
507       context.draw_surface(Resources::arrow_right,
508                            Vector(right - roff, y_pos - 8),
509                            LAYER_GUI);
510       context.draw_text(Resources::normal_font, pitem.list[pitem.selected],
511                         Vector(right - roff, y_pos - int(Resources::normal_font->get_height()/2)),
512                         ALIGN_RIGHT, LAYER_GUI, text_color);
513       break;
514     }
515     case MN_BACK:
516     {
517       context.draw_text(Resources::Resources::normal_font, pitem.text,
518                         Vector(pos.x, y_pos - int(Resources::normal_font->get_height()/2)),
519                         ALIGN_CENTER, LAYER_GUI, text_color);
520       context.draw_surface(Resources::back,
521                            Vector(x_pos + text_width/2  + 16, y_pos - 8),
522                            LAYER_GUI);
523       break;
524     }
525
526     case MN_TOGGLE:
527     {
528       context.draw_text(Resources::normal_font, pitem.text,
529                         Vector(pos.x - menu_width/2 + 16, y_pos - (Resources::normal_font->get_height()/2)),
530                         ALIGN_LEFT, LAYER_GUI, text_color);
531
532       if(pitem.toggled)
533         context.draw_surface(Resources::checkbox_checked,
534                              Vector(x_pos + (menu_width/2-16) - Resources::checkbox->get_width(), y_pos - 8),
535                              LAYER_GUI + 1);
536       else
537         context.draw_surface(Resources::checkbox,
538                              Vector(x_pos + (menu_width/2-16) - Resources::checkbox->get_width(), y_pos - 8),
539                              LAYER_GUI + 1);
540       break;
541     }
542     case MN_ACTION:
543       context.draw_text(Resources::normal_font, pitem.text,
544                         Vector(pos.x, y_pos - int(Resources::normal_font->get_height()/2)),
545                         ALIGN_CENTER, LAYER_GUI, text_color);
546       break;
547
548     case MN_GOTO:
549       context.draw_text(Resources::normal_font, pitem.text,
550                         Vector(pos.x, y_pos - int(Resources::normal_font->get_height()/2)),
551                         ALIGN_CENTER, LAYER_GUI, text_color);
552       break;
553   }
554 }
555
556 float
557 Menu::get_width() const
558 {
559   /* The width of the menu has to be more than the width of the text
560      with the most characters */
561   float menu_width = 0;
562   for(unsigned int i = 0; i < items.size(); ++i)
563   {
564     FontPtr font = Resources::Resources::normal_font;
565     if(items[i]->kind == MN_LABEL)
566       font = Resources::big_font;
567
568     float w = font->get_text_width(items[i]->text) +
569       Resources::big_font->get_text_width(items[i]->input) + 16;
570     if(items[i]->kind == MN_TOGGLE)
571       w += 32;
572     if (items[i]->kind == MN_STRINGSELECT)
573       w += font->get_text_width(items[i]->list[items[i]->selected]) + 32;
574
575
576     if(w > menu_width)
577       menu_width = w;
578   }
579
580   return menu_width + 24;
581 }
582
583 float
584 Menu::get_height() const
585 {
586   return items.size() * 24;
587 }
588
589 /* Draw the current menu. */
590 void
591 Menu::draw(DrawingContext& context)
592 {
593   if (!items[active_item]->help.empty())
594   {
595     int text_width  = (int) Resources::normal_font->get_text_width(items[active_item]->help);
596     int text_height = (int) Resources::normal_font->get_text_height(items[active_item]->help);
597
598     Rectf text_rect(pos.x - text_width/2 - 8,
599                    SCREEN_HEIGHT - 48 - text_height/2 - 4,
600                    pos.x + text_width/2 + 8,
601                    SCREEN_HEIGHT - 48 + text_height/2 + 4);
602
603     context.draw_filled_rect(Rectf(text_rect.p1 - Vector(4,4),
604                                    text_rect.p2 + Vector(4,4)),
605                              Color(0.2f, 0.3f, 0.4f, 0.8f),
606                              16.0f,
607                              LAYER_GUI-10);
608
609     context.draw_filled_rect(text_rect,
610                              Color(0.6f, 0.7f, 0.8f, 0.5f),
611                              16.0f,
612                              LAYER_GUI-10);
613
614     context.draw_text(Resources::normal_font, items[active_item]->help,
615                       Vector(pos.x, SCREEN_HEIGHT - 48 - text_height/2),
616                       ALIGN_CENTER, LAYER_GUI);
617   }
618
619   for(unsigned int i = 0; i < items.size(); ++i)
620   {
621     draw_item(context, i);
622   }
623 }
624
625 MenuItem&
626 Menu::get_item_by_id(int id)
627 {
628   for (const auto& item : items)
629   {
630     if (item->id == id)
631     {
632       return *item;
633     }
634   }
635
636   throw std::runtime_error("MenuItem not found");
637 }
638
639 const MenuItem&
640 Menu::get_item_by_id(int id) const
641 {
642   for (const auto& item : items)
643   {
644     if (item->id == id)
645     {
646       return *item;
647     }
648   }
649
650   throw std::runtime_error("MenuItem not found");
651 }
652
653 int Menu::get_active_item_id()
654 {
655   return items[active_item]->id;
656 }
657
658 bool
659 Menu::is_toggled(int id) const
660 {
661   return get_item_by_id(id).toggled;
662 }
663
664 void
665 Menu::set_toggled(int id, bool toggled)
666 {
667   get_item_by_id(id).toggled = toggled;
668 }
669
670 /* Check for menu event */
671 void
672 Menu::event(const SDL_Event& event)
673 {
674   switch(event.type) {
675     case SDL_MOUSEBUTTONDOWN:
676     if(event.button.button == SDL_BUTTON_LEFT)
677     {
678       Vector mouse_pos = Renderer::instance()->to_logical(event.motion.x, event.motion.y);
679       int x = int(mouse_pos.x);
680       int y = int(mouse_pos.y);
681
682       if(x > pos.x - get_width()/2 &&
683          x < pos.x + get_width()/2 &&
684          y > pos.y - get_height()/2 &&
685          y < pos.y + get_height()/2)
686       {
687         menuaction = MENU_ACTION_HIT;
688       }
689     }
690     break;
691
692     case SDL_MOUSEMOTION:
693     {
694       Vector mouse_pos = Renderer::instance()->to_logical(event.motion.x, event.motion.y);
695       float x = mouse_pos.x;
696       float y = mouse_pos.y;
697
698       if(x > pos.x - get_width()/2 &&
699          x < pos.x + get_width()/2 &&
700          y > pos.y - get_height()/2 &&
701          y < pos.y + get_height()/2)
702       {
703         int new_active_item
704           = static_cast<int> ((y - (pos.y - get_height()/2)) / 24);
705
706         /* only change the mouse focus to a selectable item */
707         if ((items[new_active_item]->kind != MN_HL)
708             && (items[new_active_item]->kind != MN_LABEL)
709             && (items[new_active_item]->kind != MN_INACTIVE))
710           active_item = new_active_item;
711
712         if(MouseCursor::current())
713           MouseCursor::current()->set_state(MC_LINK);
714       }
715       else
716       {
717         if(MouseCursor::current())
718           MouseCursor::current()->set_state(MC_NORMAL);
719       }
720     }
721     break;
722
723     default:
724       break;
725   }
726 }
727
728 void
729 Menu::set_active_item(int id)
730 {
731   for(size_t i = 0; i < items.size(); ++i) {
732     if(items[i]->id == id) {
733       active_item = i;
734       break;
735     }
736   }
737 }
738
739 /* EOF */