1 /**
2  * @file
3  * @brief Menus and associated malarkey.
4 **/
5 
6 #include "AppHdr.h"
7 
8 #include "menu.h"
9 
10 #include <cctype>
11 #include <functional>
12 
13 #include "cio.h"
14 #include "colour.h"
15 #include "command.h"
16 #include "coord.h"
17 #include "env.h"
18 #include "tile-env.h"
19 #include "hints.h"
20 #include "invent.h"
21 #include "libutil.h"
22 #include "macro.h"
23 #include "message.h"
24 #ifdef USE_TILE
25  #include "mon-util.h"
26 #endif
27 #include "options.h"
28 #include "player.h"
29 #include "player-save-info.h"
30 #include "state.h"
31 #include "stringutil.h"
32 #ifdef USE_TILE
33  #include "terrain.h"
34 #endif
35 #ifdef USE_TILE_LOCAL
36  #include "tilebuf.h"
37 #endif
38 #ifdef USE_TILE
39  #include "tile-flags.h"
40  #include "tile-player-flag-cut.h"
41  #include "rltiles/tiledef-dngn.h"
42  #include "rltiles/tiledef-icons.h"
43  #include "rltiles/tiledef-main.h"
44  #include "rltiles/tiledef-player.h"
45 #endif
46 #ifdef USE_TILE_LOCAL
47  #include "tilefont.h"
48 #endif
49 #ifdef USE_TILE
50  #include "tilepick.h"
51  #include "tilepick-p.h"
52 #endif
53 #ifdef USE_TILE_LOCAL
54  #include "tilereg-crt.h"
55 #endif
56 #ifdef USE_TILE
57  #include "travel.h"
58 #endif
59 #include "ui.h"
60 #include "unicode.h"
61 #include "unwind.h"
62 #ifdef USE_TILE_LOCAL
63 #include "windowmanager.h"
64 #endif
65 
66 using namespace ui;
67 
68 class UIMenu : public Widget
69 {
70     friend class UIMenuPopup;
71 public:
UIMenu(Menu * menu)72     UIMenu(Menu *menu) : m_menu(menu)
73 
74 #ifdef USE_TILE_LOCAL
75     , m_num_columns(1), m_font_entry(tiles.get_crt_font()), m_text_buf(m_font_entry)
76 #endif
77     {
78 #ifdef USE_TILE_LOCAL
79         const ImageManager *m_image = tiles.get_image_manager();
80         for (int i = 0; i < TEX_MAX; i++)
81             m_tile_buf[i].set_tex(&m_image->m_textures[i]);
82 #else
83         expand_h = true;
84 #endif
85     };
~UIMenu()86     ~UIMenu() {};
87 
88     virtual void _render() override;
89     virtual SizeReq _get_preferred_size(Direction dim, int prosp_width) override;
90     virtual void _allocate_region() override;
91 #ifdef USE_TILE_LOCAL
92     virtual bool on_event(const Event& event) override;
get_num_columns() const93     int get_num_columns() const { return m_num_columns; };
set_num_columns(int n)94     void set_num_columns(int n) {
95         m_num_columns = n;
96         _invalidate_sizereq();
97         _queue_allocation();
98     };
99 #endif
set_min_col_width(int w)100     void set_min_col_width(int w) { m_min_col_width = w; } // XX min height?
get_min_col_width()101     int get_min_col_width() { return m_min_col_width; }
102 
103     void update_item(int index);
104     void update_items();
105 
106     void set_hovered_entry(int i);
107 
108     void is_visible_item_range(int *vis_min, int *vis_max);
109     void get_item_region(int index, int *y1, int *y2);
110 
111 #ifndef USE_TILE_LOCAL
set_showable_height(int h)112     void set_showable_height(int h)
113     {
114         m_shown_height = h;
115         _invalidate_sizereq();
116     }
117 #endif
118 
119 protected:
120     Menu *m_menu;
121     int m_height; // set by do_layout()
122     int m_hover_idx = -1;
123     int m_min_col_width = -1;
124 
125 #ifdef USE_TILE_LOCAL
126     void do_layout(int mw, int num_columns);
127 
128     int get_max_viewport_height();
129     int m_nat_column_width; // set by do_layout()
130     int m_num_columns = 1;
131 
132     struct MenuItemInfo {
133         int x, y, row, column;
134         formatted_string text;
135         vector<tile_def> tiles;
136         bool heading;
137     };
138     vector<MenuItemInfo> item_info;
139     vector<int> row_heights;
140 
141     bool m_mouse_pressed = false;
142     int m_mouse_x = -1, m_mouse_y = -1;
143     void update_hovered_entry();
144 
145     void pack_buffers();
146 
147     bool m_draw_tiles;
148     FontWrapper *m_font_entry;
149     ShapeBuffer m_shape_buf;
150     LineBuffer m_line_buf, m_div_line_buf;
151     FontBuffer m_text_buf;
152     FixedVector<TileBuffer, TEX_MAX> m_tile_buf;
153 
154 public:
shown_items()155     size_t shown_items() { return item_info.size(); }
156 
157     static constexpr int item_pad = 2;
158     static constexpr int pad_right = 10;
159 #else
160     int m_shown_height {0};
161 #endif
162 };
163 
update_items()164 void UIMenu::update_items()
165 {
166     _invalidate_sizereq();
167 
168 #ifdef USE_TILE_LOCAL
169     item_info.resize(m_menu->items.size());
170 #endif
171     for (unsigned int i = 0; i < m_menu->items.size(); ++i)
172         update_item(i);
173 
174 #ifdef USE_TILE_LOCAL
175     // update m_draw_tiles
176     m_draw_tiles = false;
177     for (const auto& entry : item_info)
178         if (!entry.heading && !entry.tiles.empty())
179         {
180             m_draw_tiles = Options.tile_menu_icons;
181             break;
182         }
183 #endif
184 }
185 
is_visible_item_range(int * vis_min,int * vis_max)186 void UIMenu::is_visible_item_range(int *vis_min, int *vis_max)
187 {
188     const int viewport_height = m_menu->m_ui.scroller->get_region().height;
189     const int scroll = m_menu->m_ui.scroller->get_scroll();
190 
191 #ifdef USE_TILE_LOCAL
192     int v_min = 0, v_max = item_info.size(), i;
193     for (i = 0; i < (int)item_info.size(); ++i)
194     {
195         if (row_heights[item_info[i].row + 1] > scroll)
196         {
197             v_min = i;
198             break;
199         }
200     }
201     for (; i < (int)item_info.size(); ++i)
202     {
203         if (row_heights[item_info[i].row] >= scroll + viewport_height)
204         {
205             v_max = i;
206             break;
207         }
208     }
209 #else
210     int v_min = scroll;
211     int v_max = scroll + viewport_height;
212 #endif
213     v_max = min(v_max, (int)m_menu->items.size());
214     if (vis_min)
215         *vis_min = v_min;
216     if (vis_max)
217         *vis_max = v_max;
218 }
219 
get_item_region(int index,int * y1,int * y2)220 void UIMenu::get_item_region(int index, int *y1, int *y2)
221 {
222     ASSERT_RANGE(index, 0, (int)m_menu->items.size());
223 #ifdef USE_TILE_LOCAL
224     int row = item_info[index].row;
225     if (static_cast<size_t>(row + 1) >= row_heights.size())
226     {
227         // call before UIMenu has been laid out
228         if (y1) *y1 = -1;
229         if (y2) *y2 = -1;
230         return;
231     }
232     if (y1)
233         *y1 = row_heights[row];
234     if (y2)
235         *y2 = row_heights[row+1];
236 #else
237     if (y1)
238         *y1 = index;
239     if (y2)
240         *y2 = index+1;
241 #endif
242 }
243 
update_item(int index)244 void UIMenu::update_item(int index)
245 {
246     _invalidate_sizereq();
247     _queue_allocation();
248 #ifdef USE_TILE_LOCAL
249     const MenuEntry *me = m_menu->items[index];
250     int colour = m_menu->item_colour(me);
251     const bool needs_cursor = (m_menu->get_cursor() == index
252                                && m_menu->is_set(MF_MULTISELECT));
253     string text = me->get_text(needs_cursor);
254 
255     item_info.resize(m_menu->items.size());
256 
257     auto& entry = item_info[index];
258     entry.text.clear();
259     entry.text.textcolour(colour);
260     entry.text += formatted_string::parse_string(text);
261     entry.heading = me->level == MEL_TITLE || me->level == MEL_SUBTITLE;
262     entry.tiles.clear();
263     me->get_tiles(entry.tiles);
264 #else
265     UNUSED(index);
266 #endif
267 }
268 
269 #ifdef USE_TILE_LOCAL
_has_hotkey_prefix(const string & s)270 static bool _has_hotkey_prefix(const string &s)
271 {
272     // [enne] - Ugh, hack. Maybe MenuEntry could specify the
273     // presence and length of this substring?
274     bool let = (s[1] >= 'a' && s[1] <= 'z' || s[1] >= 'A' && s[1] <= 'Z');
275     bool plus = (s[3] == '-' || s[3] == '+' || s[3] == '#');
276     return let && plus && s[0] == ' ' && s[2] == ' ' && s[4] == ' ';
277 }
278 
do_layout(int mw,int num_columns)279 void UIMenu::do_layout(int mw, int num_columns)
280 {
281     const int min_column_width = m_min_col_width > 0 ? m_min_col_width : 400;
282     const int max_column_width = mw / num_columns;
283     const int text_height = m_font_entry->char_height();
284 
285     int column = -1; // an initial increment makes this 0
286     int column_width = 0;
287     int row_height = 0;
288     int height = 0;
289 
290     row_heights.clear();
291     row_heights.reserve(m_menu->items.size()+1);
292 
293     for (size_t i = 0; i < m_menu->items.size(); ++i)
294     {
295         auto& entry = item_info[i];
296 
297         column = entry.heading ? 0 : (column+1) % num_columns;
298 
299         if (column == 0)
300         {
301             row_height += row_height == 0 ? 0 : 2*item_pad;
302             height += row_height;
303             row_heights.push_back(height);
304             row_height = 0;
305         }
306 
307         const int text_width = m_font_entry->string_width(entry.text);
308 
309         entry.y = height;
310         entry.row = row_heights.size() - 1;
311         entry.column = column;
312 
313         if (entry.heading)
314         {
315             entry.x = 0;
316             // extra space here is used for divider line and padding; note that
317             // we only want top padding if we're not the first item, since the
318             // popup and the more already have padding.
319             row_height = text_height + (i == 0 ? 5 : 10);
320 
321             // wrap titles to two lines if they don't fit
322             if (m_draw_tiles && text_width > mw)
323             {
324                 formatted_string split = m_font_entry->split(entry.text, mw, UINT_MAX);
325                 row_height = max(row_height, (int)m_font_entry->string_height(split));
326             }
327             column = num_columns-1;
328         }
329         else
330         {
331             const int text_indent = m_draw_tiles ? 38 : 0;
332 
333             entry.x = text_indent;
334             int text_sx = text_indent;
335             int item_height = max(text_height, !entry.tiles.empty() ? 32 : 0);
336 
337             // Split menu entries that don't fit into a single line into two lines.
338             if (!m_menu->is_set(MF_NO_WRAP_ROWS))
339             if ((text_width > max_column_width-entry.x-pad_right))
340             {
341                 formatted_string text;
342                 if (_has_hotkey_prefix(entry.text.tostring()))
343                 {
344                     formatted_string header = entry.text.chop(5);
345                     text_sx += m_font_entry->string_width(header);
346                     text = entry.text;
347                     // remove hotkeys. As Enne said above, this is a monstrosity.
348                     for (int k = 0; k < 5; k++)
349                         text.del_char();
350                 }
351                 else
352                     text += entry.text;
353 
354                 int w = max_column_width - text_sx - pad_right;
355                 formatted_string split = m_font_entry->split(text, w, UINT_MAX);
356                 int string_height = m_font_entry->string_height(split);
357                 string_height = min(string_height, text_height*2);
358                 item_height = max(item_height, string_height);
359             }
360 
361             column_width = max(column_width, text_sx + text_width + pad_right);
362             row_height = max(row_height, item_height);
363         }
364     }
365     row_height += row_height == 0 ? 0 : 2*item_pad;
366     height += row_height;
367     row_heights.push_back(height);
368     column_width += 2*item_pad;
369 
370     m_height = height;
371     m_nat_column_width = max(min_column_width, min(column_width, max_column_width));
372 }
373 
get_max_viewport_height()374 int UIMenu::get_max_viewport_height()
375 {
376     // Limit page size to ensure <= 52 items visible
377     int max_viewport_height = INT_MAX;
378     size_t a = 0, b = 0, num_items = 0;
379     while (b < item_info.size())
380     {
381         if (num_items < 52)
382             num_items += !item_info[b++].heading;
383         else if (num_items == 52)
384         {
385             int item_h = row_heights[item_info[b].row] - row_heights[item_info[b-1].row];
386             int delta = item_h + item_info[b-1].y - item_info[a].y;
387             max_viewport_height = min(max_viewport_height, delta);
388             do
389             {
390                 num_items -= !item_info[a++].heading;
391             }
392             while (item_info[a].column != 0);
393         }
394     }
395     return max_viewport_height;
396 }
397 #endif
398 
_render()399 void UIMenu::_render()
400 {
401 #ifdef USE_TILE_LOCAL
402     GLW_3VF t = {(float)m_region.x, (float)m_region.y, 0}, s = {1, 1, 1};
403     glmanager->set_transform(t, s);
404 
405     m_shape_buf.draw();
406     m_div_line_buf.draw();
407     for (int i = 0; i < TEX_MAX; i++)
408         m_tile_buf[i].draw();
409     m_text_buf.draw();
410     m_line_buf.draw();
411 
412     glmanager->reset_transform();
413 #else
414 
415     int vis_min, vis_max;
416     is_visible_item_range(&vis_min, &vis_max);
417     const int scroll = m_menu->m_ui.scroller->get_scroll();
418 
419     for (int i = vis_min; i < vis_max; i++)
420     {
421         const MenuEntry *me = m_menu->items[i];
422         int y = i - vis_min + 1;
423         cgotoxy(m_region.x+1, m_region.y+scroll+y);
424         const int col = m_menu->item_colour(me);
425         textcolour(col);
426         const bool needs_cursor = (m_menu->get_cursor() == i && m_menu->is_set(MF_MULTISELECT));
427 
428         // TODO: is this highlighting good enough for accessibility purposes?
429         if (m_hover_idx == i)
430             textbackground(DARKGREY);
431         if (m_menu->get_flags() & MF_ALLOW_FORMATTING)
432         {
433             formatted_string s = formatted_string::parse_string(
434                 me->get_text(needs_cursor), col);
435             s.chop(m_region.width).display();
436         }
437         else
438         {
439             string text = me->get_text(needs_cursor);
440             text = chop_string(text, m_region.width);
441             cprintf("%s", text.c_str());
442         }
443         textbackground(BLACK);
444     }
445 #endif
446 }
447 
_get_preferred_size(Direction dim,int prosp_width)448 SizeReq UIMenu::_get_preferred_size(Direction dim, int prosp_width)
449 {
450 #ifdef USE_TILE_LOCAL
451     if (!dim)
452     {
453         do_layout(INT_MAX, m_num_columns);
454         const int em = Options.tile_font_crt_size;
455         int max_menu_width = min(93*em, m_nat_column_width * m_num_columns);
456         return {0, max_menu_width};
457     }
458     else
459     {
460         do_layout(prosp_width, m_num_columns);
461         return {0, m_height};
462     }
463 #else
464     UNUSED(prosp_width);
465     if (!dim)
466         return {0, 80};
467     else
468         return {1, max({1, (int)m_menu->items.size(), m_shown_height})};
469 #endif
470 }
471 
472 class UIMenuScroller : public Scroller
473 {
474 public:
UIMenuScroller()475     UIMenuScroller() : Scroller() {};
~UIMenuScroller()476     virtual ~UIMenuScroller() {};
_allocate_region()477     virtual void _allocate_region() override {
478         m_child->set_allocation_needed();
479         Scroller::_allocate_region();
480     };
481 };
482 
483 class UIMenuMore : public Text
484 {
485 public:
~UIMenuMore()486     virtual ~UIMenuMore() {};
set_text_immediately(const formatted_string & fs)487     void set_text_immediately(const formatted_string &fs)
488     {
489         m_text.clear();
490         m_text += fs;
491         _expose();
492         m_wrapped_size = Size(-1);
493         wrap_text_to_size(m_region.width, m_region.height);
494     };
495 };
496 
497 class UIMenuPopup : public ui::Popup
498 {
499 public:
UIMenuPopup(shared_ptr<Widget> child,Menu * menu)500     UIMenuPopup(shared_ptr<Widget> child, Menu *menu) : ui::Popup(child), m_menu(menu) {};
~UIMenuPopup()501     virtual ~UIMenuPopup() {};
502 
503     virtual void _allocate_region() override;
504 
505 private:
506     Menu *m_menu;
507 };
508 
_allocate_region()509 void UIMenuPopup::_allocate_region()
510 {
511     Popup::_allocate_region();
512 
513     int max_height = m_menu->m_ui.popup->get_max_child_size().height;
514     max_height -= m_menu->m_ui.title->get_region().height;
515     max_height -= m_menu->m_ui.title->get_margin().bottom;
516     int viewport_height = m_menu->m_ui.scroller->get_region().height;
517 
518 #ifdef USE_TILE_LOCAL
519     int menu_w = m_menu->m_ui.menu->get_region().width;
520     m_menu->m_ui.menu->do_layout(menu_w, 1);
521     int m_height = m_menu->m_ui.menu->m_height;
522 
523     int more_height = m_menu->m_ui.more->get_region().height;
524     // switch number of columns
525     int num_cols = m_menu->m_ui.menu->get_num_columns();
526     if (m_menu->m_ui.menu->m_draw_tiles && m_menu->is_set(MF_USE_TWO_COLUMNS)
527         && !Options.tile_single_column_menus)
528     {
529         if ((num_cols == 1 && m_height+more_height > max_height)
530          || (num_cols == 2 && m_height+more_height <= max_height))
531         {
532             m_menu->m_ui.menu->set_num_columns(3 - num_cols);
533             ui::restart_layout();
534         }
535     }
536     m_menu->m_ui.menu->do_layout(menu_w, num_cols);
537 #endif
538 
539 #ifndef USE_TILE_LOCAL
540     int menu_height = m_menu->m_ui.menu->get_region().height;
541 
542     // change more visibility
543     bool can_toggle_more = !m_menu->is_set(MF_ALWAYS_SHOW_MORE)
544         && !m_menu->m_ui.more->get_text().ops.empty();
545     if (can_toggle_more)
546     {
547         bool more_visible = m_menu->m_ui.more->is_visible();
548         if (more_visible ? menu_height <= max_height : menu_height > max_height)
549         {
550             m_menu->m_ui.more->set_visible(!more_visible);
551             _invalidate_sizereq();
552             m_menu->m_ui.more->_queue_allocation();
553             ui::restart_layout();
554         }
555     }
556 
557     if (m_menu->m_keyhelp_more && m_menu->m_ui.more->is_visible())
558     {
559         int scroll = m_menu->m_ui.scroller->get_scroll();
560         int scroll_percent = scroll*100/(menu_height-viewport_height);
561         string perc = scroll <= 0 ? "top"
562             : scroll_percent >= 100 ? "bot"
563             : make_stringf("%2d%%", scroll_percent);
564 
565         string scroll_more = m_menu->more.to_colour_string();
566         scroll_more = replace_all(scroll_more, "XXX", perc);
567         m_menu->m_ui.more->set_text_immediately(formatted_string::parse_string(scroll_more));
568     }
569 #endif
570 
571     // adjust maximum height
572 #ifdef USE_TILE_LOCAL
573     const int max_viewport_height = m_menu->m_ui.menu->get_max_viewport_height();
574 #else
575     const int max_viewport_height = 52;
576 #endif
577     m_menu->m_ui.scroller->max_size().height = max_viewport_height;
578     if (max_viewport_height < viewport_height)
579     {
580         m_menu->m_ui.scroller->_invalidate_sizereq();
581         m_menu->m_ui.scroller->_queue_allocation();
582         ui::restart_layout();
583     }
584 }
585 
_allocate_region()586 void UIMenu::_allocate_region()
587 {
588 #ifndef USE_TILE_LOCAL
589     // XXX: is this needed?
590     m_height = m_menu->items.size();
591 #else
592     do_layout(m_region.width, m_num_columns);
593     if (!(m_menu->flags & MF_ARROWS_SELECT) || m_menu->last_hovered < 0)
594         update_hovered_entry();
595     pack_buffers();
596 #endif
597 }
598 
set_hovered_entry(int i)599 void UIMenu::set_hovered_entry(int i)
600 {
601     m_hover_idx = i;
602 #ifdef USE_TILE_LOCAL
603     if (row_heights.size() > 0) // check for initial layout
604         pack_buffers();
605 #endif
606     _expose();
607 }
608 
609 #ifdef USE_TILE_LOCAL
update_hovered_entry()610 void UIMenu::update_hovered_entry()
611 {
612     const int x = m_mouse_x - m_region.x,
613               y = m_mouse_y - m_region.y;
614     int vis_min, vis_max;
615     is_visible_item_range(&vis_min, &vis_max);
616 
617     for (int i = vis_min; i < vis_max; ++i)
618     {
619         const auto& entry = item_info[i];
620         if (entry.heading)
621             continue;
622         const auto me = m_menu->items[i];
623         if (me->hotkeys.size() == 0)
624             continue;
625         const int w = m_region.width / m_num_columns;
626         const int entry_x = entry.column * w;
627         const int entry_h = row_heights[entry.row+1] - row_heights[entry.row];
628         if (x >= entry_x && x < entry_x+w && y >= entry.y && y < entry.y+entry_h)
629         {
630             wm->set_mouse_cursor(MOUSE_CURSOR_POINTER);
631             m_hover_idx = i;
632             m_menu->last_hovered = i;
633             return;
634         }
635     }
636     wm->set_mouse_cursor(MOUSE_CURSOR_ARROW);
637     if (!(m_menu->flags & MF_ARROWS_SELECT))
638     {
639         m_hover_idx = -1;
640         m_menu->last_hovered = -1;
641     }
642 }
643 
on_event(const Event & ev)644 bool UIMenu::on_event(const Event& ev)
645 {
646     if (Widget::on_event(ev))
647         return true;
648 
649     if (ev.type() != Event::Type::MouseMove
650      && ev.type() != Event::Type::MouseDown
651      && ev.type() != Event::Type::MouseUp
652      && ev.type() != Event::Type::MouseEnter
653      && ev.type() != Event::Type::MouseLeave)
654     {
655         return false;
656     }
657 
658     auto event = static_cast<const MouseEvent&>(ev);
659 
660     m_mouse_x = event.x();
661     m_mouse_y = event.y();
662 
663     if (event.type() == Event::Type::MouseEnter)
664     {
665         do_layout(m_region.width, m_num_columns);
666         update_hovered_entry();
667         pack_buffers();
668         _expose();
669         return false;
670     }
671 
672     if (event.type() == Event::Type::MouseLeave)
673     {
674         wm->set_mouse_cursor(MOUSE_CURSOR_ARROW);
675         m_mouse_x = -1;
676         m_mouse_y = -1;
677         m_mouse_pressed = false;
678         m_hover_idx = -1;
679         do_layout(m_region.width, m_num_columns);
680         pack_buffers();
681         _expose();
682         return false;
683     }
684 
685     if (event.type() == Event::Type::MouseMove)
686     {
687         do_layout(m_region.width, m_num_columns);
688         update_hovered_entry();
689         pack_buffers();
690         _expose();
691         return true;
692     }
693 
694     int key = -1;
695     if (event.type() ==  Event::Type::MouseDown
696         && event.button() == MouseEvent::Button::Left)
697     {
698         m_mouse_pressed = true;
699         _queue_allocation();
700     }
701     else if (event.type() == Event::Type::MouseUp
702             && event.button() == MouseEvent::Button::Left
703             && m_mouse_pressed)
704     {
705         int entry = m_hover_idx;
706         if (entry != -1 && m_menu->items[entry]->hotkeys.size() > 0)
707             key = m_menu->items[entry]->hotkeys[0];
708         m_mouse_pressed = false;
709         _queue_allocation();
710     }
711 
712     if (key != -1)
713     {
714         wm_keyboard_event wm_ev = {0};
715         wm_ev.keysym.sym = key;
716         KeyEvent key_ev(Event::Type::KeyDown, wm_ev);
717         m_menu->m_ui.popup->on_event(key_ev);
718     }
719 
720     return true;
721 }
722 
pack_buffers()723 void UIMenu::pack_buffers()
724 {
725     m_shape_buf.clear();
726     m_div_line_buf.clear();
727     for (int i = 0; i < TEX_MAX; i++)
728         m_tile_buf[i].clear();
729     m_text_buf.clear();
730     m_line_buf.clear();
731 
732     const VColour selected_colour(50, 50, 10, 255);
733     const VColour header_div_colour(64, 64, 64, 200);
734 
735     if (!item_info.size())
736         return;
737 
738     const int col_width = m_region.width / m_num_columns;
739 
740     int vis_min, vis_max;
741     is_visible_item_range(&vis_min, &vis_max);
742 
743     for (int i = vis_min; i < vis_max; ++i)
744     {
745         const auto& entry = item_info[i];
746         const auto me = m_menu->items[i];
747         const int entry_x = entry.column * col_width;
748         const int entry_ex = entry_x + col_width;
749         const int entry_h = row_heights[entry.row+1] - row_heights[entry.row];
750 
751         if (entry.heading)
752         {
753             formatted_string split = m_font_entry->split(entry.text, m_region.width, entry_h);
754             // see corresponding section in do_layout()
755             int line_y = entry.y  + (i == 0 ? 0 : 5) + item_pad;
756             if (i < (int)item_info.size()-1 && !item_info[i+1].heading)
757             {
758                 m_div_line_buf.add_square(entry.x, line_y,
759                         entry.x+m_num_columns*col_width, line_y, header_div_colour);
760             }
761             m_text_buf.add(split, entry.x, line_y+3);
762         }
763         else
764         {
765             const int ty = entry.y + max(entry_h-32, 0)/2;
766             for (const tile_def &tile : entry.tiles)
767             {
768                 // NOTE: This is not perfect. Tiles will be drawn
769                 // sorted by texture first, e.g. you can never draw
770                 // a dungeon tile over a monster tile.
771                 const auto tex = get_tile_texture(tile.tile);
772                 m_tile_buf[tex].add(tile.tile, entry_x + item_pad, ty, 0, 0, false, tile.ymax, 1, 1);
773             }
774 
775             const int text_indent = m_draw_tiles ? 38 : 0;
776             int text_sx = entry_x + text_indent + item_pad;
777             int text_sy = entry.y + (entry_h - m_font_entry->char_height())/2;
778 
779             // Split off and render any hotkey prefix first
780             formatted_string text;
781             if (_has_hotkey_prefix(entry.text.tostring()))
782             {
783                 formatted_string header = entry.text.chop(5);
784                 m_text_buf.add(header, text_sx, text_sy);
785                 text_sx += m_font_entry->string_width(header);
786                 text = entry.text;
787                 // remove hotkeys. As Enne said above, this is a monstrosity.
788                 for (int k = 0; k < 5; k++)
789                     text.del_char();
790             }
791             else
792                 text += entry.text;
793 
794             // Line wrap and render the remaining text
795             int w = entry_ex-text_sx - pad_right;
796             int h = m_font_entry->char_height();
797             h *= m_menu->is_set(MF_NO_WRAP_ROWS) ? 1 : 2;
798             formatted_string split = m_font_entry->split(text, w, h);
799             int string_height = m_font_entry->string_height(split);
800             text_sy = entry.y + (entry_h - string_height)/2;
801 
802             m_text_buf.add(split, text_sx, text_sy);
803         }
804 
805         bool hovered = i == m_hover_idx && !entry.heading && me->hotkeys.size() > 0;
806 
807         if (me->selected() && !m_menu->is_set(MF_QUIET_SELECT))
808         {
809             m_shape_buf.add(entry_x, entry.y,
810                     entry_ex, entry.y+entry_h, selected_colour);
811         }
812         else if (hovered)
813         {
814             const VColour hover_bg = m_mouse_pressed ?
815                 VColour(0, 0, 0, 255) : VColour(255, 255, 255, 25);
816             m_shape_buf.add(entry_x, entry.y,
817                     entry_ex, entry.y+entry_h, hover_bg);
818         }
819         if (hovered)
820         {
821             const VColour mouse_colour = m_mouse_pressed ?
822                 VColour(34, 34, 34, 255) : VColour(255, 255, 255, 51);
823             m_line_buf.add_square(entry_x + 1, entry.y + 1,
824                     entry_x+col_width, entry.y+entry_h, mouse_colour);
825         }
826     }
827 }
828 #endif
829 
Menu(int _flags,const string & tagname,KeymapContext kmc)830 Menu::Menu(int _flags, const string& tagname, KeymapContext kmc)
831   : f_selitem(nullptr), f_keyfilter(nullptr),
832     action_cycle(CYCLE_NONE), menu_action(ACT_EXAMINE), title(nullptr),
833     title2(nullptr), flags(_flags), tag(tagname),
834     cur_page(1), items(), sel(),
835     select_filter(), highlighter(new MenuHighlighter), num(-1), lastch(0),
836     alive(false), last_selected(-1), last_hovered(-1), m_kmc(kmc),
837     m_filter(nullptr)
838 {
839     m_ui.menu = make_shared<UIMenu>(this);
840     m_ui.scroller = make_shared<UIMenuScroller>();
841     m_ui.title = make_shared<Text>();
842     m_ui.more = make_shared<UIMenuMore>();
843     m_ui.more->set_visible(false);
844     m_ui.vbox = make_shared<Box>(Widget::VERT);
845     m_ui.vbox->set_cross_alignment(Widget::STRETCH);
846 
847     m_ui.vbox->add_child(m_ui.title);
848 #ifdef USE_TILE_LOCAL
849     m_ui.vbox->add_child(m_ui.scroller);
850 #else
851     auto scroller_wrap = make_shared<Box>(Widget::VERT, Box::Expand::EXPAND_V);
852     scroller_wrap->set_cross_alignment(Widget::STRETCH);
853     scroller_wrap->add_child(m_ui.scroller);
854     m_ui.vbox->add_child(scroller_wrap);
855 #endif
856     m_ui.vbox->add_child(m_ui.more);
857     m_ui.scroller->set_child(m_ui.menu);
858 
859     set_flags(flags);
860     set_more();
861 }
862 
check_add_formatted_line(int firstcol,int nextcol,string & line,bool check_eol)863 void Menu::check_add_formatted_line(int firstcol, int nextcol,
864                                     string &line, bool check_eol)
865 {
866     if (line.empty())
867         return;
868 
869     if (check_eol && line.find("\n") == string::npos)
870         return;
871 
872     vector<string> lines = split_string("\n", line, false, true);
873     int size = lines.size();
874 
875     // If we have stuff after EOL, leave that in the line variable and
876     // don't add an entry for it, unless the caller told us not to
877     // check EOL sanity.
878     if (check_eol && !ends_with(line, "\n"))
879         line = lines[--size];
880     else
881         line.clear();
882 
883     for (int i = 0, col = firstcol; i < size; ++i, col = nextcol)
884     {
885         string &s(lines[i]);
886 
887         trim_string_right(s);
888 
889         MenuEntry *me = new MenuEntry(s);
890         me->colour = col;
891         if (!title)
892             set_title(me);
893         else
894             add_entry(me);
895     }
896 
897     line.clear();
898 }
899 
~Menu()900 Menu::~Menu()
901 {
902     deleteAll(items);
903     delete title;
904     if (title2)
905         delete title2;
906     delete highlighter;
907 }
908 
clear()909 void Menu::clear()
910 {
911     deleteAll(items);
912     m_ui.menu->_queue_allocation();
913     last_selected = -1;
914 }
915 
set_flags(int new_flags)916 void Menu::set_flags(int new_flags)
917 {
918     flags = new_flags;
919 
920 #ifdef DEBUG
921     int sel_flag = flags & (MF_NOSELECT | MF_SINGLESELECT | MF_MULTISELECT);
922     ASSERT(sel_flag == MF_NOSELECT || sel_flag == MF_SINGLESELECT || sel_flag == MF_MULTISELECT);
923 #endif
924 }
925 
minus_is_pageup() const926 bool Menu::minus_is_pageup() const
927 {
928     return !is_set(MF_MULTISELECT) && !is_set(MF_SPECIAL_MINUS);
929 }
930 
set_more(const formatted_string & fs)931 void Menu::set_more(const formatted_string &fs)
932 {
933     m_keyhelp_more = false;
934     more = fs;
935     update_more();
936 }
937 
set_more(const string s)938 void Menu::set_more(const string s)
939 {
940     set_more(formatted_string::parse_string(s));
941 }
942 
set_more()943 void Menu::set_more()
944 {
945     m_keyhelp_more = true;
946     string pageup_keys = minus_is_pageup() ? "<w>-</w>|<w><<</w>" : "<w><<</w>";
947     more = formatted_string::parse_string(
948         "<lightgrey>[<w>+</w>|<w>></w>|<w>Space</w>]: page down        "
949         "[" + pageup_keys + "]: page up        "
950         "[<w>Esc</w>]: close        [<w>XXX</w>]</lightgrey>"
951     );
952     update_more();
953 }
954 
set_min_col_width(int w)955 void Menu::set_min_col_width(int w)
956 {
957 #ifdef USE_TILE_LOCAL
958     // w is in chars
959     m_ui.menu->set_min_col_width(w * tiles.get_crt_font()->char_width());
960 #else
961     // n.b. this currently has no effect in webtiles unless the more is visible
962     m_ui.menu->set_min_col_width(w);
963 #endif
964 }
965 
set_highlighter(MenuHighlighter * mh)966 void Menu::set_highlighter(MenuHighlighter *mh)
967 {
968     if (highlighter != mh)
969         delete highlighter;
970     highlighter = mh;
971 }
972 
set_title(MenuEntry * e,bool first,bool indent)973 void Menu::set_title(MenuEntry *e, bool first, bool indent)
974 {
975     if (first)
976     {
977         if (title != e)
978             delete title;
979 
980         title = e;
981         title->level = MEL_TITLE;
982     }
983     else
984     {
985         title2 = e;
986         title2->level = MEL_TITLE;
987     }
988     m_indent_title = indent;
989     update_title();
990 }
991 
add_entry(MenuEntry * entry)992 void Menu::add_entry(MenuEntry *entry)
993 {
994     entry->tag = tag;
995     items.push_back(entry);
996 }
997 
reset()998 void Menu::reset()
999 {
1000     m_ui.scroller->set_scroll(0);
1001 }
1002 
show(bool reuse_selections)1003 vector<MenuEntry *> Menu::show(bool reuse_selections)
1004 {
1005     cursor_control cs(false);
1006 
1007     if (reuse_selections)
1008         get_selected(&sel);
1009     else
1010         deselect_all(false);
1011 
1012     if (is_set(MF_START_AT_END))
1013         m_ui.scroller->set_scroll(INT_MAX);
1014 
1015     do_menu();
1016 
1017     return sel;
1018 }
1019 
do_menu()1020 void Menu::do_menu()
1021 {
1022     bool done = false;
1023     m_ui.popup = make_shared<UIMenuPopup>(m_ui.vbox, this);
1024 
1025     m_ui.popup->on_keydown_event([this, &done](const KeyEvent& ev) {
1026         if (m_filter)
1027         {
1028             int key = m_filter->putkey(ev.key());
1029 
1030             if (key == CK_ESCAPE)
1031                 m_filter->set_text("");
1032 
1033             if (key != -1)
1034             {
1035                 lastch = key;
1036                 delete m_filter;
1037                 m_filter = nullptr;
1038             }
1039             update_title();
1040             return true;
1041         }
1042         done = !process_key(ev.key());
1043         return true;
1044     });
1045 #ifdef TOUCH_UI
1046     auto menu_wrap_click = [this, &done](const MouseEvent& ev) {
1047         if (!m_filter && ev.button() == MouseEvent::Button::Left)
1048         {
1049             done = !process_key(CK_TOUCH_DUMMY);
1050             return true;
1051         }
1052         return false;
1053     };
1054     m_ui.title->on_mousedown_event(menu_wrap_click);
1055     m_ui.more->on_mousedown_event(menu_wrap_click);
1056 #endif
1057 
1058     update_menu();
1059     ui::push_layout(m_ui.popup, m_kmc);
1060 
1061 #ifdef USE_TILE_WEB
1062     tiles.push_menu(this);
1063     _webtiles_title_changed = false;
1064     m_ui.popup->on_layout_pop([](){ tiles.pop_menu(); });
1065 #endif
1066 
1067     alive = true;
1068     if (on_show)
1069         done = !on_show();
1070     while (alive && !done && !crawl_state.seen_hups)
1071     {
1072 #ifdef USE_TILE_WEB
1073         if (_webtiles_title_changed)
1074         {
1075             webtiles_update_title();
1076             _webtiles_title_changed = false;
1077         }
1078 #endif
1079         ui::pump_events();
1080     }
1081     alive = false;
1082     ui::pop_layout();
1083 }
1084 
get_cursor() const1085 int Menu::get_cursor() const
1086 {
1087     if (last_selected == -1)
1088         return -1;
1089 
1090     unsigned int last = last_selected % item_count();
1091     unsigned int next = (last_selected + 1) % item_count();
1092 
1093     // Items with no hotkeys are unselectable
1094     while (next != last && (items[next]->hotkeys.empty()
1095                             || items[next]->level != MEL_ITEM))
1096     {
1097         next = (next + 1) % item_count();
1098     }
1099 
1100     return next;
1101 }
1102 
is_set(int flag) const1103 bool Menu::is_set(int flag) const
1104 {
1105     return (flags & flag) == flag;
1106 }
1107 
pre_process(int k)1108 int Menu::pre_process(int k)
1109 {
1110     return k;
1111 }
1112 
post_process(int k)1113 int Menu::post_process(int k)
1114 {
1115     return k;
1116 }
1117 
filter_with_regex(const char * re)1118 bool Menu::filter_with_regex(const char *re)
1119 {
1120     text_pattern tpat(re, true);
1121     for (unsigned int i = 0; i < items.size(); ++i)
1122     {
1123         if (items[i]->level == MEL_ITEM
1124             && tpat.matches(items[i]->get_text()))
1125         {
1126             select_index(i);
1127             if (flags & MF_SINGLESELECT)
1128             {
1129                 // Return the first item found.
1130                 get_selected(&sel);
1131                 return false;
1132             }
1133         }
1134     }
1135     get_selected(&sel);
1136     return true;
1137 }
1138 
title_prompt(char linebuf[],int bufsz,const char * prompt)1139 bool Menu::title_prompt(char linebuf[], int bufsz, const char* prompt)
1140 {
1141     bool validline;
1142 
1143     ASSERT(!m_filter);
1144     // XX show cursor in console
1145 #ifdef USE_TILE_WEB
1146     tiles.json_open_object();
1147     tiles.json_write_string("msg", "title_prompt");
1148     tiles.json_write_string("prompt", prompt);
1149     tiles.json_close_object();
1150     tiles.finish_message();
1151 #endif
1152     m_filter = new resumable_line_reader(linebuf, bufsz);
1153     m_filter->set_prompt(prompt);
1154     update_title();
1155     do
1156     {
1157         ui::pump_events();
1158     }
1159     while (m_filter && !crawl_state.seen_hups);
1160     validline = linebuf[0];
1161 
1162     return validline;
1163 }
1164 
process_key(int keyin)1165 bool Menu::process_key(int keyin)
1166 {
1167     if (items.empty())
1168     {
1169         lastch = keyin;
1170         return false;
1171     }
1172 #ifdef TOUCH_UI
1173     else if (action_cycle == CYCLE_TOGGLE && (keyin == '!' || keyin == '?'
1174              || keyin == CK_TOUCH_DUMMY))
1175 #else
1176     else if (action_cycle == CYCLE_TOGGLE && (keyin == '!' || keyin == '?'))
1177 #endif
1178     {
1179         ASSERT(menu_action != ACT_MISC);
1180         if (menu_action == ACT_EXECUTE)
1181             menu_action = ACT_EXAMINE;
1182         else
1183             menu_action = ACT_EXECUTE;
1184 
1185         sel.clear();
1186         update_title();
1187         return true;
1188     }
1189 #ifdef TOUCH_UI
1190     else if (action_cycle == CYCLE_CYCLE && (keyin == '!' || keyin == '?'
1191              || keyin == CK_TOUCH_DUMMY))
1192 #else
1193     else if (action_cycle == CYCLE_CYCLE && (keyin == '!' || keyin == '?'))
1194 #endif
1195     {
1196         menu_action = (action)((menu_action+1) % ACT_NUM);
1197         sel.clear();
1198         update_title();
1199         return true;
1200     }
1201 
1202     if (f_keyfilter)
1203         keyin = (*f_keyfilter)(keyin);
1204     keyin = pre_process(keyin);
1205 
1206 #ifdef USE_TILE_WEB
1207     const int old_vis_first = get_first_visible();
1208 #endif
1209 
1210     switch (keyin)
1211     {
1212     case CK_REDRAW:
1213         return true;
1214 #ifndef TOUCH_UI
1215     case 0:
1216         return true;
1217 #endif
1218     case CK_MOUSE_B2:
1219     case CK_MOUSE_CMD:
1220     CASE_ESCAPE
1221         sel.clear();
1222         lastch = keyin;
1223         return is_set(MF_UNCANCEL) && !crawl_state.seen_hups;
1224     case ' ': case CK_PGDN: case '>': case '+':
1225     case CK_MOUSE_B1:
1226     case CK_MOUSE_CLICK:
1227         if (!page_down() && is_set(MF_WRAP))
1228             m_ui.scroller->set_scroll(0);
1229         break;
1230     case CK_PGUP:
1231     case '<':
1232         page_up();
1233         break;
1234     case CK_SHIFT_UP:
1235         line_up();
1236         break;
1237     case CK_UP:
1238         if (is_set(MF_ARROWS_SELECT))
1239             cycle_hover(true);
1240         else
1241             line_up();
1242         break;
1243     case CK_SHIFT_DOWN:
1244         line_down();
1245         break;
1246     case CK_DOWN:
1247         if (is_set(MF_ARROWS_SELECT))
1248             cycle_hover();
1249         else
1250             line_down();
1251         break;
1252     case CK_HOME:
1253         m_ui.scroller->set_scroll(0);
1254         if (is_set(MF_ARROWS_SELECT) && items.size())
1255         {
1256             set_hovered(0);
1257             if (items[last_hovered]->level != MEL_ITEM)
1258                 cycle_hover();
1259         }
1260         break;
1261     case CK_END:
1262         // setting this to INT_MAX when the last item is already visible does
1263         // unnecessary scrolling to force the last item to be exactly at the
1264         // end of the menu. (This has a weird interaction with page down.)
1265         // TODO: possibly should be fixed in ui.cc? But I don't understand that
1266         // code well enough
1267         if (items.size())
1268         {
1269             if (!in_page(static_cast<int>(items.size()) - 1, true))
1270                 m_ui.scroller->set_scroll(INT_MAX);
1271             if (is_set(MF_ARROWS_SELECT))
1272             {
1273                 set_hovered(static_cast<int>(items.size()) - 1);
1274                 if (items[last_hovered]->level != MEL_ITEM)
1275                     cycle_hover(true);
1276 
1277             }
1278         }
1279         break;
1280     case CONTROL('F'):
1281         if ((flags & MF_ALLOW_FILTER))
1282         {
1283             char linebuf[80] = "";
1284 
1285             const bool validline = title_prompt(linebuf, sizeof linebuf,
1286                                                 "Select what (regex)?");
1287 
1288             return (validline && linebuf[0]) ? filter_with_regex(linebuf) : true;
1289         }
1290         break;
1291     case '.':
1292         if (last_selected == -1 && is_set(MF_MULTISELECT))
1293             last_selected = 0;
1294 
1295         if (last_selected != -1)
1296         {
1297             const int next = get_cursor();
1298             if (next != -1)
1299             {
1300                 InvEntry::set_show_cursor(true);
1301                 select_index(next, num);
1302                 get_selected(&sel);
1303                 update_title();
1304                 if (get_cursor() < next)
1305                 {
1306                     m_ui.scroller->set_scroll(0);
1307                     break;
1308                 }
1309             }
1310 
1311             if (!in_page(last_selected))
1312                 page_down();
1313         }
1314         break;
1315 
1316     case '\'':
1317         if (last_selected == -1 && is_set(MF_MULTISELECT))
1318             last_selected = 0;
1319         else
1320             last_selected = get_cursor();
1321 
1322         if (last_selected != -1)
1323         {
1324             InvEntry::set_show_cursor(true);
1325             const int it_count = item_count();
1326             if (last_selected < it_count
1327                 && items[last_selected]->level == MEL_ITEM)
1328             {
1329                 m_ui.menu->update_item(last_selected);
1330             }
1331 
1332             const int next_cursor = get_cursor();
1333             if (next_cursor != -1)
1334             {
1335                 if (next_cursor < last_selected)
1336                     m_ui.scroller->set_scroll(0);
1337                 else if (!in_page(last_selected))
1338                     page_down();
1339                 else if (next_cursor < it_count
1340                          && items[next_cursor]->level == MEL_ITEM)
1341                 {
1342                     m_ui.menu->update_item(next_cursor);
1343                 }
1344             }
1345         }
1346         break;
1347 
1348     case '_':
1349         if (!help_key().empty())
1350             show_specific_help(help_key());
1351         break;
1352 
1353 #ifdef TOUCH_UI
1354     case CK_TOUCH_DUMMY:  // mouse click in top/bottom region of menu
1355     case 0:               // do the same as <enter> key
1356         if (!(flags & MF_MULTISELECT)) // bail out if not a multi-select
1357             return true;
1358         // seemingly intentional fallthrough
1359 #endif
1360     case CK_ENTER:
1361         // TODO: hover and multiselect?
1362         if ((flags & MF_SINGLESELECT) && last_hovered >= 0)
1363             select_item_index(last_hovered, 1);
1364         else if (!(flags & MF_PRESELECTED) || !sel.empty())
1365             return false;
1366         // else fall through
1367     default:
1368         // Even if we do return early, lastch needs to be set first,
1369         // as it's sometimes checked when leaving a menu.
1370         keyin  = post_process(keyin);
1371         lastch = keyin;
1372 
1373         // If no selection at all is allowed, exit now.
1374         if (!(flags & (MF_SINGLESELECT | MF_MULTISELECT)))
1375             return false;
1376 
1377         if (!is_set(MF_NO_SELECT_QTY) && isadigit(keyin))
1378         {
1379             if (num > 999)
1380                 num = -1;
1381             num = (num == -1) ? keyin - '0' :
1382                                 num * 10 + keyin - '0';
1383         }
1384 
1385         select_items(keyin, num);
1386         get_selected(&sel);
1387         if (sel.size() == 1 && (flags & MF_SINGLESELECT))
1388         {
1389             if (!on_single_selection)
1390                 return false;
1391             MenuEntry *item = sel[0];
1392             // TODO: per item trigger code
1393             if (!on_single_selection(*item))
1394                 return false;
1395             deselect_all();
1396             return true;
1397         }
1398 
1399         update_title();
1400 
1401         if (flags & MF_ANYPRINTABLE
1402             && (!isadigit(keyin) || is_set(MF_NO_SELECT_QTY)))
1403         {
1404             return false;
1405         }
1406 
1407         break;
1408     }
1409 
1410     if (last_selected != -1 && get_cursor() == -1)
1411         last_selected = -1;
1412 
1413     if (!isadigit(keyin))
1414         num = -1;
1415 
1416 #ifdef USE_TILE_WEB
1417     if (old_vis_first != get_first_visible())
1418         webtiles_update_scroll_pos();
1419 #endif
1420 
1421     return true;
1422 }
1423 
get_select_count_string(int count) const1424 string Menu::get_select_count_string(int count) const
1425 {
1426     string ret;
1427     if (f_selitem)
1428         ret = f_selitem(&sel);
1429     else
1430     {
1431         char buf[100] = "";
1432         if (count)
1433         {
1434             snprintf(buf, sizeof buf, " (%d item%s)", count,
1435                     (count > 1 ? "s" : ""));
1436         }
1437         ret = string(buf);
1438     }
1439     return ret + string(max(12-(int)ret.size(), 0), ' ');
1440 }
1441 
selected_entries() const1442 vector<MenuEntry*> Menu::selected_entries() const
1443 {
1444     vector<MenuEntry*> selection;
1445     get_selected(&selection);
1446     return selection;
1447 }
1448 
get_selected(vector<MenuEntry * > * selected) const1449 void Menu::get_selected(vector<MenuEntry*> *selected) const
1450 {
1451     selected->clear();
1452 
1453     for (MenuEntry *item : items)
1454         if (item->selected())
1455             selected->push_back(item);
1456 }
1457 
deselect_all(bool update_view)1458 void Menu::deselect_all(bool update_view)
1459 {
1460     for (int i = 0, count = items.size(); i < count; ++i)
1461     {
1462         if (items[i]->level == MEL_ITEM && items[i]->selected())
1463         {
1464             items[i]->select(0);
1465             if (update_view)
1466             {
1467                 m_ui.menu->update_item(i);
1468 #ifdef USE_TILE_WEB
1469                 webtiles_update_item(i);
1470 #endif
1471             }
1472         }
1473     }
1474     sel.clear();
1475 }
1476 
1477 
1478 
get_first_visible() const1479 int Menu::get_first_visible() const
1480 {
1481     int y = m_ui.scroller->get_scroll();
1482     for (int i = 0; i < (int) items.size(); i++)
1483     {
1484         // why does this use y2? It can lead to partially visible items in tiles
1485         int item_y2;
1486         m_ui.menu->get_item_region(i, nullptr, &item_y2);
1487         if (item_y2 > y)
1488             return i;
1489     }
1490     return items.size();
1491 }
1492 
is_hotkey(int i,int key)1493 bool Menu::is_hotkey(int i, int key)
1494 {
1495     bool ishotkey = items[i]->is_hotkey(key);
1496     return ishotkey && (!is_set(MF_SELECT_BY_PAGE) || in_page(i));
1497 }
1498 
select_items(int key,int qty)1499 void Menu::select_items(int key, int qty)
1500 {
1501     if (key == ',' && !!(flags & MF_MULTISELECT)) // Select all or apply filter if there is one.
1502         select_index(-1, -2);
1503     else if (key == '*' && !!(flags & MF_MULTISELECT)) // Invert selection.
1504         select_index(-1, -1);
1505     else if (key == '-' && !!(flags & MF_MULTISELECT)) // Clear selection. XX is there a singleselect menu where this should work?
1506         select_index(-1, 0);
1507     else
1508     {
1509         int first_entry = get_first_visible(), final = items.size();
1510         bool selected = false;
1511 
1512         // Process all items, in case user hits hotkey for an
1513         // item not on the current page.
1514 
1515         // We have to use some hackery to handle items that share
1516         // the same hotkey (as for pickup when there's a stack of
1517         // >52 items). If there are duplicate hotkeys, the items
1518         // are usually separated by at least a page, so we should
1519         // only select the item on the current page. This is why we
1520         // use two loops, and check to see if we've matched an item
1521         // by its primary hotkey (hotkeys[0] for multiple-selection
1522         // menus, any hotkey for single-selection menus), in which
1523         // case, we stop selecting further items.
1524         const bool check_preselected = (key == CK_ENTER);
1525         for (int i = first_entry; i < final; ++i)
1526         {
1527             if (check_preselected && items[i]->preselected)
1528             {
1529                 select_index(i, qty);
1530                 selected = true;
1531                 break;
1532             }
1533             else if (is_hotkey(i, key))
1534             {
1535                 select_index(i, qty);
1536                 if (items[i]->hotkeys[0] == key || is_set(MF_SINGLESELECT))
1537                 {
1538                     selected = true;
1539                     break;
1540                 }
1541             }
1542         }
1543 
1544         if (!selected)
1545         {
1546             for (int i = 0; i < first_entry; ++i)
1547             {
1548                 if (check_preselected && items[i]->preselected)
1549                 {
1550                     select_index(i, qty);
1551                     break;
1552                 }
1553                 else if (is_hotkey(i, key))
1554                 {
1555                     select_index(i, qty);
1556                     break;
1557                 }
1558             }
1559         }
1560     }
1561 }
1562 
get_text(const bool) const1563 string MenuEntry::get_text(const bool) const
1564 {
1565     if (level == MEL_ITEM && hotkeys.size())
1566     {
1567         return make_stringf(" %s %c %s",
1568             keycode_to_name(hotkeys[0]).c_str(),
1569             preselected ? '+' : '-',
1570             text.c_str());
1571     }
1572     else if (level == MEL_ITEM && indent_no_hotkeys)
1573         return "     " + text;
1574     else
1575         return text;
1576 }
1577 
MonsterMenuEntry(const string & str,const monster_info * mon,int hotkey)1578 MonsterMenuEntry::MonsterMenuEntry(const string &str, const monster_info* mon,
1579                                    int hotkey) :
1580     MenuEntry(str, MEL_ITEM, 1, hotkey)
1581 {
1582     data = (void*)mon;
1583     quantity = 1;
1584 }
1585 
FeatureMenuEntry(const string & str,const coord_def p,int hotkey)1586 FeatureMenuEntry::FeatureMenuEntry(const string &str, const coord_def p,
1587                                    int hotkey) :
1588     MenuEntry(str, MEL_ITEM, 1, hotkey)
1589 {
1590     if (in_bounds(p))
1591         feat = env.grid(p);
1592     else
1593         feat = DNGN_UNSEEN;
1594     pos      = p;
1595     quantity = 1;
1596 }
1597 
FeatureMenuEntry(const string & str,const dungeon_feature_type f,int hotkey)1598 FeatureMenuEntry::FeatureMenuEntry(const string &str,
1599                                    const dungeon_feature_type f,
1600                                    int hotkey) :
1601     MenuEntry(str, MEL_ITEM, 1, hotkey)
1602 {
1603     pos.reset();
1604     feat     = f;
1605     quantity = 1;
1606 }
1607 
1608 #ifdef USE_TILE
get_tiles(vector<tile_def> & tileset) const1609 bool MenuEntry::get_tiles(vector<tile_def>& tileset) const
1610 {
1611     if (!Options.tile_menu_icons || tiles.empty())
1612         return false;
1613 
1614     tileset.insert(end(tileset), begin(tiles), end(tiles));
1615     return true;
1616 }
1617 #else
get_tiles(vector<tile_def> &) const1618 bool MenuEntry::get_tiles(vector<tile_def>& /*tileset*/) const { return false; }
1619 #endif
1620 
add_tile(tile_def tile)1621 void MenuEntry::add_tile(tile_def tile)
1622 {
1623 #ifdef USE_TILE
1624     tiles.push_back(tile);
1625 #else
1626     UNUSED(tile);
1627 #endif
1628 }
1629 
1630 #ifdef USE_TILE
PlayerMenuEntry(const string & str)1631 PlayerMenuEntry::PlayerMenuEntry(const string &str) :
1632     MenuEntry(str, MEL_ITEM, 1)
1633 {
1634     quantity = 1;
1635 }
1636 
get_tiles(vector<tile_def> & tileset) const1637 bool MonsterMenuEntry::get_tiles(vector<tile_def>& tileset) const
1638 {
1639     if (!Options.tile_menu_icons)
1640         return false;
1641 
1642     monster_info* m = (monster_info*)(data);
1643     if (!m)
1644         return false;
1645 
1646     MenuEntry::get_tiles(tileset);
1647 
1648     const bool    fake = m->props.exists("fake");
1649     const coord_def c  = m->pos;
1650     tileidx_t       ch = TILE_FLOOR_NORMAL;
1651 
1652     if (!fake)
1653     {
1654         ch = tileidx_feature(c);
1655         if (ch == TILE_FLOOR_NORMAL)
1656             ch = tile_env.flv(c).floor;
1657         else if (ch == TILE_WALL_NORMAL)
1658             ch = tile_env.flv(c).wall;
1659     }
1660 
1661     tileset.emplace_back(ch);
1662 
1663     if (m->attitude == ATT_FRIENDLY)
1664         tileset.emplace_back(TILE_HALO_FRIENDLY);
1665     else if (m->attitude == ATT_GOOD_NEUTRAL || m->attitude == ATT_STRICT_NEUTRAL)
1666         tileset.emplace_back(TILE_HALO_GD_NEUTRAL);
1667     else if (m->neutral())
1668         tileset.emplace_back(TILE_HALO_NEUTRAL);
1669     else
1670         switch (m->threat)
1671         {
1672         case MTHRT_TRIVIAL:
1673             if (Options.tile_show_threat_levels.find("trivial") != string::npos)
1674                 tileset.emplace_back(TILE_THREAT_TRIVIAL);
1675             break;
1676         case MTHRT_EASY:
1677             if (Options.tile_show_threat_levels.find("easy") != string::npos)
1678                 tileset.emplace_back(TILE_THREAT_EASY);
1679             break;
1680         case MTHRT_TOUGH:
1681             if (Options.tile_show_threat_levels.find("tough") != string::npos)
1682                 tileset.emplace_back(TILE_THREAT_TOUGH);
1683             break;
1684         case MTHRT_NASTY:
1685             if (Options.tile_show_threat_levels.find("nasty") != string::npos)
1686                 tileset.emplace_back(TILE_THREAT_NASTY);
1687             break;
1688         default:
1689             break;
1690         }
1691 
1692     if (m->type == MONS_DANCING_WEAPON)
1693     {
1694         // other animated objects use regular monster tiles (TODO: animate
1695         // armour's seems broken in this menu)
1696         item_def item;
1697 
1698         if (!fake && m->inv[MSLOT_WEAPON])
1699             item = *m->inv[MSLOT_WEAPON];
1700 
1701         if (fake || !item.defined())
1702         {
1703             item.base_type = OBJ_WEAPONS;
1704             item.sub_type  = WPN_LONG_SWORD;
1705             item.quantity  = 1;
1706         }
1707         tileset.emplace_back(tileidx_item(item));
1708         tileset.emplace_back(TILEI_ANIMATED_WEAPON);
1709     }
1710     else if (mons_is_draconian(m->type))
1711     {
1712         tileset.emplace_back(tileidx_draco_base(*m));
1713         const tileidx_t job = tileidx_draco_job(*m);
1714         if (job)
1715             tileset.emplace_back(job);
1716     }
1717     else if (mons_is_demonspawn(m->type))
1718     {
1719         tileset.emplace_back(tileidx_demonspawn_base(*m));
1720         const tileidx_t job = tileidx_demonspawn_job(*m);
1721         if (job)
1722             tileset.emplace_back(job);
1723     }
1724     else
1725     {
1726         tileidx_t idx = tileidx_monster(*m) & TILE_FLAG_MASK;
1727         tileset.emplace_back(idx);
1728     }
1729 
1730     // A fake monster might not have its ghost member set up properly.
1731     if (!fake && m->ground_level())
1732     {
1733         if (ch == TILE_DNGN_LAVA)
1734             tileset.emplace_back(TILEI_MASK_LAVA);
1735         else if (ch == TILE_DNGN_SHALLOW_WATER)
1736             tileset.emplace_back(TILEI_MASK_SHALLOW_WATER);
1737         else if (ch == TILE_DNGN_DEEP_WATER)
1738             tileset.emplace_back(TILEI_MASK_DEEP_WATER);
1739         else if (ch == TILE_DNGN_SHALLOW_WATER_MURKY)
1740             tileset.emplace_back(TILEI_MASK_SHALLOW_WATER_MURKY);
1741         else if (ch == TILE_DNGN_DEEP_WATER_MURKY)
1742             tileset.emplace_back(TILEI_MASK_DEEP_WATER_MURKY);
1743     }
1744 
1745     string damage_desc;
1746     mon_dam_level_type damage_level = m->dam;
1747 
1748     switch (damage_level)
1749     {
1750     case MDAM_DEAD:
1751     case MDAM_ALMOST_DEAD:
1752         tileset.emplace_back(TILEI_MDAM_ALMOST_DEAD);
1753         break;
1754     case MDAM_SEVERELY_DAMAGED:
1755         tileset.emplace_back(TILEI_MDAM_SEVERELY_DAMAGED);
1756         break;
1757     case MDAM_HEAVILY_DAMAGED:
1758         tileset.emplace_back(TILEI_MDAM_HEAVILY_DAMAGED);
1759         break;
1760     case MDAM_MODERATELY_DAMAGED:
1761         tileset.emplace_back(TILEI_MDAM_MODERATELY_DAMAGED);
1762         break;
1763     case MDAM_LIGHTLY_DAMAGED:
1764         tileset.emplace_back(TILEI_MDAM_LIGHTLY_DAMAGED);
1765         break;
1766     case MDAM_OKAY:
1767     default:
1768         // no flag for okay.
1769         break;
1770     }
1771 
1772     if (m->attitude == ATT_FRIENDLY)
1773         tileset.emplace_back(TILEI_FRIENDLY);
1774     else if (m->attitude == ATT_GOOD_NEUTRAL || m->attitude == ATT_STRICT_NEUTRAL)
1775         tileset.emplace_back(TILEI_GOOD_NEUTRAL);
1776     else if (m->neutral())
1777         tileset.emplace_back(TILEI_NEUTRAL);
1778     else if (m->is(MB_FLEEING))
1779         tileset.emplace_back(TILEI_FLEEING);
1780     else if (m->is(MB_STABBABLE))
1781         tileset.emplace_back(TILEI_STAB_BRAND);
1782     else if (m->is(MB_DISTRACTED))
1783         tileset.emplace_back(TILEI_MAY_STAB_BRAND);
1784 
1785     return true;
1786 }
1787 
get_tiles(vector<tile_def> & tileset) const1788 bool FeatureMenuEntry::get_tiles(vector<tile_def>& tileset) const
1789 {
1790     if (!Options.tile_menu_icons)
1791         return false;
1792 
1793     if (feat == DNGN_UNSEEN)
1794         return false;
1795 
1796     MenuEntry::get_tiles(tileset);
1797 
1798     tileidx_t tile = tileidx_feature(pos);
1799     tileset.emplace_back(tile);
1800 
1801     if (in_bounds(pos) && is_unknown_stair(pos))
1802         tileset.emplace_back(TILEI_NEW_STAIR);
1803 
1804     if (in_bounds(pos) && is_unknown_transporter(pos))
1805         tileset.emplace_back(TILEI_NEW_TRANSPORTER);
1806 
1807     return true;
1808 }
1809 
get_tiles(vector<tile_def> & tileset) const1810 bool PlayerMenuEntry::get_tiles(vector<tile_def>& tileset) const
1811 {
1812     if (!Options.tile_menu_icons)
1813         return false;
1814 
1815     MenuEntry::get_tiles(tileset);
1816 
1817     const player_save_info &player = *static_cast<player_save_info*>(data);
1818     dolls_data equip_doll = player.doll;
1819 
1820     // FIXME: Implement this logic in one place in e.g. pack_doll_buf().
1821     int p_order[TILEP_PART_MAX] =
1822     {
1823         TILEP_PART_SHADOW,  //  0
1824         TILEP_PART_HALO,
1825         TILEP_PART_ENCH,
1826         TILEP_PART_DRCWING,
1827         TILEP_PART_CLOAK,
1828         TILEP_PART_BASE,    //  5
1829         TILEP_PART_BOOTS,
1830         TILEP_PART_LEG,
1831         TILEP_PART_BODY,
1832         TILEP_PART_ARM,
1833         TILEP_PART_HAIR,
1834         TILEP_PART_BEARD,
1835         TILEP_PART_DRCHEAD,  // 15
1836         TILEP_PART_HELM,
1837         TILEP_PART_HAND1,   // 10
1838         TILEP_PART_HAND2,
1839     };
1840 
1841     int flags[TILEP_PART_MAX];
1842     tilep_calc_flags(equip_doll, flags);
1843 
1844     // For skirts, boots go under the leg armour. For pants, they go over.
1845     if (equip_doll.parts[TILEP_PART_LEG] < TILEP_LEG_SKIRT_OFS)
1846     {
1847         p_order[6] = TILEP_PART_BOOTS;
1848         p_order[7] = TILEP_PART_LEG;
1849     }
1850 
1851     // Special case bardings from being cut off.
1852     bool is_naga = (equip_doll.parts[TILEP_PART_BASE] == TILEP_BASE_NAGA
1853                     || equip_doll.parts[TILEP_PART_BASE] == TILEP_BASE_NAGA + 1);
1854     if (equip_doll.parts[TILEP_PART_BOOTS] >= TILEP_BOOTS_NAGA_BARDING
1855         && equip_doll.parts[TILEP_PART_BOOTS] <= TILEP_BOOTS_NAGA_BARDING_RED)
1856     {
1857         flags[TILEP_PART_BOOTS] = is_naga ? TILEP_FLAG_NORMAL : TILEP_FLAG_HIDE;
1858     }
1859 
1860     bool is_ptng = (equip_doll.parts[TILEP_PART_BASE] == TILEP_BASE_PALENTONGA
1861                     || equip_doll.parts[TILEP_PART_BASE] == TILEP_BASE_PALENTONGA + 1);
1862     if (equip_doll.parts[TILEP_PART_BOOTS] >= TILEP_BOOTS_CENTAUR_BARDING
1863         && equip_doll.parts[TILEP_PART_BOOTS] <= TILEP_BOOTS_CENTAUR_BARDING_RED)
1864     {
1865         flags[TILEP_PART_BOOTS] = is_ptng ? TILEP_FLAG_NORMAL : TILEP_FLAG_HIDE;
1866     }
1867 
1868     for (int i = 0; i < TILEP_PART_MAX; ++i)
1869     {
1870         const int p   = p_order[i];
1871         const int idx = equip_doll.parts[p];
1872         if (idx == 0 || idx == TILEP_SHOW_EQUIP || flags[p] == TILEP_FLAG_HIDE)
1873             continue;
1874 
1875         ASSERT_RANGE(idx, TILE_MAIN_MAX, TILEP_PLAYER_MAX);
1876 
1877         int ymax = TILE_Y;
1878 
1879         if (flags[p] == TILEP_FLAG_CUT_CENTAUR
1880             || flags[p] == TILEP_FLAG_CUT_NAGA)
1881         {
1882             ymax = 18;
1883         }
1884 
1885         tileset.emplace_back(idx, ymax);
1886     }
1887 
1888     return true;
1889 }
1890 #endif
1891 
is_selectable(int item) const1892 bool Menu::is_selectable(int item) const
1893 {
1894     if (select_filter.empty())
1895         return true;
1896 
1897     string text = items[item]->get_filter_text();
1898     for (const text_pattern &pat : select_filter)
1899         if (pat.matches(text))
1900             return true;
1901 
1902     return false;
1903 }
1904 
select_item_index(int idx,int qty,bool draw_cursor)1905 void Menu::select_item_index(int idx, int qty, bool draw_cursor)
1906 {
1907     const int old_cursor = get_cursor();
1908 
1909     last_selected = idx;
1910     items[idx]->select(qty);
1911     m_ui.menu->update_item(idx);
1912 #ifdef USE_TILE_WEB
1913     webtiles_update_item(idx);
1914 #endif
1915 
1916     if (draw_cursor)
1917     {
1918         int it_count = items.size();
1919 
1920         const int new_cursor = get_cursor();
1921         if (old_cursor != -1 && old_cursor < it_count
1922             && items[old_cursor]->level == MEL_ITEM)
1923         {
1924             m_ui.menu->update_item(old_cursor);
1925         }
1926         if (new_cursor != -1 && new_cursor < it_count
1927             && items[new_cursor]->level == MEL_ITEM)
1928         {
1929             m_ui.menu->update_item(new_cursor);
1930         }
1931     }
1932 }
1933 
select_index(int index,int qty)1934 void Menu::select_index(int index, int qty)
1935 {
1936     int first_vis = get_first_visible();
1937 
1938     int si = index == -1 ? first_vis : index;
1939 
1940     if (index == -1)
1941     {
1942         if (flags & MF_MULTISELECT)
1943         {
1944             for (int i = 0, count = items.size(); i < count; ++i)
1945             {
1946                 if (items[i]->level != MEL_ITEM
1947                     || items[i]->hotkeys.empty())
1948                 {
1949                     continue;
1950                 }
1951                 if (is_hotkey(i, items[i]->hotkeys[0])
1952                     && (qty != -2 || is_selectable(i)))
1953                 {
1954                     select_item_index(i, qty);
1955                 }
1956             }
1957         }
1958     }
1959     else if (items[si]->level == MEL_SUBTITLE && (flags & MF_MULTISELECT))
1960     {
1961         for (int i = si + 1, count = items.size(); i < count; ++i)
1962         {
1963             if (items[i]->level != MEL_ITEM
1964                 || items[i]->hotkeys.empty())
1965             {
1966                 continue;
1967             }
1968             if (is_hotkey(i, items[i]->hotkeys[0]))
1969                 select_item_index(i, qty);
1970         }
1971     }
1972     else if (items[si]->level == MEL_ITEM
1973              && (flags & (MF_SINGLESELECT | MF_MULTISELECT)))
1974     {
1975         select_item_index(si, qty, (flags & MF_MULTISELECT));
1976     }
1977 }
1978 
get_entry_index(const MenuEntry * e) const1979 int Menu::get_entry_index(const MenuEntry *e) const
1980 {
1981     int index = 0;
1982     for (const auto &item : items)
1983     {
1984         if (item == e)
1985             return index;
1986 
1987         if (item->quantity != 0)
1988             index++;
1989     }
1990 
1991     return -1;
1992 }
1993 
update_menu(bool update_entries)1994 void Menu::update_menu(bool update_entries)
1995 {
1996     m_ui.menu->update_items();
1997     update_title();
1998     if (last_hovered >= 0)
1999         set_hovered(last_hovered); // sanitize in case items have changed
2000 
2001     if (!alive)
2002         return;
2003 #ifdef USE_TILE_WEB
2004     if (update_entries)
2005     {
2006         tiles.json_open_object();
2007         tiles.json_write_string("msg", "update_menu");
2008         tiles.json_write_int("total_items", items.size());
2009         tiles.json_write_int("last_hovered", last_hovered);
2010         tiles.json_close_object();
2011         tiles.finish_message();
2012         if (items.size() > 0)
2013             webtiles_update_items(0, items.size() - 1);
2014     }
2015 #else
2016     UNUSED(update_entries);
2017 #endif
2018 }
2019 
update_more()2020 void Menu::update_more()
2021 {
2022     if (crawl_state.doing_prev_cmd_again)
2023         return;
2024     formatted_string shown_more = more;
2025 #ifndef USE_TILE_LOCAL
2026     // hacky way of enforcing a min width for non-local-tiles when the more
2027     // is visible. (Really targeted at webtiles.)
2028     const int padding = m_ui.menu->get_min_col_width()
2029                                     - static_cast<int>(more.tostring().size());
2030     if (padding > 0)
2031         shown_more += string(padding, ' ');
2032 #endif
2033     m_ui.more->set_text(shown_more);
2034 
2035     // XX could force webtiles more when it is padded?
2036     bool show_more = !more.ops.empty();
2037 #ifdef USE_TILE_LOCAL
2038     show_more = show_more && !m_keyhelp_more;
2039 #endif
2040     m_ui.more->set_visible(show_more);
2041 
2042 #ifdef USE_TILE_WEB
2043     if (!alive)
2044         return;
2045     tiles.json_open_object();
2046     tiles.json_write_string("msg", "update_menu");
2047     tiles.json_write_string("more",
2048             m_keyhelp_more ? "" : shown_more.to_colour_string());
2049     tiles.json_close_object();
2050     tiles.finish_message();
2051 #endif
2052 }
2053 
item_colour(const MenuEntry * entry) const2054 int Menu::item_colour(const MenuEntry *entry) const
2055 {
2056     int icol = -1;
2057     if (highlighter)
2058         icol = highlighter->entry_colour(entry);
2059 
2060     return icol == -1 ? entry->colour : icol;
2061 }
2062 
calc_title()2063 formatted_string Menu::calc_title() { return formatted_string(); }
2064 
update_title()2065 void Menu::update_title()
2066 {
2067     if (!title || crawl_state.doing_prev_cmd_again)
2068         return;
2069 
2070     formatted_string fs;
2071 
2072     if (m_filter)
2073     {
2074         fs = formatted_string::parse_string(
2075             m_filter->get_prompt().c_str())
2076             // apply formatting only to the prompt
2077             + " " + m_filter->get_text();
2078     }
2079     else
2080         fs = calc_title();
2081 
2082     if (fs.empty())
2083     {
2084         const bool first = (action_cycle == CYCLE_NONE
2085                             || menu_action == ACT_EXECUTE);
2086         if (!first)
2087             ASSERT(title2);
2088 
2089         auto col = item_colour(first ? title : title2);
2090         string text = (first ? title->get_text() : title2->get_text());
2091 
2092         fs.textcolour(col);
2093 
2094         if (flags & MF_ALLOW_FORMATTING)
2095             fs += formatted_string::parse_string(text);
2096         else
2097             fs.cprintf("%s", text.c_str());
2098     }
2099 
2100     if (!is_set(MF_QUIET_SELECT) && is_set(MF_MULTISELECT))
2101         fs.cprintf("%s", get_select_count_string(sel.size()).c_str());
2102 
2103     if (m_indent_title)
2104     {
2105         formatted_string indented(" ");
2106         indented += fs;
2107         fs = indented;
2108     }
2109 
2110 #ifdef USE_TILE_LOCAL
2111     const bool tile_indent = m_indent_title && Options.tile_menu_icons;
2112     m_ui.title->set_margin_for_sdl(0, UIMenu::item_pad+UIMenu::pad_right, 10,
2113             UIMenu::item_pad + (tile_indent ? 38 : 0));
2114     m_ui.more->set_margin_for_sdl(10, UIMenu::item_pad+UIMenu::pad_right, 0, 0);
2115 #endif
2116     m_ui.title->set_text(fs);
2117 #ifdef USE_TILE_WEB
2118     webtiles_set_title(fs);
2119 #endif
2120 }
2121 
set_hovered(int index)2122 void Menu::set_hovered(int index)
2123 {
2124     // intentionally goes to -1 on size 0
2125     last_hovered = min(index, static_cast<int>(items.size()) - 1);
2126 #ifdef USE_TILE_LOCAL
2127     // don't crash if this gets called on local tiles before the menu has been
2128     // displayed. If your initial hover isn't showing up on local tiles, it
2129     // may be because of this -- adjust the timing so it is set after
2130     // update_menu is called.
2131     if (m_ui.menu->shown_items() == 0)
2132         return;
2133 #endif
2134 
2135     m_ui.menu->set_hovered_entry(last_hovered);
2136     if (last_hovered >= 0)
2137         snap_in_page(last_hovered);
2138 }
2139 
in_page(int index,bool strict) const2140 bool Menu::in_page(int index, bool strict) const
2141 {
2142     int y1, y2;
2143     m_ui.menu->get_item_region(index, &y1, &y2);
2144     int vph = m_ui.scroller->get_region().height;
2145     int vpy = m_ui.scroller->get_scroll();
2146     const bool upper_in = (vpy <= y1 && y1 <= vpy+vph);
2147     const bool lower_in = (vpy <= y2 && y2 <= vpy+vph);
2148     return strict ? (lower_in && upper_in) : (lower_in || upper_in);
2149 }
2150 
2151 /// Ensure that the item at index is visible in the scroller
snap_in_page(int index)2152 bool Menu::snap_in_page(int index)
2153 {
2154     if (index < 0 || index >= static_cast<int>(items.size()))
2155         return false;
2156     const int vph = m_ui.scroller->get_region().height;
2157     if (vph == 0) // ui not yet set up
2158         return false;
2159 
2160     int y1, y2;
2161     m_ui.menu->get_item_region(index, &y1, &y2);
2162     // special case: the immediately preceding item is a header of some kind.
2163     // could be generalized a bit, if sequences of headers come up? Most
2164     // important when the first item in a menu is a header
2165     if (index >= 1 && items[index - 1]->level != MEL_ITEM)
2166         m_ui.menu->get_item_region(index - 1, &y1, nullptr);
2167     const int vpy = m_ui.scroller->get_scroll();
2168 
2169     if (y2 >= vpy + vph)
2170         m_ui.scroller->set_scroll(y2 - vph
2171 #ifdef USE_TILE_LOCAL
2172             + UI_SCROLLER_SHADE_SIZE
2173 #endif
2174             );
2175     else if (y1 < vpy)
2176         m_ui.scroller->set_scroll(y1
2177 #ifdef USE_TILE_LOCAL
2178             - UI_SCROLLER_SHADE_SIZE
2179 #endif
2180             );
2181     else
2182         return false; // already in page
2183     return true;
2184 }
2185 
page_down()2186 bool Menu::page_down()
2187 {
2188     int new_hover = -1;
2189     if (is_set(MF_ARROWS_SELECT) && last_hovered < 0)
2190         last_hovered = 0;
2191     // preserve relative position
2192     if (last_hovered >= 0 && in_page(last_hovered))
2193         new_hover = last_hovered - get_first_visible();
2194     int dy = m_ui.scroller->get_region().height;
2195     int y = m_ui.scroller->get_scroll();
2196     bool at_bottom = y+dy >= m_ui.menu->get_region().height;
2197     // don't scroll further if the last item is already visible
2198     // (TODO: I don't understand why this check is necessary, but without it,
2199     // you sometimes unpredictably end up with the last element on its own page)
2200     if (!in_page(static_cast<int>(items.size()) - 1, true))
2201         m_ui.scroller->set_scroll(y+dy);
2202 
2203     if (new_hover >= 0)
2204     {
2205         // if pgdn wouldn't change the hover, move it to the last element
2206         if (is_set(MF_ARROWS_SELECT) && get_first_visible() + new_hover == last_hovered)
2207             set_hovered(items.size() - 1);
2208         else
2209             set_hovered(get_first_visible() + new_hover);
2210         if (items[last_hovered]->level != MEL_ITEM)
2211             cycle_hover(true); // reverse so we don't overshoot
2212     }
2213 
2214 #ifndef USE_TILE_LOCAL
2215     if (!at_bottom)
2216         m_ui.menu->set_showable_height(y+dy+dy);
2217 #endif
2218     return !at_bottom;
2219 }
2220 
page_up()2221 bool Menu::page_up()
2222 {
2223     int new_hover = -1;
2224     if (is_set(MF_ARROWS_SELECT) && last_hovered < 0)
2225         last_hovered = 0;
2226     if (last_hovered >= 0 && in_page(last_hovered))
2227         new_hover = last_hovered - get_first_visible();
2228     int dy = m_ui.scroller->get_region().height;
2229     int y = m_ui.scroller->get_scroll();
2230     m_ui.scroller->set_scroll(y-dy);
2231     if (new_hover >= 0)
2232     {
2233         // if pgup wouldn't change the hover, select the first element
2234         if (is_set(MF_ARROWS_SELECT) && get_first_visible() + new_hover == last_hovered)
2235             new_hover = 0;
2236         set_hovered(get_first_visible() + new_hover);
2237         if (items[last_hovered]->level != MEL_ITEM)
2238             cycle_hover(); // forward so we don't overshoot
2239     }
2240 
2241 #ifndef USE_TILE_LOCAL
2242     m_ui.menu->set_showable_height(y);
2243 #endif
2244     return y > 0;
2245 }
2246 
line_down()2247 bool Menu::line_down()
2248 {
2249     int index = get_first_visible();
2250     int first_vis_y;
2251     m_ui.menu->get_item_region(index, &first_vis_y, nullptr);
2252 
2253     index++;
2254     while (index < (int)items.size())
2255     {
2256         int y;
2257         m_ui.menu->get_item_region(index, &y, nullptr);
2258         index++;
2259         if (y == first_vis_y)
2260             continue;
2261         m_ui.scroller->set_scroll(y);
2262         return true;
2263     }
2264     return false;
2265 }
2266 
cycle_hover(bool reverse)2267 void Menu::cycle_hover(bool reverse)
2268 {
2269     int items_tried = 0;
2270     const int max_items = is_set(MF_WRAP) ? items.size()
2271                         : reverse
2272                           ? last_hovered
2273                           : items.size() - last_hovered;
2274     int new_hover = last_hovered;
2275     if (reverse && last_hovered < 0)
2276         new_hover = 0;
2277     bool found = false;
2278     while (items_tried < max_items)
2279     {
2280         new_hover = new_hover + (reverse ? -1 : 1);
2281         items_tried++;
2282         // try to find a non-heading to hover over
2283         const int sz = static_cast<int>(items.size());
2284         if (is_set(MF_WRAP))
2285             new_hover = (new_hover + sz) % sz;
2286         new_hover = max(0, min(new_hover, sz - 1));
2287 
2288         if (items[new_hover]->level == MEL_ITEM)
2289         {
2290             found = true;
2291             break;
2292         }
2293     }
2294     if (!found)
2295         return;
2296 
2297     set_hovered(new_hover);
2298 #ifdef USE_TILE_WEB
2299     // TODO: not sure if this is enough
2300     webtiles_update_scroll_pos();
2301 #endif
2302 }
2303 
2304 // XXX: doesn't do exactly what we want
line_up()2305 bool Menu::line_up()
2306 {
2307     int index = get_first_visible();
2308     if (index > 0)
2309     {
2310         int y;
2311         m_ui.menu->get_item_region(index-1, &y, nullptr);
2312         m_ui.scroller->set_scroll(y);
2313 #ifndef USE_TILE_LOCAL
2314         int dy = m_ui.scroller->get_region().height;
2315         m_ui.menu->set_showable_height(y+dy);
2316 #endif
2317         return true;
2318     }
2319     return false;
2320 }
2321 
2322 #ifdef USE_TILE_WEB
webtiles_write_menu(bool replace) const2323 void Menu::webtiles_write_menu(bool replace) const
2324 {
2325     if (crawl_state.doing_prev_cmd_again)
2326         return;
2327 
2328     tiles.json_open_object();
2329     tiles.json_write_string("msg", "menu");
2330     tiles.json_write_bool("ui-centred", !crawl_state.need_save);
2331     tiles.json_write_string("tag", tag);
2332     tiles.json_write_int("flags", flags);
2333     tiles.json_write_int("last_hovered", last_hovered);
2334     if (replace)
2335         tiles.json_write_int("replace", 1);
2336 
2337     webtiles_write_title();
2338 
2339     tiles.json_write_string("more",
2340             m_keyhelp_more ? "" : more.to_colour_string());
2341 
2342     int count = items.size();
2343     int start = 0;
2344     int end = start + count;
2345 
2346     tiles.json_write_int("total_items", count);
2347     tiles.json_write_int("chunk_start", start);
2348 
2349     int first_entry = get_first_visible();
2350     if (first_entry != 0 && !is_set(MF_START_AT_END))
2351         tiles.json_write_int("jump_to", first_entry);
2352 
2353     tiles.json_open_array("items");
2354 
2355     for (int i = start; i < end; ++i)
2356         webtiles_write_item(items[i]);
2357 
2358     tiles.json_close_array();
2359 
2360     tiles.json_close_object();
2361 }
2362 
webtiles_scroll(int first,int hover)2363 void Menu::webtiles_scroll(int first, int hover)
2364 {
2365     // catch and ignore stale scroll events
2366     if (first >= static_cast<int>(items.size()))
2367         return;
2368 
2369     int item_y;
2370     m_ui.menu->get_item_region(first, &item_y, nullptr);
2371     if (m_ui.scroller->get_scroll() != item_y)
2372     {
2373         m_ui.scroller->set_scroll(item_y);
2374         set_hovered(hover);
2375         // TODO: can the snap in set_hovered ever do anything weird in this call?
2376         webtiles_update_scroll_pos();
2377         ui::force_render();
2378     }
2379 }
2380 
webtiles_handle_item_request(int start,int end)2381 void Menu::webtiles_handle_item_request(int start, int end)
2382 {
2383     start = min(max(0, start), (int)items.size()-1);
2384     if (end < start) end = start;
2385     if (end >= (int)items.size())
2386         end = (int)items.size() - 1;
2387 
2388     tiles.json_open_object();
2389     tiles.json_write_string("msg", "update_menu_items");
2390 
2391     tiles.json_write_int("chunk_start", start);
2392 
2393     tiles.json_open_array("items");
2394 
2395     for (int i = start; i <= end; ++i)
2396         webtiles_write_item(items[i]);
2397 
2398     tiles.json_close_array();
2399 
2400     tiles.json_close_object();
2401     tiles.finish_message();
2402 }
2403 
webtiles_set_title(const formatted_string title_)2404 void Menu::webtiles_set_title(const formatted_string title_)
2405 {
2406     if (title_.to_colour_string() != _webtiles_title.to_colour_string())
2407     {
2408         _webtiles_title_changed = true;
2409         _webtiles_title = title_;
2410     }
2411 }
2412 
webtiles_update_items(int start,int end) const2413 void Menu::webtiles_update_items(int start, int end) const
2414 {
2415     ASSERT_RANGE(start, 0, (int) items.size());
2416     ASSERT_RANGE(end, start, (int) items.size());
2417 
2418     tiles.json_open_object();
2419 
2420     tiles.json_write_string("msg", "update_menu_items");
2421     tiles.json_write_int("chunk_start", start);
2422 
2423     tiles.json_open_array("items");
2424 
2425     for (int i = start; i <= end; ++i)
2426     {
2427         // TODO: why is this different from Menu::webtiles_write_item?
2428         tiles.json_open_object();
2429         const MenuEntry* me = items[i];
2430         tiles.json_write_string("text", me->get_text());
2431         int col = item_colour(me);
2432         // previous colour field is deleted by client if new one not sent
2433         if (col != MENU_ITEM_STOCK_COLOUR)
2434             tiles.json_write_int("colour", col);
2435         webtiles_write_tiles(*me);
2436         if (!me->hotkeys.empty())
2437         {
2438             tiles.json_open_array("hotkeys");
2439             for (int hotkey : me->hotkeys)
2440                 tiles.json_write_int(hotkey);
2441             tiles.json_close_array();
2442         }
2443 
2444         tiles.json_close_object();
2445     }
2446 
2447     tiles.json_close_array();
2448 
2449     tiles.json_close_object();
2450     tiles.finish_message();
2451 }
2452 
2453 
webtiles_update_item(int index) const2454 void Menu::webtiles_update_item(int index) const
2455 {
2456     webtiles_update_items(index, index);
2457 }
2458 
webtiles_update_title() const2459 void Menu::webtiles_update_title() const
2460 {
2461     tiles.json_open_object();
2462     tiles.json_write_string("msg", "update_menu");
2463     webtiles_write_title();
2464     tiles.json_close_object();
2465     tiles.finish_message();
2466 }
2467 
webtiles_update_scroll_pos() const2468 void Menu::webtiles_update_scroll_pos() const
2469 {
2470     tiles.json_open_object();
2471     tiles.json_write_string("msg", "menu_scroll");
2472     tiles.json_write_int("first", get_first_visible());
2473     tiles.json_write_int("last_hovered", last_hovered);
2474     tiles.json_close_object();
2475     tiles.finish_message();
2476 }
2477 
webtiles_write_title() const2478 void Menu::webtiles_write_title() const
2479 {
2480     // the title object only exists for backwards compatibility
2481     tiles.json_open_object("title");
2482     tiles.json_write_string("text", _webtiles_title.to_colour_string());
2483     tiles.json_close_object("title");
2484 }
2485 
webtiles_write_tiles(const MenuEntry & me) const2486 void Menu::webtiles_write_tiles(const MenuEntry& me) const
2487 {
2488     vector<tile_def> t;
2489     if (me.get_tiles(t) && !t.empty())
2490     {
2491         tiles.json_open_array("tiles");
2492 
2493         for (const tile_def &tile : t)
2494         {
2495             tiles.json_open_object();
2496 
2497             tiles.json_write_int("t", tile.tile);
2498             tiles.json_write_int("tex", get_tile_texture(tile.tile));
2499 
2500             if (tile.ymax != TILE_Y)
2501                 tiles.json_write_int("ymax", tile.ymax);
2502 
2503             tiles.json_close_object();
2504         }
2505 
2506         tiles.json_close_array();
2507     }
2508 }
2509 
webtiles_write_item(const MenuEntry * me) const2510 void Menu::webtiles_write_item(const MenuEntry* me) const
2511 {
2512     tiles.json_open_object();
2513 
2514     if (me)
2515         tiles.json_write_string("text", me->get_text());
2516     else
2517     {
2518         tiles.json_write_string("text", "");
2519         tiles.json_close_object();
2520         return;
2521     }
2522 
2523     if (me->quantity)
2524         tiles.json_write_int("q", me->quantity);
2525 
2526     int col = item_colour(me);
2527     if (col != MENU_ITEM_STOCK_COLOUR)
2528         tiles.json_write_int("colour", col);
2529 
2530     if (!me->hotkeys.empty())
2531     {
2532         tiles.json_open_array("hotkeys");
2533         for (int hotkey : me->hotkeys)
2534             tiles.json_write_int(hotkey);
2535         tiles.json_close_array();
2536     }
2537 
2538     if (me->level != MEL_NONE)
2539         tiles.json_write_int("level", me->level);
2540 
2541     if (me->preselected)
2542         tiles.json_write_int("preselected", me->preselected);
2543 
2544     webtiles_write_tiles(*me);
2545 
2546     tiles.json_close_object();
2547 }
2548 #endif // USE_TILE_WEB
2549 
2550 /////////////////////////////////////////////////////////////////
2551 // Menu colouring
2552 //
2553 
menu_colour(const string & text,const string & prefix,const string & tag)2554 int menu_colour(const string &text, const string &prefix, const string &tag)
2555 {
2556     const string tmp_text = prefix + text;
2557 
2558     for (const colour_mapping &cm : Options.menu_colour_mappings)
2559     {
2560         if ((cm.tag.empty() || cm.tag == "any" || cm.tag == tag
2561                || cm.tag == "inventory" && tag == "pickup")
2562             && cm.pattern.matches(tmp_text))
2563         {
2564             return cm.colour;
2565         }
2566     }
2567     return -1;
2568 }
2569 
entry_colour(const MenuEntry * entry) const2570 int MenuHighlighter::entry_colour(const MenuEntry *entry) const
2571 {
2572     return entry->colour != MENU_ITEM_STOCK_COLOUR ? entry->colour
2573            : entry->highlight_colour();
2574 }
2575 
2576 ///////////////////////////////////////////////////////////////////////
2577 // column_composer
2578 
column_composer(int cols,...)2579 column_composer::column_composer(int cols, ...)
2580     : columns()
2581 {
2582     ASSERT(cols > 0);
2583 
2584     va_list args;
2585     va_start(args, cols);
2586 
2587     columns.emplace_back(1);
2588     int lastcol = 1;
2589     for (int i = 1; i < cols; ++i)
2590     {
2591         int nextcol = va_arg(args, int);
2592         ASSERT(nextcol > lastcol);
2593 
2594         lastcol = nextcol;
2595         columns.emplace_back(nextcol);
2596     }
2597 
2598     va_end(args);
2599 }
2600 
clear()2601 void column_composer::clear()
2602 {
2603     flines.clear();
2604 }
2605 
add_formatted(int ncol,const string & s,bool add_separator,int margin)2606 void column_composer::add_formatted(int ncol,
2607                                     const string &s,
2608                                     bool add_separator,
2609                                     int  margin)
2610 {
2611     ASSERT_RANGE(ncol, 0, (int) columns.size());
2612 
2613     column &col = columns[ncol];
2614     vector<string> segs = split_string("\n", s, false, true);
2615 
2616     vector<formatted_string> newlines;
2617     // Add a blank line if necessary. Blank lines will not
2618     // be added at page boundaries.
2619     if (add_separator && col.lines && !segs.empty())
2620         newlines.emplace_back();
2621 
2622     for (const string &seg : segs)
2623         newlines.push_back(formatted_string::parse_string(seg));
2624 
2625     strip_blank_lines(newlines);
2626 
2627     compose_formatted_column(newlines,
2628                               col.lines,
2629                               margin == -1 ? col.margin : margin);
2630 
2631     col.lines += newlines.size();
2632 
2633     strip_blank_lines(flines);
2634 }
2635 
formatted_lines() const2636 vector<formatted_string> column_composer::formatted_lines() const
2637 {
2638     return flines;
2639 }
2640 
strip_blank_lines(vector<formatted_string> & fs) const2641 void column_composer::strip_blank_lines(vector<formatted_string> &fs) const
2642 {
2643     for (int i = fs.size() - 1; i >= 0; --i)
2644     {
2645         if (fs[i].width() == 0)
2646             fs.erase(fs.begin() + i);
2647         else
2648             break;
2649     }
2650 }
2651 
compose_formatted_column(const vector<formatted_string> & lines,int startline,int margin)2652 void column_composer::compose_formatted_column(
2653         const vector<formatted_string> &lines,
2654         int startline,
2655         int margin)
2656 {
2657     if (flines.size() < startline + lines.size())
2658         flines.resize(startline + lines.size());
2659 
2660     for (unsigned i = 0, size = lines.size(); i < size; ++i)
2661     {
2662         int f = i + startline;
2663         if (margin > 1)
2664         {
2665             int xdelta = margin - flines[f].width() - 1;
2666             if (xdelta > 0)
2667                 flines[f].cprintf("%-*s", xdelta, "");
2668         }
2669         flines[f] += lines[i];
2670     }
2671 }
2672 
linebreak_string(string & s,int maxcol,bool indent)2673 int linebreak_string(string& s, int maxcol, bool indent)
2674 {
2675     // [ds] Don't loop forever if the user is playing silly games with
2676     // their term size.
2677     if (maxcol < 1)
2678         return 0;
2679 
2680     int breakcount = 0;
2681     string res;
2682 
2683     while (!s.empty())
2684     {
2685         res += wordwrap_line(s, maxcol, true, indent);
2686         if (!s.empty())
2687         {
2688             res += "\n";
2689             breakcount++;
2690         }
2691     }
2692     s = res;
2693     return breakcount;
2694 }
2695 
get_linebreak_string(const string & s,int maxcol)2696 string get_linebreak_string(const string& s, int maxcol)
2697 {
2698     string r = s;
2699     linebreak_string(r, maxcol);
2700     return r;
2701 }
2702 
pre_process(int key)2703 int ToggleableMenu::pre_process(int key)
2704 {
2705 #ifdef TOUCH_UI
2706     if (find(toggle_keys.begin(), toggle_keys.end(), key) != toggle_keys.end()
2707         || key == CK_TOUCH_DUMMY)
2708 #else
2709     if (find(toggle_keys.begin(), toggle_keys.end(), key) != toggle_keys.end())
2710 #endif
2711     {
2712         // Toggle all menu entries
2713         for (MenuEntry *item : items)
2714             if (auto p = dynamic_cast<ToggleableMenuEntry*>(item))
2715                 p->toggle();
2716 
2717         // Toggle title
2718         if (auto pt = dynamic_cast<ToggleableMenuEntry*>(title))
2719             pt->toggle();
2720 
2721         update_menu();
2722 
2723 #ifdef USE_TILE_WEB
2724         webtiles_update_items(0, items.size() - 1);
2725 #endif
2726 
2727         if (flags & MF_TOGGLE_ACTION)
2728         {
2729             if (menu_action == ACT_EXECUTE)
2730                 menu_action = ACT_EXAMINE;
2731             else
2732                 menu_action = ACT_EXECUTE;
2733         }
2734 
2735         // Don't further process the key
2736 #ifdef TOUCH_UI
2737         return CK_TOUCH_DUMMY;
2738 #else
2739         return 0;
2740 #endif
2741     }
2742     return key;
2743 }
2744