The Battle for Wesnoth  1.19.0+dev
chat_log.cpp
Go to the documentation of this file.
1 /*
2  Copyright (C) 2011 - 2024
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/text_box.hpp"
23 #include "gui/widgets/window.hpp"
25 #include "gui/widgets/slider.hpp"
26 
27 #include "font/pango/escape.hpp"
28 #include "desktop/clipboard.hpp"
31 #include "log.hpp"
32 #include "replay.hpp"
33 #include "gettext.hpp"
34 
35 #include <functional>
36 #include "utils/iterable_pair.hpp"
37 
38 #include <vector>
39 
40 static lg::log_domain log_chat_log("chat_log");
41 #define DBG_CHAT_LOG LOG_STREAM(debug, log_chat_log)
42 #define LOG_CHAT_LOG LOG_STREAM(info, log_chat_log)
43 #define WRN_CHAT_LOG LOG_STREAM(warn, log_chat_log)
44 #define ERR_CHAT_LOG LOG_STREAM(err, log_chat_log)
45 
46 namespace gui2::dialogs
47 {
48 
49 REGISTER_DIALOG(chat_log)
50 
51 // The model is an interface defining the data to be displayed or otherwise
52 // acted upon in the user interface.
54 {
55 public:
56  model(const vconfig& c, const replay& r)
57  : cfg(c)
58  , msg_label(nullptr)
59  , chat_log_history(r.build_chat_log())
60  , page(0)
61  , page_number()
62  , page_label()
63  , previous_page()
64  , next_page()
65  , filter()
66  , copy_button()
67  {
68  LOG_CHAT_LOG << "entering chat_log::model...";
69  LOG_CHAT_LOG << "finished chat_log::model...";
70  }
71 
74  const std::vector<chat_msg>& chat_log_history;
75  int page;
76  static const int COUNT_PER_PAGE = 100;
83 
85  {
86  msg_label->set_label("");
87  }
88 
89  int count_of_pages() const
90  {
91  int size = chat_log_history.size();
92  return (size % COUNT_PER_PAGE == 0) ? (size / COUNT_PER_PAGE)
93  : (size / COUNT_PER_PAGE) + 1;
94  }
95 
96  void stream_log(std::ostringstream& s,
97  int first,
98  int last,
99  bool raw = false)
100  {
101  if(first >= last) {
102  return;
103  }
104 
105  const std::string& lcfilter = utf8::lowercase(filter->get_value());
106  LOG_CHAT_LOG << "entering chat_log::model::stream_log";
107 
108  for(const auto & t : make_pair(chat_log_history.begin() + first,
109  chat_log_history.begin() + last))
110  {
111  const std::string& timestamp
112  = prefs::get().get_chat_timestamp(t.time());
113 
114  if(!lcfilter.empty()) {
115  const std::string& lcsample = utf8::lowercase(timestamp)
116  + utf8::lowercase(t.nick())
117  + utf8::lowercase(t.text());
118 
119  if(lcsample.find(lcfilter) == std::string::npos) {
120  continue;
121  }
122  }
123 
124  const std::string me_prefix = "/me";
125  const bool is_me = t.text().compare(0, me_prefix.size(),
126  me_prefix) == 0;
127 
128  std::string nick_prefix, nick_suffix;
129 
130  if(!raw) {
131  nick_prefix = "<span color=\"" + t.color() + "\">";
132  nick_suffix = "</span> ";
133  } else {
134  nick_suffix = " ";
135  }
136 
137  const std::string lbracket = raw ? "<" : "&lt;";
138  const std::string rbracket = raw ? ">" : "&gt;";
139 
140  //
141  // Chat line format:
142  //
143  // is_me == true: "<[TS] nick message text here>\n"
144  // is_me == false: "<[TS] nick> message text here\n"
145  //
146 
147  s << nick_prefix << lbracket;
148 
149  if(raw) {
150  s << timestamp
151  << t.nick();
152  } else {
154  << font::escape_text(t.nick());
155  }
156 
157  if(is_me) {
158  if(!raw) {
159  s << font::escape_text(t.text().substr(3));
160  } else {
161  s << t.text().substr(3);
162  }
163  s << rbracket << nick_suffix;
164  } else {
165  // <[TS] nick> message text here
166  s << rbracket << nick_suffix;
167  if(!raw) {
168  s << font::escape_text(t.text());
169  } else {
170  s << t.text();
171  }
172  }
173 
174  s << '\n';
175  }
176  }
177 
178  void populate_chat_message_list(int first, int last)
179  {
180  std::ostringstream s;
181  stream_log(s, first, last);
182  msg_label->set_label(s.str());
183 
184  // It makes sense to always scroll to the bottom, since the newest messages are there.
185  // The only time this might not be desired is tabbing forward through the pages, since
186  // one might want to continue reading the conversation in order.
187  //
188  // TODO: look into implementing the above suggestion
189  dynamic_cast<scroll_label&>(*msg_label).scroll_vertical_scrollbar(scrollbar_base::END);
190  }
191 
192  void chat_message_list_to_clipboard(int first, int last)
193  {
194  std::ostringstream s;
195  stream_log(s, first, last, true);
197  }
198 };
199 
200 // The controller acts upon the model. It retrieves data from repositories,
201 // persists it, manipulates it, and determines how it will be displayed in the
202 // view.
204 {
205 public:
207  {
208  LOG_CHAT_LOG << "Entering chat_log::controller";
209  LOG_CHAT_LOG << "Exiting chat_log::controller";
210  }
211 
212  void next_page()
213  {
214  LOG_CHAT_LOG << "Entering chat_log::controller::next_page";
215  if(model_.page >= model_.count_of_pages() - 1) {
216  return;
217  }
218  model_.page++;
219  LOG_CHAT_LOG << "Set page to " << model_.page + 1;
221  LOG_CHAT_LOG << "Exiting chat_log::controller::next_page";
222  }
223 
225  {
226  LOG_CHAT_LOG << "Entering chat_log::controller::previous_page";
227  if(model_.page == 0) {
228  return;
229  }
230  model_.page--;
231  LOG_CHAT_LOG << "Set page to " << model_.page + 1;
233  LOG_CHAT_LOG << "Exiting chat_log::controller::previous_page";
234  }
235 
236  void filter()
237  {
238  LOG_CHAT_LOG << "Entering chat_log::controller::filter";
240  LOG_CHAT_LOG << "Exiting chat_log::controller::filter";
241  }
242 
244  {
246  << "Entering chat_log::controller::handle_page_number_changed";
248  LOG_CHAT_LOG << "Set page to " << model_.page + 1;
251  << "Exiting chat_log::controller::handle_page_number_changed";
252  }
253 
254  std::pair<int, int> calculate_log_line_range()
255  {
256  const int log_size = model_.chat_log_history.size();
257  const int page_size = model_.COUNT_PER_PAGE;
258 
259  const int page = model_.page;
260  const int count_of_pages = std::max(1, model_.count_of_pages());
261 
262  LOG_CHAT_LOG << "Page: " << page + 1 << " of " << count_of_pages;
263 
264  const int first = page * page_size;
265  const int last = page < (count_of_pages - 1)
266  ? first + page_size
267  : log_size;
268 
269  LOG_CHAT_LOG << "First " << first << ", last " << last;
270 
271  return std::pair(first, last);
272  }
273 
274  void update_view_from_model(bool select_last_page = false)
275  {
277  << "Entering chat_log::controller::update_view_from_model";
279  int size = model_.chat_log_history.size();
280  LOG_CHAT_LOG << "Number of chat messages: " << size;
281  // determine count of pages
282  const int count_of_pages = std::max(1, model_.count_of_pages());
283  if(select_last_page) {
284  model_.page = count_of_pages - 1;
285  }
286  // get page
287  const int page = model_.page;
288  // determine first and last
289  const std::pair<int, int>& range = calculate_log_line_range();
290  const int first = range.first;
291  const int last = range.second;
292  // determine has previous, determine has next
293  bool has_next = page + 1 < count_of_pages;
294  bool has_previous = page > 0;
295  model_.previous_page->set_active(has_previous);
296  model_.next_page->set_active(has_next);
297  model_.populate_chat_message_list(first, last);
298  model_.page_number->set_value_range(1, count_of_pages);
299  model_.page_number->set_active(count_of_pages > 1);
301  << "Maximum value of page number slider: " << count_of_pages;
302  model_.page_number->set_value(page + 1);
303 
304  std::ostringstream cur_page_text;
305  cur_page_text << (page + 1) << '/' << std::max(1, count_of_pages);
306  model_.page_label->set_label(cur_page_text.str());
307 
309  << "Exiting chat_log::controller::update_view_from_model";
310  }
311 
313  {
314  const std::pair<int, int>& range = calculate_log_line_range();
315  model_.chat_message_list_to_clipboard(range.first, range.second);
316  }
317 
318 private:
320 };
321 
322 
323 // The view is an interface that displays data (the model) and routes user
324 // commands to the controller to act upon that data.
326 {
327 public:
328  view(const vconfig& cfg, const replay& r) : model_(cfg, r), controller_(model_)
329  {
330  }
331 
332  void pre_show()
333  {
334  LOG_CHAT_LOG << "Entering chat_log::view::pre_show";
336  LOG_CHAT_LOG << "Exiting chat_log::view::pre_show";
337  }
338 
340  {
342  }
343 
344  void next_page()
345  {
347  }
348 
350  {
352  }
353 
354  void filter()
355  {
357  }
358 
360  {
362  }
363 
365  {
366  LOG_CHAT_LOG << "Entering chat_log::view::bind";
367  model_.msg_label = find_widget<styled_widget>(&window, "msg", false, true);
369  = find_widget<slider>(&window, "page_number", false, true);
372  std::bind(&view::handle_page_number_changed, this));
373 
375  = find_widget<button>(&window, "previous_page", false, true);
377  std::bind(&view::previous_page, this));
378 
379  model_.next_page = find_widget<button>(&window, "next_page", false, true);
381  std::bind(&view::next_page, this));
382 
383  model_.filter = find_widget<text_box>(&window, "filter", false, true);
385  std::bind(&view::filter, this));
387 
388  model_.copy_button = find_widget<button>(&window, "copy", false, true);
392  this,
393  std::ref(window)));
395  model_.copy_button->set_active(false);
396  model_.copy_button->set_tooltip(_("Clipboard support not found, contact your packager"));
397  }
398 
399  model_.page_label = find_widget<styled_widget>(&window, "page_label", false, true);
400 
401  LOG_CHAT_LOG << "Exiting chat_log::view::bind";
402  }
403 
404 private:
407 };
408 
409 
410 chat_log::chat_log(const vconfig& cfg, const replay& r)
411  : modal_dialog(window_id())
412  , view_()
413 {
414  LOG_CHAT_LOG << "Entering chat_log::chat_log";
415  view_ = std::make_shared<view>(cfg, r);
416  LOG_CHAT_LOG << "Exiting chat_log::chat_log";
417 }
418 
419 std::shared_ptr<chat_log::view> chat_log::get_view() const
420 {
421  return view_;
422 }
423 
425 {
426  LOG_CHAT_LOG << "Entering chat_log::pre_show";
427  view_->bind(window);
428  view_->pre_show();
429  LOG_CHAT_LOG << "Exiting chat_log::pre_show";
430 }
431 
432 } // namespace dialogs
double t
Definition: astarsearch.cpp:63
Simple push button.
Definition: button.hpp:36
virtual void connect_click_handler(const event::signal &signal) override
Inherited from clickable_item.
Definition: button.hpp:54
virtual void set_active(const bool active) override
See styled_widget::set_active.
Definition: button.cpp:64
void update_view_from_model(bool select_last_page=false)
Definition: chat_log.cpp:274
std::pair< int, int > calculate_log_line_range()
Definition: chat_log.cpp:254
static const int COUNT_PER_PAGE
Definition: chat_log.cpp:76
styled_widget * page_label
Definition: chat_log.cpp:78
void populate_chat_message_list(int first, int last)
Definition: chat_log.cpp:178
void chat_message_list_to_clipboard(int first, int last)
Definition: chat_log.cpp:192
model(const vconfig &c, const replay &r)
Definition: chat_log.cpp:56
const std::vector< chat_msg > & chat_log_history
Definition: chat_log.cpp:74
void stream_log(std::ostringstream &s, int first, int last, bool raw=false)
Definition: chat_log.cpp:96
void handle_copy_button_clicked(window &)
Definition: chat_log.cpp:359
view(const vconfig &cfg, const replay &r)
Definition: chat_log.cpp:328
void bind(window &window)
Definition: chat_log.cpp:364
std::shared_ptr< view > get_view() const
Definition: chat_log.cpp:419
chat_log(const vconfig &cfg, const replay &replay)
Definition: chat_log.cpp:410
virtual void pre_show(window &window) override
Actions to be taken before showing the window.
Definition: chat_log.cpp:424
std::shared_ptr< view > view_
Definition: chat_log.hpp:43
Abstract base class for all modal dialogs.
@ END
Go to the end position.
Definition: scrollbar.hpp:58
void scroll_vertical_scrollbar(const scrollbar_base::scroll_mode scroll)
Scrolls the vertical scrollbar.
virtual void set_active(const bool active) override
See styled_widget::set_active.
virtual void set_value(int value) override
Inherited from integer_selector.
Definition: slider.cpp:81
void set_value_range(int min_value, int max_value)
Definition: slider.cpp:250
virtual int get_value() const override
Inherited from integer_selector.
Definition: slider.hpp:52
void set_tooltip(const t_string &tooltip)
virtual void set_label(const t_string &text)
virtual void set_use_markup(bool use_markup)
std::string get_value() const
void set_text_changed_callback(std::function< void(text_box_base *textbox, const std::string text)> cb)
Set the text_changed callback.
A widget that allows the user to input text in single line.
Definition: text_box.hpp:125
base class of top level items, the only item which needs to store the final canvases to draw on.
Definition: window.hpp:61
void keyboard_capture(widget *widget)
Definition: window.cpp:1221
static prefs & get()
std::string get_chat_timestamp(const std::time_t &t)
A variable-expanding proxy for the config class.
Definition: variable.hpp:45
static std::string _(const char *str)
Definition: gettext.hpp:93
static lg::log_domain log_chat_log("chat_log")
#define LOG_CHAT_LOG
Definition: chat_log.cpp:42
This file contains the window object, this object is a top level container which has the event manage...
static bool timestamp
Definition: log.cpp:58
Standard logging facilities (interface).
void copy_to_clipboard(const std::string &text, const bool)
Copies text to the clipboard.
Definition: clipboard.cpp:32
bool available()
Whether wesnoth was compiled with support for a clipboard.
Definition: clipboard.cpp:53
std::string escape_text(const std::string &text)
Escapes the pango markup characters in a text.
Definition: escape.hpp:33
REGISTER_DIALOG(editor_edit_unit)
void connect_signal_notify_modified(dispatcher &dispatcher, const signal_notification &signal)
Connects a signal handler for getting a notification upon modification.
Definition: dispatcher.cpp:203
void connect_signal_mouse_left_click(dispatcher &dispatcher, const signal &signal)
Connects a signal handler for a left mouse button click.
Definition: dispatcher.cpp:177
std::string lowercase(const std::string &s)
Returns a lowercased version of the string.
Definition: unicode.cpp:50
std::size_t size(const std::string &str)
Length in characters of a UTF-8 string.
Definition: unicode.cpp:85
Replay control code.
mock_char c
static map_location::DIRECTION s