1 #include "messages.h"
2
3 #include "cached_options.h"
4 #include "calendar.h"
5 #include "catacharset.h"
6 #include "color.h"
7 #include "cursesdef.h"
8 #include "debug.h"
9 #include "enums.h"
10 #include "game.h"
11 #include "input.h"
12 #include "json.h"
13 #include "output.h"
14 #include "panels.h"
15 #include "point.h"
16 #include "string_formatter.h"
17 #include "string_input_popup.h"
18 #include "translations.h"
19 #include "ui_manager.h"
20 #include "viewer.h"
21
22 #if defined(__ANDROID__)
23 #include <SDL_keyboard.h>
24 #endif
25 #include <algorithm>
26 #include <deque>
27 #include <functional>
28 #include <iterator>
29 #include <memory>
30 #include <string>
31
32 #include "options.h"
33
34 namespace
35 {
36
37 struct game_message : public JsonDeserializer, public JsonSerializer {
38 std::string message;
39 time_point timestamp_in_turns = calendar::turn_zero;
40 int timestamp_in_user_actions = 0;
41 int count = 1;
42 // number of times this message has been seen while it was in cooldown.
43 unsigned cooldown_seen = 1;
44 // hide the message, because at some point it was in cooldown period.
45 bool cooldown_hidden = false;
46 game_message_type type = m_neutral;
47
48 game_message() = default;
game_message__anon1701ceb10111::game_message49 game_message( std::string &&msg, game_message_type const t ) :
50 message( std::move( msg ) ),
51 timestamp_in_turns( calendar::turn ),
52 timestamp_in_user_actions( g->get_user_action_counter() ),
53 type( t ) {
54 }
55
turn__anon1701ceb10111::game_message56 const time_point &turn() const {
57 return timestamp_in_turns;
58 }
59
get_with_count__anon1701ceb10111::game_message60 std::string get_with_count() const {
61 if( count <= 1 ) {
62 return message;
63 }
64 //~ Message %s on the message log was repeated %d times, e.g. "You hear a whack! x 12"
65 return string_format( _( "%s x %d" ), message, count );
66 }
67
68 /** Get whether or not a message should not be displayed (hidden) in the side bar because it's in a cooldown period.
69 * @returns `true` if the message should **not** be displayed, `false` otherwise.
70 */
is_in_cooldown__anon1701ceb10111::game_message71 bool is_in_cooldown() const {
72 return cooldown_hidden;
73 }
74
is_new__anon1701ceb10111::game_message75 bool is_new( const time_point ¤t ) const {
76 return turn() >= current;
77 }
78
is_recent__anon1701ceb10111::game_message79 bool is_recent( const time_point ¤t ) const {
80 return turn() + 5_turns >= current;
81 }
82
get_color__anon1701ceb10111::game_message83 nc_color get_color( const time_point ¤t ) const {
84 if( is_new( current ) ) {
85 // color for new messages
86 return msgtype_to_color( type, false );
87
88 } else if( is_recent( current ) ) {
89 // color for slightly old messages
90 return msgtype_to_color( type, true );
91 }
92
93 // color for old messages
94 return c_dark_gray;
95 }
96
deserialize__anon1701ceb10111::game_message97 void deserialize( JsonIn &jsin ) override {
98 JsonObject obj = jsin.get_object();
99 obj.read( "turn", timestamp_in_turns );
100 message = obj.get_string( "message" );
101 count = obj.get_int( "count" );
102 type = static_cast<game_message_type>( obj.get_int( "type" ) );
103 }
104
serialize__anon1701ceb10111::game_message105 void serialize( JsonOut &jsout ) const override {
106 jsout.start_object();
107 jsout.member( "turn", timestamp_in_turns );
108 jsout.member( "message", message );
109 jsout.member( "count", count );
110 jsout.member( "type", static_cast<int>( type ) );
111 jsout.end_object();
112 }
113 };
114
115 class messages_impl
116 {
117 public:
118 std::deque<game_message> messages; // Messages to be printed
119 std::vector<game_message> cooldown_templates; // Message cooldown
120 time_point curmes = calendar::turn_zero; // The last-seen message.
121 bool active = true;
122
has_undisplayed_messages() const123 bool has_undisplayed_messages() const {
124 return !messages.empty() && messages.back().turn() > curmes;
125 }
126
history(const int i) const127 const game_message &history( const int i ) const {
128 return messages[messages.size() - i - 1];
129 }
130
131 // coalesce recent like messages
coalesce_messages(const game_message & m)132 bool coalesce_messages( const game_message &m ) {
133 if( messages.empty() ) {
134 return false;
135 }
136
137 auto &last_msg = messages.back();
138 if( last_msg.turn() + 3_turns < calendar::turn ) {
139 return false;
140 }
141
142 if( m.type != last_msg.type || m.message != last_msg.message ) {
143 return false;
144 }
145
146 // update the cooldown message timer due to coalescing
147 const auto cooldown_it = std::find_if( cooldown_templates.begin(), cooldown_templates.end(),
148 [&m]( game_message & am ) -> bool {
149 return m.message == am.message;
150 } );
151 if( cooldown_it != cooldown_templates.end() ) {
152 cooldown_it->timestamp_in_turns = calendar::turn;
153 }
154
155 // coalesce messages
156 last_msg.count++;
157 last_msg.timestamp_in_turns = calendar::turn;
158 last_msg.timestamp_in_user_actions = g->get_user_action_counter();
159 last_msg.type = m.type;
160
161 return true;
162 }
163
add_msg_string(std::string && msg)164 void add_msg_string( std::string &&msg ) {
165 add_msg_string( std::move( msg ), m_neutral, gmf_none );
166 }
167
add_msg_string(std::string && msg,const game_message_params & params)168 void add_msg_string( std::string &&msg, const game_message_params ¶ms ) {
169 add_msg_string( std::move( msg ), params.type, params.flags );
170 }
171
add_msg_string(std::string && msg,game_message_type const type,const game_message_flags flags)172 void add_msg_string( std::string &&msg, game_message_type const type,
173 const game_message_flags flags ) {
174 if( msg.empty() || !active ) {
175 return;
176 }
177
178 if( type == m_debug && !debug_mode ) {
179 return;
180 }
181
182 game_message m = game_message( std::move( msg ), type );
183
184 refresh_cooldown( m, flags );
185 hide_message_in_cooldown( m );
186
187 if( coalesce_messages( m ) ) {
188 return;
189 }
190
191 unsigned int message_limit = get_option<int>( "MESSAGE_LIMIT" );
192 while( messages.size() > message_limit ) {
193 messages.pop_front();
194 }
195
196 messages.emplace_back( m );
197 }
198
199 /** Check if the current message needs to be prevented (hidden) or not from being displayed in the side bar.
200 * @param message The message to be checked.
201 */
hide_message_in_cooldown(game_message & message)202 void hide_message_in_cooldown( game_message &message ) {
203 message.cooldown_hidden = false;
204
205 if( message_cooldown <= 0 || message.turn() <= calendar::turn_zero ) {
206 return;
207 }
208
209 // We look for **exactly the same** message string in the cooldown templates
210 // If there is one, this means the same message was already displayed.
211 const auto cooldown_it = std::find_if( cooldown_templates.begin(), cooldown_templates.end(),
212 [&message]( game_message & m_cooldown ) -> bool {
213 return m_cooldown.message == message.message;
214 } );
215 if( cooldown_it == cooldown_templates.end() ) {
216 // nothing found, not in cooldown.
217 return;
218 }
219
220 // note: from this point the current message (`message`) has the same string than one of the active cooldown template messages (`cooldown_it`).
221
222 // check how much times this message has been seen during its cooldown.
223 // If it's only one time, then no need to hide it.
224 if( cooldown_it->cooldown_seen == 1 ) {
225 return;
226 }
227
228 // check if it's the message that started the cooldown timer.
229 if( message.turn() == cooldown_it->turn() ) {
230 return;
231 }
232
233 // current message turn.
234 const int cm_turn = to_turn<int>( message.turn() );
235 // maximum range of the cooldown timer.
236 const int max_cooldown_range = to_turn<int>( cooldown_it->turn() ) + message_cooldown;
237 // If the current message is in the cooldown range then hide it.
238 if( cm_turn <= max_cooldown_range ) {
239 message.cooldown_hidden = true;
240 }
241 }
242
recent_messages(size_t count) const243 std::vector<std::pair<std::string, std::string>> recent_messages( size_t count ) const {
244 count = std::min( count, messages.size() );
245
246 std::vector<std::pair<std::string, std::string>> result;
247 result.reserve( count );
248
249 const int offset = static_cast<std::ptrdiff_t>( messages.size() - count );
250
251 std::transform( begin( messages ) + offset, end( messages ), back_inserter( result ),
252 []( const game_message & msg ) {
253 return std::make_pair( to_string_time_of_day( msg.timestamp_in_turns ),
254 msg.get_with_count() );
255 } );
256
257 return result;
258 }
259
260 /** Refresh the cooldown timers, removing elapsed ones and making new ones if needed.
261 * @param message The current message that needs to be checked.
262 * @param flags Flags pertaining to the message.
263 */
refresh_cooldown(const game_message & message,const game_message_flags flags)264 void refresh_cooldown( const game_message &message, const game_message_flags flags ) {
265 // is cooldown used? (also checks for messages arriving here at game initialization: we don't care about them).
266 if( message_cooldown <= 0 || message.turn() <= calendar::turn_zero ) {
267 return;
268 }
269
270 // housekeeping: remove any cooldown message with an expired cooldown time from the cooldown queue.
271 const time_point now = calendar::turn;
272 for( auto it = cooldown_templates.begin(); it != cooldown_templates.end(); ) {
273 // number of turns elapsed since the cooldown started.
274 const int turns = to_turns<int>( now - it->turn() );
275 if( turns >= message_cooldown ) {
276 // time elapsed! remove it.
277 it = cooldown_templates.erase( it );
278 } else {
279 ++it;
280 }
281 }
282
283 // do not hide messages which bypasses cooldown.
284 if( ( flags & gmf_bypass_cooldown ) != 0 ) {
285 return;
286 }
287
288 // Is the message string already in the cooldown queue?
289 // If it's not we must put it in the cooldown queue now, otherwise just increment the number of times we have seen it.
290 const auto cooldown_message_it = std::find_if( cooldown_templates.begin(),
291 cooldown_templates.end(), [&message]( game_message & cooldown_message ) -> bool {
292 return cooldown_message.message == message.message;
293 } );
294 if( cooldown_message_it == cooldown_templates.end() ) {
295 // push current message to cooldown message templates.
296 cooldown_templates.emplace_back( message );
297 } else {
298 // increment the number of time we have seen this message.
299 cooldown_message_it->cooldown_seen++;
300 }
301 }
302 };
303
304 // Messages object.
305 messages_impl player_messages;
306
message_exceeds_ttl(const game_message & message)307 bool message_exceeds_ttl( const game_message &message )
308 {
309 return message_ttl > 0 &&
310 message.timestamp_in_user_actions + message_ttl <= g->get_user_action_counter();
311 }
312
313 } //namespace
314
recent_messages(const size_t count)315 std::vector<std::pair<std::string, std::string>> Messages::recent_messages( const size_t count )
316 {
317 return player_messages.recent_messages( count );
318 }
319
serialize(JsonOut & json)320 void Messages::serialize( JsonOut &json )
321 {
322 json.member( "player_messages" );
323 json.start_object();
324 json.member( "messages", player_messages.messages );
325 json.member( "curmes", player_messages.curmes );
326 json.end_object();
327 }
328
deserialize(const JsonObject & json)329 void Messages::deserialize( const JsonObject &json )
330 {
331 if( !json.has_member( "player_messages" ) ) {
332 return;
333 }
334
335 JsonObject obj = json.get_object( "player_messages" );
336 obj.read( "messages", player_messages.messages );
337 obj.read( "curmes", player_messages.curmes );
338 }
339
add_msg(std::string msg)340 void Messages::add_msg( std::string msg )
341 {
342 player_messages.add_msg_string( std::move( msg ) );
343 }
344
add_msg(const game_message_params & params,std::string msg)345 void Messages::add_msg( const game_message_params ¶ms, std::string msg )
346 {
347 player_messages.add_msg_string( std::move( msg ), params );
348 }
349
clear_messages()350 void Messages::clear_messages()
351 {
352 player_messages.messages.clear();
353 player_messages.active = true;
354 }
355
deactivate()356 void Messages::deactivate()
357 {
358 player_messages.active = false;
359 }
360
size()361 size_t Messages::size()
362 {
363 return player_messages.messages.size();
364 }
365
has_undisplayed_messages()366 bool Messages::has_undisplayed_messages()
367 {
368 return player_messages.has_undisplayed_messages();
369 }
370
371 // Returns pairs of message log type id and untranslated name
msg_type_and_names()372 static const std::vector<std::pair<game_message_type, const char *>> &msg_type_and_names()
373 {
374 static const std::vector<std::pair<game_message_type, const char *>> type_n_names = {
375 { m_good, translate_marker_context( "message type", "good" ) },
376 { m_bad, translate_marker_context( "message type", "bad" ) },
377 { m_mixed, translate_marker_context( "message type", "mixed" ) },
378 { m_warning, translate_marker_context( "message type", "warning" ) },
379 { m_info, translate_marker_context( "message type", "info" ) },
380 { m_neutral, translate_marker_context( "message type", "neutral" ) },
381 { m_debug, translate_marker_context( "message type", "debug" ) },
382 };
383 return type_n_names;
384 }
385
386 // Get message type from translated name, returns true if name is a valid translated name
msg_type_from_name(game_message_type & type,const std::string & name)387 static bool msg_type_from_name( game_message_type &type, const std::string &name )
388 {
389 for( const auto &p : msg_type_and_names() ) {
390 if( name == pgettext( "message type", p.second ) ) {
391 type = p.first;
392 return true;
393 }
394 }
395 return false;
396 }
397
398 namespace Messages
399 {
400 // NOLINTNEXTLINE(cata-xy)
401 class dialog
402 {
403 public:
404 dialog();
405 void run();
406 private:
407 void init( ui_adaptor &ui );
408 void show();
409 void input();
410 void do_filter( const std::string &filter_str );
411 static std::vector<std::string> filter_help_text( int width );
412
413 const nc_color border_color;
414 const nc_color filter_color;
415 const nc_color time_color;
416 const nc_color bracket_color;
417 const nc_color filter_help_color;
418
419 // border_width padding_width border_width
420 // v v v
421 //
422 // | 12 seconds Never mind. x 2 |
423 //
424 // '-----v---' '---------v---------'
425 // time_width msg_width
426 static constexpr int border_width = 1;
427 static constexpr int padding_width = 1;
428 int time_width = 0;
429 int msg_width = 0;
430
431 size_t max_lines = 0; // Max number of lines the window can show at once
432
433 int w_x = 0;
434 int w_y = 0;
435 int w_width = 0;
436 int w_height = 0; // Main window position
437 catacurses::window w; // Main window
438
439 int w_fh_x = 0;
440 int w_fh_y = 0;
441 int w_fh_width = 0;
442 int w_fh_height = 0; // Filter help window position
443 catacurses::window w_filter_help; // Filter help window
444
445 std::vector<std::string> help_text; // Folded filter help text
446
447 string_input_popup filter;
448 bool filtering = false;
449 std::string filter_str;
450
451 input_context ctxt;
452
453 // Message indices and folded strings
454 std::vector<std::pair<size_t, std::string>> folded_all;
455 // Indices of filtered messages
456 std::vector<size_t> folded_filtered;
457
458 size_t offset = 0; // Index of the first printed message
459
460 bool canceled = false;
461 bool errored = false;
462
463 bool first_init = true;
464 };
465 } // namespace Messages
466
dialog()467 Messages::dialog::dialog()
468 : border_color( BORDER_COLOR ), filter_color( c_white ),
469 time_color( c_light_blue ), bracket_color( c_dark_gray ),
470 filter_help_color( c_cyan )
471 {
472 }
473
init(ui_adaptor & ui)474 void Messages::dialog::init( ui_adaptor &ui )
475 {
476 const int left_panel_width = panel_manager::get_manager().get_width_left();
477 const int right_panel_width = panel_manager::get_manager().get_width_right();
478 w_height = TERMY;
479 w_y = 0;
480 // try to center and not obscure sidebar
481 w_x = std::max( left_panel_width, right_panel_width );
482 w_width = TERMX - 2 * w_x;
483 if( w_width < w_height * 3 ) {
484 // try not to obscure sidebar
485 w_x = left_panel_width;
486 w_width = TERMX - left_panel_width - right_panel_width;
487 }
488
489 w = catacurses::newwin( w_height, w_width, point( w_x, w_y ) );
490
491 if( first_init ) {
492 ctxt = input_context( "MESSAGE_LOG" );
493 ctxt.register_action( "UP", to_translation( "Scroll up" ) );
494 ctxt.register_action( "DOWN", to_translation( "Scroll down" ) );
495 ctxt.register_action( "PAGE_UP" );
496 ctxt.register_action( "PAGE_DOWN" );
497 ctxt.register_action( "FILTER" );
498 ctxt.register_action( "RESET_FILTER" );
499 ctxt.register_action( "QUIT" );
500 ctxt.register_action( "HELP_KEYBINDINGS" );
501
502 // Calculate time string display width. The translated strings are expected to
503 // be aligned, so we choose an arbitrary duration here to calculate the width.
504 time_width = utf8_width( to_string_clipped( 1_turns, clipped_align::right ) );
505 }
506
507 if( border_width * 2 + time_width + padding_width >= w_width ||
508 border_width * 2 >= w_height ) {
509
510 errored = true;
511 return;
512 }
513 msg_width = w_width - border_width * 2 - time_width - padding_width;
514 max_lines = static_cast<size_t>( w_height - border_width * 2 );
515
516 // Initialize filter help text and window
517 w_fh_width = w_width;
518 w_fh_x = w_x;
519 help_text = filter_help_text( w_fh_width - border_width * 2 );
520 w_fh_height = help_text.size() + border_width * 2;
521 w_fh_y = w_y + w_height - w_fh_height;
522 w_filter_help = catacurses::newwin( w_fh_height, w_fh_width, point( w_fh_x, w_fh_y ) );
523
524 // Initialize filter input
525 filter.window( w_filter_help, point( border_width + 2, w_fh_height - 1 ),
526 w_fh_width - border_width - 2 );
527
528 // Initialize folded messages
529 folded_all.clear();
530 folded_filtered.clear();
531 const size_t msg_count = size();
532 for( size_t ind = 0; ind < msg_count; ++ind ) {
533 const size_t msg_ind = log_from_top ? ind : msg_count - 1 - ind;
534 const game_message &msg = player_messages.history( msg_ind );
535 const auto &folded = foldstring( msg.get_with_count(), msg_width );
536 for( const auto &it : folded ) {
537 folded_filtered.emplace_back( folded_all.size() );
538 folded_all.emplace_back( msg_ind, it );
539 }
540 }
541
542 do_filter( filter_str );
543
544 ui.position_from_window( w );
545
546 first_init = false;
547 }
548
show()549 void Messages::dialog::show()
550 {
551 werase( w );
552 draw_border( w, border_color );
553
554 scrollbar()
555 .offset_x( 0 )
556 .offset_y( border_width )
557 .content_size( folded_filtered.size() )
558 .viewport_pos( offset )
559 .viewport_size( max_lines )
560 .apply( w );
561
562 // Range of window lines to print
563 size_t line_from = 0, line_to;
564 if( offset < folded_filtered.size() ) {
565 line_to = std::min( max_lines, folded_filtered.size() - offset );
566 } else {
567 line_to = 0;
568 }
569
570 if( !log_from_top ) {
571 // Always print from new to old
572 std::swap( line_from, line_to );
573 }
574 std::string prev_time_str;
575 bool printing_range = false;
576 for( size_t line = line_from; line != line_to; ) {
577 // Decrement here if printing from bottom to get the correct line number
578 if( !log_from_top ) {
579 --line;
580 }
581
582 const size_t folded_ind = offset + line;
583 const size_t msg_ind = folded_all[folded_filtered[folded_ind]].first;
584 const game_message &msg = player_messages.history( msg_ind );
585
586 nc_color col = msgtype_to_color( msg.type, false );
587
588 // Print current line
589 print_colored_text( w, point( border_width + time_width + padding_width, border_width + line ),
590 col, col, folded_all[folded_filtered[folded_ind]].second );
591
592 // Generate aligned time string
593 const time_point msg_time = msg.timestamp_in_turns;
594 const std::string time_str = to_string_clipped( calendar::turn - msg_time, clipped_align::right );
595
596 if( time_str != prev_time_str ) {
597 // Time changed, print time string
598 prev_time_str = time_str;
599 right_print( w, border_width + line, border_width + msg_width + padding_width,
600 time_color, time_str );
601 printing_range = false;
602 } else {
603 // Print line brackets to mark ranges of time
604 if( printing_range ) {
605 const size_t last_line = log_from_top ? line - 1 : line + 1;
606 wattron( w, bracket_color );
607 mvwaddch( w, point( border_width + time_width - 1, border_width + last_line ), LINE_XOXO );
608 wattroff( w, bracket_color );
609 }
610 wattron( w, bracket_color );
611 mvwaddch( w, point( border_width + time_width - 1, border_width + line ),
612 log_from_top ? LINE_XXOO : LINE_OXXO );
613 wattroff( w, bracket_color );
614 printing_range = true;
615 }
616
617 // Decrement for !log_from_top is done at the beginning
618 if( log_from_top ) {
619 ++line;
620 }
621 }
622
623 if( filtering ) {
624 wnoutrefresh( w );
625 // Print the help text
626 werase( w_filter_help );
627 draw_border( w_filter_help, border_color );
628 for( size_t line = 0; line < help_text.size(); ++line ) {
629 nc_color col = filter_help_color;
630 print_colored_text( w_filter_help, point( border_width, border_width + line ), col, col,
631 help_text[line] );
632 }
633 mvwprintz( w_filter_help, point( border_width, w_fh_height - 1 ), border_color, "< " );
634 mvwprintz( w_filter_help, point( w_fh_width - border_width - 2, w_fh_height - 1 ), border_color,
635 " >" );
636 wnoutrefresh( w_filter_help );
637
638 // This line is preventing this method from being const
639 filter.query( false, true ); // Draw only
640 } else {
641 if( filter_str.empty() ) {
642 mvwprintz( w, point( border_width, w_height - 1 ), border_color,
643 _( "< Press %s to filter, %s to reset >" ),
644 ctxt.get_desc( "FILTER" ), ctxt.get_desc( "RESET_FILTER" ) );
645 } else {
646 mvwprintz( w, point( border_width, w_height - 1 ), border_color, "< %s >", filter_str );
647 mvwprintz( w, point( border_width + 2, w_height - 1 ), filter_color, "%s", filter_str );
648 }
649 wnoutrefresh( w );
650 }
651 }
652
do_filter(const std::string & filter_str)653 void Messages::dialog::do_filter( const std::string &filter_str )
654 {
655 // Split the search string into type and text
656 bool has_type_filter = false;
657 game_message_type filter_type = m_neutral;
658 std::string filter_text;
659 const auto colon = filter_str.find( ':' );
660 if( colon != std::string::npos ) {
661 has_type_filter = msg_type_from_name( filter_type, filter_str.substr( 0, colon ) );
662 filter_text = filter_str.substr( colon + 1 );
663 } else {
664 filter_text = filter_str;
665 }
666
667 // Start filtering the log
668 folded_filtered.clear();
669 for( size_t folded_ind = 0; folded_ind < folded_all.size(); ) {
670 const size_t msg_ind = folded_all[folded_ind].first;
671 const game_message &msg = player_messages.history( msg_ind );
672 const bool match = ( !has_type_filter || filter_type == msg.type ) &&
673 ci_find_substr( remove_color_tags( msg.get_with_count() ), filter_text ) >= 0;
674
675 // Always advance the index, but only add to filtered list if the original message matches
676 for( ; folded_ind < folded_all.size() && folded_all[folded_ind].first == msg_ind; ++folded_ind ) {
677 if( match ) {
678 folded_filtered.emplace_back( folded_ind );
679 }
680 }
681 }
682
683 // Reset view
684 if( log_from_top || max_lines > folded_filtered.size() ) {
685 offset = 0;
686 } else {
687 offset = folded_filtered.size() - max_lines;
688 }
689 }
690
input()691 void Messages::dialog::input()
692 {
693 canceled = false;
694 if( filtering ) {
695 filter.query( false );
696 if( filter.confirmed() || filter.canceled() ) {
697 filtering = false;
698 }
699 if( !filter.canceled() ) {
700 const std::string &new_filter_str = filter.text();
701 if( new_filter_str != filter_str ) {
702 filter_str = new_filter_str;
703
704 do_filter( filter_str );
705 }
706 } else {
707 filter.text( filter_str );
708 }
709 } else {
710 const std::string &action = ctxt.handle_input();
711 if( action == "DOWN" && offset + max_lines < folded_filtered.size() ) {
712 ++offset;
713 } else if( action == "UP" && offset > 0 ) {
714 --offset;
715 } else if( action == "PAGE_DOWN" ) {
716 if( offset + max_lines * 2 <= folded_filtered.size() ) {
717 offset += max_lines;
718 } else if( max_lines <= folded_filtered.size() ) {
719 offset = folded_filtered.size() - max_lines;
720 } else {
721 offset = 0;
722 }
723 } else if( action == "PAGE_UP" ) {
724 if( offset >= max_lines ) {
725 offset -= max_lines;
726 } else {
727 offset = 0;
728 }
729 } else if( action == "FILTER" ) {
730 filtering = true;
731 } else if( action == "RESET_FILTER" ) {
732 filter_str.clear();
733 filter.text( filter_str );
734 do_filter( filter_str );
735 } else if( action == "QUIT" ) {
736 canceled = true;
737 }
738 }
739 }
740
run()741 void Messages::dialog::run()
742 {
743 ui_adaptor ui;
744 ui.on_screen_resize( [this]( ui_adaptor & ui ) {
745 init( ui );
746 } );
747 ui.mark_resize();
748 ui.on_redraw( [this]( const ui_adaptor & ) {
749 show();
750 } );
751
752 while( !errored && !canceled ) {
753 ui_manager::redraw();
754 input();
755 }
756 }
757
filter_help_text(int width)758 std::vector<std::string> Messages::dialog::filter_help_text( int width )
759 {
760 const auto &help_fmt = _(
761 "<color_light_gray>The default is to search the entire message log. "
762 "Use message-types as prefixes followed by (:) to filter more specific.\n"
763 "Valid message-type values are:</color> %s\n"
764 "\n"
765 "<color_white>Examples:</color>\n"
766 " <color_light_green>good</color><color_white>:mutation\n"
767 " :you pick up: 1</color>\n"
768 " <color_light_red>bad</color><color_white>:</color>\n"
769 "\n"
770 );
771 std::string type_text;
772 const auto &type_list = msg_type_and_names();
773 for( auto it = type_list.begin(); it != type_list.end(); ++it ) {
774 // Skip m_debug outside debug mode (but allow searching for it)
775 if( debug_mode || it->first != m_debug ) {
776 const auto &col_name = get_all_colors().get_name( msgtype_to_color( it->first ) );
777 auto next_it = std::next( it );
778 // Skip m_debug outside debug mode
779 if( !debug_mode && next_it != type_list.end() && next_it->first == m_debug ) {
780 next_it = std::next( next_it );
781 }
782 if( next_it != type_list.end() ) {
783 //~ the 2nd %s is a type name, this is used to format a list of type names
784 type_text += string_format( pgettext( "message log", "<color_%s>%s</color>, " ),
785 col_name, pgettext( "message type", it->second ) );
786 } else {
787 //~ the 2nd %s is a type name, this is used to format the last type name in a list of type names
788 type_text += string_format( pgettext( "message log", "<color_%s>%s</color>." ),
789 col_name, pgettext( "message type", it->second ) );
790 }
791 }
792 }
793 return foldstring( string_format( help_fmt, type_text ), width );
794 }
795
display_messages()796 void Messages::display_messages()
797 {
798 dialog dlg;
799 dlg.run();
800 player_messages.curmes = calendar::turn;
801 }
802
display_messages(const catacurses::window & ipk_target,const int left,const int top,const int right,const int bottom)803 void Messages::display_messages( const catacurses::window &ipk_target, const int left,
804 const int top, const int right, const int bottom )
805 {
806 if( !size() ) {
807 return;
808 }
809
810 const int maxlength = right - left;
811 int line = log_from_top ? top : bottom;
812
813 if( log_from_top ) {
814 for( int i = size() - 1; i >= 0; --i ) {
815 if( line > bottom ) {
816 break;
817 }
818
819 const game_message &m = player_messages.messages[i];
820 if( message_exceeds_ttl( m ) ) {
821 break;
822 }
823
824 const nc_color col = m.get_color( player_messages.curmes );
825 std::string message_text = m.get_with_count();
826 if( !m.is_recent( player_messages.curmes ) ) {
827 message_text = remove_color_tags( message_text );
828 }
829
830 for( const std::string &folded : foldstring( message_text, maxlength ) ) {
831 if( line > bottom ) {
832 break;
833 }
834 // Redrawing line to ensure new messages similar to previous
835 // messages will not be missed by screen readers
836 wredrawln( ipk_target, line, 1 );
837 nc_color col_out = col;
838 print_colored_text( ipk_target, point( left, line++ ), col_out, col, folded );
839 }
840 }
841 } else {
842 for( int i = size() - 1; i >= 0; --i ) {
843 if( line < top ) {
844 break;
845 }
846
847 const game_message &m = player_messages.messages[i];
848 if( message_exceeds_ttl( m ) ) {
849 break;
850 }
851
852 if( m.is_in_cooldown() ) {
853 // message is still (or was at some point) into a cooldown period.
854 continue;
855 }
856
857 const nc_color col = m.get_color( player_messages.curmes );
858 std::string message_text = m.get_with_count();
859 if( !m.is_recent( player_messages.curmes ) ) {
860 message_text = remove_color_tags( message_text );
861 }
862
863 const auto folded_strings = foldstring( message_text, maxlength );
864 const auto folded_rend = folded_strings.rend();
865 for( auto string_iter = folded_strings.rbegin();
866 string_iter != folded_rend && line >= top; ++string_iter, line-- ) {
867 // Redrawing line to ensure new messages similar to previous
868 // messages will not be missed by screen readers
869 wredrawln( ipk_target, line, 1 );
870 nc_color col_out = col;
871 print_colored_text( ipk_target, point( left, line ), col_out, col, *string_iter );
872 }
873 }
874 }
875
876 player_messages.curmes = calendar::turn;
877 }
878
add_msg(std::string msg)879 void add_msg( std::string msg )
880 {
881 Messages::add_msg( std::move( msg ) );
882 }
883
add_msg(const game_message_params & params,std::string msg)884 void add_msg( const game_message_params ¶ms, std::string msg )
885 {
886 Messages::add_msg( params, std::move( msg ) );
887 }
888
add_msg_if_player_sees(const tripoint & target,std::string msg)889 void add_msg_if_player_sees( const tripoint &target, std::string msg )
890 {
891 if( get_player_view().sees( target ) ) {
892 Messages::add_msg( std::move( msg ) );
893 }
894 }
895
add_msg_if_player_sees(const Creature & target,std::string msg)896 void add_msg_if_player_sees( const Creature &target, std::string msg )
897 {
898 if( get_player_view().sees( target ) ) {
899 Messages::add_msg( std::move( msg ) );
900 }
901 }
902
add_msg_if_player_sees(const tripoint & target,const game_message_params & params,std::string msg)903 void add_msg_if_player_sees( const tripoint &target, const game_message_params ¶ms,
904 std::string msg )
905 {
906 if( get_player_view().sees( target ) ) {
907 Messages::add_msg( params, std::move( msg ) );
908 }
909 }
910
add_msg_if_player_sees(const Creature & target,const game_message_params & params,std::string msg)911 void add_msg_if_player_sees( const Creature &target, const game_message_params ¶ms,
912 std::string msg )
913 {
914 if( get_player_view().sees( target ) ) {
915 Messages::add_msg( params, std::move( msg ) );
916 }
917 }
918