The Battle for Wesnoth  1.15.2+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 "utils/functional.hpp"
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
48 {
49 namespace dialogs
50 {
51 
52 /*WIKI
53  * @page = GUIWindowDefinitionWML
54  * @order = 3_chat_log
55  *
56  * == Settings manager ==
57  *
58  * This shows the settings manager
59  *
60  */
61 
62 
63 REGISTER_DIALOG(chat_log)
64 
65 // The model is an interface defining the data to be displayed or otherwise
66 // acted upon in the user interface.
68 {
69 public:
70  model(const vconfig& c, const replay& r)
71  : cfg(c)
72  , msg_label(nullptr)
73  , chat_log_history(r.build_chat_log())
74  , page(0)
75  , page_number()
76  , page_label()
77  , previous_page()
78  , next_page()
79  , filter()
80  , copy_button()
81  {
82  LOG_CHAT_LOG << "entering chat_log::model...\n";
83  LOG_CHAT_LOG << "finished chat_log::model...\n";
84  }
85 
88  const std::vector<chat_msg>& chat_log_history;
89  int page;
90  static const int COUNT_PER_PAGE = 100;
97 
99  {
100  msg_label->set_label("");
101  }
102 
103  int count_of_pages() const
104  {
105  int size = chat_log_history.size();
106  return (size % COUNT_PER_PAGE == 0) ? (size / COUNT_PER_PAGE)
107  : (size / COUNT_PER_PAGE) + 1;
108  }
109 
110  void stream_log(std::ostringstream& s,
111  int first,
112  int last,
113  bool raw = false)
114  {
115  if(first >= last) {
116  return;
117  }
118 
119  const std::string& lcfilter = utf8::lowercase(filter->get_value());
120  LOG_CHAT_LOG << "entering chat_log::model::stream_log\n";
121 
122  for(const auto & t : make_pair(chat_log_history.begin() + first,
123  chat_log_history.begin() + last))
124  {
125  const std::string& timestamp
127 
128  if(!lcfilter.empty()) {
129  const std::string& lcsample = utf8::lowercase(timestamp)
130  + utf8::lowercase(t.nick())
131  + utf8::lowercase(t.text());
132 
133  if(lcsample.find(lcfilter) == std::string::npos) {
134  continue;
135  }
136  }
137 
138  const std::string me_prefix = "/me";
139  const bool is_me = t.text().compare(0, me_prefix.size(),
140  me_prefix) == 0;
141 
142  std::string nick_prefix, nick_suffix;
143 
144  if(!raw) {
145  nick_prefix = "<span color=\"" + t.color() + "\">";
146  nick_suffix = "</span> ";
147  } else {
148  nick_suffix = " ";
149  }
150 
151  const std::string lbracket = raw ? "<" : "&lt;";
152  const std::string rbracket = raw ? ">" : "&gt;";
153 
154  //
155  // Chat line format:
156  //
157  // is_me == true: "<[TS] nick message text here>\n"
158  // is_me == false: "<[TS] nick> message text here\n"
159  //
160 
161  s << nick_prefix << lbracket;
162 
163  if(raw) {
164  s << timestamp
165  << t.nick();
166  } else {
167  s << font::escape_text(timestamp)
168  << font::escape_text(t.nick());
169  }
170 
171  if(is_me) {
172  if(!raw) {
173  s << font::escape_text(t.text().substr(3));
174  } else {
175  s << t.text().substr(3);
176  }
177  s << rbracket << nick_suffix;
178  } else {
179  // <[TS] nick> message text here
180  s << rbracket << nick_suffix;
181  if(!raw) {
182  s << font::escape_text(t.text());
183  } else {
184  s << t.text();
185  }
186  }
187 
188  s << '\n';
189  }
190  }
191 
192  void populate_chat_message_list(int first, int last)
193  {
194  std::ostringstream s;
195  stream_log(s, first, last);
196  msg_label->set_label(s.str());
197 
198  // It makes sense to always scroll to the bottom, since the newest messages are there.
199  // The only time this might not be desired is tabbing forward through the pages, since
200  // one might want to continue reading the conversation in order.
201  //
202  // TODO: look into implementing the above suggestion
203  dynamic_cast<scroll_label&>(*msg_label).scroll_vertical_scrollbar(scrollbar_base::END);
204  }
205 
206  void chat_message_list_to_clipboard(int first, int last)
207  {
208  std::ostringstream s;
209  stream_log(s, first, last, true);
211  }
212 };
213 
214 // The controller acts upon the model. It retrieves data from repositories,
215 // persists it, manipulates it, and determines how it will be displayed in the
216 // view.
218 {
219 public:
221  {
222  LOG_CHAT_LOG << "Entering chat_log::controller" << std::endl;
223  LOG_CHAT_LOG << "Exiting chat_log::controller" << std::endl;
224  }
225 
226  void next_page()
227  {
228  LOG_CHAT_LOG << "Entering chat_log::controller::next_page"
229  << std::endl;
230  if(model_.page >= model_.count_of_pages() - 1) {
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::next_page" << std::endl;
237  }
238 
240  {
241  LOG_CHAT_LOG << "Entering chat_log::controller::previous_page"
242  << std::endl;
243  if(model_.page == 0) {
244  return;
245  }
246  model_.page--;
247  LOG_CHAT_LOG << "Set page to " << model_.page + 1 << std::endl;
249  LOG_CHAT_LOG << "Exiting chat_log::controller::previous_page"
250  << std::endl;
251  }
252 
253  void filter()
254  {
255  LOG_CHAT_LOG << "Entering chat_log::controller::filter" << std::endl;
257  LOG_CHAT_LOG << "Exiting chat_log::controller::filter" << std::endl;
258  }
259 
261  {
263  << "Entering chat_log::controller::handle_page_number_changed"
264  << std::endl;
266  LOG_CHAT_LOG << "Set page to " << model_.page + 1 << std::endl;
269  << "Exiting chat_log::controller::handle_page_number_changed"
270  << std::endl;
271  }
272 
273  std::pair<int, int> calculate_log_line_range()
274  {
275  const int log_size = model_.chat_log_history.size();
276  const int page_size = model_.COUNT_PER_PAGE;
277 
278  const int page = model_.page;
279  const int count_of_pages = std::max(1, model_.count_of_pages());
280 
281  LOG_CHAT_LOG << "Page: " << page + 1 << " of " << count_of_pages
282  << '\n';
283 
284  const int first = page * page_size;
285  const int last = page < (count_of_pages - 1)
286  ? first + page_size
287  : log_size;
288 
289  LOG_CHAT_LOG << "First " << first << ", last " << last << '\n';
290 
291  return std::make_pair(first, last);
292  }
293 
294  void update_view_from_model(bool select_last_page = false)
295  {
296  LOG_CHAT_LOG << "Entering chat_log::controller::update_view_from_model"
297  << std::endl;
299  int size = model_.chat_log_history.size();
300  LOG_CHAT_LOG << "Number of chat messages: " << size << std::endl;
301  // determine count of pages
302  const int count_of_pages = std::max(1, model_.count_of_pages());
303  if(select_last_page) {
304  model_.page = count_of_pages - 1;
305  }
306  // get page
307  const int page = model_.page;
308  // determine first and last
309  const std::pair<int, int>& range = calculate_log_line_range();
310  const int first = range.first;
311  const int last = range.second;
312  // determine has previous, determine has next
313  bool has_next = page + 1 < count_of_pages;
314  bool has_previous = page > 0;
315  model_.previous_page->set_active(has_previous);
316  model_.next_page->set_active(has_next);
317  model_.populate_chat_message_list(first, last);
318  model_.page_number->set_value_range(1, count_of_pages);
319  model_.page_number->set_active(count_of_pages > 1);
320  LOG_CHAT_LOG << "Maximum value of page number slider: "
321  << count_of_pages << std::endl;
322  model_.page_number->set_value(page + 1);
323 
324  std::ostringstream cur_page_text;
325  cur_page_text << (page + 1) << '/' << std::max(1, count_of_pages);
326  model_.page_label->set_label(cur_page_text.str());
327 
328  LOG_CHAT_LOG << "Exiting chat_log::controller::update_view_from_model"
329  << std::endl;
330  }
331 
333  {
334  const std::pair<int, int>& range = calculate_log_line_range();
335  model_.chat_message_list_to_clipboard(range.first, range.second);
336  }
337 
338 private:
340 };
341 
342 
343 // The view is an interface that displays data (the model) and routes user
344 // commands to the controller to act upon that data.
346 {
347 public:
348  view(const vconfig& cfg, const replay& r) : model_(cfg, r), controller_(model_)
349  {
350  }
351 
352  void pre_show()
353  {
354  LOG_CHAT_LOG << "Entering chat_log::view::pre_show" << std::endl;
355  controller_.update_view_from_model(true);
356  LOG_CHAT_LOG << "Exiting chat_log::view::pre_show" << std::endl;
357  }
358 
360  {
361  controller_.handle_page_number_changed();
362  }
363 
364  void next_page()
365  {
366  controller_.next_page();
367  }
368 
370  {
371  controller_.previous_page();
372  }
373 
374  void filter()
375  {
376  controller_.filter();
377  }
378 
380  {
381  controller_.handle_copy_button_clicked();
382  }
383 
385  {
386  LOG_CHAT_LOG << "Entering chat_log::view::bind" << std::endl;
387  model_.msg_label = find_widget<styled_widget>(&window, "msg", false, true);
389  = find_widget<slider>(&window, "page_number", false, true);
392  std::bind(&view::handle_page_number_changed, this));
393 
395  = find_widget<button>(&window, "previous_page", false, true);
397  std::bind(&view::previous_page, this));
398 
399  model_.next_page = find_widget<button>(&window, "next_page", false, true);
401  std::bind(&view::next_page, this));
402 
403  model_.filter = find_widget<text_box>(&window, "filter", false, true);
405  std::bind(&view::filter, this));
406  window.keyboard_capture(model_.filter);
407 
408  model_.copy_button = find_widget<button>(&window, "copy", false, true);
412  this,
413  std::ref(window)));
415  model_.copy_button->set_active(false);
416  model_.copy_button->set_tooltip(_("Clipboard support not found, contact your packager"));
417  }
418 
419  model_.page_label = find_widget<styled_widget>(&window, "page_label", false, true);
420 
421  LOG_CHAT_LOG << "Exiting chat_log::view::bind" << std::endl;
422  }
423 
424 private:
427 };
428 
429 
430 chat_log::chat_log(const vconfig& cfg, const replay& r) : view_()
431 {
432  LOG_CHAT_LOG << "Entering chat_log::chat_log" << std::endl;
433  view_ = std::make_shared<view>(cfg, r);
434  LOG_CHAT_LOG << "Exiting chat_log::chat_log" << std::endl;
435 }
436 
437 std::shared_ptr<chat_log::view> chat_log::get_view()
438 {
439  return view_;
440 }
441 
443 {
444  LOG_CHAT_LOG << "Entering chat_log::pre_show" << std::endl;
445  view_->bind(window);
446  view_->pre_show();
447  LOG_CHAT_LOG << "Exiting chat_log::pre_show" << std::endl;
448 }
449 
450 } // namespace dialogs
451 } // namespace gui2
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:46
void handle_copy_button_clicked(window &)
Definition: chat_log.cpp:379
bool available()
Whether wesnoth was compiled with support for a clipboard.
Definition: clipboard.cpp:56
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:206
static lg::log_domain log_chat_log("chat_log")
Replay control code.
std::pair< int, int > calculate_log_line_range()
Definition: chat_log.cpp:273
virtual void pre_show(window &window) override
Inherited from modal_dialog.
Definition: chat_log.cpp:442
static const int COUNT_PER_PAGE
Definition: chat_log.cpp:90
Class for a single line text area.
Definition: text_box.hpp:121
styled_widget * page_label
Definition: chat_log.cpp:92
Generic file dialog.
Definition: field-fwd.hpp:22
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:192
static UNUSEDNOWARN std::string _(const char *str)
Definition: gettext.hpp:91
void set_value_range(int min_value, int max_value)
Definition: slider.cpp:254
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:248
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:233
std::shared_ptr< view > get_view()
Definition: chat_log.cpp:437
model(const vconfig &c, const replay &r)
Definition: chat_log.cpp:70
virtual void set_use_markup(bool use_markup)
Label showing a text.
Various uncategorised dialogs.
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:348
#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:88
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:53
std::string get_chat_timestamp(const std::time_t &t)
Definition: game.cpp:906
void update_view_from_model(bool select_last_page=false)
Definition: chat_log.cpp:294
void bind(window &window)
Definition: chat_log.cpp:384
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:35
void stream_log(std::ostringstream &s, int first, int last, bool raw=false)
Definition: chat_log.cpp:110
double t
Definition: astarsearch.cpp:64
static bool timestamp
Definition: log.cpp:45
chat_log(const vconfig &cfg, const replay &replay)
Definition: chat_log.cpp:430
A slider.
Definition: slider.hpp:33
A variable-expanding proxy for the config class.
Definition: variable.hpp:42
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:86
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:62