1 /* GG is a GUI for OpenGL.
2    Copyright (C) 2003-2008 T. Zachary Laine
3 
4    This library is free software; you can redistribute it and/or
5    modify it under the terms of the GNU Lesser General Public License
6    as published by the Free Software Foundation; either version 2.1
7    of the License, or (at your option) any later version.
8 
9    This library 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 GNU
12    Lesser General Public License for more details.
13 
14    You should have received a copy of the GNU Lesser General Public
15    License along with this library; if not, write to the Free
16    Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
17    02111-1307 USA
18 
19    If you do not wish to comply with the terms of the LGPL please
20    contact the author as other terms are available for a fee.
21 
22    Zach Laine
23    whatwasthataddress@gmail.com */
24 
25 #include <GG/Menu.h>
26 
27 #include <GG/GUI.h>
28 #include <GG/DrawUtil.h>
29 #include <GG/StyleFactory.h>
30 #include <GG/TextControl.h>
31 #include <GG/WndEvent.h>
32 
33 
34 using namespace GG;
35 
36 namespace {
37     const int BORDER_THICKNESS = 1; // thickness with which to draw menu borders
38 }
39 
40 
41 ////////////////////////////////////////////////
42 // GG::MenuItem
43 ////////////////////////////////////////////////
MenuItem()44 MenuItem::MenuItem() :
45     MenuItem("", false, false)
46 {}
47 
MenuItem(bool separator_)48 MenuItem::MenuItem(bool separator_) :
49     disabled(true),
50     checked(false),
51     separator(true),
52     next_level(),
53     m_selected_on_close_callback{}
54 {}
55 
MenuItem(const std::string & str,bool disable,bool check,std::function<void ()> selected_on_close_callback)56 MenuItem::MenuItem(const std::string& str, bool disable, bool check,
57                    std::function<void()> selected_on_close_callback) :
58     label(str),
59     disabled(disable),
60     checked(check),
61     separator(false),
62     next_level(),
63     m_selected_on_close_callback{selected_on_close_callback}
64 {}
65 
~MenuItem()66 MenuItem::~MenuItem()
67 {}
68 
69 
70 ////////////////////////////////////////////////
71 // GG::PopupMenu
72 ////////////////////////////////////////////////
73 namespace {
74     // distance to leave between edge of PopupMenuClassic contents and the control's border
75     const X HORIZONTAL_MARGIN(3);
76 }
77 
78 const std::size_t PopupMenu::INVALID_CARET = std::numeric_limits<std::size_t>::max();
79 
PopupMenu(X x,Y y,const std::shared_ptr<Font> & font,Clr text_color,Clr border_color,Clr interior_color,Clr hilite_color)80 PopupMenu::PopupMenu(X x, Y y, const std::shared_ptr<Font>& font, Clr text_color/* = CLR_WHITE*/,
81                      Clr border_color/* = CLR_BLACK*/, Clr interior_color/* = CLR_SHADOW*/, Clr hilite_color/* = CLR_GRAY*/) :
82     Wnd(X0, Y0, GUI::GetGUI()->AppWidth() - 1, GUI::GetGUI()->AppHeight() - 1, INTERACTIVE | MODAL),
83     m_font(font),
84     m_border_color(border_color),
85     m_int_color(interior_color),
86     m_text_color(text_color),
87     m_hilite_color(hilite_color),
88     m_sel_text_color(text_color),
89     m_menu_data(MenuItem()),
90     m_caret(1, INVALID_CARET),
91     m_origin(x, y)
92 {
93     m_open_levels.resize(1);
94 }
95 
AddMenuItem(MenuItem && menu_item)96 void PopupMenu::AddMenuItem(MenuItem&& menu_item)
97 {
98     m_menu_data.next_level.push_back(std::forward<MenuItem>(menu_item));
99 }
100 
ClientUpperLeft() const101 Pt PopupMenu::ClientUpperLeft() const
102 { return m_origin; }
103 
BorderColor() const104 Clr PopupMenu::BorderColor() const
105 { return m_border_color; }
106 
InteriorColor() const107 Clr PopupMenu::InteriorColor() const
108 { return m_int_color; }
109 
TextColor() const110 Clr PopupMenu::TextColor() const
111 { return m_text_color; }
112 
HiliteColor() const113 Clr PopupMenu::HiliteColor() const
114 { return m_hilite_color; }
115 
SelectedTextColor() const116 Clr PopupMenu::SelectedTextColor() const
117 { return m_sel_text_color; }
118 
Render()119 void PopupMenu::Render()
120 {
121     if (m_menu_data.next_level.size())
122     {
123         Pt ul = ClientUpperLeft();
124 
125         const Y INDICATOR_VERTICAL_MARGIN(3);
126         const Y INDICATOR_HEIGHT = m_font->Lineskip() - 2 * INDICATOR_VERTICAL_MARGIN;
127         const Y CHECK_HEIGHT = INDICATOR_HEIGHT;
128         const X CHECK_WIDTH(Value(CHECK_HEIGHT));
129 
130         X next_menu_x_offset(0);
131         Y next_menu_y_offset(0);
132         for (std::size_t i = 0; i < m_caret.size(); ++i) {
133             bool needs_indicator = false;
134 
135             // get the correct submenu
136             MenuItem* menu_ptr = &m_menu_data;
137             for (std::size_t j = 0; j < i; ++j)
138                 menu_ptr = &menu_ptr->next_level[m_caret[j]];
139             MenuItem& menu = *menu_ptr;
140 
141             // determine the total size of the menu, render it, and record its bounding rect
142             std::string str;
143             for (std::size_t j = 0; j < menu.next_level.size(); ++j) {
144                 str += menu.next_level[j].label + (static_cast<int>(j) < static_cast<int>(menu.next_level.size()) - 1 ? "\n" : "");
145                 if (menu.next_level[j].next_level.size() || menu.next_level[j].checked)
146                     needs_indicator = true;
147             }
148             Flags<TextFormat> fmt = FORMAT_LEFT | FORMAT_TOP;
149             auto text_elements = m_font->ExpensiveParseFromTextToTextElements(str, fmt);
150             auto lines = m_font->DetermineLines(str, fmt, X0, text_elements);
151             Pt menu_sz = m_font->TextExtent(lines); // get dimensions of text in menu
152             menu_sz.x += 2 * HORIZONTAL_MARGIN;
153             if (needs_indicator)
154                 menu_sz.x += CHECK_WIDTH + 2 * HORIZONTAL_MARGIN; // make room for the little arrow
155             Rect r(ul.x + next_menu_x_offset, ul.y + next_menu_y_offset,
156                    ul.x + next_menu_x_offset + menu_sz.x, ul.y + next_menu_y_offset + menu_sz.y);
157 
158             if (r.lr.x > GUI::GetGUI()->AppWidth()) {
159                 X offset = r.lr.x - GUI::GetGUI()->AppWidth();
160                 r.ul.x -= offset;
161                 r.lr.x -= offset;
162             }
163             if (r.lr.y > GUI::GetGUI()->AppHeight()) {
164                 Y offset = r.lr.y - GUI::GetGUI()->AppHeight();
165                 r.ul.y -= offset;
166                 r.lr.y -= offset;
167             }
168             next_menu_x_offset = menu_sz.x;
169             next_menu_y_offset = static_cast<int>(m_caret[i]) * m_font->Lineskip();
170             FlatRectangle(r.ul, r.lr, m_int_color, m_border_color, BORDER_THICKNESS);
171             m_open_levels[i] = r;
172 
173             // paint caret, if any
174             if (m_caret[i] != INVALID_CARET &&
175                 !menu.next_level[m_caret[i]].separator &&
176                 !menu.next_level[m_caret[i]].disabled)
177             {
178                 Rect tmp_r = r;
179                 tmp_r.ul.y += static_cast<int>(m_caret[i]) * m_font->Lineskip();
180                 tmp_r.lr.y = tmp_r.ul.y + m_font->Lineskip() + 3;
181                 tmp_r.ul.x += BORDER_THICKNESS;
182                 tmp_r.lr.x -= BORDER_THICKNESS;
183                 if (m_caret[i] == 0)
184                     tmp_r.ul.y += BORDER_THICKNESS;
185                 if (m_caret[i] == menu.next_level.size() - 1)
186                     tmp_r.lr.y -= BORDER_THICKNESS;
187                 FlatRectangle(tmp_r.ul, tmp_r.lr, m_hilite_color, CLR_ZERO, 0);
188             }
189 
190             // paint menu text and submenu indicator arrows
191             Rect line_rect = r;
192             line_rect.ul.x += HORIZONTAL_MARGIN;
193             line_rect.lr.x -= HORIZONTAL_MARGIN;
194             for (std::size_t j = 0; j < menu.next_level.size(); ++j) {
195                 Clr clr =   (m_caret[i] == j)
196                                 ? (menu.next_level[j].disabled
197                                     ? DisabledColor(m_sel_text_color)
198                                     : m_sel_text_color)
199                                 : (menu.next_level[j].disabled
200                                     ? DisabledColor(m_text_color)
201                                     : m_text_color);
202                 glColor3ub(clr.r, clr.g, clr.b);
203 
204                 if (!menu.next_level[j].separator) {
205                     // TODO cache line data v expensive calculation
206                     auto element_data = m_font->ExpensiveParseFromTextToTextElements(menu.next_level[j].label, fmt);
207                     auto line_data = m_font->DetermineLines(menu.next_level[j].label, fmt, X0, element_data);
208 
209                     m_font->RenderText(line_rect.ul, line_rect.lr, menu.next_level[j].label, fmt, line_data);
210 
211                 } else {
212                     Line(line_rect.ul.x + HORIZONTAL_MARGIN, line_rect.ul.y + INDICATOR_HEIGHT/2 + INDICATOR_VERTICAL_MARGIN,
213                          line_rect.lr.x - HORIZONTAL_MARGIN, line_rect.ul.y + INDICATOR_HEIGHT/2 + INDICATOR_VERTICAL_MARGIN);
214                 }
215 
216                 if (menu.next_level[j].checked) {
217                     FlatCheck(Pt(line_rect.lr.x - CHECK_WIDTH - HORIZONTAL_MARGIN, line_rect.ul.y + INDICATOR_VERTICAL_MARGIN),
218                               Pt(line_rect.lr.x - HORIZONTAL_MARGIN, line_rect.ul.y + INDICATOR_VERTICAL_MARGIN + CHECK_HEIGHT),
219                               clr);
220                 }
221 
222                 // submenu indicator arrow
223                 if (menu.next_level[j].next_level.size() > 0u) {
224                     Triangle(line_rect.lr.x - Value(INDICATOR_HEIGHT/2) - HORIZONTAL_MARGIN,
225                              line_rect.ul.y + INDICATOR_VERTICAL_MARGIN,
226                              line_rect.lr.x - Value(INDICATOR_HEIGHT/2) - HORIZONTAL_MARGIN,
227                              line_rect.ul.y + m_font->Lineskip() - INDICATOR_VERTICAL_MARGIN,
228                              line_rect.lr.x - HORIZONTAL_MARGIN,
229                              line_rect.ul.y + m_font->Lineskip()/2);
230                     glEnd();
231                     glEnable(GL_TEXTURE_2D);
232                 }
233                 line_rect.ul.y += m_font->Lineskip();
234             }
235         }
236     }
237 }
238 
LButtonUp(const Pt & pt,Flags<ModKey> mod_keys)239 void PopupMenu::LButtonUp(const Pt& pt, Flags<ModKey> mod_keys)
240 {
241     if (m_caret[0] != INVALID_CARET) {
242         MenuItem* menu_ptr = &m_menu_data;
243         for (std::size_t caret : m_caret) {
244             if (caret != INVALID_CARET) {
245                 menu_ptr = &menu_ptr->next_level[caret];
246             }
247         }
248         if (!menu_ptr->disabled && !menu_ptr->separator) {
249             m_item_selected = menu_ptr;
250             m_done = true;
251         }
252     } else {
253         m_done = true;
254     }
255 }
256 
LClick(const Pt & pt,Flags<ModKey> mod_keys)257 void PopupMenu::LClick(const Pt& pt, Flags<ModKey> mod_keys)
258 { LButtonUp(pt, mod_keys); }
259 
LDrag(const Pt & pt,const Pt & move,Flags<ModKey> mod_keys)260 void PopupMenu::LDrag(const Pt& pt, const Pt& move, Flags<ModKey> mod_keys)
261 {
262     bool cursor_is_in_menu = false;
263     for (int i = static_cast<int>(m_open_levels.size()) - 1; i >= 0; --i) {
264         // get the correct submenu
265         MenuItem* menu_ptr = &m_menu_data;
266         for (int j = 0; j < i; ++j)
267             menu_ptr = &menu_ptr->next_level[m_caret[j]];
268         MenuItem& menu = *menu_ptr;
269 
270         if (pt.x >= m_open_levels[i].ul.x && pt.x <= m_open_levels[i].lr.x &&
271             pt.y >= m_open_levels[i].ul.y && pt.y <= m_open_levels[i].lr.y)
272         {
273             std::size_t row_selected = Value((pt.y - m_open_levels[i].ul.y) / m_font->Lineskip());
274             if (row_selected == m_caret[i]) {
275                 cursor_is_in_menu = true;
276             } else if (row_selected < menu.next_level.size()) {
277                 m_caret[i] = row_selected;
278                 m_open_levels.resize(i + 1);
279                 m_caret.resize(i + 1);
280                 if (!menu.next_level[row_selected].disabled && menu.next_level[row_selected].next_level.size()) {
281                     m_caret.push_back(INVALID_CARET);
282                     m_open_levels.push_back(Rect());
283                 }
284                 cursor_is_in_menu = true;
285             }
286         }
287     }
288     if (!cursor_is_in_menu) {
289         m_open_levels.resize(1);
290         m_caret.resize(1);
291         m_caret[0] = INVALID_CARET;
292     }
293 }
294 
RButtonUp(const Pt & pt,Flags<ModKey> mod_keys)295 void PopupMenu::RButtonUp(const Pt& pt, Flags<ModKey> mod_keys)
296 { LButtonUp(pt, mod_keys); }
297 
RClick(const Pt & pt,Flags<ModKey> mod_keys)298 void PopupMenu::RClick(const Pt& pt, Flags<ModKey> mod_keys)
299 { LButtonUp(pt, mod_keys); }
300 
MouseHere(const Pt & pt,Flags<ModKey> mod_keys)301 void PopupMenu::MouseHere(const Pt& pt, Flags<ModKey> mod_keys)
302 { LDrag(pt, Pt(), mod_keys); }
303 
Run()304 bool PopupMenu::Run()
305 {
306     bool retval = Wnd::Run();
307     if (retval
308         && m_item_selected
309         && m_item_selected->m_selected_on_close_callback)
310     {
311         m_item_selected->m_selected_on_close_callback();
312     }
313 
314     return retval;
315 }
316 
SetBorderColor(Clr clr)317 void PopupMenu::SetBorderColor(Clr clr)
318 { m_border_color = clr; }
319 
SetInteriorColor(Clr clr)320 void PopupMenu::SetInteriorColor(Clr clr)
321 { m_int_color = clr; }
322 
SetTextColor(Clr clr)323 void PopupMenu::SetTextColor(Clr clr)
324 { m_text_color = clr; }
325 
SetHiliteColor(Clr clr)326 void PopupMenu::SetHiliteColor(Clr clr)
327 { m_hilite_color = clr; }
328 
SetSelectedTextColor(Clr clr)329 void PopupMenu::SetSelectedTextColor(Clr clr)
330 { m_sel_text_color = clr; }
331 
GetFont() const332 const std::shared_ptr<Font>& PopupMenu::GetFont() const
333 { return m_font; }
334 
MenuData() const335 const MenuItem& PopupMenu::MenuData() const
336 { return m_menu_data; }
337 
OpenLevels() const338 const std::vector<Rect>& PopupMenu::OpenLevels() const
339 { return m_open_levels; }
340 
Caret() const341 const std::vector<std::size_t>& PopupMenu::Caret() const
342 { return m_caret; }
343 
ItemSelected() const344 const MenuItem* PopupMenu::ItemSelected() const
345 { return m_item_selected; }
346