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