1 /**
2  * @file
3  * @brief Menus and associated malarkey.
4 **/
5 
6 #pragma once
7 
8 #include <algorithm>
9 #include <cstdio>
10 #include <ctime>
11 #include <chrono>
12 #include <functional>
13 #include <string>
14 #include <vector>
15 
16 #include "tiles-build-specific.h"
17 #include "cio.h"
18 #include "format.h"
19 #ifdef USE_TILE
20  #include "tiledoll.h"
21 #endif
22 #ifdef USE_TILE_LOCAL
23  #include "tilebuf.h"
24 #endif
25 #include "ui.h"
26 
27 class formatted_string;
28 
29 enum MenuEntryLevel
30 {
31     MEL_NONE = -1,
32     MEL_TITLE,
33     MEL_SUBTITLE,
34     MEL_ITEM,
35 };
36 
37 struct menu_letter
38 {
39     char letter;
40 
menu_lettermenu_letter41     menu_letter() : letter('a') { }
menu_lettermenu_letter42     menu_letter(char c) : letter(c) { }
43 
44     operator char () const { return letter; }
45 
46     const menu_letter &operator ++ ()
47     {
48         letter = letter == 'z'? 'A' :
49                  letter == 'Z'? 'a' :
50                                 letter + 1;
51         return *this;
52     }
53 
54     // dummy postfix argument unnamed to stop gcc from complaining
55     menu_letter operator ++ (int)
56     {
57         menu_letter copy = *this;
58         operator++();
59         return copy;
60     }
61 };
62 
63 // XXX Use inheritance instead of duplicate code
64 struct menu_letter2
65 {
66     char letter;
67 
menu_letter2menu_letter268     menu_letter2() : letter('a') { }
menu_letter2menu_letter269     menu_letter2(char c) : letter(c) { }
70 
71     operator char () const { return letter; }
72     const menu_letter2 &operator ++ ()
73     {
74         letter = letter == 'z'? '0' :
75                  letter == '9'? 'a' :
76                                 letter + 1;
77         return *this;
78     }
79 
80     menu_letter2 operator ++ (int)
81     {
82         menu_letter2 copy = *this;
83         operator++();
84         return copy;
85     }
86 };
87 
88 struct item_def;
89 class Menu;
90 
91 int menu_colour(const string &itemtext,
92                 const string &prefix = "",
93                 const string &tag = "");
94 
95 const int MENU_ITEM_STOCK_COLOUR = LIGHTGREY;
96 class MenuEntry
97 {
98 public:
99     string tag;
100     string text;
101     int quantity, selected_qty;
102     colour_t colour;
103     vector<int> hotkeys;
104     MenuEntryLevel level;
105     bool preselected;
106     bool indent_no_hotkeys;
107     void *data;
108 
109 #ifdef USE_TILE
110     vector<tile_def> tiles;
111 #endif
112 
113 public:
114     MenuEntry(const string &txt = string(),
115                MenuEntryLevel lev = MEL_ITEM,
116                int qty  = 0,
117                int hotk = 0,
118                bool preselect = false) :
text(txt)119         text(txt), quantity(qty), selected_qty(0), colour(-1),
120         hotkeys(), level(lev), preselected(preselect),
121         indent_no_hotkeys(false),
122         data(nullptr)
123     {
124         colour = (lev == MEL_ITEM     ?  MENU_ITEM_STOCK_COLOUR :
125                   lev == MEL_SUBTITLE ?  BLUE  :
126                                          WHITE);
127         if (hotk)
128             hotkeys.push_back(hotk);
129     }
~MenuEntry()130     virtual ~MenuEntry() { }
131 
132     bool operator<(const MenuEntry& rhs) const
133     {
134         return text < rhs.text;
135     }
136 
add_hotkey(int key)137     void add_hotkey(int key)
138     {
139         if (key && !is_hotkey(key))
140             hotkeys.push_back(key);
141     }
142 
is_hotkey(int key)143     bool is_hotkey(int key) const
144     {
145         return find(hotkeys.begin(), hotkeys.end(), key) != hotkeys.end();
146     }
147 
is_primary_hotkey(int key)148     bool is_primary_hotkey(int key) const
149     {
150         return hotkeys.size() && hotkeys[0] == key;
151     }
152 
153     virtual string get_text(const bool unused = false) const;
154 
highlight_colour()155     virtual int highlight_colour() const
156     {
157         return menu_colour(get_text(), "", tag);
158     }
159 
selected()160     virtual bool selected() const
161     {
162         return selected_qty > 0 && quantity;
163     }
164 
165     // -1: Invert
166     // -2: Select all
167     virtual void select(int qty = -1)
168     {
169         if (qty == -2)
170             selected_qty = quantity;
171         else if (selected())
172             selected_qty = 0;
173         else if (quantity)
174             selected_qty = (qty == -1 ? quantity : qty);
175     }
176 
get_filter_text()177     virtual string get_filter_text() const
178     {
179         return get_text();
180     }
181 
182     virtual bool get_tiles(vector<tile_def>& tileset) const;
183 
184     virtual void add_tile(tile_def tile);
185 };
186 
187 class ToggleableMenuEntry : public MenuEntry
188 {
189 public:
190     string alt_text;
191 
192     ToggleableMenuEntry(const string &txt = string(),
193                         const string &alt_txt = string(),
194                         MenuEntryLevel lev = MEL_ITEM,
195                         int qty = 0, int hotk = 0,
196                         bool preselect = false) :
MenuEntry(txt,lev,qty,hotk,preselect)197         MenuEntry(txt, lev, qty, hotk, preselect), alt_text(alt_txt) {}
198 
toggle()199     void toggle() { text.swap(alt_text); }
200 };
201 
202 class MonsterMenuEntry : public MenuEntry
203 {
204 public:
205     MonsterMenuEntry(const string &str, const monster_info* mon, int hotkey);
206 
207 #ifdef USE_TILE
208     virtual bool get_tiles(vector<tile_def>& tileset) const override;
209 #endif
210 };
211 
212 #ifdef USE_TILE
213 class PlayerMenuEntry : public MenuEntry
214 {
215 public:
216     PlayerMenuEntry(const string &str);
217 
218     virtual bool get_tiles(vector<tile_def>& tileset) const override;
219 };
220 #endif
221 
222 class FeatureMenuEntry : public MenuEntry
223 {
224 public:
225     coord_def            pos;
226     dungeon_feature_type feat;
227 
228     FeatureMenuEntry(const string &str, const coord_def p, int hotkey);
229     FeatureMenuEntry(const string &str, const dungeon_feature_type f,
230                      int hotkey);
231 
232 #ifdef USE_TILE
233     virtual bool get_tiles(vector<tile_def>& tileset) const override;
234 #endif
235 };
236 
237 class MenuHighlighter
238 {
239 public:
240     virtual int entry_colour(const MenuEntry *entry) const;
~MenuHighlighter()241     virtual ~MenuHighlighter() { }
242 };
243 
244 // if you update this, update mf in enums.js
245 enum MenuFlag
246 {
247     MF_NOSELECT         = 0x00001,   ///< No selection is permitted
248     MF_SINGLESELECT     = 0x00002,   ///< Select just one item
249     MF_MULTISELECT      = 0x00004,   ///< Select multiple items
250     MF_NO_SELECT_QTY    = 0x00008,   ///< Disallow partial selections
251     MF_ANYPRINTABLE     = 0x00010,   ///< Any printable character is valid, and
252                                      ///< closes the menu.
253     MF_SELECT_BY_PAGE   = 0x00020,   ///< Allow selections to occur only on
254                                      ///< currently-visible page.
255     MF_ALWAYS_SHOW_MORE = 0x00040,   ///< Always show the -more- footer
256     MF_WRAP             = 0x00080,   ///< Paging past the end will wrap back.
257     MF_ALLOW_FILTER     = 0x00100,   ///< Control-F will ask for regex and
258                                      ///< select the appropriate items.
259     MF_ALLOW_FORMATTING = 0x00200,   ///< Parse index for formatted-string
260     MF_TOGGLE_ACTION    = 0x00400,   ///< ToggleableMenu toggles action as well
261     MF_NO_WRAP_ROWS     = 0x00800,   ///< For menus used as tables (eg. ability)
262     MF_START_AT_END     = 0x01000,   ///< Scroll to end of list
263     MF_PRESELECTED      = 0x02000,   ///< Has a preselected entry.
264     MF_QUIET_SELECT     = 0x04000,   ///< No selection box and no count.
265 
266     MF_USE_TWO_COLUMNS  = 0x08000,   ///< Only valid for tiles menus
267     MF_UNCANCEL         = 0x10000,   ///< Menu is uncancellable
268     MF_SPECIAL_MINUS    = 0x20000,   ///< '-' isn't PGUP or clear multiselect
269     MF_ARROWS_SELECT    = 0x40000,   ///< arrow keys select, rather than scroll
270 };
271 
272 class UIMenu;
273 class UIMenuPopup;
274 class UIShowHide;
275 class UIMenuMore;
276 
277 ///////////////////////////////////////////////////////////////////////
278 // NOTE
279 // As a general contract, any pointers you pass to Menu methods are OWNED BY
280 // THE MENU, and will be deleted by the Menu on destruction. So any pointers
281 // you pass in MUST be allocated with new, or Crawl will crash.
282 
283 #define NUMBUFSIZ 10
284 
285 class Menu
286 {
287     friend class UIMenu;
288     friend class UIMenuPopup;
289 public:
290     Menu(int flags = MF_MULTISELECT, const string& tagname = "", KeymapContext kmc = KMC_MENU);
291 
292     virtual ~Menu();
293 
294     // Remove all items from the Menu, leave title intact.
295     void clear();
296 
297     virtual void set_flags(int new_flags);
get_flags()298     int  get_flags() const        { return flags; }
299     virtual bool is_set(int flag) const;
set_tag(const string & t)300     void set_tag(const string& t) { tag = t; }
301 
302     bool minus_is_pageup() const;
303     // Sets a replacement for the default -more- string.
304     void set_more(const formatted_string &more);
305     void set_more(const string s);
306     // Shows a stock message about scrolling the menu instead of -more-
307     void set_more();
get_more()308     const formatted_string &get_more() const { return more; }
309     void set_min_col_width(int w);
310 
311     void set_highlighter(MenuHighlighter *h);
312     void set_title(MenuEntry *e, bool first = true, bool indent = false);
313     void add_entry(MenuEntry *entry);
add_entry(unique_ptr<MenuEntry> entry)314     void add_entry(unique_ptr<MenuEntry> entry)
315     {
316         add_entry(entry.release());
317     }
318     void get_selected(vector<MenuEntry*> *sel) const;
319     virtual int get_cursor() const;
320 
set_select_filter(vector<text_pattern> filter)321     void set_select_filter(vector<text_pattern> filter)
322     {
323         select_filter = filter;
324     }
325 
326     void update_menu(bool update_entries = false);
327     void set_hovered(int index);
328 
getkey()329     virtual int getkey() const { return lastch; }
330 
331     void reset();
332     virtual vector<MenuEntry *> show(bool reuse_selections = false);
333     vector<MenuEntry *> selected_entries() const;
334 
item_count()335     size_t item_count() const    { return items.size(); }
336 
337     // Get entry index, skipping quantity 0 entries. Returns -1 if not found.
338     int get_entry_index(const MenuEntry *e) const;
339 
340     virtual int item_colour(const MenuEntry *me) const;
341 
342     typedef string (*selitem_tfn)(const vector<MenuEntry*> *sel);
343     typedef int (*keyfilter_tfn)(int keyin);
344 
345     selitem_tfn      f_selitem;
346     keyfilter_tfn    f_keyfilter;
347     function<bool(const MenuEntry&)> on_single_selection;
348     function<bool()> on_show;
349 
350     enum cycle  { CYCLE_NONE, CYCLE_TOGGLE, CYCLE_CYCLE } action_cycle;
351     enum action { ACT_EXECUTE, ACT_EXAMINE, ACT_MISC, ACT_NUM } menu_action;
352     void cycle_hover(bool reverse=false);
353 
354     bool title_prompt(char linebuf[], int bufsz, const char* prompt);
355 
356     virtual bool process_key(int keyin);
357 
358 #ifdef USE_TILE_WEB
359     void webtiles_write_menu(bool replace = false) const;
360     void webtiles_scroll(int first, int hover);
361     void webtiles_handle_item_request(int start, int end);
362 #endif
363 protected:
364     MenuEntry *title;
365     MenuEntry *title2;
366     bool m_indent_title;
367 
368     int flags;
369     string tag;
370 
371     int cur_page;
372     int num_pages;
373 
374     formatted_string more;
375     bool m_keyhelp_more;
376 
377     vector<MenuEntry*>  items;
378     vector<MenuEntry*>  sel;
379     vector<text_pattern> select_filter;
380 
381     // Class that is queried to colour menu entries.
382     MenuHighlighter *highlighter;
383 
384     int num;
385 
386     int lastch;
387 
388     bool alive;
389 
390     int last_selected;
391     int last_hovered;
392     KeymapContext m_kmc;
393 
394     resumable_line_reader *m_filter;
395 
396     struct {
397         shared_ptr<ui::Popup> popup;
398         shared_ptr<UIMenu> menu;
399         shared_ptr<ui::Scroller> scroller;
400         shared_ptr<ui::Text> title;
401         shared_ptr<UIMenuMore> more;
402         shared_ptr<UIShowHide> more_bin;
403         shared_ptr<ui::Box> vbox;
404     } m_ui;
405 
406     void check_add_formatted_line(int firstcol, int nextcol,
407                                   string &line, bool check_eol);
408     void do_menu();
409     virtual string get_select_count_string(int count) const;
410 
411 #ifdef USE_TILE_WEB
412     void webtiles_set_title(const formatted_string title);
413 
414     void webtiles_write_tiles(const MenuEntry& me) const;
415     void webtiles_update_items(int start, int end) const;
416     void webtiles_update_item(int index) const;
417     void webtiles_update_title() const;
418     void webtiles_update_scroll_pos() const;
419 
420     virtual void webtiles_write_title() const;
421     virtual void webtiles_write_item(const MenuEntry *me) const;
422 
423     bool _webtiles_title_changed;
424     formatted_string _webtiles_title;
425 #endif
426 
427     virtual formatted_string calc_title();
428     void update_more();
429     virtual bool page_down();
430     virtual bool line_down();
431     virtual bool page_up();
432     virtual bool line_up();
433 
434     virtual int pre_process(int key);
435     virtual int post_process(int key);
436 
437     bool in_page(int index, bool strict=false) const;
438     bool snap_in_page(int index);
439     int get_first_visible() const;
440 
441     void deselect_all(bool update_view = true);
442     virtual void select_items(int key, int qty = -1);
443     virtual void select_item_index(int idx, int qty, bool draw_cursor = true);
444     void select_index(int index, int qty = -1);
445 
446     bool is_hotkey(int index, int key);
447     virtual bool is_selectable(int index) const;
448 
help_key()449     virtual string help_key() const { return ""; }
450 
451     virtual void update_title();
452     bool filter_with_regex(const char *re);
453 };
454 
455 /// Allows toggling by specific keys.
456 class ToggleableMenu : public Menu
457 {
458 public:
459     ToggleableMenu(int _flags = MF_MULTISELECT)
Menu(_flags)460         : Menu(_flags) {}
add_toggle_key(int newkey)461     void add_toggle_key(int newkey) { toggle_keys.push_back(newkey); }
462 protected:
463     virtual int pre_process(int key) override;
464 
465     vector<int> toggle_keys;
466 };
467 
468 // This is only tangentially related to menus, but what the heck.
469 // Note, column margins start on 1, not 0.
470 class column_composer
471 {
472 public:
473     // Number of columns and left margins for 2nd, 3rd, ... nth column.
474     column_composer(int ncols, ...);
475 
476     void clear();
477     void add_formatted(int ncol,
478             const string &tagged_text,
479             bool  add_separator = true,
480             int   margin = -1);
481 
482     vector<formatted_string> formatted_lines() const;
483 
484 private:
485     struct column;
486     void compose_formatted_column(
487             const vector<formatted_string> &lines,
488             int start_col,
489             int margin);
490     void strip_blank_lines(vector<formatted_string> &) const;
491 
492 private:
493     struct column
494     {
495         int margin;
496         int lines;
497 
margincolumn498         column(int marg = 1) : margin(marg), lines(0) { }
499     };
500 
501     vector<column> columns;
502     vector<formatted_string> flines;
503 };
504 
505 int linebreak_string(string& s, int maxcol, bool indent = false);
506 string get_linebreak_string(const string& s, int maxcol);
507