The Battle for Wesnoth 1.21.0-dev
 
Loading...
Searching...
No Matches
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
19
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>
36
37#include <vector>
38
39static 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
45namespace gui2::dialogs
46{
47
48REGISTER_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{
54public:
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
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{
205public:
207 {
208 LOG_CHAT_LOG << "Entering chat_log::controller";
209 LOG_CHAT_LOG << "Exiting chat_log::controller";
210 }
211
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);
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
318private:
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{
327public:
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
343
345 {
347 }
348
350 {
352 }
353
354 void filter()
355 {
357 }
358
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);
370 connect_signal_notify_modified(
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(); });
385#ifdef __IPHONEOS__
386 // On iOS, opening the chat log should not immediately summon the
387 // software keyboard just because the optional filter field exists.
389#else
391#endif
392
393 model_.copy_button = window.find_widget<button>("copy", false, true);
394 connect_signal_mouse_left_click(
396 std::bind(&view::handle_copy_button_clicked, this));
397
398 model_.page_label = window.find_widget<styled_widget>("page_label", false, true);
399
400 LOG_CHAT_LOG << "Exiting chat_log::view::bind";
401 }
402
403private:
406};
407
408
410 : modal_dialog(window_id())
411 , view_()
412{
413 LOG_CHAT_LOG << "Entering chat_log::chat_log";
414 view_ = std::make_shared<view>(cfg, r);
415 LOG_CHAT_LOG << "Exiting chat_log::chat_log";
416}
417
418std::shared_ptr<chat_log::view> chat_log::get_view() const
419{
420 return view_;
421}
422
424{
425 LOG_CHAT_LOG << "Entering chat_log::pre_show";
426 view_->bind(*this);
427 view_->pre_show();
428 LOG_CHAT_LOG << "Exiting chat_log::pre_show";
429}
430
431} // namespace dialogs
double t
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
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:418
virtual void pre_show() override
Actions to be taken before showing the window.
Definition chat_log.cpp:423
chat_log(const vconfig &cfg, const replay &replay)
Definition chat_log.cpp:409
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).
#define REGISTER_DIALOG(window_id)
Wrapper for REGISTER_DIALOG2.
void copy_to_clipboard(const std::string &text)
Copies text to the clipboard.
Definition clipboard.cpp:24
std::string escape_text(std::string_view text)
Escapes the pango markup characters in a text.
Definition escape.hpp:33
std::string lowercase(std::string_view s)
Returns a lowercased version of the string.
Definition unicode.cpp:50
Replay control code.
static map_location::direction s
#define c