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__anon65ab38430111::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__anon65ab38430111::game_message56     const time_point &turn() const {
57         return timestamp_in_turns;
58     }
59 
get_with_count__anon65ab38430111::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__anon65ab38430111::game_message71     bool is_in_cooldown() const {
72         return cooldown_hidden;
73     }
74 
is_new__anon65ab38430111::game_message75     bool is_new( const time_point &current ) const {
76         return turn() >= current;
77     }
78 
is_recent__anon65ab38430111::game_message79     bool is_recent( const time_point &current ) const {
80         return turn() + 5_turns >= current;
81     }
82 
get_color__anon65ab38430111::game_message83     nc_color get_color( const time_point &current ) 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__anon65ab38430111::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__anon65ab38430111::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 &params ) {
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 &params, 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 &params, 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 &params,
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 &params,
912                              std::string msg )
913 {
914     if( get_player_view().sees( target ) ) {
915         Messages::add_msg( params, std::move( msg ) );
916     }
917 }
918