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