The Battle for Wesnoth  1.17.10+dev
chat_log.cpp
Go to the documentation of this file.
1 /*
2  Copyright (C) 2011 - 2022
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...";
71  LOG_CHAT_LOG << "finished chat_log::model...";
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";
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";
211  LOG_CHAT_LOG << "Exiting chat_log::controller";
212  }
213 
214  void next_page()
215  {
216  LOG_CHAT_LOG << "Entering chat_log::controller::next_page";
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;
223  LOG_CHAT_LOG << "Exiting chat_log::controller::next_page";
224  }
225 
227  {
228  LOG_CHAT_LOG << "Entering chat_log::controller::previous_page";
229  if(model_.page == 0) {
230  return;
231  }
232  model_.page--;
233  LOG_CHAT_LOG << "Set page to " << model_.page + 1;
235  LOG_CHAT_LOG << "Exiting chat_log::controller::previous_page";
236  }
237 
238  void filter()
239  {
240  LOG_CHAT_LOG << "Entering chat_log::controller::filter";
242  LOG_CHAT_LOG << "Exiting chat_log::controller::filter";
243  }
244 
246  {
248  << "Entering chat_log::controller::handle_page_number_changed";
250  LOG_CHAT_LOG << "Set page to " << model_.page + 1;
253  << "Exiting chat_log::controller::handle_page_number_changed";
254  }
255 
256  std::pair<int, int> calculate_log_line_range()
257  {
258  const int log_size = model_.chat_log_history.size();
259  const int page_size = model_.COUNT_PER_PAGE;
260 
261  const int page = model_.page;
262  const int count_of_pages = std::max(1, model_.count_of_pages());
263 
264  LOG_CHAT_LOG << "Page: " << page + 1 << " of " << count_of_pages;
265 
266  const int first = page * page_size;
267  const int last = page < (count_of_pages - 1)
268  ? first + page_size
269  : log_size;
270 
271  LOG_CHAT_LOG << "First " << first << ", last " << last;
272 
273  return std::pair(first, last);
274  }
275 
276  void update_view_from_model(bool select_last_page = false)
277  {
279  << "Entering chat_log::controller::update_view_from_model";
281  int size = model_.chat_log_history.size();
282  LOG_CHAT_LOG << "Number of chat messages: " << size;
283  // determine count of pages
284  const int count_of_pages = std::max(1, model_.count_of_pages());
285  if(select_last_page) {
286  model_.page = count_of_pages - 1;
287  }
288  // get page
289  const int page = model_.page;
290  // determine first and last
291  const std::pair<int, int>& range = calculate_log_line_range();
292  const int first = range.first;
293  const int last = range.second;
294  // determine has previous, determine has next
295  bool has_next = page + 1 < count_of_pages;
296  bool has_previous = page > 0;
297  model_.previous_page->set_active(has_previous);
298  model_.next_page->set_active(has_next);
299  model_.populate_chat_message_list(first, last);
300  model_.page_number->set_value_range(1, count_of_pages);
301  model_.page_number->set_active(count_of_pages > 1);
303  << "Maximum value of page number slider: " << count_of_pages;
304  model_.page_number->set_value(page + 1);
305 
306  std::ostringstream cur_page_text;
307  cur_page_text << (page + 1) << '/' << std::max(1, count_of_pages);
308  model_.page_label->set_label(cur_page_text.str());
309 
311  << "Exiting chat_log::controller::update_view_from_model";
312  }
313 
315  {
316  const std::pair<int, int>& range = calculate_log_line_range();
317  model_.chat_message_list_to_clipboard(range.first, range.second);
318  }
319 
320 private:
322 };
323 
324 
325 // The view is an interface that displays data (the model) and routes user
326 // commands to the controller to act upon that data.
328 {
329 public:
330  view(const vconfig& cfg, const replay& r) : model_(cfg, r), controller_(model_)
331  {
332  }
333 
334  void pre_show()
335  {
336  LOG_CHAT_LOG << "Entering chat_log::view::pre_show";
337  controller_.update_view_from_model(true);
338  LOG_CHAT_LOG << "Exiting chat_log::view::pre_show";
339  }
340 
342  {
343  controller_.handle_page_number_changed();
344  }
345 
346  void next_page()
347  {
348  controller_.next_page();
349  }
350 
352  {
353  controller_.previous_page();
354  }
355 
356  void filter()
357  {
358  controller_.filter();
359  }
360 
362  {
363  controller_.handle_copy_button_clicked();
364  }
365 
367  {
368  LOG_CHAT_LOG << "Entering chat_log::view::bind";
369  model_.msg_label = find_widget<styled_widget>(&window, "msg", false, true);
371  = find_widget<slider>(&window, "page_number", false, true);
374  std::bind(&view::handle_page_number_changed, this));
375 
377  = find_widget<button>(&window, "previous_page", false, true);
379  std::bind(&view::previous_page, this));
380 
381  model_.next_page = find_widget<button>(&window, "next_page", false, true);
383  std::bind(&view::next_page, this));
384 
385  model_.filter = find_widget<text_box>(&window, "filter", false, true);
387  std::bind(&view::filter, this));
388  window.keyboard_capture(model_.filter);
389 
390  model_.copy_button = find_widget<button>(&window, "copy", false, true);
394  this,
395  std::ref(window)));
397  model_.copy_button->set_active(false);
398  model_.copy_button->set_tooltip(_("Clipboard support not found, contact your packager"));
399  }
400 
401  model_.page_label = find_widget<styled_widget>(&window, "page_label", false, true);
402 
403  LOG_CHAT_LOG << "Exiting chat_log::view::bind";
404  }
405 
406 private:
409 };
410 
411 
412 chat_log::chat_log(const vconfig& cfg, const replay& r)
414  , view_()
415 {
416  LOG_CHAT_LOG << "Entering chat_log::chat_log";
417  view_ = std::make_shared<view>(cfg, r);
418  LOG_CHAT_LOG << "Exiting chat_log::chat_log";
419 }
420 
421 std::shared_ptr<chat_log::view> chat_log::get_view() const
422 {
423  return view_;
424 }
425 
427 {
428  LOG_CHAT_LOG << "Entering chat_log::pre_show";
429  view_->bind(window);
430  view_->pre_show();
431  LOG_CHAT_LOG << "Exiting chat_log::pre_show";
432 }
433 
434 } // namespace dialogs
window(const builder_window::window_resolution &definition)
< Needs to be initialized in show.
Definition: window.cpp:263
void set_text_changed_callback(std::function< void(text_box_base *textbox, const std::string text)> cb)
Set the text_changed callback.
virtual const std::string & window_id() const override
The ID of the window to build.
std::shared_ptr< view > view_
Definition: chat_log.hpp:43
void handle_copy_button_clicked(window &)
Definition: chat_log.cpp:361
bool available()
Whether wesnoth was compiled with support for a clipboard.
Definition: clipboard.cpp:55
void connect_signal_mouse_left_click(dispatcher &dispatcher, const signal &signal)
Connects a signal handler for a left mouse button click.
Definition: dispatcher.cpp:179
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:256
static std::string _(const char *str)
Definition: gettext.hpp:93
virtual void connect_click_handler(const event::signal &signal) override
Inherited from clickable_item.
Definition: button.hpp:53
virtual void pre_show(window &window) override
Actions to be taken before showing the window.
Definition: chat_log.cpp:426
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.
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:251
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)
This file contains the settings handling of the widget library.
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:421
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:330
#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:864
void update_view_from_model(bool select_last_page=false)
Definition: chat_log.cpp:276
void bind(window &window)
Definition: chat_log.cpp:366
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:56
chat_log(const vconfig &cfg, const replay &replay)
Definition: chat_log.cpp:412
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
Abstract base class for all modal dialogs.
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:82
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:66
void connect_signal_notify_modified(dispatcher &dispatcher, const signal_notification &signal)
Connects a signal handler for getting a notification upon modification.
Definition: dispatcher.cpp:205