Add: Show house information in house placer.
[openttd-github.git] / src / network / network_chat_gui.cpp
blob92cac80a765577db464369793591db05359a6bf7
1 /*
2 * This file is part of OpenTTD.
3 * OpenTTD is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 2.
4 * OpenTTD is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
5 * See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with OpenTTD. If not, see <http://www.gnu.org/licenses/>.
6 */
8 /** @file network_chat_gui.cpp GUI for handling chat messages. */
10 #include "../stdafx.h"
11 #include "../strings_func.h"
12 #include "../autocompletion.h"
13 #include "../blitter/factory.hpp"
14 #include "../console_func.h"
15 #include "../video/video_driver.hpp"
16 #include "../querystring_gui.h"
17 #include "../town.h"
18 #include "../window_func.h"
19 #include "../toolbar_gui.h"
20 #include "../core/geometry_func.hpp"
21 #include "../zoom_func.h"
22 #include "../timer/timer.h"
23 #include "../timer/timer_window.h"
24 #include "network.h"
25 #include "network_client.h"
26 #include "network_base.h"
28 #include "../widgets/network_chat_widget.h"
30 #include "table/strings.h"
32 #include "../safeguards.h"
34 /** Spacing between chat lines. */
35 static const uint NETWORK_CHAT_LINE_SPACING = 3;
37 /** Container for a message. */
38 struct ChatMessage {
39 std::string message; ///< The action message.
40 TextColour colour; ///< The colour of the message.
41 std::chrono::steady_clock::time_point remove_time; ///< The time to remove the message.
44 /* used for chat window */
45 static std::deque<ChatMessage> _chatmsg_list; ///< The actual chat message list.
46 static bool _chatmessage_dirty = false; ///< Does the chat message need repainting?
47 static bool _chatmessage_visible = false; ///< Is a chat message visible.
48 static uint MAX_CHAT_MESSAGES = 0; ///< The limit of chat messages to show.
50 /**
51 * Time the chat history was marked dirty. This is used to determine if expired
52 * messages have recently expired and should cause a redraw to hide them.
54 static std::chrono::steady_clock::time_point _chatmessage_dirty_time;
56 /**
57 * The chatbox grows from the bottom so the coordinates are pixels from
58 * the left and pixels from the bottom. The height is the maximum height.
60 static PointDimension _chatmsg_box;
61 static ReusableBuffer<uint8_t> _chatmessage_backup; ///< Backup in case text is moved.
63 /**
64 * Test if there are any chat messages to display.
65 * @param show_all Set if all messages should be included, instead of unexpired only.
66 * @return True iff there are chat messages to display.
68 static inline bool HaveChatMessages(bool show_all)
70 if (show_all) return !_chatmsg_list.empty();
72 auto now = std::chrono::steady_clock::now();
73 for (auto &cmsg : _chatmsg_list) {
74 if (cmsg.remove_time >= now) return true;
77 return false;
80 /**
81 * Add a text message to the 'chat window' to be shown
82 * @param colour The colour this message is to be shown in
83 * @param duration The duration of the chat message in seconds
84 * @param message message itself
86 void CDECL NetworkAddChatMessage(TextColour colour, uint duration, const std::string &message)
88 if (_chatmsg_list.size() == MAX_CHAT_MESSAGES) {
89 _chatmsg_list.pop_back();
92 ChatMessage *cmsg = &_chatmsg_list.emplace_front();
93 cmsg->message = message;
94 cmsg->colour = colour;
95 cmsg->remove_time = std::chrono::steady_clock::now() + std::chrono::seconds(duration);
97 _chatmessage_dirty_time = std::chrono::steady_clock::now();
98 _chatmessage_dirty = true;
101 /** Initialize all font-dependent chat box sizes. */
102 void NetworkReInitChatBoxSize()
104 _chatmsg_box.y = 3 * GetCharacterHeight(FS_NORMAL);
105 _chatmsg_box.height = MAX_CHAT_MESSAGES * (GetCharacterHeight(FS_NORMAL) + ScaleGUITrad(NETWORK_CHAT_LINE_SPACING)) + ScaleGUITrad(4);
108 /** Initialize all buffers of the chat visualisation. */
109 void NetworkInitChatMessage()
111 MAX_CHAT_MESSAGES = _settings_client.gui.network_chat_box_height;
113 _chatmsg_list.clear();
114 _chatmsg_box.x = ScaleGUITrad(10);
115 _chatmsg_box.width = _settings_client.gui.network_chat_box_width_pct * _screen.width / 100;
116 NetworkReInitChatBoxSize();
117 _chatmessage_visible = false;
120 /** Hide the chatbox */
121 void NetworkUndrawChatMessage()
123 /* Sometimes we also need to hide the cursor
124 * This is because both textmessage and the cursor take a shot of the
125 * screen before drawing.
126 * Now the textmessage takes its shot and paints its data before the cursor
127 * does, so in the shot of the cursor is the screen-data of the textmessage
128 * included when the cursor hangs somewhere over the textmessage. To
129 * avoid wrong repaints, we undraw the cursor in that case, and everything
130 * looks nicely ;)
131 * (and now hope this story above makes sense to you ;))
133 if (_cursor.visible &&
134 _cursor.draw_pos.x + _cursor.draw_size.x >= _chatmsg_box.x &&
135 _cursor.draw_pos.x <= _chatmsg_box.x + _chatmsg_box.width &&
136 _cursor.draw_pos.y + _cursor.draw_size.y >= _screen.height - _chatmsg_box.y - _chatmsg_box.height &&
137 _cursor.draw_pos.y <= _screen.height - _chatmsg_box.y) {
138 UndrawMouseCursor();
141 if (_chatmessage_visible) {
142 Blitter *blitter = BlitterFactory::GetCurrentBlitter();
143 int x = _chatmsg_box.x;
144 int y = _screen.height - _chatmsg_box.y - _chatmsg_box.height;
145 int width = _chatmsg_box.width;
146 int height = _chatmsg_box.height;
147 if (y < 0) {
148 height = std::max(height + y, std::min(_chatmsg_box.height, _screen.height));
149 y = 0;
151 if (x + width >= _screen.width) {
152 width = _screen.width - x;
154 if (width <= 0 || height <= 0) return;
156 _chatmessage_visible = false;
157 /* Put our 'shot' back to the screen */
158 blitter->CopyFromBuffer(blitter->MoveTo(_screen.dst_ptr, x, y), _chatmessage_backup.GetBuffer(), width, height);
159 /* And make sure it is updated next time */
160 VideoDriver::GetInstance()->MakeDirty(x, y, width, height);
162 _chatmessage_dirty_time = std::chrono::steady_clock::now();
163 _chatmessage_dirty = true;
167 /** Check if a message is expired on a regular interval. */
168 static IntervalTimer<TimerWindow> network_message_expired_interval(std::chrono::seconds(1), [](auto) {
169 auto now = std::chrono::steady_clock::now();
170 for (auto &cmsg : _chatmsg_list) {
171 /* Message has expired, remove from the list */
172 if (now > cmsg.remove_time && _chatmessage_dirty_time < cmsg.remove_time) {
173 _chatmessage_dirty_time = now;
174 _chatmessage_dirty = true;
175 break;
180 /** Draw the chat message-box */
181 void NetworkDrawChatMessage()
183 Blitter *blitter = BlitterFactory::GetCurrentBlitter();
184 if (!_chatmessage_dirty) return;
186 const Window *w = FindWindowByClass(WC_SEND_NETWORK_MSG);
187 bool show_all = (w != nullptr);
189 /* First undraw if needed */
190 NetworkUndrawChatMessage();
192 if (_iconsole_mode == ICONSOLE_FULL) return;
194 /* Check if we have anything to draw at all */
195 if (!HaveChatMessages(show_all)) return;
197 int x = _chatmsg_box.x;
198 int y = _screen.height - _chatmsg_box.y - _chatmsg_box.height;
199 int width = _chatmsg_box.width;
200 int height = _chatmsg_box.height;
201 if (y < 0) {
202 height = std::max(height + y, std::min(_chatmsg_box.height, _screen.height));
203 y = 0;
205 if (x + width >= _screen.width) {
206 width = _screen.width - x;
208 if (width <= 0 || height <= 0) return;
210 /* Make a copy of the screen as it is before painting (for undraw) */
211 uint8_t *buffer = _chatmessage_backup.Allocate(BlitterFactory::GetCurrentBlitter()->BufferSize(width, height));
212 blitter->CopyToBuffer(blitter->MoveTo(_screen.dst_ptr, x, y), buffer, width, height);
214 _cur_dpi = &_screen; // switch to _screen painting
216 auto now = std::chrono::steady_clock::now();
217 int string_height = 0;
218 for (auto &cmsg : _chatmsg_list) {
219 if (!show_all && cmsg.remove_time < now) continue;
220 SetDParamStr(0, cmsg.message);
221 string_height += GetStringLineCount(STR_JUST_RAW_STRING, width - 1) * GetCharacterHeight(FS_NORMAL) + NETWORK_CHAT_LINE_SPACING;
224 string_height = std::min<uint>(string_height, MAX_CHAT_MESSAGES * (GetCharacterHeight(FS_NORMAL) + NETWORK_CHAT_LINE_SPACING));
226 int top = _screen.height - _chatmsg_box.y - string_height - 2;
227 int bottom = _screen.height - _chatmsg_box.y - 2;
228 /* Paint a half-transparent box behind the chat messages */
229 GfxFillRect(_chatmsg_box.x, top - 2, _chatmsg_box.x + _chatmsg_box.width - 1, bottom,
230 PALETTE_TO_TRANSPARENT, FILLRECT_RECOLOUR // black, but with some alpha for background
233 /* Paint the chat messages starting with the lowest at the bottom */
234 int ypos = bottom - 2;
236 for (auto &cmsg : _chatmsg_list) {
237 if (!show_all && cmsg.remove_time < now) continue;
238 ypos = DrawStringMultiLine(_chatmsg_box.x + ScaleGUITrad(3), _chatmsg_box.x + _chatmsg_box.width - 1, top, ypos, cmsg.message, cmsg.colour, SA_LEFT | SA_BOTTOM | SA_FORCE) - NETWORK_CHAT_LINE_SPACING;
239 if (ypos < top) break;
242 /* Make sure the data is updated next flush */
243 VideoDriver::GetInstance()->MakeDirty(x, y, width, height);
245 _chatmessage_visible = true;
246 _chatmessage_dirty = false;
250 * Send an actual chat message.
251 * @param buf The message to send.
252 * @param type The type of destination.
253 * @param dest The actual destination index.
255 static void SendChat(const std::string &buf, DestType type, int dest)
257 if (buf.empty()) return;
258 if (!_network_server) {
259 MyClient::SendChat((NetworkAction)(NETWORK_ACTION_CHAT + type), type, dest, buf, 0);
260 } else {
261 NetworkServerSendChat((NetworkAction)(NETWORK_ACTION_CHAT + type), type, dest, buf, CLIENT_ID_SERVER);
265 class NetworkChatAutoCompletion final : public AutoCompletion {
266 public:
267 using AutoCompletion::AutoCompletion;
269 private:
270 std::vector<std::string> GetSuggestions([[maybe_unused]] std::string_view prefix, std::string_view query) override
272 std::vector<std::string> suggestions;
273 for (NetworkClientInfo *ci : NetworkClientInfo::Iterate()) {
274 if (ci->client_name.starts_with(query)) {
275 suggestions.push_back(ci->client_name);
278 for (const Town *t : Town::Iterate()) {
279 /* Get the town-name via the string-system */
280 SetDParam(0, t->index);
281 std::string town_name = GetString(STR_TOWN_NAME);
282 if (town_name.starts_with(query)) {
283 suggestions.push_back(std::move(town_name));
286 return suggestions;
289 void ApplySuggestion(std::string_view prefix, std::string_view suggestion) override
291 /* Add ': ' if we are at the start of the line (pretty) */
292 if (prefix.empty()) {
293 this->textbuf->Assign(fmt::format("{}: ", suggestion));
294 } else {
295 this->textbuf->Assign(fmt::format("{}{} ", prefix, suggestion));
300 /** Window to enter the chat message in. */
301 struct NetworkChatWindow : public Window {
302 DestType dtype; ///< The type of destination.
303 int dest; ///< The identifier of the destination.
304 QueryString message_editbox; ///< Message editbox.
305 NetworkChatAutoCompletion chat_tab_completion; ///< Holds the state and logic of auto-completion of player names and towns on Tab press.
308 * Create a chat input window.
309 * @param desc Description of the looks of the window.
310 * @param type The type of destination.
311 * @param dest The actual destination index.
313 NetworkChatWindow(WindowDesc &desc, DestType type, int dest)
314 : Window(desc), message_editbox(NETWORK_CHAT_LENGTH), chat_tab_completion(&message_editbox.text)
316 this->dtype = type;
317 this->dest = dest;
318 this->querystrings[WID_NC_TEXTBOX] = &this->message_editbox;
319 this->message_editbox.cancel_button = WID_NC_CLOSE;
320 this->message_editbox.ok_button = WID_NC_SENDBUTTON;
322 static const StringID chat_captions[] = {
323 STR_NETWORK_CHAT_ALL_CAPTION,
324 STR_NETWORK_CHAT_COMPANY_CAPTION,
325 STR_NETWORK_CHAT_CLIENT_CAPTION
327 assert((uint)this->dtype < lengthof(chat_captions));
329 this->CreateNestedTree();
330 this->GetWidget<NWidgetCore>(WID_NC_DESTINATION)->widget_data = chat_captions[this->dtype];
331 this->FinishInitNested(type);
333 this->SetFocusedWidget(WID_NC_TEXTBOX);
334 InvalidateWindowData(WC_NEWS_WINDOW, 0, this->height);
336 PositionNetworkChatWindow(this);
339 void Close([[maybe_unused]] int data = 0) override
341 InvalidateWindowData(WC_NEWS_WINDOW, 0, 0);
342 this->Window::Close();
345 void FindWindowPlacementAndResize([[maybe_unused]] int def_width, [[maybe_unused]] int def_height) override
347 Window::FindWindowPlacementAndResize(_toolbar_width, def_height);
351 * See if we can auto-complete the current text of the user.
353 void ChatTabCompletion()
355 if (this->chat_tab_completion.AutoComplete()) {
356 this->SetDirty();
360 Point OnInitialPosition([[maybe_unused]] int16_t sm_width, [[maybe_unused]] int16_t sm_height, [[maybe_unused]] int window_number) override
362 Point pt = { 0, _screen.height - sm_height - FindWindowById(WC_STATUS_BAR, 0)->height };
363 return pt;
366 void SetStringParameters(WidgetID widget) const override
368 if (widget != WID_NC_DESTINATION) return;
370 if (this->dtype == DESTTYPE_CLIENT) {
371 SetDParamStr(0, NetworkClientInfo::GetByClientID((ClientID)this->dest)->client_name);
375 void OnClick([[maybe_unused]] Point pt, WidgetID widget, [[maybe_unused]] int click_count) override
377 switch (widget) {
378 case WID_NC_SENDBUTTON: /* Send */
379 SendChat(this->message_editbox.text.buf, this->dtype, this->dest);
380 [[fallthrough]];
382 case WID_NC_CLOSE: /* Cancel */
383 this->Close();
384 break;
388 EventState OnKeyPress([[maybe_unused]] char32_t key, uint16_t keycode) override
390 EventState state = ES_NOT_HANDLED;
391 if (keycode == WKC_TAB) {
392 ChatTabCompletion();
393 state = ES_HANDLED;
395 return state;
398 void OnEditboxChanged(WidgetID widget) override
400 if (widget == WID_NC_TEXTBOX) {
401 this->chat_tab_completion.Reset();
406 * Some data on this window has become invalid.
407 * @param data Information about the changed data.
408 * @param gui_scope Whether the call is done from GUI scope. You may not do everything when not in GUI scope. See #InvalidateWindowData() for details.
410 void OnInvalidateData([[maybe_unused]] int data = 0, [[maybe_unused]] bool gui_scope = true) override
412 if (data == this->dest) this->Close();
416 /** The widgets of the chat window. */
417 static constexpr NWidgetPart _nested_chat_window_widgets[] = {
418 NWidget(NWID_HORIZONTAL),
419 NWidget(WWT_CLOSEBOX, COLOUR_GREY, WID_NC_CLOSE),
420 NWidget(WWT_PANEL, COLOUR_GREY, WID_NC_BACKGROUND),
421 NWidget(NWID_HORIZONTAL),
422 NWidget(WWT_TEXT, COLOUR_GREY, WID_NC_DESTINATION), SetMinimalSize(62, 12), SetPadding(1, 0, 1, 0), SetAlignment(SA_VERT_CENTER | SA_RIGHT), SetDataTip(STR_NULL, STR_NULL),
423 NWidget(WWT_EDITBOX, COLOUR_GREY, WID_NC_TEXTBOX), SetMinimalSize(100, 0), SetPadding(1, 0, 1, 0), SetResize(1, 0),
424 SetDataTip(STR_NETWORK_CHAT_OSKTITLE, STR_NULL),
425 NWidget(WWT_PUSHTXTBTN, COLOUR_GREY, WID_NC_SENDBUTTON), SetMinimalSize(62, 12), SetPadding(1, 0, 1, 0), SetDataTip(STR_NETWORK_CHAT_SEND, STR_NULL),
426 EndContainer(),
427 EndContainer(),
428 EndContainer(),
431 /** The description of the chat window. */
432 static WindowDesc _chat_window_desc(
433 WDP_MANUAL, nullptr, 0, 0,
434 WC_SEND_NETWORK_MSG, WC_NONE,
436 _nested_chat_window_widgets
441 * Show the chat window.
442 * @param type The type of destination.
443 * @param dest The actual destination index.
445 void ShowNetworkChatQueryWindow(DestType type, int dest)
447 CloseWindowByClass(WC_SEND_NETWORK_MSG);
448 new NetworkChatWindow(_chat_window_desc, type, dest);