The Battle for Wesnoth  1.19.19+dev
chat_log.cpp
Go to the documentation of this file.
1 /*
2  Copyright (C) 2011 - 2025
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  // FIXME: use markup::span_color
131  nick_prefix = "<span color=\"" + t.color().to_hex_string() + "\">";
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 = window.find_widget<styled_widget>("msg", false, true);
369  = window.find_widget<slider>("page_number", false, true);
372  std::bind(&view::handle_page_number_changed, this));
373 
375  = window.find_widget<button>("previous_page", false, true);
377  std::bind(&view::previous_page, this));
378 
379  model_.next_page = window.find_widget<button>("next_page", false, true);
381  std::bind(&view::next_page, this));
382 
383  model_.filter = window.find_widget<text_box>("filter", false, true);
384  model_.filter->on_modified([this](const auto&) { filter(); });
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 
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:274
std::pair< int, int > calculate_log_line_range()
Definition: chat_log.cpp:254
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: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: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:328
void bind(window &window)
Definition: chat_log.cpp:364
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:249
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)
void on_modified(const Func &f)
Registers a NOTIFY_MODIFIED handler.
A widget that allows the user to input text in single line.
Definition: text_box.hpp:125
T * find_widget(const std::string_view id, const bool must_be_active, const bool must_exist)
Gets a widget with the wanted id.
Definition: widget.hpp:747
base class of top level items, the only item which needs to store the final canvases to draw on.
Definition: window.hpp:59
void keyboard_capture(widget *widget)
Definition: window.cpp:1197
static prefs & get()
std::string get_chat_timestamp(const std::chrono::system_clock::time_point &t)
A variable-expanding proxy for the config class.
Definition: variable.hpp:45
const config * cfg
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:89
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(std::string_view 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:189
void connect_signal_mouse_left_click(dispatcher &dispatcher, const signal &signal)
Connects a signal handler for a left mouse button click.
Definition: dispatcher.cpp:163
std::string lowercase(std::string_view s)
Returns a lowercased version of the string.
Definition: unicode.cpp:50
std::size_t size(std::string_view str)
Length in characters of a UTF-8 string.
Definition: unicode.cpp:81
constexpr auto filter
Definition: ranges.hpp:42
Replay control code.
mock_char c
static map_location::direction s