2 // Copyright (C) 2006 Matthias Braun <matze@braunis.de>
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.
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.
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/>.
17 #include "gui/menu.hpp"
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 #include "video/video_system.hpp"
36 static const float MENU_REPEAT_INITIAL = 0.4f;
37 static const float MENU_REPEAT_RATE = 0.1f;
51 pos.x = SCREEN_WIDTH/2;
52 pos.y = SCREEN_HEIGHT/2;
62 Menu::set_center_pos(float x, float y)
68 /* Add an item to a menu */
70 Menu::add_item(std::unique_ptr<MenuItem> new_item)
72 items.push_back(std::move(new_item));
73 MenuItem* item = items.back().get();
75 /* If a new menu is being built, the active item shouldn't be set to
76 * something that isn't selectable. Set the active_item to the first
77 * selectable item added.
80 && item->kind != MN_HL
81 && item->kind != MN_LABEL
82 && item->kind != MN_INACTIVE)
84 active_item = items.size() - 1;
93 std::unique_ptr<MenuItem> item(new MenuItem(MN_HL));
94 return add_item(std::move(item));
98 Menu::add_label(const std::string& text)
100 std::unique_ptr<MenuItem> item(new MenuItem(MN_LABEL));
102 return add_item(std::move(item));
106 Menu::add_controlfield(int id, const std::string& text,
107 const std::string& mapping)
109 std::unique_ptr<MenuItem> item(new MenuItem(MN_CONTROLFIELD, id));
110 item->change_text(text);
111 item->change_input(mapping);
112 return add_item(std::move(item));
116 Menu::add_entry(int id, const std::string& text)
118 std::unique_ptr<MenuItem> item(new MenuItem(MN_ACTION, id));
120 return add_item(std::move(item));
124 Menu::add_inactive(int id, const std::string& text)
126 std::unique_ptr<MenuItem> item(new MenuItem(MN_INACTIVE, id));
128 return add_item(std::move(item));
132 Menu::add_toggle(int id, const std::string& text, bool toogled)
134 std::unique_ptr<MenuItem> item(new MenuItem(MN_TOGGLE, id));
136 item->toggled = toogled;
137 return add_item(std::move(item));
141 Menu::add_string_select(int id, const std::string& text)
143 std::unique_ptr<MenuItem> item(new MenuItem(MN_STRINGSELECT, id));
145 return add_item(std::move(item));
149 Menu::add_back(const std::string& text)
151 std::unique_ptr<MenuItem> item(new MenuItem(MN_BACK));
153 return add_item(std::move(item));
157 Menu::add_submenu(const std::string& text, int submenu)
159 std::unique_ptr<MenuItem> item(new MenuItem(MN_GOTO));
161 item->target_menu = submenu;
162 return add_item(std::move(item));
173 Menu::process_input()
175 int menu_height = (int) get_height();
176 if (menu_height > SCREEN_HEIGHT)
178 int scroll_offset = (menu_height - SCREEN_HEIGHT) / 2 + 32;
179 pos.y = SCREEN_HEIGHT/2 - scroll_offset * ((float(active_item) / (items.size()-1)) - 0.5f) * 2.0f;
182 MenuAction menuaction = MENU_ACTION_NONE;
183 Controller* controller = InputManager::current()->get_controller();
184 /** check main input controller... */
185 if(controller->pressed(Controller::UP)) {
186 menuaction = MENU_ACTION_UP;
187 menu_repeat_time = real_time + MENU_REPEAT_INITIAL;
189 if(controller->hold(Controller::UP) &&
190 menu_repeat_time != 0 && real_time > menu_repeat_time) {
191 menuaction = MENU_ACTION_UP;
192 menu_repeat_time = real_time + MENU_REPEAT_RATE;
195 if(controller->pressed(Controller::DOWN)) {
196 menuaction = MENU_ACTION_DOWN;
197 menu_repeat_time = real_time + MENU_REPEAT_INITIAL;
199 if(controller->hold(Controller::DOWN) &&
200 menu_repeat_time != 0 && real_time > menu_repeat_time) {
201 menuaction = MENU_ACTION_DOWN;
202 menu_repeat_time = real_time + MENU_REPEAT_RATE;
205 if(controller->pressed(Controller::LEFT)) {
206 menuaction = MENU_ACTION_LEFT;
207 menu_repeat_time = real_time + MENU_REPEAT_INITIAL;
209 if(controller->hold(Controller::LEFT) &&
210 menu_repeat_time != 0 && real_time > menu_repeat_time) {
211 menuaction = MENU_ACTION_LEFT;
212 menu_repeat_time = real_time + MENU_REPEAT_RATE;
215 if(controller->pressed(Controller::RIGHT)) {
216 menuaction = MENU_ACTION_RIGHT;
217 menu_repeat_time = real_time + MENU_REPEAT_INITIAL;
219 if(controller->hold(Controller::RIGHT) &&
220 menu_repeat_time != 0 && real_time > menu_repeat_time) {
221 menuaction = MENU_ACTION_RIGHT;
222 menu_repeat_time = real_time + MENU_REPEAT_RATE;
225 if(controller->pressed(Controller::ACTION)
226 || controller->pressed(Controller::MENU_SELECT)) {
227 menuaction = MENU_ACTION_HIT;
229 if(controller->pressed(Controller::PAUSE_MENU)
230 || controller->pressed(Controller::MENU_BACK)) {
231 menuaction = MENU_ACTION_BACK;
234 if(items.size() == 0)
237 // The menu_action() call can pop() the menu from the stack and thus
238 // delete it, so it's important that no further member variables are
239 // accessed after this call
240 process_action(menuaction);
244 Menu::process_action(MenuAction menuaction)
246 int last_active_item = active_item;
253 active_item = int(items.size())-1;
254 } while ((items[active_item]->kind == MN_HL
255 || items[active_item]->kind == MN_LABEL
256 || items[active_item]->kind == MN_INACTIVE)
257 && (active_item != last_active_item));
261 case MENU_ACTION_DOWN:
263 if(active_item < int(items.size())-1 )
267 } while ((items[active_item]->kind == MN_HL
268 || items[active_item]->kind == MN_LABEL
269 || items[active_item]->kind == MN_INACTIVE)
270 && (active_item != last_active_item));
274 case MENU_ACTION_LEFT:
275 if(items[active_item]->kind == MN_STRINGSELECT) {
276 if(items[active_item]->selected > 0)
277 items[active_item]->selected--;
279 items[active_item]->selected = items[active_item]->list.size()-1;
281 menu_action(items[active_item].get());
285 case MENU_ACTION_RIGHT:
286 if(items[active_item]->kind == MN_STRINGSELECT) {
287 if(items[active_item]->selected+1 < items[active_item]->list.size())
288 items[active_item]->selected++;
290 items[active_item]->selected = 0;
292 menu_action(items[active_item].get());
296 case MENU_ACTION_HIT: {
297 switch (items[active_item]->kind) {
299 assert(items[active_item]->target_menu != 0);
300 MenuManager::instance().push_menu(items[active_item]->target_menu);
304 items[active_item]->toggled = !items[active_item]->toggled;
305 menu_action(items[active_item].get());
308 case MN_CONTROLFIELD:
309 menu_action(items[active_item].get());
313 menu_action(items[active_item].get());
316 case MN_STRINGSELECT:
317 if(items[active_item]->selected+1 < items[active_item]->list.size())
318 items[active_item]->selected++;
320 items[active_item]->selected = 0;
322 menu_action(items[active_item].get());
327 menuaction = MENU_ACTION_DOWN;
332 MenuManager::instance().pop_menu();
341 case MENU_ACTION_REMOVE:
342 if(items[active_item]->kind == MN_TEXTFIELD
343 || items[active_item]->kind == MN_NUMFIELD)
345 if(!items[active_item]->input.empty())
347 int i = items[active_item]->input.size();
349 while(delete_character > 0) /* remove characters */
351 items[active_item]->input.resize(i-1);
358 case MENU_ACTION_INPUT:
359 if(items[active_item]->kind == MN_TEXTFIELD
360 || (items[active_item]->kind == MN_NUMFIELD
361 && mn_input_char >= '0' && mn_input_char <= '9'))
363 items[active_item]->input.push_back(mn_input_char);
367 case MENU_ACTION_BACK:
368 MenuManager::instance().pop_menu();
371 case MENU_ACTION_NONE:
377 Menu::draw_item(DrawingContext& context, int index)
379 float menu_height = get_height();
380 float menu_width = get_width();
382 MenuItem& pitem = *(items[index]);
384 Color text_color = default_color;
386 float y_pos = pos.y + 24*index - menu_height/2 + 12;
387 int text_width = int(Resources::normal_font->get_text_width(pitem.text));
388 int input_width = int(Resources::normal_font->get_text_width(pitem.input) + 10);
391 float left = pos.x - menu_width/2 + 16;
392 float right = pos.x + menu_width/2 - 16;
394 if(pitem.list.size() > 0) {
395 list_width = (int) Resources::normal_font->get_text_width(pitem.list[pitem.selected]);
399 x_pos += 24 - menu_width/2 + (text_width + input_width + list_width)/2;
401 if(index == active_item)
403 text_color = active_color;
406 if(active_item == index)
408 float blink = (sinf(real_time * M_PI * 1.0f)/2.0f + 0.5f) * 0.5f + 0.25f;
409 context.draw_filled_rect(Rectf(Vector(pos.x - menu_width/2 + 10 - 2, y_pos - 12 - 2),
410 Vector(pos.x + menu_width/2 - 10 + 2, y_pos + 12 + 2)),
411 Color(1.0f, 1.0f, 1.0f, blink),
414 context.draw_filled_rect(Rectf(Vector(pos.x - menu_width/2 + 10, y_pos - 12),
415 Vector(pos.x + menu_width/2 - 10, y_pos + 12)),
416 Color(1.0f, 1.0f, 1.0f, 0.5f),
425 context.draw_text(Resources::normal_font, pitem.text,
426 Vector(pos.x, y_pos - int(Resources::normal_font->get_height()/2)),
427 ALIGN_CENTER, LAYER_GUI, inactive_color);
434 float x = pos.x - menu_width/2;
435 float y = y_pos - 12;
436 /* Draw a horizontal line with a little 3d effect */
437 context.draw_filled_rect(Vector(x, y + 6),
438 Vector(menu_width, 4),
439 Color(0.6f, 0.7f, 1.0f, 1.0f), LAYER_GUI);
440 context.draw_filled_rect(Vector(x, y + 6),
441 Vector(menu_width, 2),
442 Color(1.0f, 1.0f, 1.0f, 1.0f), LAYER_GUI);
447 context.draw_text(Resources::big_font, pitem.text,
448 Vector(pos.x, y_pos - int(Resources::big_font->get_height()/2)),
449 ALIGN_CENTER, LAYER_GUI, label_color);
454 case MN_CONTROLFIELD:
456 if(pitem.kind == MN_TEXTFIELD || pitem.kind == MN_NUMFIELD)
458 if(active_item == index)
459 context.draw_text(Resources::normal_font,
460 pitem.get_input_with_symbol(true),
461 Vector(right, y_pos - int(Resources::normal_font->get_height()/2)),
462 ALIGN_RIGHT, LAYER_GUI, field_color);
464 context.draw_text(Resources::normal_font,
465 pitem.get_input_with_symbol(false),
466 Vector(right, y_pos - int(Resources::normal_font->get_height()/2)),
467 ALIGN_RIGHT, LAYER_GUI, field_color);
470 context.draw_text(Resources::normal_font, pitem.input,
471 Vector(right, y_pos - int(Resources::normal_font->get_height()/2)),
472 ALIGN_RIGHT, LAYER_GUI, field_color);
474 context.draw_text(Resources::normal_font, pitem.text,
475 Vector(left, y_pos - int(Resources::normal_font->get_height()/2)),
476 ALIGN_LEFT, LAYER_GUI, text_color);
479 case MN_STRINGSELECT:
481 float roff = Resources::arrow_left->get_width();
483 context.draw_text(Resources::normal_font, pitem.text,
484 Vector(left, y_pos - int(Resources::normal_font->get_height()/2)),
485 ALIGN_LEFT, LAYER_GUI, text_color);
488 context.draw_surface(Resources::arrow_left,
489 Vector(right - list_width - roff - roff, y_pos - 8),
491 context.draw_surface(Resources::arrow_right,
492 Vector(right - roff, y_pos - 8),
494 context.draw_text(Resources::normal_font, pitem.list[pitem.selected],
495 Vector(right - roff, y_pos - int(Resources::normal_font->get_height()/2)),
496 ALIGN_RIGHT, LAYER_GUI, text_color);
501 context.draw_text(Resources::Resources::normal_font, pitem.text,
502 Vector(pos.x, y_pos - int(Resources::normal_font->get_height()/2)),
503 ALIGN_CENTER, LAYER_GUI, text_color);
504 context.draw_surface(Resources::back,
505 Vector(x_pos + text_width/2 + 16, y_pos - 8),
512 context.draw_text(Resources::normal_font, pitem.text,
513 Vector(pos.x - menu_width/2 + 16, y_pos - (Resources::normal_font->get_height()/2)),
514 ALIGN_LEFT, LAYER_GUI, text_color);
517 context.draw_surface(Resources::checkbox_checked,
518 Vector(x_pos + (menu_width/2-16) - Resources::checkbox->get_width(), y_pos - 8),
521 context.draw_surface(Resources::checkbox,
522 Vector(x_pos + (menu_width/2-16) - Resources::checkbox->get_width(), y_pos - 8),
527 context.draw_text(Resources::normal_font, pitem.text,
528 Vector(pos.x, y_pos - int(Resources::normal_font->get_height()/2)),
529 ALIGN_CENTER, LAYER_GUI, text_color);
533 context.draw_text(Resources::normal_font, pitem.text,
534 Vector(pos.x, y_pos - int(Resources::normal_font->get_height()/2)),
535 ALIGN_CENTER, LAYER_GUI, text_color);
541 Menu::get_width() const
543 /* The width of the menu has to be more than the width of the text
544 with the most characters */
545 float menu_width = 0;
546 for(unsigned int i = 0; i < items.size(); ++i)
548 FontPtr font = Resources::Resources::normal_font;
549 if(items[i]->kind == MN_LABEL)
550 font = Resources::big_font;
552 float w = font->get_text_width(items[i]->text) +
553 Resources::big_font->get_text_width(items[i]->input) + 16;
554 if(items[i]->kind == MN_TOGGLE)
556 if (items[i]->kind == MN_STRINGSELECT)
557 w += font->get_text_width(items[i]->list[items[i]->selected]) + 32;
564 return menu_width + 24;
568 Menu::get_height() const
570 return items.size() * 24;
574 Menu::on_window_resize()
576 pos.x = SCREEN_WIDTH / 2;
577 pos.y = SCREEN_HEIGHT / 2;
581 Menu::draw(DrawingContext& context)
583 if (!items[active_item]->help.empty())
585 int text_width = (int) Resources::normal_font->get_text_width(items[active_item]->help);
586 int text_height = (int) Resources::normal_font->get_text_height(items[active_item]->help);
588 Rectf text_rect(pos.x - text_width/2 - 8,
589 SCREEN_HEIGHT - 48 - text_height/2 - 4,
590 pos.x + text_width/2 + 8,
591 SCREEN_HEIGHT - 48 + text_height/2 + 4);
593 context.draw_filled_rect(Rectf(text_rect.p1 - Vector(4,4),
594 text_rect.p2 + Vector(4,4)),
595 Color(0.2f, 0.3f, 0.4f, 0.8f),
599 context.draw_filled_rect(text_rect,
600 Color(0.6f, 0.7f, 0.8f, 0.5f),
604 context.draw_text(Resources::normal_font, items[active_item]->help,
605 Vector(pos.x, SCREEN_HEIGHT - 48 - text_height/2),
606 ALIGN_CENTER, LAYER_GUI);
609 for(unsigned int i = 0; i < items.size(); ++i)
611 draw_item(context, i);
616 Menu::get_item_by_id(int id)
618 for (const auto& item : items)
626 throw std::runtime_error("MenuItem not found");
630 Menu::get_item_by_id(int id) const
632 for (const auto& item : items)
640 throw std::runtime_error("MenuItem not found");
643 int Menu::get_active_item_id()
645 return items[active_item]->id;
649 Menu::is_toggled(int id) const
651 return get_item_by_id(id).toggled;
655 Menu::set_toggled(int id, bool toggled)
657 get_item_by_id(id).toggled = toggled;
661 Menu::event(const SDL_Event& ev)
664 case SDL_MOUSEBUTTONDOWN:
665 if(ev.button.button == SDL_BUTTON_LEFT)
667 Vector mouse_pos = VideoSystem::current()->get_renderer().to_logical(ev.motion.x, ev.motion.y);
668 int x = int(mouse_pos.x);
669 int y = int(mouse_pos.y);
671 if(x > pos.x - get_width()/2 &&
672 x < pos.x + get_width()/2 &&
673 y > pos.y - get_height()/2 &&
674 y < pos.y + get_height()/2)
676 process_action(MENU_ACTION_HIT);
681 case SDL_MOUSEMOTION:
683 Vector mouse_pos = VideoSystem::current()->get_renderer().to_logical(ev.motion.x, ev.motion.y);
684 float x = mouse_pos.x;
685 float y = mouse_pos.y;
687 if(x > pos.x - get_width()/2 &&
688 x < pos.x + get_width()/2 &&
689 y > pos.y - get_height()/2 &&
690 y < pos.y + get_height()/2)
693 = static_cast<int> ((y - (pos.y - get_height()/2)) / 24);
695 /* only change the mouse focus to a selectable item */
696 if ((items[new_active_item]->kind != MN_HL)
697 && (items[new_active_item]->kind != MN_LABEL)
698 && (items[new_active_item]->kind != MN_INACTIVE))
699 active_item = new_active_item;
701 if(MouseCursor::current())
702 MouseCursor::current()->set_state(MC_LINK);
706 if(MouseCursor::current())
707 MouseCursor::current()->set_state(MC_NORMAL);
718 Menu::set_active_item(int id)
720 for(size_t i = 0; i < items.size(); ++i) {
721 if(items[i]->id == id) {