1 #include "popup.h"
2 
3 #include <algorithm>
4 #include <array>
5 #include <memory>
6 
7 #include "cached_options.h"
8 #include "catacharset.h"
9 #include "input.h"
10 #include "output.h"
11 #include "ui_manager.h"
12 
query_popup()13 query_popup::query_popup()
14     : cur( 0 ), default_text_color( c_white ), anykey( false ), cancel( false ), ontop( false ),
15       fullscr( false ), pref_kbd_mode( keyboard_mode::keycode )
16 {
17 }
18 
context(const std::string & cat)19 query_popup &query_popup::context( const std::string &cat )
20 {
21     invalidate_ui();
22     category = cat;
23     return *this;
24 }
25 
option(const std::string & opt)26 query_popup &query_popup::option( const std::string &opt )
27 {
28     invalidate_ui();
29     options.emplace_back( opt, []( const input_event & ) {
30         return true;
31     } );
32     return *this;
33 }
34 
option(const std::string & opt,const std::function<bool (const input_event &)> & filter)35 query_popup &query_popup::option( const std::string &opt,
36                                   const std::function<bool( const input_event & )> &filter )
37 {
38     invalidate_ui();
39     options.emplace_back( opt, filter );
40     return *this;
41 }
42 
allow_anykey(bool allow)43 query_popup &query_popup::allow_anykey( bool allow )
44 {
45     // Change does not affect cache, do not invalidate the window
46     anykey = allow;
47     return *this;
48 }
49 
allow_cancel(bool allow)50 query_popup &query_popup::allow_cancel( bool allow )
51 {
52     // Change does not affect cache, do not invalidate the window
53     cancel = allow;
54     return *this;
55 }
56 
on_top(bool top)57 query_popup &query_popup::on_top( bool top )
58 {
59     invalidate_ui();
60     ontop = top;
61     return *this;
62 }
63 
full_screen(bool full)64 query_popup &query_popup::full_screen( bool full )
65 {
66     invalidate_ui();
67     fullscr = full;
68     return *this;
69 }
70 
cursor(size_t pos)71 query_popup &query_popup::cursor( size_t pos )
72 {
73     // Change does not affect cache, do not invalidate window
74     cur = pos;
75     return *this;
76 }
77 
default_color(const nc_color & d_color)78 query_popup &query_popup::default_color( const nc_color &d_color )
79 {
80     default_text_color = d_color;
81     return *this;
82 }
83 
preferred_keyboard_mode(const keyboard_mode mode)84 query_popup &query_popup::preferred_keyboard_mode( const keyboard_mode mode )
85 {
86     invalidate_ui();
87     pref_kbd_mode = mode;
88     return *this;
89 }
90 
fold_query(const std::string & category,const keyboard_mode pref_kbd_mode,const std::vector<query_option> & options,const int max_width,const int horz_padding)91 std::vector<std::vector<std::string>> query_popup::fold_query(
92                                        const std::string &category,
93                                        const keyboard_mode pref_kbd_mode,
94                                        const std::vector<query_option> &options,
95                                        const int max_width, const int horz_padding )
96 {
97     input_context ctxt( category, pref_kbd_mode );
98 
99     std::vector<std::vector<std::string>> folded_query;
100     folded_query.emplace_back();
101 
102     int query_cnt = 0;
103     int query_width = 0;
104     for( const auto &opt : options ) {
105         const auto &name = ctxt.get_action_name( opt.action );
106         const auto &desc = ctxt.get_desc( opt.action, name, opt.filter );
107         const int this_query_width = utf8_width( desc, true ) + horz_padding;
108         ++query_cnt;
109         query_width += this_query_width;
110         if( query_width > max_width + horz_padding ) {
111             if( query_cnt == 1 ) {
112                 // Each line has at least one query, so keep this query in the current line
113                 folded_query.back().emplace_back( desc );
114                 folded_query.emplace_back();
115                 query_cnt = 0;
116                 query_width = 0;
117             } else {
118                 // Wrap this query to the next line
119                 folded_query.emplace_back();
120                 folded_query.back().emplace_back( desc );
121                 query_cnt = 1;
122                 query_width = this_query_width;
123             }
124         } else {
125             folded_query.back().emplace_back( desc );
126         }
127     }
128 
129     if( folded_query.back().empty() ) {
130         folded_query.pop_back();
131     }
132 
133     return folded_query;
134 }
135 
invalidate_ui() const136 void query_popup::invalidate_ui() const
137 {
138     if( win ) {
139         win = {};
140         folded_msg.clear();
141         buttons.clear();
142     }
143     std::shared_ptr<ui_adaptor> ui = adaptor.lock();
144     if( ui ) {
145         ui->mark_resize();
146     }
147 }
148 
149 static constexpr int border_width = 1;
150 
init() const151 void query_popup::init() const
152 {
153     constexpr int horz_padding = 2;
154     constexpr int vert_padding = 1;
155     const int max_line_width = FULL_SCREEN_WIDTH - border_width * 2;
156 
157     // Fold message text
158     folded_msg = foldstring( text, max_line_width );
159 
160     // Fold query buttons
161     const auto &folded_query = fold_query( category, pref_kbd_mode, options, max_line_width,
162                                            horz_padding );
163 
164     // Calculate size of message part
165     int msg_width = 0;
166     int msg_height = folded_msg.size();
167 
168     for( const auto &line : folded_msg ) {
169         msg_width = std::max( msg_width, utf8_width( line, true ) );
170     }
171 
172     // Calculate width with query buttons
173     for( const auto &line : folded_query ) {
174         if( !line.empty() ) {
175             int button_width = 0;
176             for( const auto &opt : line ) {
177                 button_width += utf8_width( opt, true );
178             }
179             msg_width = std::max( msg_width, button_width +
180                                   horz_padding * static_cast<int>( line.size() - 1 ) );
181         }
182     }
183     msg_width = std::min( msg_width, max_line_width );
184 
185     // Calculate height with query buttons & button positions
186     buttons.clear();
187     if( !folded_query.empty() ) {
188         msg_height += vert_padding;
189         for( const auto &line : folded_query ) {
190             if( !line.empty() ) {
191                 int button_width = 0;
192                 for( const auto &opt : line ) {
193                     button_width += utf8_width( opt, true );
194                 }
195                 // Right align.
196                 // TODO: multi-line buttons
197                 int button_x = std::max( 0, msg_width - button_width -
198                                          horz_padding * static_cast<int>( line.size() - 1 ) );
199                 for( const auto &opt : line ) {
200                     buttons.emplace_back( opt, point( button_x, msg_height ) );
201                     button_x += utf8_width( opt, true ) + horz_padding;
202                 }
203                 msg_height += 1 + vert_padding;
204             }
205         }
206         msg_height -= vert_padding;
207     }
208 
209     // Calculate window size
210     const int win_width = std::min( TERMX,
211                                     fullscr ? FULL_SCREEN_WIDTH : msg_width + border_width * 2 );
212     const int win_height = std::min( TERMY,
213                                      fullscr ? FULL_SCREEN_HEIGHT : msg_height + border_width * 2 );
214     const int win_x = ( TERMX - win_width ) / 2;
215     const int win_y = ontop ? 0 : ( TERMY - win_height ) / 2;
216     win = catacurses::newwin( win_height, win_width, point( win_x, win_y ) );
217 
218     std::shared_ptr<ui_adaptor> ui = adaptor.lock();
219     if( ui ) {
220         ui->position_from_window( win );
221     }
222 }
223 
show() const224 void query_popup::show() const
225 {
226     if( !win ) {
227         init();
228     }
229 
230     werase( win );
231     draw_border( win );
232 
233     for( size_t line = 0; line < folded_msg.size(); ++line ) {
234         nc_color col = default_text_color;
235         print_colored_text( win, point( border_width, border_width + line ), col, col,
236                             folded_msg[line] );
237     }
238 
239     for( size_t ind = 0; ind < buttons.size(); ++ind ) {
240         nc_color col = ind == cur ? hilite( c_white ) : c_white;
241         const auto &btn = buttons[ind];
242         print_colored_text( win, btn.pos + point( border_width, border_width ),
243                             col, col, btn.text );
244     }
245 
246     wnoutrefresh( win );
247 }
248 
create_or_get_adaptor()249 std::shared_ptr<ui_adaptor> query_popup::create_or_get_adaptor()
250 {
251     std::shared_ptr<ui_adaptor> ui = adaptor.lock();
252     if( !ui ) {
253         adaptor = ui = std::make_shared<ui_adaptor>();
254         ui->on_redraw( [this]( const ui_adaptor & ) {
255             show();
256         } );
257         ui->on_screen_resize( [this]( ui_adaptor & ) {
258             init();
259         } );
260         ui->mark_resize();
261     }
262     return ui;
263 }
264 
query_once()265 query_popup::result query_popup::query_once()
266 {
267     if( !anykey && !cancel && options.empty() ) {
268         return { false, "ERROR", {} };
269     }
270 
271     if( test_mode ) {
272         return { false, "ERROR", {} };
273     }
274 
275     std::shared_ptr<ui_adaptor> ui = create_or_get_adaptor();
276 
277     ui_manager::redraw();
278 
279     input_context ctxt( category, pref_kbd_mode );
280     if( cancel || !options.empty() ) {
281         ctxt.register_action( "HELP_KEYBINDINGS" );
282     }
283     if( !options.empty() ) {
284         ctxt.register_action( "LEFT" );
285         ctxt.register_action( "RIGHT" );
286         ctxt.register_action( "CONFIRM" );
287         for( const auto &opt : options ) {
288             ctxt.register_action( opt.action );
289         }
290     }
291     if( anykey ) {
292         ctxt.register_action( "ANY_INPUT" );
293         // Mouse movement, button, and wheel
294         ctxt.register_action( "COORDINATE" );
295     }
296     if( cancel ) {
297         ctxt.register_action( "QUIT" );
298     }
299 
300     result res;
301     // Assign outside construction of `res` to ensure execution order
302     res.wait_input = !anykey;
303     do {
304         res.action = ctxt.handle_input();
305         res.evt = ctxt.get_raw_input();
306     } while(
307         // Always ignore mouse movement
308         ( res.evt.type == input_event_t::mouse && res.evt.get_first_input() == MOUSE_MOVE ) ||
309         // Ignore window losing focus in SDL
310         ( res.evt.type == input_event_t::keyboard_char && res.evt.sequence.empty() )
311     );
312 
313     if( cancel && res.action == "QUIT" ) {
314         res.wait_input = false;
315     } else if( res.action == "LEFT" ) {
316         if( cur > 0 ) {
317             --cur;
318         } else {
319             cur = options.size() - 1;
320         }
321     } else if( res.action == "RIGHT" ) {
322         if( cur + 1 < options.size() ) {
323             ++cur;
324         } else {
325             cur = 0;
326         }
327     } else if( res.action == "CONFIRM" ) {
328         if( cur < options.size() ) {
329             res.wait_input = false;
330             res.action = options[cur].action;
331         }
332     } else if( res.action == "HELP_KEYBINDINGS" ) {
333         // Keybindings may have changed, regenerate the UI
334         init();
335     } else {
336         for( size_t ind = 0; ind < options.size(); ++ind ) {
337             if( res.action == options[ind].action ) {
338                 cur = ind;
339                 if( options[ind].filter( res.evt ) ) {
340                     res.wait_input = false;
341                     break;
342                 }
343             }
344         }
345     }
346 
347     return res;
348 }
349 
query()350 query_popup::result query_popup::query()
351 {
352     std::shared_ptr<ui_adaptor> ui = create_or_get_adaptor();
353 
354     result res;
355     do {
356         res = query_once();
357     } while( res.wait_input );
358     return res;
359 }
360 
wait_text(const std::string & text,const nc_color & bar_color)361 std::string query_popup::wait_text( const std::string &text, const nc_color &bar_color )
362 {
363     static const std::array<std::string, 4> phase_icons = {{ "|", "/", "-", "\\" }};
364     static size_t phase = phase_icons.size() - 1;
365     phase = ( phase + 1 ) % phase_icons.size();
366     return string_format( " %s %s", colorize( phase_icons[phase], bar_color ), text );
367 }
368 
wait_text(const std::string & text)369 std::string query_popup::wait_text( const std::string &text )
370 {
371     return wait_text( text, c_light_green );
372 }
373 
result()374 query_popup::result::result()
375     : wait_input( false ), action( "ERROR" )
376 {
377 }
378 
result(bool wait_input,const std::string & action,const input_event & evt)379 query_popup::result::result( bool wait_input, const std::string &action, const input_event &evt )
380     : wait_input( wait_input ), action( action ), evt( evt )
381 {
382 }
383 
query_option(const std::string & action,const std::function<bool (const input_event &)> & filter)384 query_popup::query_option::query_option(
385     const std::string &action,
386     const std::function<bool( const input_event & )> &filter )
387     : action( action ), filter( filter )
388 {
389 }
390 
button(const std::string & text,const point & p)391 query_popup::button::button( const std::string &text, const point &p )
392     : text( text ), pos( p )
393 {
394 }
395 
static_popup()396 static_popup::static_popup()
397 {
398     ui = create_or_get_adaptor();
399 }
400