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 ? "<" : "&lt;";
152 			const std::string rbracket = raw ? ">" : "&gt;";
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