1 #include "ui.h"
2 
3 #include <cctype>
4 #include <algorithm>
5 #include <climits>
6 #include <cstdlib>
7 #include <iterator>
8 #include <memory>
9 #include <set>
10 
11 #include "avatar.h"
12 #include "cached_options.h" // IWYU pragma: keep
13 #include "cata_assert.h"
14 #include "cata_utility.h"
15 #include "catacharset.h"
16 #include "game.h"
17 #include "input.h"
18 #include "memory_fast.h"
19 #include "output.h"
20 #include "sdltiles.h"
21 #include "string_input_popup.h"
22 #include "translations.h"
23 #include "ui_manager.h"
24 
25 #if defined(__ANDROID__)
26 #include <SDL_keyboard.h>
27 
28 #include "options.h"
29 #endif
30 
new_centered_win(int nlines,int ncols)31 catacurses::window new_centered_win( int nlines, int ncols )
32 {
33     int height = std::min( nlines, TERMY );
34     int width = std::min( ncols, TERMX );
35     point pos( ( TERMX - width ) / 2, ( TERMY - height ) / 2 );
36     return catacurses::newwin( height, width, pos );
37 }
38 
39 /**
40 * \defgroup UI "The UI Menu."
41 * @{
42 */
43 
hotkey_from_char(const int ch)44 static cata::optional<input_event> hotkey_from_char( const int ch )
45 {
46     if( ch == MENU_AUTOASSIGN ) {
47         return cata::nullopt;
48     } else if( ch <= 0 || ch == ' ' ) {
49         return input_event();
50     }
51     switch( input_manager::actual_keyboard_mode( keyboard_mode::keycode ) ) {
52         case keyboard_mode::keycode:
53             if( ch >= 'A' && ch <= 'Z' ) {
54                 return input_event( std::set<keymod_t>( { keymod_t::shift } ),
55                                     ch - 'A' + 'a', input_event_t::keyboard_code );
56             } else {
57                 return input_event( ch, input_event_t::keyboard_code );
58             }
59         case keyboard_mode::keychar:
60             return input_event( ch, input_event_t::keyboard_char );
61     }
62     return input_event();
63 }
64 
uilist_entry(const std::string & T)65 uilist_entry::uilist_entry( const std::string &T )
66     : retval( -1 ), enabled( true ), hotkey( cata::nullopt ), txt( T ),
67       text_color( c_red_red )
68 {
69 }
70 
uilist_entry(const std::string & T,const std::string & D)71 uilist_entry::uilist_entry( const std::string &T, const std::string &D )
72     : retval( -1 ), enabled( true ), hotkey( cata::nullopt ), txt( T ),
73       desc( D ), text_color( c_red_red )
74 {
75 }
76 
uilist_entry(const std::string & T,const int K)77 uilist_entry::uilist_entry( const std::string &T, const int K )
78     : retval( -1 ), enabled( true ), hotkey( hotkey_from_char( K ) ), txt( T ),
79       text_color( c_red_red )
80 {
81 }
82 
uilist_entry(const std::string & T,const cata::optional<input_event> & K)83 uilist_entry::uilist_entry( const std::string &T, const cata::optional<input_event> &K )
84     : retval( -1 ), enabled( true ), hotkey( K ), txt( T ),
85       text_color( c_red_red )
86 {
87 }
88 
uilist_entry(const int R,const bool E,const int K,const std::string & T)89 uilist_entry::uilist_entry( const int R, const bool E, const int K,
90                             const std::string &T )
91     : retval( R ), enabled( E ), hotkey( hotkey_from_char( K ) ), txt( T ),
92       text_color( c_red_red )
93 {
94 }
95 
uilist_entry(const int R,const bool E,const cata::optional<input_event> & K,const std::string & T)96 uilist_entry::uilist_entry( const int R, const bool E,
97                             const cata::optional<input_event> &K,
98                             const std::string &T )
99     : retval( R ), enabled( E ), hotkey( K ), txt( T ),
100       text_color( c_red_red )
101 {
102 }
103 
uilist_entry(const int R,const bool E,const int K,const std::string & T,const std::string & D)104 uilist_entry::uilist_entry( const int R, const bool E, const int K,
105                             const std::string &T, const std::string &D )
106     : retval( R ), enabled( E ), hotkey( hotkey_from_char( K ) ), txt( T ),
107       desc( D ), text_color( c_red_red )
108 {
109 }
110 
uilist_entry(const int R,const bool E,const int K,const std::string & T,const std::string & D,const std::string & C)111 uilist_entry::uilist_entry( const int R, const bool E, const int K,
112                             const std::string &T, const std::string &D,
113                             const std::string &C )
114     : retval( R ), enabled( E ), hotkey( hotkey_from_char( K ) ), txt( T ),
115       desc( D ), ctxt( C ), text_color( c_red_red )
116 {
117 }
118 
uilist_entry(const int R,const bool E,const cata::optional<input_event> & K,const std::string & T,const std::string & D,const std::string & C)119 uilist_entry::uilist_entry( const int R, const bool E,
120                             const cata::optional<input_event> &K,
121                             const std::string &T, const std::string &D,
122                             const std::string &C )
123     : retval( R ), enabled( E ), hotkey( K ), txt( T ),
124       desc( D ), ctxt( C ), text_color( c_red_red )
125 {
126 }
127 
uilist_entry(const int R,const bool E,const int K,const std::string & T,const nc_color & H,const nc_color & C)128 uilist_entry::uilist_entry( const int R, const bool E, const int K,
129                             const std::string &T,
130                             const nc_color &H, const nc_color &C )
131     : retval( R ), enabled( E ), hotkey( hotkey_from_char( K ) ), txt( T ),
132       hotkey_color( H ), text_color( C )
133 {
134 }
135 
operator =(auto_assign)136 uilist::size_scalar &uilist::size_scalar::operator=( auto_assign )
137 {
138     fun = nullptr;
139     return *this;
140 }
141 
operator =(const int val)142 uilist::size_scalar &uilist::size_scalar::operator=( const int val )
143 {
144     fun = [val]() -> int {
145         return val;
146     };
147     return *this;
148 }
149 
operator =(const std::function<int ()> & fun)150 uilist::size_scalar &uilist::size_scalar::operator=( const std::function<int()> &fun )
151 {
152     this->fun = fun;
153     return *this;
154 }
155 
operator =(auto_assign)156 uilist::pos_scalar &uilist::pos_scalar::operator=( auto_assign )
157 {
158     fun = nullptr;
159     return *this;
160 }
161 
operator =(const int val)162 uilist::pos_scalar &uilist::pos_scalar::operator=( const int val )
163 {
164     fun = [val]( int ) -> int {
165         return val;
166     };
167     return *this;
168 }
169 
operator =(const std::function<int (int)> & fun)170 uilist::pos_scalar &uilist::pos_scalar::operator=( const std::function<int( int )> &fun )
171 {
172     this->fun = fun;
173     return *this;
174 }
175 
uilist()176 uilist::uilist()
177 {
178     init();
179 }
180 
uilist(const std::string & msg,const std::vector<uilist_entry> & opts)181 uilist::uilist( const std::string &msg, const std::vector<uilist_entry> &opts )
182 {
183     init();
184     text = msg;
185     entries = opts;
186     query();
187 }
188 
uilist(const std::string & msg,const std::vector<std::string> & opts)189 uilist::uilist( const std::string &msg, const std::vector<std::string> &opts )
190 {
191     init();
192     text = msg;
193     for( const std::string &opt : opts ) {
194         entries.emplace_back( opt );
195     }
196     query();
197 }
198 
uilist(const std::string & msg,std::initializer_list<const char * const> opts)199 uilist::uilist( const std::string &msg, std::initializer_list<const char *const> opts )
200 {
201     init();
202     text = msg;
203     for( const char *const opt : opts ) {
204         entries.emplace_back( opt );
205     }
206     query();
207 }
208 
~uilist()209 uilist::~uilist()
210 {
211     shared_ptr_fast<ui_adaptor> current_ui = ui.lock();
212     if( current_ui ) {
213         current_ui->reset();
214     }
215 }
216 
color_error(const bool report)217 void uilist::color_error( const bool report )
218 {
219     if( report ) {
220         _color_error = report_color_error::yes;
221     } else {
222         _color_error = report_color_error::no;
223     }
224 }
225 
226 /*
227  * Enables oneshot construction -> running -> exit
228  */
operator int() const229 uilist::operator int() const
230 {
231     return ret;
232 }
233 
234 /**
235  * Sane defaults on initialization
236  */
init()237 void uilist::init()
238 {
239     cata_assert( !test_mode ); // uilist should not be used in tests where there's no place for it
240     w_x_setup = pos_scalar::auto_assign {};
241     w_y_setup = pos_scalar::auto_assign {};
242     w_width_setup = size_scalar::auto_assign {};
243     w_height_setup = size_scalar::auto_assign {};
244     w_x = 0;
245     w_y = 0;
246     w_width = 0;
247     w_height = 0;
248     ret = UILIST_WAIT_INPUT;
249     text.clear();          // header text, after (maybe) folding, populates:
250     textformatted.clear(); // folded to textwidth
251     textwidth = MENU_AUTOASSIGN; // if unset, folds according to w_width
252     title.clear();         // Makes use of the top border, no folding, sets min width if w_width is auto
253     ret_evt = input_event(); // last input event
254     window = catacurses::window();         // our window
255     keymap.clear();        // keymap[input_event] == index, for entries[index]
256     selected = 0;          // current highlight, for entries[index]
257     entries.clear();       // uilist_entry(int returnval, bool enabled, int keycode, std::string text, ... TODO: submenu stuff)
258     started = false;       // set to true when width and key calculations are done, and window is generated.
259     pad_left_setup = 0;
260     pad_right_setup = 0;
261     pad_left = 0;          // make a blank space to the left
262     pad_right = 0;         // or right
263     desc_enabled = false;  // don't show option description by default
264     desc_lines_hint = 6;   // default number of lines for description
265     desc_lines = 6;
266     footer_text.clear();   // takes precedence over per-entry descriptions.
267     border_color = c_magenta; // border color
268     text_color = c_light_gray;  // text color
269     title_color = c_green;  // title color
270     hilight_color = h_white; // highlight for up/down selection bar
271     hotkey_color = c_light_green; // hotkey text to the right of menu entry's text
272     disabled_color = c_dark_gray; // disabled menu entry
273     allow_disabled = false;  // disallow selecting disabled options
274     allow_anykey = false;    // do not return on unbound keys
275     allow_cancel = true;     // allow canceling with "QUIT" action
276     allow_additional = false; // do not return on unhandled additional actions
277     hilight_disabled =
278         false; // if false, hitting 'down' onto a disabled entry will advance downward to the first enabled entry
279     vshift = 0;              // scrolling menu offset
280     vmax = 0;                // max entries area rows
281     callback = nullptr;         // * uilist_callback
282     filter.clear();          // filter string. If "", show everything
283     fentries.clear();        // fentries is the actual display after filtering, and maps displayed entry number to actual entry number
284     fselected = 0;           // fentries[selected]
285     filtering = true;        // enable list display filtering via '/' or '.'
286     filtering_nocase = true; // ignore case when filtering
287     max_entry_len = 0;
288     max_column_len = 0;      // for calculating space for second column
289 
290     input_category = "UILIST";
291     additional_actions.clear();
292 }
293 
294 /**
295  * repopulate filtered entries list (fentries) and set fselected accordingly
296  */
filterlist()297 void uilist::filterlist()
298 {
299     bool notfiltering = ( !filtering || filter.empty() );
300     int num_entries = entries.size();
301     // TODO: && is_all_lc( filter )
302     bool nocase = filtering_nocase;
303     std::string fstr;
304     fstr.reserve( filter.size() );
305     if( nocase ) {
306         transform( filter.begin(), filter.end(), std::back_inserter( fstr ), tolower );
307     } else {
308         fstr = filter;
309     }
310     fentries.clear();
311     fselected = -1;
312     int f = 0;
313     for( int i = 0; i < num_entries; i++ ) {
314         if( notfiltering || ( !nocase && static_cast<int>( entries[i].txt.find( filter ) ) != -1 ) ||
315             lcmatch( entries[i].txt, fstr ) ) {
316             fentries.push_back( i );
317             if( i == selected && ( hilight_disabled || entries[i].enabled ) ) {
318                 fselected = f;
319             } else if( i > selected && fselected == -1 && ( hilight_disabled || entries[i].enabled ) ) {
320                 // Past the previously selected entry, which has been filtered out,
321                 // choose another nearby entry instead.
322                 fselected = f;
323             }
324             f++;
325         }
326     }
327     if( fselected == -1 ) {
328         fselected = 0;
329         vshift = 0;
330         if( fentries.empty() ) {
331             selected = -1;
332         } else {
333             selected = fentries [ 0 ];
334         }
335     } else if( fselected < static_cast<int>( fentries.size() ) ) {
336         selected = fentries[fselected];
337     } else {
338         fselected = selected = -1;
339     }
340     // scroll to top of screen if all remaining entries fit the screen.
341     if( static_cast<int>( fentries.size() ) <= vmax ) {
342         vshift = 0;
343     }
344     if( callback != nullptr ) {
345         callback->select( this );
346     }
347 }
348 
inputfilter()349 void uilist::inputfilter()
350 {
351     input_context ctxt( input_category, keyboard_mode::keychar );
352     ctxt.register_updown();
353     ctxt.register_action( "PAGE_UP", to_translation( "Fast scroll up" ) );
354     ctxt.register_action( "PAGE_DOWN", to_translation( "Fast scroll down" ) );
355     ctxt.register_action( "SCROLL_UP" );
356     ctxt.register_action( "SCROLL_DOWN" );
357     ctxt.register_action( "ANY_INPUT" );
358     filter_popup = std::make_unique<string_input_popup>();
359     filter_popup->context( ctxt ).text( filter )
360     .ignore_custom_actions( false )
361     .max_length( 256 )
362     .window( window, point( 4, w_height - 1 ), w_width - 4 );
363     do {
364         ui_manager::redraw();
365         filter = filter_popup->query_string( false );
366         if( !filter_popup->canceled() ) {
367             const std::string action = ctxt.input_to_action( ctxt.get_raw_input() );
368             if( filter_popup->handled() || !scrollby( scroll_amount_from_action( action ) ) ) {
369                 filterlist();
370             }
371         }
372     } while( !filter_popup->confirmed() && !filter_popup->canceled() );
373 
374     if( filter_popup->canceled() ) {
375         filterlist();
376     }
377 
378     filter_popup.reset();
379 }
380 
381 /**
382  * Find the minimum width between max( min_width, 1 ) and
383  * max( max_width, min_width, 1 ) to fold the string to no more than max_lines,
384  * or no more than the minimum number of lines possible, assuming that
385  * foldstring( width ).size() decreases monotonously with width.
386  **/
find_minimum_fold_width(const std::string & str,int max_lines,int min_width,int max_width)387 static int find_minimum_fold_width( const std::string &str, int max_lines,
388                                     int min_width, int max_width )
389 {
390     if( str.empty() ) {
391         return std::max( min_width, 1 );
392     }
393     min_width = std::max( min_width, 1 );
394     // max_width could be further limited by the string width, but utf8_width is
395     // not handling linebreaks properly.
396 
397     if( min_width < max_width ) {
398         // If with max_width the string still folds to more than max_lines, find the
399         // minimum width that folds the string to such number of lines instead.
400         max_lines = std::max<int>( max_lines, foldstring( str, max_width ).size() );
401         while( min_width < max_width ) {
402             int width = ( min_width + max_width ) / 2;
403             // width may equal min_width, but will always be less than max_width.
404             int lines = foldstring( str, width ).size();
405             // If the current width folds the string to no more than max_lines
406             if( lines <= max_lines ) {
407                 // The minimum width is between min_width and width.
408                 max_width = width;
409             } else {
410                 // The minimum width is between width + 1 and max_width.
411                 min_width = width + 1;
412             }
413             // The new interval will always be smaller than the previous one,
414             // so the loop is guaranteed to end.
415         }
416     }
417     return min_width;
418 }
419 
420 /**
421  * Calculate sizes, populate arrays, initialize window
422  */
setup()423 void uilist::setup()
424 {
425     bool w_auto = !w_width_setup.fun;
426 
427     // Space for a line between text and entries. Only needed if there is actually text.
428     const int text_separator_line = text.empty() ? 0 : 1;
429     if( w_auto ) {
430         w_width = 4;
431         if( !title.empty() ) {
432             w_width = utf8_width( title ) + 5;
433         }
434     } else {
435         w_width = w_width_setup.fun();
436     }
437     const int max_desc_width = w_auto ? TERMX - 4 : w_width - 4;
438 
439     bool h_auto = !w_height_setup.fun;
440     if( h_auto ) {
441         w_height = 4;
442     } else {
443         w_height = w_height_setup.fun();
444     }
445 
446     max_entry_len = 0;
447     max_column_len = 0;
448     desc_lines = desc_lines_hint;
449     std::vector<int> autoassign;
450     pad_left = pad_left_setup.fun ? pad_left_setup.fun() : 0;
451     pad_right = pad_right_setup.fun ? pad_right_setup.fun() : 0;
452     int pad = pad_left + pad_right + 2;
453     int descwidth_final = 0; // for description width guard
454     for( size_t i = 0; i < entries.size(); i++ ) {
455         int txtwidth = utf8_width( remove_color_tags( entries[i].txt ) );
456         int ctxtwidth = utf8_width( remove_color_tags( entries[i].ctxt ) );
457         if( txtwidth > max_entry_len ) {
458             max_entry_len = txtwidth;
459         }
460         if( ctxtwidth > max_column_len ) {
461             max_column_len = ctxtwidth;
462         }
463         int clen = ( ctxtwidth > 0 ) ? ctxtwidth + 2 : 0;
464         if( entries[ i ].enabled ) {
465             if( !entries[i].hotkey.has_value() ) {
466                 autoassign.emplace_back( i );
467             } else if( entries[i].hotkey.value() != input_event() ) {
468                 keymap[entries[i].hotkey.value()] = i;
469             }
470             if( entries[ i ].retval == -1 ) {
471                 entries[ i ].retval = i;
472             }
473             if( w_auto && w_width < txtwidth + pad + 4 + clen ) {
474                 w_width = txtwidth + pad + 4 + clen;
475             }
476         } else {
477             if( w_auto && w_width < txtwidth + pad + 4 + clen ) {
478                 // TODO: or +5 if header
479                 w_width = txtwidth + pad + 4 + clen;
480             }
481         }
482         if( desc_enabled ) {
483             const int min_desc_width = std::min( max_desc_width, std::max( w_width, descwidth_final ) - 4 );
484             int descwidth = find_minimum_fold_width( footer_text.empty() ? entries[i].desc : footer_text,
485                             desc_lines, min_desc_width, max_desc_width );
486             descwidth += 4; // 2x border + 2x ' ' pad
487             if( descwidth_final < descwidth ) {
488                 descwidth_final = descwidth;
489             }
490         }
491         if( entries[ i ].text_color == c_red_red ) {
492             entries[ i ].text_color = text_color;
493         }
494     }
495     input_context ctxt( input_category );
496     const hotkey_queue &hotkeys = hotkey_queue::alpha_digits();
497     input_event hotkey = ctxt.first_unassigned_hotkey( hotkeys );
498     for( auto it = autoassign.begin(); it != autoassign.end() &&
499          hotkey != input_event(); ++it ) {
500         bool assigned = false;
501         do {
502             if( keymap.count( hotkey ) == 0 ) {
503                 entries[*it].hotkey = hotkey;
504                 keymap[hotkey] = *it;
505                 assigned = true;
506             }
507             hotkey = ctxt.next_unassigned_hotkey( hotkeys, hotkey );
508         } while( !assigned && hotkey != input_event() );
509     }
510 
511     if( desc_enabled ) {
512         if( descwidth_final > TERMX ) {
513             desc_enabled = false; // give up
514         } else if( descwidth_final > w_width ) {
515             w_width = descwidth_final;
516         }
517 
518     }
519 
520     if( !text.empty() ) {
521         int twidth = utf8_width( remove_color_tags( text ) );
522         bool formattxt = true;
523         int realtextwidth = 0;
524         if( textwidth == -1 ) {
525             if( !w_auto ) {
526                 realtextwidth = w_width - 4;
527             } else {
528                 realtextwidth = twidth;
529                 if( twidth + 4 > w_width ) {
530                     if( realtextwidth + 4 > TERMX ) {
531                         realtextwidth = TERMX - 4;
532                     }
533                     textformatted = foldstring( text, realtextwidth );
534                     formattxt = false;
535                     realtextwidth = 10;
536                     for( auto &l : textformatted ) {
537                         const int w = utf8_width( remove_color_tags( l ) );
538                         if( w > realtextwidth ) {
539                             realtextwidth = w;
540                         }
541                     }
542                     if( realtextwidth + 4 > w_width ) {
543                         w_width = realtextwidth + 4;
544                     }
545                 }
546             }
547         } else if( textwidth != -1 ) {
548             realtextwidth = textwidth;
549             if( realtextwidth + 4 > w_width ) {
550                 w_width = realtextwidth + 4;
551             }
552         }
553         if( formattxt ) {
554             textformatted = foldstring( text, realtextwidth );
555         }
556     }
557 
558     // shrink-to-fit
559     if( desc_enabled ) {
560         desc_lines = 0;
561         for( const uilist_entry &ent : entries ) {
562             // -2 for borders, -2 for padding
563             desc_lines = std::max<int>( desc_lines, foldstring( footer_text.empty() ? ent.desc : footer_text,
564                                         w_width - 4 ).size() );
565         }
566         if( desc_lines <= 0 ) {
567             desc_enabled = false;
568         }
569     }
570 
571     if( w_auto && w_width > TERMX ) {
572         w_width = TERMX;
573     }
574 
575     vmax = entries.size();
576     int additional_lines = 2 + text_separator_line + // add two for top & bottom borders
577                            static_cast<int>( textformatted.size() );
578     if( desc_enabled ) {
579         additional_lines += desc_lines + 1; // add one for description separator line
580     }
581 
582     if( h_auto ) {
583         w_height = vmax + additional_lines;
584     }
585 
586     if( w_height > TERMY ) {
587         w_height = TERMY;
588     }
589 
590     if( vmax + additional_lines > w_height ) {
591         vmax = w_height - additional_lines;
592     }
593 
594     if( !w_x_setup.fun ) {
595         w_x = static_cast<int>( ( TERMX - w_width ) / 2 );
596     } else {
597         w_x = w_x_setup.fun( w_width );
598     }
599     if( !w_y_setup.fun ) {
600         w_y = static_cast<int>( ( TERMY - w_height ) / 2 );
601     } else {
602         w_y  = w_y_setup.fun( w_height );
603     }
604 
605     window = catacurses::newwin( w_height, w_width, point( w_x, w_y ) );
606     if( !window ) {
607         abort();
608     }
609 
610     if( !started ) {
611         filterlist();
612     }
613 
614     started = true;
615 }
616 
reposition(ui_adaptor & ui)617 void uilist::reposition( ui_adaptor &ui )
618 {
619     setup();
620     if( filter_popup ) {
621         filter_popup->window( window, point( 4, w_height - 1 ), w_width - 4 );
622     }
623     ui.position_from_window( window );
624 }
625 
apply_scrollbar()626 void uilist::apply_scrollbar()
627 {
628     int sbside = ( pad_left <= 0 ? 0 : w_width - 1 );
629     int estart = textformatted.size();
630     if( estart > 0 ) {
631         estart += 2;
632     } else {
633         estart = 1;
634     }
635 
636     scrollbar()
637     .offset_x( sbside )
638     .offset_y( estart )
639     .content_size( fentries.size() )
640     .viewport_pos( vshift )
641     .viewport_size( vmax )
642     .border_color( border_color )
643     .arrow_color( border_color )
644     .slot_color( c_light_gray )
645     .bar_color( c_cyan_cyan )
646     .scroll_to_last( false )
647     .apply( window );
648 }
649 
650 /**
651  * Generate and refresh output
652  */
show()653 void uilist::show()
654 {
655     if( !started ) {
656         setup();
657     }
658 
659     werase( window );
660     draw_border( window, border_color );
661     if( !title.empty() ) {
662         // NOLINTNEXTLINE(cata-use-named-point-constants)
663         mvwprintz( window, point( 1, 0 ), border_color, "< " );
664         wprintz( window, title_color, title );
665         wprintz( window, border_color, " >" );
666     }
667 
668     const int text_lines = textformatted.size();
669     int estart = 1;
670     if( !textformatted.empty() ) {
671         for( int i = 0; i < text_lines; i++ ) {
672             trim_and_print( window, point( 2, 1 + i ), getmaxx( window ) - 4,
673                             text_color, _color_error, "%s", textformatted[i] );
674         }
675 
676         mvwputch( window, point( 0, text_lines + 1 ), border_color, LINE_XXXO );
677         for( int i = 1; i < w_width - 1; ++i ) {
678             mvwputch( window, point( i, text_lines + 1 ), border_color, LINE_OXOX );
679         }
680         mvwputch( window, point( w_width - 1, text_lines + 1 ), border_color, LINE_XOXX );
681         estart += text_lines + 1; // +1 for the horizontal line.
682     }
683 
684     calcStartPos( vshift, fselected, vmax, fentries.size() );
685 
686     const int pad_size = std::max( 0, w_width - 2 - pad_left - pad_right );
687     const std::string padspaces = std::string( pad_size, ' ' );
688 
689     for( int fei = vshift, si = 0; si < vmax; fei++, si++ ) {
690         if( fei < static_cast<int>( fentries.size() ) ) {
691             int ei = fentries [ fei ];
692             nc_color co = ( ei == selected ?
693                             hilight_color :
694                             ( entries[ ei ].enabled || entries[ei].force_color ?
695                               entries[ ei ].text_color :
696                               disabled_color )
697                           );
698 
699             mvwprintz( window, point( pad_left + 1, estart + si ), co, padspaces );
700             if( entries[ei].hotkey.has_value() && entries[ei].hotkey.value() != input_event() ) {
701                 const nc_color hotkey_co = ei == selected ? hilight_color : hotkey_color;
702                 mvwprintz( window, point( pad_left + 1, estart + si ), entries[ ei ].enabled ? hotkey_co : co,
703                            "%s", right_justify( entries[ei].hotkey.value().short_description(), 2 ) );
704             }
705             if( pad_size > 3 ) {
706                 // pad_size indicates the maximal width of the entry, it is used above to
707                 // activate the highlighting, it is used to override previous text there, but in both
708                 // cases printing starts at pad_left+1, here it starts at pad_left+4, so 3 cells less
709                 // to be used.
710                 const utf8_wrapper entry = utf8_wrapper( ei == selected ? remove_color_tags( entries[ ei ].txt ) :
711                                            entries[ ei ].txt );
712                 int x = pad_left + 4;
713                 int y = estart + si;
714                 entries[ei].drawn_rect.p_min = point( x, y );
715                 entries[ei].drawn_rect.p_max = point( x + max_entry_len - 1, y );
716                 trim_and_print( window, point( x, y ), max_entry_len,
717                                 co, _color_error, "%s", entry.str() );
718 
719                 if( max_column_len && !entries[ ei ].ctxt.empty() ) {
720                     const utf8_wrapper centry = utf8_wrapper( ei == selected ? remove_color_tags( entries[ ei ].ctxt ) :
721                                                 entries[ ei ].ctxt );
722                     trim_and_print( window, point( getmaxx( window ) - max_column_len - 2, estart + si ),
723                                     max_column_len, co, _color_error, "%s", centry.str() );
724                 }
725             }
726             mvwzstr menu_entry_extra_text = entries[ei].extratxt;
727             if( !menu_entry_extra_text.txt.empty() ) {
728                 mvwprintz( window, point( pad_left + 1 + menu_entry_extra_text.left, estart + si ),
729                            menu_entry_extra_text.color, menu_entry_extra_text.txt );
730             }
731             if( menu_entry_extra_text.sym != 0 ) {
732                 mvwputch( window, point( pad_left + 1 + menu_entry_extra_text.left, estart + si ),
733                           menu_entry_extra_text.color, menu_entry_extra_text.sym );
734             }
735         } else {
736             mvwprintz( window, point( pad_left + 1, estart + si ), c_light_gray, padspaces );
737         }
738     }
739 
740     if( desc_enabled ) {
741         // draw border
742         mvwputch( window, point( 0, w_height - desc_lines - 2 ), border_color, LINE_XXXO );
743         for( int i = 1; i < w_width - 1; ++i ) {
744             mvwputch( window, point( i, w_height - desc_lines - 2 ), border_color, LINE_OXOX );
745         }
746         mvwputch( window, point( w_width - 1, w_height - desc_lines - 2 ), border_color, LINE_XOXX );
747 
748         // clear previous desc the ugly way
749         for( int y = desc_lines + 1; y > 1; --y ) {
750             for( int x = 2; x < w_width - 2; ++x ) {
751                 mvwputch( window, point( x, w_height - y ), text_color, " " );
752             }
753         }
754 
755         if( static_cast<size_t>( selected ) < entries.size() ) {
756             fold_and_print( window, point( 2, w_height - desc_lines - 1 ), w_width - 4, text_color,
757                             footer_text.empty() ? entries[selected].desc : footer_text );
758         }
759     }
760 
761     if( filter_popup ) {
762         mvwprintz( window, point( 2, w_height - 1 ), border_color, "< " );
763         mvwprintz( window, point( w_width - 3, w_height - 1 ), border_color, " >" );
764         filter_popup->query( /*loop=*/false, /*draw_only=*/true );
765     } else {
766         if( !filter.empty() ) {
767             mvwprintz( window, point( 2, w_height - 1 ), border_color, "< %s >", filter );
768             mvwprintz( window, point( 4, w_height - 1 ), text_color, filter );
769         }
770     }
771     apply_scrollbar();
772 
773     wnoutrefresh( window );
774     if( callback != nullptr ) {
775         callback->refresh( this );
776     }
777 }
778 
scroll_amount_from_action(const std::string & action)779 int uilist::scroll_amount_from_action( const std::string &action )
780 {
781     const int scroll_rate = vmax > 20 ? 10 : 3;
782     if( action == "UP" ) {
783         return -1;
784     } else if( action == "PAGE_UP" ) {
785         return -scroll_rate;
786     } else if( action == "SCROLL_UP" ) {
787         return -3;
788     } else if( action == "DOWN" ) {
789         return 1;
790     } else if( action == "PAGE_DOWN" ) {
791         return scroll_rate;
792     } else if( action == "SCROLL_DOWN" ) {
793         return +3;
794     } else {
795         return 0;
796     }
797 }
798 
799 /**
800  * check for valid scrolling keypress and handle. return false if invalid keypress
801  */
scrollby(const int scrollby)802 bool uilist::scrollby( const int scrollby )
803 {
804     if( scrollby == 0 ) {
805         return false;
806     }
807 
808     bool looparound = ( scrollby == -1 || scrollby == 1 );
809     bool backwards = ( scrollby < 0 );
810     int recmax = static_cast<int>( fentries.size() );
811 
812     fselected += scrollby;
813     if( !looparound ) {
814         if( backwards && fselected < 0 ) {
815             fselected = 0;
816         } else if( fselected >= recmax ) {
817             fselected = fentries.size() - 1;
818         }
819     }
820 
821     if( backwards ) {
822         if( fselected < 0 ) {
823             fselected = fentries.size() - 1;
824         }
825         for( size_t i = 0; i < fentries.size(); ++i ) {
826             if( hilight_disabled || entries[ fentries [ fselected ] ].enabled ) {
827                 break;
828             }
829             --fselected;
830             if( fselected < 0 ) {
831                 fselected = fentries.size() - 1;
832             }
833         }
834     } else {
835         if( fselected >= recmax ) {
836             fselected = 0;
837         }
838         for( size_t i = 0; i < fentries.size(); ++i ) {
839             if( hilight_disabled || entries[ fentries [ fselected ] ].enabled ) {
840                 break;
841             }
842             ++fselected;
843             if( fselected >= recmax ) {
844                 fselected = 0;
845             }
846         }
847     }
848     if( static_cast<size_t>( fselected ) < fentries.size() ) {
849         selected = fentries [ fselected ];
850         if( callback != nullptr ) {
851             callback->select( this );
852         }
853     }
854     return true;
855 }
856 
create_or_get_ui_adaptor()857 shared_ptr_fast<ui_adaptor> uilist::create_or_get_ui_adaptor()
858 {
859     shared_ptr_fast<ui_adaptor> current_ui = ui.lock();
860     if( !current_ui ) {
861         ui = current_ui = make_shared_fast<ui_adaptor>();
862         current_ui->on_redraw( [this]( const ui_adaptor & ) {
863             show();
864         } );
865         current_ui->on_screen_resize( [this]( ui_adaptor & ui ) {
866             reposition( ui );
867         } );
868         current_ui->mark_resize();
869     }
870     return current_ui;
871 }
872 
873 /**
874  * Handle input and update display
875  *
876  */
query(bool loop,int timeout)877 void uilist::query( bool loop, int timeout )
878 {
879     ret_evt = input_event();
880     if( entries.empty() ) {
881         ret = UILIST_ERROR;
882         return;
883     }
884     ret = UILIST_WAIT_INPUT;
885 
886     input_context ctxt( input_category, keyboard_mode::keycode );
887     ctxt.register_updown();
888     ctxt.register_action( "PAGE_UP", to_translation( "Fast scroll up" ) );
889     ctxt.register_action( "PAGE_DOWN", to_translation( "Fast scroll down" ) );
890     ctxt.register_action( "SCROLL_UP" );
891     ctxt.register_action( "SCROLL_DOWN" );
892     if( allow_cancel ) {
893         ctxt.register_action( "QUIT" );
894     }
895     ctxt.register_action( "SELECT" );
896     ctxt.register_action( "CONFIRM" );
897     ctxt.register_action( "FILTER" );
898     ctxt.register_action( "ANY_INPUT" );
899     ctxt.register_action( "HELP_KEYBINDINGS" );
900     for( const auto &additional_action : additional_actions ) {
901         ctxt.register_action( additional_action.first, additional_action.second );
902     }
903 
904     shared_ptr_fast<ui_adaptor> ui = create_or_get_ui_adaptor();
905 
906     ui_manager::redraw();
907 
908 #if defined(__ANDROID__)
909     for( const auto &entry : entries ) {
910         if( entry.enabled && entry.hotkey.has_value()
911             && entry.hotkey.value() != input_event() ) {
912             ctxt.register_manual_key( entry.hotkey.value().get_first_input(), entry.txt );
913         }
914     }
915 #endif
916 
917     do {
918         ret_act = ctxt.handle_input( timeout );
919         const input_event event = ctxt.get_raw_input();
920         ret_evt = event;
921         const auto iter = keymap.find( ret_evt );
922 
923         if( scrollby( scroll_amount_from_action( ret_act ) ) ) {
924             /* nothing */
925         } else if( filtering && ret_act == "FILTER" ) {
926             inputfilter();
927         } else if( iter != keymap.end() ) {
928             selected = iter->second;
929             if( entries[ selected ].enabled ) {
930                 ret = entries[ selected ].retval; // valid
931             } else if( allow_disabled ) {
932                 ret = entries[selected].retval; // disabled
933             }
934             if( callback != nullptr ) {
935                 callback->select( this );
936             }
937         } else if( !fentries.empty() && ret_act == "SELECT" ) {
938             cata::optional<point> p = ctxt.get_coordinates_text( window );
939             if( p ) {
940                 if( window_contains_point_relative( window, p.value() ) ) {
941                     uilist_entry *entry = find_entry_by_coordinate( p.value() );
942                     if( entry != nullptr ) {
943                         if( entry->enabled ) {
944                             ret = entry->retval;
945                         }
946                     }
947                 }
948             }
949         } else if( !fentries.empty() && ret_act == "CONFIRM" ) {
950             if( entries[ selected ].enabled ) {
951                 ret = entries[ selected ].retval; // valid
952             } else if( allow_disabled ) {
953                 // disabled
954                 ret = entries[selected].retval;
955             }
956         } else if( allow_cancel && ret_act == "QUIT" ) {
957             ret = UILIST_CANCEL;
958         } else if( ret_act == "TIMEOUT" ) {
959             ret = UILIST_TIMEOUT;
960         } else {
961             // including HELP_KEYBINDINGS, in case the caller wants to refresh their contents
962             bool unhandled = callback == nullptr || !callback->key( ctxt, event, selected, this );
963             if( unhandled && allow_anykey ) {
964                 ret = UILIST_UNBOUND;
965             } else if( unhandled && allow_additional ) {
966                 for( const auto &it : additional_actions ) {
967                     if( it.first == ret_act ) {
968                         ret = UILIST_ADDITIONAL;
969                         break;
970                     }
971                 }
972             }
973         }
974 
975         ui_manager::redraw();
976     } while( loop && ret == UILIST_WAIT_INPUT );
977 }
978 
find_entry_by_coordinate(const point & p)979 uilist_entry *uilist::find_entry_by_coordinate( const point &p )
980 {
981     for( int i : fentries ) {
982         uilist_entry &entry = entries[i];
983         if( entry.drawn_rect.contains( p ) ) {
984             return &entry;
985         }
986     }
987     return nullptr;
988 }
989 
990 ///@}
991 /**
992  * cleanup
993  */
reset()994 void uilist::reset()
995 {
996     window = catacurses::window();
997     init();
998 }
999 
addentry(const std::string & str)1000 void uilist::addentry( const std::string &str )
1001 {
1002     entries.emplace_back( str );
1003 }
1004 
addentry(int r,bool e,int k,const std::string & str)1005 void uilist::addentry( int r, bool e, int k, const std::string &str )
1006 {
1007     entries.emplace_back( r, e, k, str );
1008 }
1009 
addentry(const int r,const bool e,const cata::optional<input_event> & k,const std::string & str)1010 void uilist::addentry( const int r, const bool e,
1011                        const cata::optional<input_event> &k,
1012                        const std::string &str )
1013 {
1014     entries.emplace_back( r, e, k, str );
1015 }
1016 
addentry_desc(const std::string & str,const std::string & desc)1017 void uilist::addentry_desc( const std::string &str, const std::string &desc )
1018 {
1019     entries.emplace_back( str, desc );
1020 }
1021 
addentry_desc(int r,bool e,int k,const std::string & str,const std::string & desc)1022 void uilist::addentry_desc( int r, bool e, int k, const std::string &str, const std::string &desc )
1023 {
1024     entries.emplace_back( r, e, k, str, desc );
1025 }
1026 
addentry_col(int r,bool e,int k,const std::string & str,const std::string & column,const std::string & desc)1027 void uilist::addentry_col( int r, bool e, int k, const std::string &str, const std::string &column,
1028                            const std::string &desc )
1029 {
1030     entries.emplace_back( r, e, k, str, desc, column );
1031 }
1032 
addentry_col(const int r,const bool e,const cata::optional<input_event> & k,const std::string & str,const std::string & column,const std::string & desc)1033 void uilist::addentry_col( const int r, const bool e,
1034                            const cata::optional<input_event> &k,
1035                            const std::string &str, const std::string &column,
1036                            const std::string &desc )
1037 {
1038     entries.emplace_back( r, e, k, str, desc, column );
1039 }
1040 
settext(const std::string & str)1041 void uilist::settext( const std::string &str )
1042 {
1043     text = str;
1044 }
1045 
1046 struct pointmenu_cb::impl_t {
1047     const std::vector< tripoint > &points;
1048     int last; // to suppress redrawing
1049     tripoint last_view; // to reposition the view after selecting
1050     shared_ptr_fast<game::draw_callback_t> terrain_draw_cb;
1051 
1052     explicit impl_t( const std::vector<tripoint> &pts );
1053     ~impl_t();
1054 
1055     void select( uilist *menu );
1056 };
1057 
impl_t(const std::vector<tripoint> & pts)1058 pointmenu_cb::impl_t::impl_t( const std::vector<tripoint> &pts ) : points( pts )
1059 {
1060     last = INT_MIN;
1061     avatar &player_character = get_avatar();
1062     last_view = player_character.view_offset;
1063     terrain_draw_cb = make_shared_fast<game::draw_callback_t>( [this, &player_character]() {
1064         if( last >= 0 && static_cast<size_t>( last ) < points.size() ) {
1065             g->draw_trail_to_square( player_character.view_offset, true );
1066         }
1067     } );
1068     g->add_draw_callback( terrain_draw_cb );
1069 }
1070 
~impl_t()1071 pointmenu_cb::impl_t::~impl_t()
1072 {
1073     get_avatar().view_offset = last_view;
1074 }
1075 
select(uilist * const menu)1076 void pointmenu_cb::impl_t::select( uilist *const menu )
1077 {
1078     if( last == menu->selected ) {
1079         return;
1080     }
1081     last = menu->selected;
1082     avatar &player_character = get_avatar();
1083     if( menu->selected < 0 || menu->selected >= static_cast<int>( points.size() ) ) {
1084         player_character.view_offset = tripoint_zero;
1085     } else {
1086         const tripoint &center = points[menu->selected];
1087         player_character.view_offset = center - player_character.pos();
1088         // TODO: Remove this line when it's safe
1089         player_character.view_offset.z = 0;
1090     }
1091     g->invalidate_main_ui_adaptor();
1092 }
1093 
pointmenu_cb(const std::vector<tripoint> & pts)1094 pointmenu_cb::pointmenu_cb( const std::vector<tripoint> &pts ) : impl( pts )
1095 {
1096 }
1097 
1098 pointmenu_cb::~pointmenu_cb() = default;
1099 
select(uilist * const menu)1100 void pointmenu_cb::select( uilist *const menu )
1101 {
1102     impl->select( menu );
1103 }
1104