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 "control/input_manager.hpp"
20#include "gui/item_action.hpp"
21#include "gui/item_back.hpp"
22#include "gui/item_badguy_select.hpp"
23#include "gui/item_color.hpp"
24#include "gui/item_colorchannel.hpp"
25#include "gui/item_colordisplay.hpp"
26#include "gui/item_controlfield.hpp"
27#include "gui/item_file.hpp"
28#include "gui/item_goto.hpp"
29#include "gui/item_hl.hpp"
30#include "gui/item_inactive.hpp"
31#include "gui/item_intfield.hpp"
32#include "gui/item_label.hpp"
33#include "gui/item_floatfield.hpp"
34#include "gui/item_script.hpp"
35#include "gui/item_script_line.hpp"
36#include "gui/item_stringselect.hpp"
37#include "gui/item_textfield.hpp"
38#include "gui/item_toggle.hpp"
39#include "gui/menu_item.hpp"
40#include "gui/menu_manager.hpp"
41#include "gui/mousecursor.hpp"
42#include "math/util.hpp"
43#include "supertux/globals.hpp"
44#include "supertux/resources.hpp"
45#include "video/drawing_context.hpp"
46#include "video/renderer.hpp"
47#include "video/video_system.hpp"
48#include "video/viewport.hpp"
49
50static const float MENU_REPEAT_INITIAL = 0.4f;
51static const float MENU_REPEAT_RATE = 0.1f;
52
53Menu::Menu() :
54 m_pos(Vector(static_cast<float>(SCREEN_WIDTH) / 2.0f,
55 static_cast<float>(SCREEN_HEIGHT) / 2.0f)),
56 m_delete_character(0),
57 m_mn_input_char('\0'),
58 m_menu_repeat_time(),
59 m_menu_width(),
60 m_items(),
61 m_arrange_left(0),
62 m_active_item(-1)
63{
64}
65
66Menu::~Menu()
67{
68}
69
70void
71Menu::set_center_pos(float x, float y)
72{
73 m_pos.x = x;
74 m_pos.y = y;
75}
76
77/* Add an item to a menu */
78MenuItem&
79Menu::add_item(std::unique_ptr<MenuItem> new_item)
80{
81 m_items.push_back(std::move(new_item));
82 MenuItem& item = *m_items.back();
83
84 /* If a new menu is being built, the active item shouldn't be set to
85 * something that isn't selectable. Set the active_item to the first
86 * selectable item added.
87 */
88
89 if (m_active_item == -1 && !item.skippable())
90 {
91 m_active_item = static_cast<int>(m_items.size()) - 1;
92 }
93
94 calculate_width();
95
96 return item;
97}
98
99MenuItem&
100Menu::add_item(std::unique_ptr<MenuItem> new_item, int pos_)
101{
102 m_items.insert(m_items.begin()+pos_,std::move(new_item));
103 MenuItem& item = *m_items[pos_];
104
105 // When the item is inserted before the selected item, the
106 // same menu item should be still selected.
107
108 if (m_active_item >= pos_)
109 {
110 m_active_item++;
111 }
112
113 calculate_width();
114
115 return item;
116}
117
118void
119Menu::delete_item(int pos_)
120{
121 m_items.erase(m_items.begin()+pos_);
122
123 // When the item is deleted before the selected item, the
124 // same menu item should be still selected.
125
126 if (m_active_item >= pos_)
127 {
128 do {
129 if (m_active_item > 0)
130 --m_active_item;
131 else
132 m_active_item = int(m_items.size())-1;
133 } while (m_items[m_active_item]->skippable());
134 }
135}
136
137ItemHorizontalLine&
138Menu::add_hl()
139{
140 auto item = std::make_unique<ItemHorizontalLine>();
141 auto item_ptr = item.get();
142 add_item(std::move(item));
143 return *item_ptr;
144}
145
146ItemLabel&
147Menu::add_label(const std::string& text)
148{
149 auto item = std::make_unique<ItemLabel>(text);
150 auto item_ptr = item.get();
151 add_item(std::move(item));
152 return *item_ptr;
153}
154
155ItemControlField&
156Menu::add_controlfield(int id, const std::string& text,
157 const std::string& mapping)
158{
159 auto item = std::make_unique<ItemControlField>(text, mapping, id);
160 auto item_ptr = item.get();
161 add_item(std::move(item));
162 return *item_ptr;
163}
164
165ItemTextField&
166Menu::add_textfield(const std::string& text, std::string* input, int id)
167{
168 auto item = std::make_unique<ItemTextField>(text, input, id);
169 auto item_ptr = item.get();
170 add_item(std::move(item));
171 return *item_ptr;
172}
173
174ItemScript&
175Menu::add_script(const std::string& text, std::string* script, int id)
176{
177 auto item = std::make_unique<ItemScript>(text, script, id);
178 auto item_ptr = item.get();
179 add_item(std::move(item));
180 return *item_ptr;
181}
182
183ItemScriptLine&
184Menu::add_script_line(std::string* input, int id)
185{
186 auto item = std::make_unique<ItemScriptLine>(input, id);
187 auto item_ptr = item.get();
188 add_item(std::move(item));
189 return *item_ptr;
190}
191
192ItemIntField&
193Menu::add_intfield(const std::string& text, int* input, int id)
194{
195 auto item = std::make_unique<ItemIntField>(text, input, id);
196 auto item_ptr = item.get();
197 add_item(std::move(item));
198 return *item_ptr;
199}
200
201ItemFloatField&
202Menu::add_floatfield(const std::string& text, float* input, int id)
203{
204 auto item = std::make_unique<ItemFloatField>(text, input, id);
205 auto item_ptr = item.get();
206 add_item(std::move(item));
207 return *item_ptr;
208}
209
210ItemAction&
211Menu::add_entry(int id, const std::string& text)
212{
213 auto item = std::make_unique<ItemAction>(text, id);
214 auto item_ptr = item.get();
215 add_item(std::move(item));
216 return *item_ptr;
217}
218
219ItemAction&
220Menu::add_entry(const std::string& text, std::function<void()> callback)
221{
222 auto item = std::make_unique<ItemAction>(text, -1, callback);
223 auto item_ptr = item.get();
224 add_item(std::move(item));
225 return *item_ptr;
226}
227
228ItemInactive&
229Menu::add_inactive(const std::string& text)
230{
231 auto item = std::make_unique<ItemInactive>(text);
232 auto item_ptr = item.get();
233 add_item(std::move(item));
234 return *item_ptr;
235}
236
237ItemToggle&
238Menu::add_toggle(int id, const std::string& text, bool* toggled)
239{
240 auto item = std::make_unique<ItemToggle>(text, toggled, id);
241 auto item_ptr = item.get();
242 add_item(std::move(item));
243 return *item_ptr;
244}
245
246ItemToggle&
247Menu::add_toggle(int id, const std::string& text,
248 std::function<bool()> get_func,
249 std::function<void(bool)> set_func)
250{
251 auto item = std::make_unique<ItemToggle>(text, get_func, set_func, id);
252 auto item_ptr = item.get();
253 add_item(std::move(item));
254 return *item_ptr;
255}
256
257ItemStringSelect&
258Menu::add_string_select(int id, const std::string& text, int* selected, const std::vector<std::string>& strings)
259{
260 auto item = std::make_unique<ItemStringSelect>(text, strings, selected, id);
261 auto item_ptr = item.get();
262 add_item(std::move(item));
263 return *item_ptr;
264}
265
266ItemFile&
267Menu::add_file(const std::string& text, std::string* input, const std::vector<std::string>& extensions,
268 const std::string& basedir, int id)
269{
270 auto item = std::make_unique<ItemFile>(text, input, extensions, basedir, id);
271 auto item_ptr = item.get();
272 add_item(std::move(item));
273 return *item_ptr;
274}
275
276ItemBack&
277Menu::add_back(const std::string& text, int id)
278{
279 auto item = std::make_unique<ItemBack>(text, id);
280 auto item_ptr = item.get();
281 add_item(std::move(item));
282 return *item_ptr;
283}
284
285ItemGoTo&
286Menu::add_submenu(const std::string& text, int submenu, int id)
287{
288 auto item = std::make_unique<ItemGoTo>(text, submenu, id);
289 auto item_ptr = item.get();
290 add_item(std::move(item));
291 return *item_ptr;
292}
293
294ItemColorChannel&
295Menu::add_color_channel(float* input, Color channel, int id, bool is_linear) {
296 auto item = std::make_unique<ItemColorChannel>(input, channel, id, is_linear);
297 auto item_ptr = item.get();
298 add_item(std::move(item));
299 return *item_ptr;
300}
301
302ItemColorDisplay&
303Menu::add_color_display(Color* color, int id) {
304 auto item = std::make_unique<ItemColorDisplay>(color, id);
305 auto item_ptr = item.get();
306 add_item(std::move(item));
307 return *item_ptr;
308}
309
310ItemColor&
311Menu::add_color(const std::string& text, Color* color, int id) {
312 auto item = std::make_unique<ItemColor>(text, color, id);
313 auto item_ptr = item.get();
314 add_item(std::move(item));
315 return *item_ptr;
316}
317
318ItemBadguySelect&
319Menu::add_badguy_select(const std::string& text, std::vector<std::string>* badguys, int id) {
320 auto item = std::make_unique<ItemBadguySelect>(text, badguys, id);
321 auto item_ptr = item.get();
322 add_item(std::move(item));
323 return *item_ptr;
324}
325
326void
327Menu::clear()
328{
329 m_items.clear();
330 m_active_item = -1;
331}
332
333void
334Menu::process_input(const Controller& controller)
335{
336 { // Scrolling
337
338 // If a help text is present, make some space at the bottom of the
339 // menu so that the last few items don't overlap with the help
340 // text.
341 float help_height = 0.0f;
342 for (auto& item : m_items) {
343 if (!item->get_help().empty()) {
344 help_height = 96.0f;
345 break;
346 }
347 }
348
349 // Find the first and last selectable item in the current menu, so
350 // that the top most selected item gives a scroll_pos of -1.0f and
351 // the bottom most gives 1.0f, as otherwise the non-selectable
352 // header would be cut off.
353 size_t first_idx = m_items.size();
354 size_t last_idx = m_items.size();
355 for (size_t i = 0; i < m_items.size(); ++i) {
356 if (!m_items[i]->skippable()) {
357 if (first_idx == m_items.size()) {
358 first_idx = i;
359 }
360 last_idx = i;
361 }
362 }
363
364 const float screen_height = static_cast<float>(SCREEN_HEIGHT);
365 const float menu_area = screen_height - help_height;
366 // get_height() doesn't include the border, so we manually add some
367 const float menu_height = get_height() + 32.0f;
368 const float center_y = menu_area / 2.0f;
369 if (menu_height > menu_area)
370 {
371 const float scroll_range = (menu_height - menu_area) / 2.0f;
372 const float scroll_pos = ((static_cast<float>(m_active_item - first_idx)
373 / static_cast<float>(last_idx - first_idx)) - 0.5f) * 2.0f;
374
375 m_pos.y = floorf(center_y - scroll_range * scroll_pos);
376 }
377 else
378 {
379 if (help_height != 0.0f) {
380 m_pos.y = floorf(center_y);
381 }
382 }
383 }
384
385 MenuAction menuaction = MenuAction::NONE;
386
387 /** check main input controller... */
388 if (controller.pressed(Control::UP)) {
389 menuaction = MenuAction::UP;
390 m_menu_repeat_time = g_real_time + MENU_REPEAT_INITIAL;
391 }
392 if (controller.hold(Control::UP) &&
393 m_menu_repeat_time != 0 && g_real_time > m_menu_repeat_time) {
394 menuaction = MenuAction::UP;
395 m_menu_repeat_time = g_real_time + MENU_REPEAT_RATE;
396 }
397
398 if (controller.pressed(Control::DOWN)) {
399 menuaction = MenuAction::DOWN;
400 m_menu_repeat_time = g_real_time + MENU_REPEAT_INITIAL;
401 }
402 if (controller.hold(Control::DOWN) &&
403 m_menu_repeat_time != 0 && g_real_time > m_menu_repeat_time) {
404 menuaction = MenuAction::DOWN;
405 m_menu_repeat_time = g_real_time + MENU_REPEAT_RATE;
406 }
407
408 if (controller.pressed(Control::LEFT)) {
409 menuaction = MenuAction::LEFT;
410 m_menu_repeat_time = g_real_time + MENU_REPEAT_INITIAL;
411 }
412 if (controller.hold(Control::LEFT) &&
413 m_menu_repeat_time != 0 && g_real_time > m_menu_repeat_time) {
414 menuaction = MenuAction::LEFT;
415 m_menu_repeat_time = g_real_time + MENU_REPEAT_RATE;
416 }
417
418 if (controller.pressed(Control::RIGHT)) {
419 menuaction = MenuAction::RIGHT;
420 m_menu_repeat_time = g_real_time + MENU_REPEAT_INITIAL;
421 }
422 if (controller.hold(Control::RIGHT) &&
423 m_menu_repeat_time != 0 && g_real_time > m_menu_repeat_time) {
424 menuaction = MenuAction::RIGHT;
425 m_menu_repeat_time = g_real_time + MENU_REPEAT_RATE;
426 }
427
428 if (controller.pressed(Control::ACTION) ||
429 controller.pressed(Control::MENU_SELECT) ||
430 (!is_sensitive() && controller.pressed(Control::MENU_SELECT_SPACE))) {
431 menuaction = MenuAction::HIT;
432 }
433
434 if (controller.pressed(Control::ESCAPE) ||
435 controller.pressed(Control::CHEAT_MENU) ||
436 controller.pressed(Control::DEBUG_MENU) ||
437 controller.pressed(Control::MENU_BACK)) {
438 menuaction = MenuAction::BACK;
439 }
440
441 if (controller.pressed(Control::REMOVE)) {
442 menuaction = MenuAction::REMOVE;
443 m_menu_repeat_time = g_real_time + MENU_REPEAT_INITIAL;
444 }
445 if (controller.hold(Control::REMOVE) &&
446 m_menu_repeat_time != 0 && g_real_time > m_menu_repeat_time) {
447 menuaction = MenuAction::REMOVE;
448 m_menu_repeat_time = g_real_time + MENU_REPEAT_RATE;
449 }
450
451 if (m_items.size() == 0)
452 return;
453
454 // The menu_action() call can pop() the menu from the stack and thus
455 // delete it, so it's important that no further member variables are
456 // accessed after this call
457 process_action(menuaction);
458}
459
460void
461Menu::process_action(const MenuAction& menuaction)
462{
463 const int last_active_item = m_active_item;
464
465 switch (menuaction) {
466 case MenuAction::UP:
467 do {
468 if (m_active_item > 0)
469 --m_active_item;
470 else
471 m_active_item = int(m_items.size())-1;
472 } while (m_items[m_active_item]->skippable()
473 && (m_active_item != last_active_item));
474 break;
475
476 case MenuAction::DOWN:
477 do {
478 if (m_active_item < int(m_items.size())-1 )
479 ++m_active_item;
480 else
481 m_active_item = 0;
482 } while (m_items[m_active_item]->skippable()
483 && (m_active_item != last_active_item));
484 break;
485
486 case MenuAction::BACK:
487 if (on_back_action()) {
488 MenuManager::instance().pop_menu();
489 }
490 return;
491
492 default:
493 break;
494 }
495
496 if (m_items[m_active_item]->no_other_action()) {
497 m_items[m_active_item]->process_action(menuaction);
498 return;
499 }
500
501 m_items[m_active_item]->process_action(menuaction);
502 if (m_items[m_active_item]->changes_width()) {
503 calculate_width();
504 }
505 if (menuaction == MenuAction::HIT) {
506 menu_action(*m_items[m_active_item]);
507 }
508}
509
510void
511Menu::draw_item(DrawingContext& context, int index)
512{
513 const float menu_height = get_height();
514 const float menu_width = get_width();
515
516 MenuItem* pitem = m_items[index].get();
517
518 const float x_pos = m_pos.x - menu_width / 2.0f;
519 const float y_pos = m_pos.y + 24.0f * static_cast<float>(index) - menu_height / 2.0f + 12.0f;
520
521 pitem->draw(context, Vector(x_pos, y_pos), static_cast<int>(menu_width), m_active_item == index);
522
523 if (m_active_item == index)
524 {
525 float blink = (sinf(g_real_time * math::PI * 1.0f)/2.0f + 0.5f) * 0.5f + 0.25f;
526 context.color().draw_filled_rect(Rectf(Vector(m_pos.x - menu_width/2 + 10 - 2, y_pos - 12 - 2),
527 Vector(m_pos.x + menu_width/2 - 10 + 2, y_pos + 12 + 2)),
528 Color(1.0f, 1.0f, 1.0f, blink),
529 14.0f,
530 LAYER_GUI-10);
531 context.color().draw_filled_rect(Rectf(Vector(m_pos.x - menu_width/2 + 10, y_pos - 12),
532 Vector(m_pos.x + menu_width/2 - 10, y_pos + 12)),
533 Color(1.0f, 1.0f, 1.0f, 0.5f),
534 12.0f,
535 LAYER_GUI-10);
536 }
537}
538
539void
540Menu::calculate_width()
541{
542 /* The width of the menu has to be more than the width of the text
543 with the most characters */
544 float max_width = 0;
545 for (unsigned int i = 0; i < m_items.size(); ++i)
546 {
547 float w = static_cast<float>(m_items[i]->get_width());
548 if (w > max_width)
549 max_width = w;
550 }
551 m_menu_width = max_width;
552}
553
554float
555Menu::get_width() const
556{
557 return m_menu_width + 24;
558}
559
560float
561Menu::get_height() const
562{
563 return static_cast<float>(m_items.size() * 24);
564}
565
566void
567Menu::on_window_resize()
568{
569 m_pos.x = static_cast<float>(SCREEN_WIDTH) / 2.0f;
570 m_pos.y = static_cast<float>(SCREEN_HEIGHT) / 2.0f;
571}
572
573void
574Menu::draw(DrawingContext& context)
575{
576 for (unsigned int i = 0; i < m_items.size(); ++i)
577 {
578 draw_item(context, i);
579 }
580
581 if (!m_items[m_active_item]->get_help().empty())
582 {
583 const int text_width = static_cast<int>(Resources::normal_font->get_text_width(m_items[m_active_item]->get_help()));
584 const int text_height = static_cast<int>(Resources::normal_font->get_text_height(m_items[m_active_item]->get_help()));
585
586 const Rectf text_rect(m_pos.x - static_cast<float>(text_width) / 2.0f - 8.0f,
587 static_cast<float>(SCREEN_HEIGHT) - 48.0f - static_cast<float>(text_height) / 2.0f - 4.0f,
588 m_pos.x + static_cast<float>(text_width) / 2.0f + 8.0f,
589 static_cast<float>(SCREEN_HEIGHT) - 48.0f + static_cast<float>(text_height) / 2.0f + 4.0f);
590
591 context.color().draw_filled_rect(Rectf(text_rect.p1() - Vector(4,4),
592 text_rect.p2() + Vector(4,4)),
593 Color(0.5f, 0.6f, 0.7f, 0.8f),
594 16.0f,
595 LAYER_GUI);
596
597 context.color().draw_filled_rect(text_rect,
598 Color(0.8f, 0.9f, 1.0f, 0.5f),
599 16.0f,
600 LAYER_GUI);
601
602 context.color().draw_text(Resources::normal_font, m_items[m_active_item]->get_help(),
603 Vector(m_pos.x, static_cast<float>(SCREEN_HEIGHT) - 48.0f - static_cast<float>(text_height) / 2.0f),
604 ALIGN_CENTER, LAYER_GUI);
605 }
606}
607
608MenuItem&
609Menu::get_item_by_id(int id)
610{
611 for (const auto& item : m_items)
612 {
613 if (item->get_id() == id)
614 {
615 return *item;
616 }
617 }
618
619 throw std::runtime_error("MenuItem not found: " + std::to_string(id));
620}
621
622const MenuItem&
623Menu::get_item_by_id(int id) const
624{
625 for (const auto& item : m_items)
626 {
627 if (item->get_id() == id)
628 {
629 return *item;
630 }
631 }
632
633 throw std::runtime_error("MenuItem not found");
634}
635
636int Menu::get_active_item_id() const
637{
638 return m_items[m_active_item]->get_id();
639}
640
641void
642Menu::event(const SDL_Event& ev)
643{
644 m_items[m_active_item]->event(ev);
645 switch (ev.type)
646 {
647 case SDL_KEYDOWN:
648 case SDL_TEXTINPUT:
649 if (((ev.type == SDL_KEYDOWN && ev.key.keysym.sym == SDLK_BACKSPACE) ||
650 ev.type == SDL_TEXTINPUT) && m_items[m_active_item]->changes_width())
651 {
652 // Changed item value? Let's recalculate width:
653 calculate_width();
654 }
655 break;
656
657 case SDL_MOUSEBUTTONDOWN:
658 if (ev.button.button == SDL_BUTTON_LEFT)
659 {
660 Vector mouse_pos = VideoSystem::current()->get_viewport().to_logical(ev.motion.x, ev.motion.y);
661
662 if (mouse_pos.x > m_pos.x - get_width() / 2.0f &&
663 mouse_pos.x < m_pos.x + get_width() / 2.0f &&
664 mouse_pos.y > m_pos.y - get_height() / 2.0f &&
665 mouse_pos.y < m_pos.y + get_height() / 2.0f)
666 {
667 process_action(MenuAction::HIT);
668 }
669 }
670 break;
671
672 case SDL_MOUSEMOTION:
673 {
674 Vector mouse_pos = VideoSystem::current()->get_viewport().to_logical(ev.motion.x, ev.motion.y);
675 float x = mouse_pos.x;
676 float y = mouse_pos.y;
677
678 if (x > m_pos.x - get_width()/2 &&
679 x < m_pos.x + get_width()/2 &&
680 y > m_pos.y - get_height()/2 &&
681 y < m_pos.y + get_height()/2)
682 {
683 int new_active_item
684 = static_cast<int> ((y - (m_pos.y - get_height()/2)) / 24);
685
686 /* only change the mouse focus to a selectable item */
687 if (!m_items[new_active_item]->skippable())
688 m_active_item = new_active_item;
689
690 if (MouseCursor::current())
691 MouseCursor::current()->set_state(MouseCursorState::LINK);
692 }
693 else
694 {
695 if (MouseCursor::current())
696 MouseCursor::current()->set_state(MouseCursorState::NORMAL);
697 }
698 }
699 break;
700
701 default:
702 break;
703 }
704}
705
706void
707Menu::set_active_item(int id)
708{
709 for (size_t i = 0; i < m_items.size(); ++i) {
710 if (m_items[i]->get_id() == id) {
711 m_active_item = static_cast<int>(i);
712 break;
713 }
714 }
715}
716
717bool
718Menu::is_sensitive() const
719{
720 return false;
721}
722
723/* EOF */
724