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