The Battle for Wesnoth  1.17.0-dev
chat_log.cpp
Go to the documentation of this file.
1 /*
2  Copyright (C) 2011 - 2021
3  by Yurii Chernyi <terraninfo@terraninfo.net>
4  Part of the Battle for Wesnoth Project https://www.wesnoth.org/
5 
6  This program is free software; you can redistribute it and/or modify
7  it under the terms of the GNU General Public License as published by
8  the Free Software Foundation; either version 2 of the License, or
9  (at your option) any later version.
10  This program is distributed in the hope that it will be useful,
11  but WITHOUT ANY WARRANTY.
12 
13  See the COPYING file for more details.
14 */
15 
16 #define GETTEXT_DOMAIN "wesnoth-lib"
17 
18 #include "gui/dialogs/chat_log.hpp"
19 
21 #include "gui/widgets/button.hpp"
22 #include "gui/widgets/listbox.hpp"
23 #include "gui/widgets/settings.hpp"
24 #include "gui/widgets/text_box.hpp"
25 #include "gui/widgets/window.hpp"
27 #include "gui/widgets/slider.hpp"
28 
29 #include "font/pango/escape.hpp"
30 #include "desktop/clipboard.hpp"
32 #include "preferences/game.hpp"
33 #include "log.hpp"
34 #include "replay.hpp"
35 #include "gettext.hpp"
36 
37 #include <functional>
38 #include "utils/iterable_pair.hpp"
39 
40 #include <vector>
41 
42 static lg::log_domain log_chat_log("chat_log");
43 #define DBG_CHAT_LOG LOG_STREAM(debug, log_chat_log)
44 #define LOG_CHAT_LOG LOG_STREAM(info, log_chat_log)
45 #define WRN_CHAT_LOG LOG_STREAM(warn, log_chat_log)
46 #define ERR_CHAT_LOG LOG_STREAM(err, log_chat_log)
47 
48 namespace gui2::dialogs
49 {
50 
51 REGISTER_DIALOG(chat_log)
52 
53 // The model is an interface defining the data to be displayed or otherwise
54 // acted upon in the user interface.
56 {
57 public:
58  model(const vconfig& c, const replay& r)
59  : cfg(c)
60  , msg_label(nullptr)
61  , chat_log_history(r.build_chat_log())
62  , page(0)
63  , page_number()
64  , page_label()
65  , previous_page()
66  , next_page()
67  , filter()
68  , copy_button()
69  {
70  LOG_CHAT_LOG << "entering chat_log::model...\n";
71  LOG_CHAT_LOG << "finished chat_log::model...\n";
72  }
73 
76  const std::vector<chat_msg>& chat_log_history;
77  int page;
78  static const int COUNT_PER_PAGE = 100;
85 
87  {
88  msg_label->set_label("");
89  }
90 
91  int count_of_pages() const
92  {
93  int size = chat_log_history.size();
94  return (size % COUNT_PER_PAGE == 0) ? (size / COUNT_PER_PAGE)
95  : (size / COUNT_PER_PAGE) + 1;
96  }
97 
98  void stream_log(std::ostringstream& s,
99  int first,
100  int last,
101  bool raw = false)
102  {
103  if(first >= last) {
104  return;
105  }
106 
107  const std::string& lcfilter = utf8::lowercase(filter->get_value());
108  LOG_CHAT_LOG << "entering chat_log::model::stream_log\n";
109 
110  for(const auto & t : make_pair(chat_log_history.begin() + first,
111  chat_log_history.begin() + last))
112  {
113  const std::string& timestamp
115 
116  if(!lcfilter.empty()) {
117  const std::string& lcsample = utf8::lowercase(timestamp)
118  + utf8::lowercase(t.nick())
119  + utf8::lowercase(t.text());
120 
121  if(lcsample.find(lcfilter) == std::string::npos) {
122  continue;
123  }
124  }
125 
126  const std::string me_prefix = "/me";
127  const bool is_me = t.text().compare(0, me_prefix.size(),
128  me_prefix) == 0;
129 
130  std::string nick_prefix, nick_suffix;
131 
132  if(!raw) {
133  nick_prefix = "<span color=\"" + t.color() + "\">";
134  nick_suffix = "</span> ";
135  } else {
136  nick_suffix = " ";
137  }
138 
139  const std::string lbracket = raw ? "<" : "&lt;";
140  const std::string rbracket = raw ? ">" : "&gt;";
141 
142  //
143  // Chat line format:
144  //
145  // is_me == true: "<[TS] nick message text here>\n"
146  // is_me == false: "<[TS] nick> message text here\n"
147  //
148 
149  s << nick_prefix << lbracket;
150 
151  if(raw) {
152  s << timestamp
153  << t.nick();
154  } else {
155  s << font::escape_text(timestamp)
156  << font::escape_text(t.nick());
157  }
158 
159  if(is_me) {
160  if(!raw) {
161  s << font::escape_text(t.text().substr(3));
162  } else {
163  s << t.text().substr(3);
164  }
165  s << rbracket << nick_suffix;
166  } else {
167  // <[TS] nick> message text here
168  s << rbracket << nick_suffix;
169  if(!raw) {
170  s << font::escape_text(t.text());
171  } else {
172  s << t.text();
173  }
174  }
175 
176  s << '\n';
177  }
178  }
179 
180  void populate_chat_message_list(int first, int last)
181  {
182  std::ostringstream s;
183  stream_log(s, first, last);
184  msg_label->set_label(s.str());
185 
186  // It makes sense to always scroll to the bottom, since the newest messages are there.
187  // The only time this might not be desired is tabbing forward through the pages, since
188  // one might want to continue reading the conversation in order.
189  //
190  // TODO: look into implementing the above suggestion
191  dynamic_cast<scroll_label&>(*msg_label).scroll_vertical_scrollbar(scrollbar_base::END);
192  }
193 
194  void chat_message_list_to_clipboard(int first, int last)
195  {
196  std::ostringstream s;
197  stream_log(s, first, last, true);
199  }
200 };
201 
202 // The controller acts upon the model. It retrieves data from repositories,
203 // persists it, manipulates it, and determines how it will be displayed in the
204 // view.
206 {
207 public:
209  {
210  LOG_CHAT_LOG << "Entering chat_log::controller" << std::endl;
211  LOG_CHAT_LOG << "Exiting chat_log::controller" << std::endl;
212  }
213 
214  void next_page()
215  {
216  LOG_CHAT_LOG << "Entering chat_log::controller::next_page"
217  << std::endl;
218  if(model_.page >= model_.count_of_pages() - 1) {
219  return;
220  }
221  model_.page++;
222  LOG_CHAT_LOG << "Set page to " << model_.page + 1 << std::endl;
224  LOG_CHAT_LOG << "Exiting chat_log::controller::next_page" << std::endl;
225  }
226 
228  {
229  LOG_CHAT_LOG << "Entering chat_log::controller::previous_page"
230  << std::endl;
231  if(model_.page == 0) {
232  return;
233  }
234  model_.page--;
235  LOG_CHAT_LOG << "Set page to " << model_.page + 1 << std::endl;
237  LOG_CHAT_LOG << "Exiting chat_log::controller::previous_page"
238  << std::endl;
239  }
240 
241  void filter()
242  {
243  LOG_CHAT_LOG << "Entering chat_log::controller::filter" << std::endl;
245  LOG_CHAT_LOG << "Exiting chat_log::controller::filter" << std::endl;
246  }
247 
249  {
251  << "Entering chat_log::controller::handle_page_number_changed"
252  << std::endl;
254  LOG_CHAT_LOG << "Set page to " << model_.page + 1 << std::endl;
257  << "Exiting chat_log::controller::handle_page_number_changed"
258  << std::endl;
259  }
260 
261  std::pair<int, int> calculate_log_line_range()
262  {
263  const int log_size = model_.chat_log_history.size();
264  const int page_size = model_.COUNT_PER_PAGE;
265 
266  const int page = model_.page;
267  const int count_of_pages = std::max(1, model_.count_of_pages());
268 
269  LOG_CHAT_LOG << "Page: " << page + 1 << " of " << count_of_pages
270  << '\n';
271 
272  const int first = page * page_size;
273  const int last = page < (count_of_pages - 1)
274  ? first + page_size
275  : log_size;
276 
277  LOG_CHAT_LOG << "First " << first << ", last " << last << '\n';
278 
279  return std::pair(first, last);
280  }
281 
282  void update_view_from_model(bool select_last_page = false)
283  {
284  LOG_CHAT_LOG << "Entering chat_log::controller::update_view_from_model"
285  << std::endl;
287  int size = model_.chat_log_history.size();
288  LOG_CHAT_LOG << "Number of chat messages: " << size << std::endl;
289  // determine count of pages
290  const int count_of_pages = std::max(1, model_.count_of_pages());
291  if(select_last_page) {
292  model_.page = count_of_pages - 1;
293  }
294  // get page
295  const int page = model_.page;
296  // determine first and last
297  const std::pair<int, int>& range = calculate_log_line_range();
298  const int first = range.first;
299  const int last = range.second;
300  // determine has previous, determine has next
301  bool has_next = page + 1 < count_of_pages;
302  bool has_previous = page > 0;
303  model_.previous_page->set_active(has_previous);
304  model_.next_page->set_active(has_next);
305  model_.populate_chat_message_list(first, last);
306  model_.page_number->set_value_range(1, count_of_pages);
307  model_.page_number->set_active(count_of_pages > 1);
308  LOG_CHAT_LOG << "Maximum value of page number slider: "
309  << count_of_pages << std::endl;
310  model_.page_number->set_value(page + 1);
311 
312  std::ostringstream cur_page_text;
313  cur_page_text << (page + 1) << '/' << std::max(1, count_of_pages);
314  model_.page_label->set_label(cur_page_text.str());
315 
316  LOG_CHAT_LOG << "Exiting chat_log::controller::update_view_from_model"
317  << std::endl;
318  }
319 
321  {
322  const std::pair<int, int>& range = calculate_log_line_range();
323  model_.chat_message_list_to_clipboard(range.first, range.second);
324  }
325 
326 private:
328 };
329 
330 
331 // The view is an interface that displays data (the model) and routes user
332 // commands to the controller to act upon that data.
334 {
335 public:
336  view(const vconfig& cfg, const replay& r) : model_(cfg, r), controller_(model_)
337  {
338  }
339 
340  void pre_show()
341  {
342  LOG_CHAT_LOG << "Entering chat_log::view::pre_show" << std::endl;
343  controller_.update_view_from_model(true);
344  LOG_CHAT_LOG << "Exiting chat_log::view::pre_show" << std::endl;
345  }
346 
348  {
349  controller_.handle_page_number_changed();
350  }
351 
352  void next_page()
353  {
354  controller_.next_page();
355  }
356 
358  {
359  controller_.previous_page();
360  }
361 
362  void filter()
363  {
364  controller_.filter();
365  }
366 
368  {
369  controller_.handle_copy_button_clicked();
370  }
371 
373  {
374  LOG_CHAT_LOG << "Entering chat_log::view::bind" << std::endl;
375  model_.msg_label = find_widget<styled_widget>(&window, "msg", false, true);
377  = find_widget<slider>(&window, "page_number", false, true);
380  std::bind(&view::handle_page_number_changed, this));
381 
383  = find_widget<button>(&window, "previous_page", false, true);
385  std::bind(&view::previous_page, this));
386 
387  model_.next_page = find_widget<button>(&window, "next_page", false, true);
389  std::bind(&view::next_page, this));
390 
391  model_.filter = find_widget<text_box>(&window, "filter", false, true);
393  std::bind(&view::filter, this));
394  window.keyboard_capture(model_.filter);
395 
396  model_.copy_button = find_widget<button>(&window, "copy", false, true);
400  this,
401  std::ref(window)));
403  model_.copy_button->set_active(false);
404  model_.copy_button->set_tooltip(_("Clipboard support not found, contact your packager"));
405  }
406 
407  model_.page_label = find_widget<styled_widget>(&window, "page_label", false, true);
408 
409  LOG_CHAT_LOG << "Exiting chat_log::view::bind" << std::endl;
410  }
411 
412 private:
415 };
416 
417 
418 chat_log::chat_log(const vconfig& cfg, const replay& r) : view_()
419 {
420  LOG_CHAT_LOG << "Entering chat_log::chat_log" << std::endl;
421  view_ = std::make_shared<view>(cfg, r);
422  LOG_CHAT_LOG << "Exiting chat_log::chat_log" << std::endl;
423 }
424 
425 std::shared_ptr<chat_log::view> chat_log::get_view() const
426 {
427  return view_;
428 }
429 
431 {
432  LOG_CHAT_LOG << "Entering chat_log::pre_show" << std::endl;
433  view_->bind(window);
434  view_->pre_show();
435  LOG_CHAT_LOG << "Exiting chat_log::pre_show" << std::endl;
436 }
437 
438 } // namespace dialogs
void set_text_changed_callback(std::function< void(text_box_base *textbox, const std::string text)> cb)
Set the text_changed callback.
std::shared_ptr< view > view_
Definition: chat_log.hpp:43
void handle_copy_button_clicked(window &)
Definition: chat_log.cpp:367
bool available()
Whether wesnoth was compiled with support for a clipboard.
Definition: clipboard.cpp:55
std::string get_value() const
This file contains the window object, this object is a top level container which has the event manage...
void chat_message_list_to_clipboard(int first, int last)
Definition: chat_log.cpp:194
static lg::log_domain log_chat_log("chat_log")
Replay control code.
std::pair< int, int > calculate_log_line_range()
Definition: chat_log.cpp:261
static std::string _(const char *str)
Definition: gettext.hpp:93
virtual void pre_show(window &window) override
Actions to be taken before showing the window.
Definition: chat_log.cpp:430
static const int COUNT_PER_PAGE
Definition: chat_log.cpp:78
Class for a single line text area.
Definition: text_box.hpp:141
styled_widget * page_label
Definition: chat_log.cpp:80
Go to the end position.
Definition: scrollbar.hpp:61
virtual void set_label(const t_string &label)
virtual void set_active(const bool active) override
See styled_widget::set_active.
virtual void connect_click_handler(const event::signal_function &signal) override
Inherited from clickable_item.
Definition: button.hpp:53
void populate_chat_message_list(int first, int last)
Definition: chat_log.cpp:180
void set_value_range(int min_value, int max_value)
Definition: slider.cpp:249
std::size_t size(const std::string &str)
Length in characters of a UTF-8 string.
Definition: unicode.cpp:87
void set_tooltip(const t_string &tooltip)
void connect_signal_notify_modified(dispatcher &dispatcher, const signal_notification_function &signal)
Connects a signal handler for getting a notification upon modification.
Definition: dispatcher.cpp:187
This file contains the settings handling of the widget library.
void connect_signal_mouse_left_click(dispatcher &dispatcher, const signal_function &signal)
Connects a signal handler for a left mouse button click.
Definition: dispatcher.cpp:172
model(const vconfig &c, const replay &r)
Definition: chat_log.cpp:58
virtual void set_use_markup(bool use_markup)
Label showing a text.
std::shared_ptr< view > get_view() const
Definition: chat_log.cpp:425
std::string escape_text(const std::string &text)
Escapes the pango markup characters in a text.
Definition: escape.hpp:33
view(const vconfig &cfg, const replay &r)
Definition: chat_log.cpp:336
#define LOG_CHAT_LOG
Definition: chat_log.cpp:44
static map_location::DIRECTION s
void scroll_vertical_scrollbar(const scrollbar_base::scroll_mode scroll)
Scrolls the vertical scrollbar.
const std::vector< chat_msg > & chat_log_history
Definition: chat_log.cpp:76
Base class for all visible items.
std::string lowercase(const std::string &s)
Returns a lowercased version of the string.
Definition: unicode.cpp:52
virtual int get_value() const override
Inherited from integer_selector.
Definition: slider.hpp:79
std::string get_chat_timestamp(const std::time_t &t)
Definition: game.cpp:877
void update_view_from_model(bool select_last_page=false)
Definition: chat_log.cpp:282
void bind(window &window)
Definition: chat_log.cpp:372
virtual void set_active(const bool active) override
See styled_widget::set_active.
Definition: button.cpp:63
void copy_to_clipboard(const std::string &text, const bool)
Copies text to the clipboard.
Definition: clipboard.cpp:34
void stream_log(std::ostringstream &s, int first, int last, bool raw=false)
Definition: chat_log.cpp:98
double t
Definition: astarsearch.cpp:65
static bool timestamp
Definition: log.cpp:44
chat_log(const vconfig &cfg, const replay &replay)
Definition: chat_log.cpp:418
A slider is a control that can select a value by moving a grip on a groove.
Definition: slider.hpp:59
A variable-expanding proxy for the config class.
Definition: variable.hpp:44
Simple push button.
Definition: button.hpp:36
Standard logging facilities (interface).
virtual void set_value(int value) override
Inherited from integer_selector.
Definition: slider.cpp:81
mock_char c
base class of top level items, the only item which needs to store the final canvases to draw on...
Definition: window.hpp:65