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 ¢er = 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