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/>.
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"
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"
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. */
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.
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
;
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.
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;
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
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
) {
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
;
148 height
= std::max(height
+ y
, std::min(_chatmsg_box
.height
, _screen
.height
));
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;
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
;
202 height
= std::max(height
+ y
, std::min(_chatmsg_box
.height
, _screen
.height
));
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);
261 NetworkServerSendChat((NetworkAction
)(NETWORK_ACTION_CHAT
+ type
), type
, dest
, buf
, CLIENT_ID_SERVER
);
265 class NetworkChatAutoCompletion final
: public AutoCompletion
{
267 using AutoCompletion::AutoCompletion
;
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
));
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
));
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
)
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()) {
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
};
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
378 case WID_NC_SENDBUTTON
: /* Send */
379 SendChat(this->message_editbox
.text
.buf
, this->dtype
, this->dest
);
382 case WID_NC_CLOSE
: /* Cancel */
388 EventState
OnKeyPress([[maybe_unused
]] char32_t key
, uint16_t keycode
) override
390 EventState state
= ES_NOT_HANDLED
;
391 if (keycode
== WKC_TAB
) {
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
),
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
);