1 /*
2 Copyright (C) 2011 - 2018 by Yurii Chernyi <terraninfo@terraninfo.net>
3 Part of the Battle for Wesnoth Project https://www.wesnoth.org/
4
5 This program is free software; you can redistribute it and/or modify
6 it under the terms of the GNU General Public License as published by
7 the Free Software Foundation; either version 2 of the License, or
8 (at your option) any later version.
9 This program is distributed in the hope that it will be useful,
10 but WITHOUT ANY WARRANTY.
11
12 See the COPYING file for more details.
13 */
14
15 #define GETTEXT_DOMAIN "wesnoth-lib"
16
17 #include "gui/dialogs/chat_log.hpp"
18
19 #include "gui/auxiliary/find_widget.hpp"
20 #include "gui/widgets/button.hpp"
21 #include "gui/widgets/listbox.hpp"
22 #include "gui/widgets/settings.hpp"
23 #include "gui/widgets/text_box.hpp"
24 #include "gui/widgets/window.hpp"
25 #include "gui/widgets/scroll_label.hpp"
26 #include "gui/widgets/slider.hpp"
27
28 #include "font/pango/escape.hpp"
29 #include "desktop/clipboard.hpp"
30 #include "serialization/unicode.hpp"
31 #include "preferences/game.hpp"
32 #include "log.hpp"
33 #include "replay.hpp"
34 #include "gettext.hpp"
35
36 #include "utils/functional.hpp"
37 #include "utils/iterable_pair.hpp"
38
39 #include <vector>
40
41 static lg::log_domain log_chat_log("chat_log");
42 #define DBG_CHAT_LOG LOG_STREAM(debug, log_chat_log)
43 #define LOG_CHAT_LOG LOG_STREAM(info, log_chat_log)
44 #define WRN_CHAT_LOG LOG_STREAM(warn, log_chat_log)
45 #define ERR_CHAT_LOG LOG_STREAM(err, log_chat_log)
46
47 namespace gui2
48 {
49 namespace dialogs
50 {
51
52 /*WIKI
53 * @page = GUIWindowDefinitionWML
54 * @order = 3_chat_log
55 *
56 * == Settings manager ==
57 *
58 * This shows the settings manager
59 *
60 */
61
62
63 REGISTER_DIALOG(chat_log)
64
65 // The model is an interface defining the data to be displayed or otherwise
66 // acted upon in the user interface.
67 class chat_log::model
68 {
69 public:
model(const vconfig & c,const replay & r)70 model(const vconfig& c, const replay& r)
71 : cfg(c)
72 , msg_label(nullptr)
73 , chat_log_history(r.build_chat_log())
74 , page(0)
75 , page_number()
76 , page_label()
77 , previous_page()
78 , next_page()
79 , filter()
80 , copy_button()
81 {
82 LOG_CHAT_LOG << "entering chat_log::model...\n";
83 LOG_CHAT_LOG << "finished chat_log::model...\n";
84 }
85
86 vconfig cfg;
87 styled_widget* msg_label;
88 const std::vector<chat_msg>& chat_log_history;
89 int page;
90 static const int COUNT_PER_PAGE = 100;
91 slider* page_number;
92 styled_widget* page_label;
93 button* previous_page;
94 button* next_page;
95 text_box* filter;
96 button* copy_button;
97
clear_chat_msg_list()98 void clear_chat_msg_list()
99 {
100 msg_label->set_label("");
101 }
102
count_of_pages() const103 int count_of_pages() const
104 {
105 int size = chat_log_history.size();
106 return (size % COUNT_PER_PAGE == 0) ? (size / COUNT_PER_PAGE)
107 : (size / COUNT_PER_PAGE) + 1;
108 }
109
stream_log(std::ostringstream & s,int first,int last,bool raw=false)110 void stream_log(std::ostringstream& s,
111 int first,
112 int last,
113 bool raw = false)
114 {
115 if(first >= last) {
116 return;
117 }
118
119 const std::string& lcfilter = utf8::lowercase(filter->get_value());
120 LOG_CHAT_LOG << "entering chat_log::model::stream_log\n";
121
122 for(const auto & t : make_pair(chat_log_history.begin() + first,
123 chat_log_history.begin() + last))
124 {
125 const std::string& timestamp
126 = preferences::get_chat_timestamp(t.time());
127
128 if(!lcfilter.empty()) {
129 const std::string& lcsample = utf8::lowercase(timestamp)
130 + utf8::lowercase(t.nick())
131 + utf8::lowercase(t.text());
132
133 if(lcsample.find(lcfilter) == std::string::npos) {
134 continue;
135 }
136 }
137
138 const std::string me_prefix = "/me";
139 const bool is_me = t.text().compare(0, me_prefix.size(),
140 me_prefix) == 0;
141
142 std::string nick_prefix, nick_suffix;
143
144 if(!raw) {
145 nick_prefix = "<span color=\"" + t.color() + "\">";
146 nick_suffix = "</span> ";
147 } else {
148 nick_suffix = " ";
149 }
150
151 const std::string lbracket = raw ? "<" : "<";
152 const std::string rbracket = raw ? ">" : ">";
153
154 //
155 // Chat line format:
156 //
157 // is_me == true: "<[TS] nick message text here>\n"
158 // is_me == false: "<[TS] nick> message text here\n"
159 //
160
161 s << nick_prefix << lbracket;
162
163 if(raw) {
164 s << timestamp
165 << t.nick();
166 } else {
167 s << font::escape_text(timestamp)
168 << font::escape_text(t.nick());
169 }
170
171 if(is_me) {
172 if(!raw) {
173 s << font::escape_text(t.text().substr(3));
174 } else {
175 s << t.text().substr(3);
176 }
177 s << rbracket << nick_suffix;
178 } else {
179 // <[TS] nick> message text here
180 s << rbracket << nick_suffix;
181 if(!raw) {
182 s << font::escape_text(t.text());
183 } else {
184 s << t.text();
185 }
186 }
187
188 s << '\n';
189 }
190 }
191
populate_chat_message_list(int first,int last)192 void populate_chat_message_list(int first, int last)
193 {
194 std::ostringstream s;
195 stream_log(s, first, last);
196 msg_label->set_label(s.str());
197
198 // It makes sense to always scroll to the bottom, since the newest messages are there.
199 // The only time this might not be desired is tabbing forward through the pages, since
200 // one might want to continue reading the conversation in order.
201 //
202 // TODO: look into implementing the above suggestion
203 dynamic_cast<scroll_label&>(*msg_label).scroll_vertical_scrollbar(scrollbar_base::END);
204 }
205
chat_message_list_to_clipboard(int first,int last)206 void chat_message_list_to_clipboard(int first, int last)
207 {
208 std::ostringstream s;
209 stream_log(s, first, last, true);
210 desktop::clipboard::copy_to_clipboard(s.str(), false);
211 }
212 };
213
214 // The controller acts upon the model. It retrieves data from repositories,
215 // persists it, manipulates it, and determines how it will be displayed in the
216 // view.
217 class chat_log::controller
218 {
219 public:
controller(model & m)220 controller(model& m) : model_(m)
221 {
222 LOG_CHAT_LOG << "Entering chat_log::controller" << std::endl;
223 LOG_CHAT_LOG << "Exiting chat_log::controller" << std::endl;
224 }
225
next_page()226 void next_page()
227 {
228 LOG_CHAT_LOG << "Entering chat_log::controller::next_page"
229 << std::endl;
230 if(model_.page >= model_.count_of_pages() - 1) {
231 return;
232 }
233 model_.page++;
234 LOG_CHAT_LOG << "Set page to " << model_.page + 1 << std::endl;
235 update_view_from_model();
236 LOG_CHAT_LOG << "Exiting chat_log::controller::next_page" << std::endl;
237 }
238
previous_page()239 void previous_page()
240 {
241 LOG_CHAT_LOG << "Entering chat_log::controller::previous_page"
242 << std::endl;
243 if(model_.page == 0) {
244 return;
245 }
246 model_.page--;
247 LOG_CHAT_LOG << "Set page to " << model_.page + 1 << std::endl;
248 update_view_from_model();
249 LOG_CHAT_LOG << "Exiting chat_log::controller::previous_page"
250 << std::endl;
251 }
252
filter()253 void filter()
254 {
255 LOG_CHAT_LOG << "Entering chat_log::controller::filter" << std::endl;
256 update_view_from_model();
257 LOG_CHAT_LOG << "Exiting chat_log::controller::filter" << std::endl;
258 }
259
handle_page_number_changed()260 void handle_page_number_changed()
261 {
262 LOG_CHAT_LOG
263 << "Entering chat_log::controller::handle_page_number_changed"
264 << std::endl;
265 model_.page = model_.page_number->get_value() - 1;
266 LOG_CHAT_LOG << "Set page to " << model_.page + 1 << std::endl;
267 update_view_from_model();
268 LOG_CHAT_LOG
269 << "Exiting chat_log::controller::handle_page_number_changed"
270 << std::endl;
271 }
272
calculate_log_line_range()273 std::pair<int, int> calculate_log_line_range()
274 {
275 const int log_size = model_.chat_log_history.size();
276 const int page_size = model_.COUNT_PER_PAGE;
277
278 const int page = model_.page;
279 const int count_of_pages = std::max(1, model_.count_of_pages());
280
281 LOG_CHAT_LOG << "Page: " << page + 1 << " of " << count_of_pages
282 << '\n';
283
284 const int first = page * page_size;
285 const int last = page < (count_of_pages - 1)
286 ? first + page_size
287 : log_size;
288
289 LOG_CHAT_LOG << "First " << first << ", last " << last << '\n';
290
291 return std::make_pair(first, last);
292 }
293
update_view_from_model(bool select_last_page=false)294 void update_view_from_model(bool select_last_page = false)
295 {
296 LOG_CHAT_LOG << "Entering chat_log::controller::update_view_from_model"
297 << std::endl;
298 model_.msg_label->set_use_markup(true);
299 int size = model_.chat_log_history.size();
300 LOG_CHAT_LOG << "Number of chat messages: " << size << std::endl;
301 // determine count of pages
302 const int count_of_pages = std::max(1, model_.count_of_pages());
303 if(select_last_page) {
304 model_.page = count_of_pages - 1;
305 }
306 // get page
307 const int page = model_.page;
308 // determine first and last
309 const std::pair<int, int>& range = calculate_log_line_range();
310 const int first = range.first;
311 const int last = range.second;
312 // determine has previous, determine has next
313 bool has_next = page + 1 < count_of_pages;
314 bool has_previous = page > 0;
315 model_.previous_page->set_active(has_previous);
316 model_.next_page->set_active(has_next);
317 model_.populate_chat_message_list(first, last);
318 model_.page_number->set_value_range(1, count_of_pages);
319 model_.page_number->set_active(count_of_pages > 1);
320 LOG_CHAT_LOG << "Maximum value of page number slider: "
321 << count_of_pages << std::endl;
322 model_.page_number->set_value(page + 1);
323
324 std::ostringstream cur_page_text;
325 cur_page_text << (page + 1) << '/' << std::max(1, count_of_pages);
326 model_.page_label->set_label(cur_page_text.str());
327
328 LOG_CHAT_LOG << "Exiting chat_log::controller::update_view_from_model"
329 << std::endl;
330 }
331
handle_copy_button_clicked()332 void handle_copy_button_clicked()
333 {
334 const std::pair<int, int>& range = calculate_log_line_range();
335 model_.chat_message_list_to_clipboard(range.first, range.second);
336 }
337
338 private:
339 model& model_;
340 };
341
342
343 // The view is an interface that displays data (the model) and routes user
344 // commands to the controller to act upon that data.
345 class chat_log::view
346 {
347 public:
view(const vconfig & cfg,const replay & r)348 view(const vconfig& cfg, const replay& r) : model_(cfg, r), controller_(model_)
349 {
350 }
351
pre_show()352 void pre_show()
353 {
354 LOG_CHAT_LOG << "Entering chat_log::view::pre_show" << std::endl;
355 controller_.update_view_from_model(true);
356 LOG_CHAT_LOG << "Exiting chat_log::view::pre_show" << std::endl;
357 }
358
handle_page_number_changed()359 void handle_page_number_changed()
360 {
361 controller_.handle_page_number_changed();
362 }
363
next_page()364 void next_page()
365 {
366 controller_.next_page();
367 }
368
previous_page()369 void previous_page()
370 {
371 controller_.previous_page();
372 }
373
filter()374 void filter()
375 {
376 controller_.filter();
377 }
378
handle_copy_button_clicked(window &)379 void handle_copy_button_clicked(window& /*window*/)
380 {
381 controller_.handle_copy_button_clicked();
382 }
383
bind(window & window)384 void bind(window& window)
385 {
386 LOG_CHAT_LOG << "Entering chat_log::view::bind" << std::endl;
387 model_.msg_label = find_widget<styled_widget>(&window, "msg", false, true);
388 model_.page_number
389 = find_widget<slider>(&window, "page_number", false, true);
390 connect_signal_notify_modified(
391 *model_.page_number,
392 std::bind(&view::handle_page_number_changed, this));
393
394 model_.previous_page
395 = find_widget<button>(&window, "previous_page", false, true);
396 model_.previous_page->connect_click_handler(
397 std::bind(&view::previous_page, this));
398
399 model_.next_page = find_widget<button>(&window, "next_page", false, true);
400 model_.next_page->connect_click_handler(
401 std::bind(&view::next_page, this));
402
403 model_.filter = find_widget<text_box>(&window, "filter", false, true);
404 model_.filter->set_text_changed_callback(
405 std::bind(&view::filter, this));
406 window.keyboard_capture(model_.filter);
407
408 model_.copy_button = find_widget<button>(&window, "copy", false, true);
409 connect_signal_mouse_left_click(
410 *model_.copy_button,
411 std::bind(&view::handle_copy_button_clicked,
412 this,
413 std::ref(window)));
414 if (!desktop::clipboard::available()) {
415 model_.copy_button->set_active(false);
416 model_.copy_button->set_tooltip(_("Clipboard support not found, contact your packager"));
417 }
418
419 model_.page_label = find_widget<styled_widget>(&window, "page_label", false, true);
420
421 LOG_CHAT_LOG << "Exiting chat_log::view::bind" << std::endl;
422 }
423
424 private:
425 model model_;
426 controller controller_;
427 };
428
429
chat_log(const vconfig & cfg,const replay & r)430 chat_log::chat_log(const vconfig& cfg, const replay& r) : view_()
431 {
432 LOG_CHAT_LOG << "Entering chat_log::chat_log" << std::endl;
433 view_ = std::make_shared<view>(cfg, r);
434 LOG_CHAT_LOG << "Exiting chat_log::chat_log" << std::endl;
435 }
436
get_view()437 std::shared_ptr<chat_log::view> chat_log::get_view()
438 {
439 return view_;
440 }
441
pre_show(window & window)442 void chat_log::pre_show(window& window)
443 {
444 LOG_CHAT_LOG << "Entering chat_log::pre_show" << std::endl;
445 view_->bind(window);
446 view_->pre_show();
447 LOG_CHAT_LOG << "Exiting chat_log::pre_show" << std::endl;
448 }
449
450 } // namespace dialogs
451 } // namespace gui2
452