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