The Battle for Wesnoth  1.19.5+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 
20 #include "gui/widgets/button.hpp"
21 #include "gui/widgets/text_box.hpp"
22 #include "gui/widgets/window.hpp"
24 #include "gui/widgets/slider.hpp"
25 
26 #include "font/pango/escape.hpp"
27 #include "desktop/clipboard.hpp"
30 #include "log.hpp"
31 #include "replay.hpp"
32 #include "gettext.hpp"
33 
34 #include <functional>
35 #include "utils/iterable_pair.hpp"
36 
37 #include <vector>
38 
39 static lg::log_domain log_chat_log("chat_log");
40 #define DBG_CHAT_LOG LOG_STREAM(debug, log_chat_log)
41 #define LOG_CHAT_LOG LOG_STREAM(info, log_chat_log)
42 #define WRN_CHAT_LOG LOG_STREAM(warn, log_chat_log)
43 #define ERR_CHAT_LOG LOG_STREAM(err, log_chat_log)
44 
45 namespace gui2::dialogs
46 {
47 
48 REGISTER_DIALOG(chat_log)
49 
50 // The model is an interface defining the data to be displayed or otherwise
51 // acted upon in the user interface.
53 {
54 public:
55  model(const vconfig& c, const replay& r)
56  : cfg(c)
57  , msg_label(nullptr)
58  , chat_log_history(r.build_chat_log())
59  , page(0)
60  , page_number()
61  , page_label()
62  , previous_page()
63  , next_page()
64  , filter()
65  , copy_button()
66  {
67  LOG_CHAT_LOG << "entering chat_log::model...";
68  LOG_CHAT_LOG << "finished chat_log::model...";
69  }
70 
73  const std::vector<chat_msg>& chat_log_history;
74  int page;
75  static const int COUNT_PER_PAGE = 100;
82 
84  {
85  msg_label->set_label("");
86  }
87 
88  int count_of_pages() const
89  {
90  int size = chat_log_history.size();
91  return (size % COUNT_PER_PAGE == 0) ? (size / COUNT_PER_PAGE)
92  : (size / COUNT_PER_PAGE) + 1;
93  }
94 
95  void stream_log(std::ostringstream& s,
96  int first,
97  int last,
98  bool raw = false)
99  {
100  if(first >= last) {
101  return;
102  }
103 
104  const std::string& lcfilter = utf8::lowercase(filter->get_value());
105  LOG_CHAT_LOG << "entering chat_log::model::stream_log";
106 
107  for(const auto & t : make_pair(chat_log_history.begin() + first,
108  chat_log_history.begin() + last))
109  {
110  const std::string& timestamp
111  = prefs::get().get_chat_timestamp(t.time());
112 
113  if(!lcfilter.empty()) {
114  const std::string& lcsample = utf8::lowercase(timestamp)
115  + utf8::lowercase(t.nick())
116  + utf8::lowercase(t.text());
117 
118  if(lcsample.find(lcfilter) == std::string::npos) {
119  continue;
120  }
121  }
122 
123  const std::string me_prefix = "/me";
124  const bool is_me = t.text().compare(0, me_prefix.size(),
125  me_prefix) == 0;
126 
127  std::string nick_prefix, nick_suffix;
128 
129  if(!raw) {
130  nick_prefix = "<span color=\"" + t.color() + "\">";
131  nick_suffix = "</span> ";
132  } else {
133  nick_suffix = " ";
134  }
135 
136  const std::string lbracket = raw ? "<" : "&lt;";
137  const std::string rbracket = raw ? ">" : "&gt;";
138 
139  //
140  // Chat line format:
141  //
142  // is_me == true: "<[TS] nick message text here>\n"
143  // is_me == false: "<[TS] nick> message text here\n"
144  //
145 
146  s << nick_prefix << lbracket;
147 
148  if(raw) {
149  s << timestamp
150  << t.nick();
151  } else {
153  << font::escape_text(t.nick());
154  }
155 
156  if(is_me) {
157  if(!raw) {
158  s << font::escape_text(t.text().substr(3));
159  } else {
160  s << t.text().substr(3);
161  }
162  s << rbracket << nick_suffix;
163  } else {
164  // <[TS] nick> message text here
165  s << rbracket << nick_suffix;
166  if(!raw) {
167  s << font::escape_text(t.text());
168  } else {
169  s << t.text();
170  }
171  }
172 
173  s << '\n';
174  }
175  }
176 
177  void populate_chat_message_list(int first, int last)
178  {
179  std::ostringstream s;
180  stream_log(s, first, last);
181  msg_label->set_label(s.str());
182 
183  // It makes sense to always scroll to the bottom, since the newest messages are there.
184  // The only time this might not be desired is tabbing forward through the pages, since
185  // one might want to continue reading the conversation in order.
186  //
187  // TODO: look into implementing the above suggestion
188  dynamic_cast<scroll_label&>(*msg_label).scroll_vertical_scrollbar(scrollbar_base::END);
189  }
190 
191  void chat_message_list_to_clipboard(int first, int last)
192  {
193  std::ostringstream s;
194  stream_log(s, first, last, true);
196  }
197 };
198 
199 // The controller acts upon the model. It retrieves data from repositories,
200 // persists it, manipulates it, and determines how it will be displayed in the
201 // view.
203 {
204 public:
206  {
207  LOG_CHAT_LOG << "Entering chat_log::controller";
208  LOG_CHAT_LOG << "Exiting chat_log::controller";
209  }
210 
211  void next_page()
212  {
213  LOG_CHAT_LOG << "Entering chat_log::controller::next_page";
214  if(model_.page >= model_.count_of_pages() - 1) {
215  return;
216  }
217  model_.page++;
218  LOG_CHAT_LOG << "Set page to " << model_.page + 1;
220  LOG_CHAT_LOG << "Exiting chat_log::controller::next_page";
221  }
222 
224  {
225  LOG_CHAT_LOG << "Entering chat_log::controller::previous_page";
226  if(model_.page == 0) {
227  return;
228  }
229  model_.page--;
230  LOG_CHAT_LOG << "Set page to " << model_.page + 1;
232  LOG_CHAT_LOG << "Exiting chat_log::controller::previous_page";
233  }
234 
235  void filter()
236  {
237  LOG_CHAT_LOG << "Entering chat_log::controller::filter";
239  LOG_CHAT_LOG << "Exiting chat_log::controller::filter";
240  }
241 
243  {
245  << "Entering chat_log::controller::handle_page_number_changed";
247  LOG_CHAT_LOG << "Set page to " << model_.page + 1;
250  << "Exiting chat_log::controller::handle_page_number_changed";
251  }
252 
253  std::pair<int, int> calculate_log_line_range()
254  {
255  const int log_size = model_.chat_log_history.size();
256  const int page_size = model_.COUNT_PER_PAGE;
257 
258  const int page = model_.page;
259  const int count_of_pages = std::max(1, model_.count_of_pages());
260 
261  LOG_CHAT_LOG << "Page: " << page + 1 << " of " << count_of_pages;
262 
263  const int first = page * page_size;
264  const int last = page < (count_of_pages - 1)
265  ? first + page_size
266  : log_size;
267 
268  LOG_CHAT_LOG << "First " << first << ", last " << last;
269 
270  return std::pair(first, last);
271  }
272 
273  void update_view_from_model(bool select_last_page = false)
274  {
276  << "Entering chat_log::controller::update_view_from_model";
278  int size = model_.chat_log_history.size();
279  LOG_CHAT_LOG << "Number of chat messages: " << size;
280  // determine count of pages
281  const int count_of_pages = std::max(1, model_.count_of_pages());
282  if(select_last_page) {
283  model_.page = count_of_pages - 1;
284  }
285  // get page
286  const int page = model_.page;
287  // determine first and last
288  const std::pair<int, int>& range = calculate_log_line_range();
289  const int first = range.first;
290  const int last = range.second;
291  // determine has previous, determine has next
292  bool has_next = page + 1 < count_of_pages;
293  bool has_previous = page > 0;
294  model_.previous_page->set_active(has_previous);
295  model_.next_page->set_active(has_next);
296  model_.populate_chat_message_list(first, last);
297  model_.page_number->set_value_range(1, count_of_pages);
298  model_.page_number->set_active(count_of_pages > 1);
300  << "Maximum value of page number slider: " << count_of_pages;
301  model_.page_number->set_value(page + 1);
302 
303  std::ostringstream cur_page_text;
304  cur_page_text << (page + 1) << '/' << std::max(1, count_of_pages);
305  model_.page_label->set_label(cur_page_text.str());
306 
308  << "Exiting chat_log::controller::update_view_from_model";
309  }
310 
312  {
313  const std::pair<int, int>& range = calculate_log_line_range();
314  model_.chat_message_list_to_clipboard(range.first, range.second);
315  }
316 
317 private:
319 };
320 
321 
322 // The view is an interface that displays data (the model) and routes user
323 // commands to the controller to act upon that data.
325 {
326 public:
327  view(const vconfig& cfg, const replay& r) : model_(cfg, r), controller_(model_)
328  {
329  }
330 
331  void pre_show()
332  {
333  LOG_CHAT_LOG << "Entering chat_log::view::pre_show";
335  LOG_CHAT_LOG << "Exiting chat_log::view::pre_show";
336  }
337 
339  {
341  }
342 
343  void next_page()
344  {
346  }
347 
349  {
351  }
352 
353  void filter()
354  {
356  }
357 
359  {
361  }
362 
364  {
365  LOG_CHAT_LOG << "Entering chat_log::view::bind";
366  model_.msg_label = window.find_widget<styled_widget>("msg", false, true);
368  = window.find_widget<slider>("page_number", false, true);
371  std::bind(&view::handle_page_number_changed, this));
372 
374  = window.find_widget<button>("previous_page", false, true);
376  std::bind(&view::previous_page, this));
377 
378  model_.next_page = window.find_widget<button>("next_page", false, true);
380  std::bind(&view::next_page, this));
381 
382  model_.filter = window.find_widget<text_box>("filter", false, true);
384  std::bind(&view::filter, this));
386 
387  model_.copy_button = window.find_widget<button>("copy", false, true);
390  std::bind(&view::handle_copy_button_clicked, this));
391 
392  model_.page_label = window.find_widget<styled_widget>("page_label", false, true);
393 
394  LOG_CHAT_LOG << "Exiting chat_log::view::bind";
395  }
396 
397 private:
400 };
401 
402 
403 chat_log::chat_log(const vconfig& cfg, const replay& r)
404  : modal_dialog(window_id())
405  , view_()
406 {
407  LOG_CHAT_LOG << "Entering chat_log::chat_log";
408  view_ = std::make_shared<view>(cfg, r);
409  LOG_CHAT_LOG << "Exiting chat_log::chat_log";
410 }
411 
412 std::shared_ptr<chat_log::view> chat_log::get_view() const
413 {
414  return view_;
415 }
416 
418 {
419  LOG_CHAT_LOG << "Entering chat_log::pre_show";
420  view_->bind(*this);
421  view_->pre_show();
422  LOG_CHAT_LOG << "Exiting chat_log::pre_show";
423 }
424 
425 } // 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:273
std::pair< int, int > calculate_log_line_range()
Definition: chat_log.cpp:253
static const int COUNT_PER_PAGE
Definition: chat_log.cpp:75
styled_widget * page_label
Definition: chat_log.cpp:77
void populate_chat_message_list(int first, int last)
Definition: chat_log.cpp:177
void chat_message_list_to_clipboard(int first, int last)
Definition: chat_log.cpp:191
model(const vconfig &c, const replay &r)
Definition: chat_log.cpp:55
const std::vector< chat_msg > & chat_log_history
Definition: chat_log.cpp:73
void stream_log(std::ostringstream &s, int first, int last, bool raw=false)
Definition: chat_log.cpp:95
view(const vconfig &cfg, const replay &r)
Definition: chat_log.cpp:327
void bind(window &window)
Definition: chat_log.cpp:363
std::shared_ptr< view > get_view() const
Definition: chat_log.cpp:412
virtual void pre_show() override
Actions to be taken before showing the window.
Definition: chat_log.cpp:417
chat_log(const vconfig &cfg, const replay &replay)
Definition: chat_log.cpp:403
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
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
NOT_DANGLING T * find_widget(const std::string &id, const bool must_be_active, const bool must_exist)
Gets a widget with the wanted id.
Definition: widget.hpp:742
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:1207
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 lg::log_domain log_chat_log("chat_log")
#define LOG_CHAT_LOG
Definition: chat_log.cpp:41
This file contains the window object, this object is a top level container which has the event manage...
static bool timestamp
Definition: log.cpp:61
Standard logging facilities (interface).
void copy_to_clipboard(const std::string &text)
Copies text to the clipboard.
Definition: clipboard.cpp:27
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