1/*
2 * LegacyClonk
3 *
4 * Copyright (c) RedWolf Design
5 * Copyright (c) 2007, Sven2
6 * Copyright (c) 2017-2020, The LegacyClonk Team and contributors
7 *
8 * Distributed under the terms of the ISC license; see accompanying file
9 * "COPYING" for details.
10 *
11 * "Clonk" is a registered trademark of Matthes Bender, used with permission.
12 * See accompanying file "TRADEMARK" for details.
13 *
14 * To redistribute this file separately, substitute the full license texts
15 * for the above references.
16 */
17
18// IRC client dialog
19
20#include "C4GuiEdit.h"
21#include "C4GuiListBox.h"
22#include "C4GuiResource.h"
23#include "C4GuiTabular.h"
24#include "C4Include.h"
25#include "C4ChatDlg.h"
26#include "C4Game.h"
27#include "C4InputValidation.h"
28#include "C4Network2IRC.h"
29
30#include <format>
31
32void convUTF8toWindows(StdStrBuf &sText)
33{
34 // workaround until we have UTF-8 support: convert German umlauts and ß
35 sText.Replace(szOld: "Ä", szNew: "\xc4");
36 sText.Replace(szOld: "Ö", szNew: "\xd6");
37 sText.Replace(szOld: "Ü", szNew: "\xdc");
38 sText.Replace(szOld: "ä", szNew: "\xe4");
39 sText.Replace(szOld: "ö", szNew: "\xf6");
40 sText.Replace(szOld: "ü", szNew: "\xfc");
41 sText.Replace(szOld: "ß", szNew: "\xdf");
42}
43
44// one open channel or query
45class C4ChatControl::ChatSheet : public C4GUI::Tabular::Sheet
46{
47public:
48 // one item in the nick list
49 class NickItem : public C4GUI::Window
50 {
51 private:
52 C4GUI::Icon *pStatusIcon; // status icon indicating channel status (e.g. operator)
53 C4GUI::Label *pNameLabel; // the nickname
54 bool fFlaggedExisting; // flagged for existance; used by user update func
55 int32_t iStatus;
56
57 public:
58 NickItem(class C4Network2IRCUser *pByUser);
59
60 protected:
61 virtual void UpdateOwnPos() override;
62
63 public:
64 const char *GetNick() const { return pNameLabel->GetText(); }
65 int32_t GetStatus() const { return iStatus; }
66
67 void Update(class C4Network2IRCUser *pByUser);
68 void SetFlaggedExisting(bool fToVal) { fFlaggedExisting = fToVal; }
69 bool IsFlaggedExisting() const { return fFlaggedExisting; }
70
71 static int32_t SortFunc(const C4GUI::Element *pEl1, const C4GUI::Element *pEl2, void *);
72 };
73
74private:
75 C4ChatControl *pChatControl;
76 C4GUI::TextWindow *pChatBox;
77 C4GUI::ListBox *pNickList;
78 C4GUI::WoodenLabel *pInputLbl;
79 C4GUI::Edit *pInputEdit;
80 int32_t iBackBufferIndex; // chat message history index
81 SheetType eType;
82 StdStrBuf sIdent;
83 bool fHasUnread;
84 StdStrBuf sChatTitle; // topic for channels; name+ident for queries; server name for server sheet
85
86 C4KeyBinding *pKeyHistoryUp, *pKeyHistoryDown; // keys used to scroll through chat history
87
88public:
89 ChatSheet(C4ChatControl *pChatControl, const char *szTitle, const char *szIdent, SheetType eType);
90 virtual ~ChatSheet();
91
92 C4GUI::Edit *GetInputEdit() const { return pInputEdit; }
93 SheetType GetSheetType() const { return eType; }
94 const char *GetIdent() const { return sIdent.getData(); }
95 void SetIdent(const char *szToIdent) { sIdent.Copy(pnData: szToIdent); }
96 const char *GetChatTitle() const { return sChatTitle.getData(); }
97 void SetChatTitle(const char *szNewTitle) { sChatTitle = szNewTitle; }
98
99 void AddTextLine(const char *szText, uint32_t dwClr);
100 void DoError(const char *szError);
101 void Update(bool fLock);
102 void UpdateUsers(C4Network2IRCUser *pUsers);
103 void ResetUnread(); // mark messages as read
104
105protected:
106 virtual void UpdateSize() override;
107 virtual void OnShown(bool fByUser) override;
108 virtual void UserClose() override; // user pressed close button: Close queries, part channels, etc.
109
110 C4GUI::InputResult OnChatInput(C4GUI::Edit *edt, bool fPasting, bool fPastingMore);
111 bool KeyHistoryUpDown(bool fUp);
112 void OnNickDblClick(class C4GUI::Element *pEl);
113
114private:
115 NickItem *GetNickItem(const char *szByNick);
116 NickItem *GetFirstNickItem() { return pNickList ? static_cast<NickItem *>(pNickList->GetFirst()) : nullptr; }
117 NickItem *GetNextNickItem(NickItem *pPrev) { return static_cast<NickItem *>(pPrev->GetNext()); }
118};
119
120/* C4ChatControl::ChatSheet::NickItem */
121
122C4ChatControl::ChatSheet::NickItem::NickItem(class C4Network2IRCUser *pByUser) : pStatusIcon(nullptr), pNameLabel(nullptr), fFlaggedExisting(false), iStatus(0)
123{
124 // create elements - will be positioned when resized
125 C4Rect rcDefault(0, 0, 10, 10);
126 AddElement(pChild: pStatusIcon = new C4GUI::Icon(rcDefault, C4GUI::Ico_None));
127 AddElement(pChild: pNameLabel = new C4GUI::Label("", rcDefault, ALeft, C4GUI_CaptionFontClr, nullptr, false, false, false));
128 // set height (pos and width set when added to the list)
129 CStdFont *pUseFont = &C4GUI::GetRes()->TextFont;
130 rcBounds.Set(iX: 0, iY: 0, iWdt: 100, iHgt: pUseFont->GetLineHeight());
131 // initial update
132 Update(pByUser);
133}
134
135void C4ChatControl::ChatSheet::NickItem::UpdateOwnPos()
136{
137 typedef C4GUI::Window ParentClass;
138 ParentClass::UpdateOwnPos();
139 // reposition elements
140 if (pStatusIcon && pNameLabel)
141 {
142 C4GUI::ComponentAligner caMain(GetContainedClientRect(), 1, 0);
143 pStatusIcon->SetBounds(caMain.GetFromLeft(iWdt: caMain.GetInnerHeight()));
144 pNameLabel->SetBounds(caMain.GetAll());
145 }
146}
147
148void C4ChatControl::ChatSheet::NickItem::Update(class C4Network2IRCUser *pByUser)
149{
150 // set status icon
151 const char *szPrefix = pByUser->getPrefix();
152 if (!szPrefix) szPrefix = "";
153 C4GUI::Icons eStatusIcon;
154 switch (*szPrefix)
155 {
156 case '!': eStatusIcon = C4GUI::Ico_Rank1; iStatus = 4; break;
157 case '@': eStatusIcon = C4GUI::Ico_Rank2; iStatus = 3; break;
158 case '%': eStatusIcon = C4GUI::Ico_Rank3; iStatus = 2; break;
159 case '+': eStatusIcon = C4GUI::Ico_AddPlr; iStatus = 1; break;
160 case '\0': eStatusIcon = C4GUI::Ico_Player; iStatus = 0; break;
161 default: eStatusIcon = C4GUI::Ico_Player; iStatus = 0; break;
162 }
163 pStatusIcon->SetIcon(eStatusIcon);
164 // set name
165 pNameLabel->SetText(szText: pByUser->getName());
166 // tooltip is status+name
167 SetToolTip(std::format(fmt: "{}{}", args&: szPrefix, args: pByUser->getName()).c_str());
168}
169
170int32_t C4ChatControl::ChatSheet::NickItem::SortFunc(const C4GUI::Element *pEl1, const C4GUI::Element *pEl2, void *)
171{
172 const NickItem *pNickItem1 = static_cast<const NickItem *>(pEl1);
173 const NickItem *pNickItem2 = static_cast<const NickItem *>(pEl2);
174 int32_t s1 = pNickItem1->GetStatus(), s2 = pNickItem2->GetStatus();
175 if (s1 != s2) return s1 - s2;
176 return stricmp(s1: pNickItem2->GetNick(), s2: pNickItem1->GetNick());
177}
178
179/* C4ChatControl::ChatSheet */
180
181C4ChatControl::ChatSheet::ChatSheet(C4ChatControl *pChatControl, const char *szTitle, const char *szIdent, SheetType eType)
182 : C4GUI::Tabular::Sheet(szTitle, C4Rect(0, 0, 10, 10), C4GUI::Ico_None, true, false),
183 iBackBufferIndex(-1),
184 eType(eType),
185 pNickList(nullptr),
186 pInputLbl(nullptr),
187 pChatControl(pChatControl),
188 fHasUnread(false),
189 sChatTitle{szIdent}
190{
191 if (szIdent) sIdent.Copy(pnData: szIdent);
192 // create elements - positioned later
193 C4Rect rcDefault(0, 0, 10, 10);
194 pChatBox = new C4GUI::TextWindow(rcDefault);
195 pChatBox->SetDecoration(fDrawBG: false, fDrawFrame: false, pToGfx: nullptr, fAutoScroll: false);
196 AddElement(pChild: pChatBox);
197 if (eType == CS_Channel)
198 {
199 pNickList = new C4GUI::ListBox(rcDefault);
200 pNickList->SetDecoration(fDrawBG: false, pToGfx: nullptr, fAutoScroll: true, fDrawBorder: false);
201 pNickList->SetSelectionDblClickFn(new C4GUI::CallbackHandler<C4ChatControl::ChatSheet>(this, &C4ChatControl::ChatSheet::OnNickDblClick));
202 AddElement(pChild: pNickList);
203 }
204 if (eType != CS_Server)
205 pInputLbl = new C4GUI::WoodenLabel(LoadResStr(id: C4ResStrTableKey::IDS_DLG_CHAT), rcDefault, C4GUI_CaptionFontClr, &C4GUI::GetRes()->TextFont);
206 pInputEdit = new C4GUI::CallbackEdit<C4ChatControl::ChatSheet>(rcDefault, this, &C4ChatControl::ChatSheet::OnChatInput);
207 pInputEdit->SetToolTip(LoadResStr(id: C4ResStrTableKey::IDS_DLGTIP_CHAT));
208 if (pInputLbl)
209 {
210 pInputLbl->SetToolTip(LoadResStr(id: C4ResStrTableKey::IDS_DLGTIP_CHAT));
211 pInputLbl->SetClickFocusControl(pInputEdit);
212 AddElement(pChild: pInputLbl);
213 }
214 AddElement(pChild: pInputEdit);
215 // key bindings
216 pKeyHistoryUp = new C4KeyBinding(C4KeyCodeEx(K_UP), "ChatHistoryUp", KEYSCOPE_Gui, new C4GUI::DlgKeyCBEx<C4ChatControl::ChatSheet, bool>(*this, true, &C4ChatControl::ChatSheet::KeyHistoryUpDown), C4CustomKey::PRIO_CtrlOverride);
217 pKeyHistoryDown = new C4KeyBinding(C4KeyCodeEx(K_DOWN), "ChatHistoryDown", KEYSCOPE_Gui, new C4GUI::DlgKeyCBEx<C4ChatControl::ChatSheet, bool>(*this, false, &C4ChatControl::ChatSheet::KeyHistoryUpDown), C4CustomKey::PRIO_CtrlOverride);
218}
219
220C4ChatControl::ChatSheet::~ChatSheet()
221{
222 delete pKeyHistoryUp;
223 delete pKeyHistoryDown;
224}
225
226void C4ChatControl::ChatSheet::UpdateSize()
227{
228 // parent update
229 typedef C4GUI::Window ParentClass;
230 ParentClass::UpdateSize();
231 // position child elements
232 C4GUI::ComponentAligner caMain(GetContainedClientRect(), 0, 0);
233 C4GUI::ComponentAligner caChat(caMain.GetFromBottom(iHgt: C4GUI::Edit::GetDefaultEditHeight()), 0, 0);
234 if (pNickList) pNickList->SetBounds(caMain.GetFromRight(iWdt: std::max<int32_t>(a: caMain.GetInnerWidth() / 5, b: 100)));
235 pChatBox->SetBounds(caMain.GetAll());
236 if (pInputLbl) pInputLbl->SetBounds(caChat.GetFromLeft(iWdt: 40));
237 pInputEdit->SetBounds(caChat.GetAll());
238}
239
240void C4ChatControl::ChatSheet::OnShown(bool fByUser)
241{
242 ResetUnread();
243 if (fByUser)
244 {
245 Update(fLock: true);
246 pChatControl->UpdateTitle();
247 }
248}
249
250C4GUI::InputResult C4ChatControl::ChatSheet::OnChatInput(C4GUI::Edit *edt, bool fPasting, bool fPastingMore)
251{
252 C4GUI::InputResult eResult = C4GUI::IR_None;
253 // get edit text
254 const char *szInputText = pInputEdit->GetText();
255 // no input?
256 if (!szInputText || !*szInputText)
257 {
258 // do some error sound then
259 DoError(szError: nullptr);
260 }
261 else
262 {
263 // remember in history
264 Game.MessageInput.StoreBackBuffer(szMessage: szInputText);
265 // forward to chat control for processing
266 if (!pChatControl->ProcessInput(szInput: szInputText, pChatSheet: this)) eResult = C4GUI::IR_Abort;
267 }
268 // clear edit field after text has been processed
269 pInputEdit->SelectAll(); pInputEdit->DeleteSelection();
270 // reset backbuffer-index of chat history
271 iBackBufferIndex = -1;
272 // OK, on we go
273 return eResult;
274}
275
276bool C4ChatControl::ChatSheet::KeyHistoryUpDown(bool fUp)
277{
278 // chat input only
279 if (!IsFocused(pCtrl: pInputEdit)) return false;
280 pInputEdit->SelectAll(); pInputEdit->DeleteSelection();
281 const char *szPrevInput = Game.MessageInput.GetBackBuffer(iIndex: fUp ? (++iBackBufferIndex) : (--iBackBufferIndex));
282 if (!szPrevInput || !*szPrevInput)
283 iBackBufferIndex = -1;
284 else
285 {
286 pInputEdit->InsertText(text: szPrevInput, fUser: true);
287 pInputEdit->SelectAll();
288 }
289 return true;
290}
291
292void C4ChatControl::ChatSheet::OnNickDblClick(class C4GUI::Element *pEl)
293{
294 if (!pEl) return;
295 NickItem *pNickItem = static_cast<NickItem *>(pEl);
296 pChatControl->OpenQuery(szForNick: pNickItem->GetNick(), fSelect: true, szIdentFallback: nullptr);
297}
298
299void C4ChatControl::ChatSheet::AddTextLine(const char *szText, uint32_t dwClr)
300{
301 // strip stuff that would confuse Clonk
302 StdStrBuf sText; sText.Copy(pnData: szText);
303 for (char c = '\x01'; c < ' '; ++c)
304 sText.ReplaceChar(cOld: c, cNew: ' ');
305 // convert incoming UTF-8
306 convUTF8toWindows(sText);
307 // add text line to chat box
308 CStdFont *pUseFont = &C4GUI::GetRes()->TextFont;
309 pChatBox->AddTextLine(szText: sText.getData(), pFont: pUseFont, dwClr, fDoUpdate: true, fMakeReadableOnBlack: false);
310 pChatBox->ScrollToBottom();
311 // sheet now has unread messages if not selected
312 if (!fHasUnread && !IsActiveSheet())
313 {
314 fHasUnread = true;
315 SetCaptionColor(C4GUI_Caption2FontClr);
316 }
317}
318
319void C4ChatControl::ChatSheet::ResetUnread()
320{
321 // mark messages as read
322 if (fHasUnread)
323 {
324 fHasUnread = false;
325 SetCaptionColor();
326 }
327}
328
329void C4ChatControl::ChatSheet::DoError(const char *szError)
330{
331 if (szError)
332 {
333 AddTextLine(szText: szError, C4GUI_ErrorFontClr);
334 }
335 C4GUI::GUISound(szSound: "Error");
336}
337
338void C4ChatControl::ChatSheet::Update(bool fLock)
339{
340 // lock IRC client data if desired
341 if (fLock)
342 {
343 CStdLock Lock(pChatControl->getIRCClient()->getCSec());
344 Update(fLock: false);
345 return;
346 }
347 // only channels need updates
348 if (eType == CS_Channel)
349 {
350 C4Network2IRCChannel *pIRCChan = pChatControl->getIRCClient()->getChannel(szName: GetIdent());
351 if (pIRCChan)
352 {
353 // update user list (if not locked, becuase it's being received)
354 if (!pIRCChan->isUsersLocked()) UpdateUsers(pUsers: pIRCChan->getUsers());
355 // update topic
356 const char *szTopic = pIRCChan->getTopic();
357 sChatTitle.Copy(Buf2: sIdent);
358 if (szTopic)
359 {
360 sChatTitle.Append(pnData: ": ");
361 sChatTitle.Append(pnData: szTopic);
362 }
363 convUTF8toWindows(sText&: sChatTitle);
364 }
365 }
366}
367
368void C4ChatControl::ChatSheet::UpdateUsers(C4Network2IRCUser *pUsers)
369{
370 NickItem *pNickItem, *pNextNickItem;
371 // update existing users
372 for (; pUsers; pUsers = pUsers->getNext())
373 {
374 if (pNickItem = GetNickItem(szByNick: pUsers->getName()))
375 {
376 pNickItem->Update(pByUser: pUsers);
377 }
378 else
379 {
380 // new user!
381 pNickItem = new NickItem(pUsers);
382 pNickList->AddElement(pChild: pNickItem);
383 }
384 pNickItem->SetFlaggedExisting(true);
385 }
386 // remove left users
387 pNextNickItem = GetFirstNickItem();
388 while (pNickItem = pNextNickItem)
389 {
390 pNextNickItem = GetNextNickItem(pPrev: pNickItem);
391 if (!pNickItem->IsFlaggedExisting())
392 {
393 // this user left
394 delete pNickItem;
395 }
396 else
397 {
398 // user didn't leave; reset flag for next check
399 pNickItem->SetFlaggedExisting(false);
400 }
401 }
402 // sort the rest
403 pNickList->SortElements(SortFunc: &NickItem::SortFunc, par: nullptr);
404}
405
406void C4ChatControl::ChatSheet::UserClose()
407{
408 typedef C4GUI::Tabular::Sheet ParentClass;
409 switch (eType)
410 {
411 case CS_Server:
412 // closing server window? Always forward to control
413 pChatControl->UserQueryQuit();
414 break;
415
416 case CS_Channel:
417 // channel: Send part. Close done by server.
418 pChatControl->getIRCClient()->Part(szChannel: sIdent.getData());
419 break;
420
421 case CS_Query:
422 // query: Always allow simple close
423 ParentClass::UserClose();
424 break;
425 }
426}
427
428C4ChatControl::ChatSheet::NickItem *C4ChatControl::ChatSheet::GetNickItem(const char *szByNick)
429{
430 // find by name
431 for (NickItem *pNickItem = GetFirstNickItem(); pNickItem; pNickItem = GetNextNickItem(pPrev: pNickItem))
432 if (SEqualNoCase(szStr1: pNickItem->GetNick(), szStr2: szByNick))
433 return pNickItem;
434 // not found
435 return nullptr;
436}
437
438/* C4ChatControl */
439
440C4ChatControl::C4ChatControl(C4Network2IRCClient *pnIRCClient) : C4GUI::Window(), pTitleChangeBC(nullptr), pIRCClient(pnIRCClient), fInitialMessagesReceived(false)
441{
442 // create elements - positioned later
443 C4Rect rcDefault(0, 0, 10, 10);
444 // main tabular tabs between chat components (login and channels)
445 pTabMain = new C4GUI::Tabular(rcDefault, C4GUI::Tabular::tbNone);
446 pTabMain->SetDrawDecoration(false);
447 pTabMain->SetSheetMargin(0);
448 AddElement(pChild: pTabMain);
449 C4GUI::Tabular::Sheet *pSheetLogin = pTabMain->AddSheet(szTitle: nullptr);
450 C4GUI::Tabular::Sheet *pSheetChats = pTabMain->AddSheet(szTitle: nullptr);
451 // login sheet
452 CStdFont *pUseFont = &C4GUI::GetRes()->TextFont;
453 pSheetLogin->AddElement(pChild: pLblLoginNick = new C4GUI::Label(LoadResStr(id: C4ResStrTableKey::IDS_CTL_NICK), rcDefault, ALeft, C4GUI_CaptionFontClr, pUseFont, false, true));
454 pSheetLogin->AddElement(pChild: pEdtLoginNick = new C4GUI::CallbackEdit<C4ChatControl>(rcDefault, this, &C4ChatControl::OnLoginDataEnter));
455 pSheetLogin->AddElement(pChild: pLblLoginPass = new C4GUI::Label(LoadResStr(id: C4ResStrTableKey::IDS_CTL_PASSWORDOPTIONAL), rcDefault, ALeft, C4GUI_CaptionFontClr, pUseFont, false, true));
456 pSheetLogin->AddElement(pChild: pEdtLoginPass = new C4GUI::CallbackEdit<C4ChatControl>(rcDefault, this, &C4ChatControl::OnLoginDataEnter));
457 pEdtLoginPass->SetPasswordMask('*');
458 pSheetLogin->AddElement(pChild: pLblLoginRealName = new C4GUI::Label(LoadResStr(id: C4ResStrTableKey::IDS_CTL_REALNAME), rcDefault, ALeft, C4GUI_CaptionFontClr, pUseFont, false, true));
459 pSheetLogin->AddElement(pChild: pEdtLoginRealName = new C4GUI::CallbackEdit<C4ChatControl>(rcDefault, this, &C4ChatControl::OnLoginDataEnter));
460 pSheetLogin->AddElement(pChild: pLblLoginChannel = new C4GUI::Label(LoadResStr(id: C4ResStrTableKey::IDS_CTL_CHANNEL), rcDefault, ALeft, C4GUI_CaptionFontClr, pUseFont, false, true));
461 pSheetLogin->AddElement(pChild: pEdtLoginChannel = new C4GUI::CallbackEdit<C4ChatControl>(rcDefault, this, &C4ChatControl::OnLoginDataEnter));
462 pSheetLogin->AddElement(pChild: pBtnLogin = new C4GUI::CallbackButtonEx<C4ChatControl>(LoadResStr(id: C4ResStrTableKey::IDS_BTN_CONNECT), rcDefault, this, &C4ChatControl::OnConnectBtn));
463 // channel/query tabular
464 pTabChats = new C4GUI::Tabular(rcDefault, C4GUI::Tabular::tbTop);
465 pTabChats->SetSheetMargin(0);
466 pSheetChats->AddElement(pChild: pTabChats);
467 // initial connection values
468 const char *szNick = Config.IRC.Nick, *szRealName = Config.IRC.RealName;
469 StdStrBuf sNick, sRealName;
470 if (!*szNick) szNick = Config.Network.Nick.getData();
471 pEdtLoginNick->SetText(text: szNick, fUser: false);
472 pEdtLoginRealName->SetText(text: szRealName, fUser: false);
473 pEdtLoginChannel->SetText(text: Config.IRC.Channel, fUser: false);
474 // initial sheets
475 ClearChatSheets();
476 // set IRC event callback
477 Application.InteractiveThread.SetCallback(eEvent: Ev_IRC_Message, pnNetworkCallback: this);
478 sTitle.Ref(pnData: "");
479}
480
481C4ChatControl::~C4ChatControl()
482{
483 Application.InteractiveThread.ClearCallback(eEvent: Ev_IRC_Message, pnNetworkCallback: this);
484 delete pTitleChangeBC;
485}
486
487void C4ChatControl::SetTitleChangeCB(C4GUI::BaseInputCallback *pNewCB)
488{
489 delete pTitleChangeBC;
490 pTitleChangeBC = pNewCB;
491 // initial title
492 if (pTitleChangeBC) pTitleChangeBC->OnOK(sText: sTitle);
493}
494
495void C4ChatControl::UpdateSize()
496{
497 // parent update
498 typedef C4GUI::Window ParentClass;
499 ParentClass::UpdateSize();
500 // position child elements
501 pTabMain->SetBounds(GetContainedClientRect());
502 pTabChats->SetBounds(pTabChats->GetParent()->GetContainedClientRect());
503 C4GUI::Tabular::Sheet *pSheetLogin = pTabMain->GetSheet(iIndex: 0);
504 C4GUI::ComponentAligner caLoginSheet(pSheetLogin->GetContainedClientRect(), 0, 0, false);
505 CStdFont *pUseFont = &C4GUI::GetRes()->TextFont;
506 int32_t iIndent1 = C4GUI_DefDlgSmallIndent / 2, iIndent2 = C4GUI_DefDlgIndent / 2;
507 int32_t iLoginHgt = pUseFont->GetLineHeight() * 8 + iIndent1 * 10 + iIndent2 * 10 + C4GUI_ButtonHgt + 20;
508 int32_t iLoginWdt = iLoginHgt * 2 / 3;
509 C4GUI::ComponentAligner caLogin(caLoginSheet.GetCentered(iWdt: std::min<int32_t>(a: iLoginWdt, b: caLoginSheet.GetInnerWidth()), iHgt: std::min<int32_t>(a: iLoginHgt, b: caLoginSheet.GetInnerHeight())), iIndent1, iIndent1);
510 pLblLoginNick->SetBounds(caLogin.GetFromTop(iHgt: pUseFont->GetLineHeight()));
511 pEdtLoginNick->SetBounds(caLogin.GetFromTop(iHgt: C4GUI::Edit::GetDefaultEditHeight()));
512 caLogin.ExpandTop(iByHgt: 2 * (iIndent1 - iIndent2));
513 pLblLoginPass->SetBounds(caLogin.GetFromTop(iHgt: pUseFont->GetLineHeight()));
514 pEdtLoginPass->SetBounds(caLogin.GetFromTop(iHgt: C4GUI::Edit::GetDefaultEditHeight()));
515 caLogin.ExpandTop(iByHgt: 2 * (iIndent1 - iIndent2));
516 pLblLoginRealName->SetBounds(caLogin.GetFromTop(iHgt: pUseFont->GetLineHeight()));
517 pEdtLoginRealName->SetBounds(caLogin.GetFromTop(iHgt: C4GUI::Edit::GetDefaultEditHeight()));
518 caLogin.ExpandTop(iByHgt: 2 * (iIndent1 - iIndent2));
519 pLblLoginChannel->SetBounds(caLogin.GetFromTop(iHgt: pUseFont->GetLineHeight()));
520 pEdtLoginChannel->SetBounds(caLogin.GetFromTop(iHgt: C4GUI::Edit::GetDefaultEditHeight()));
521 caLogin.ExpandTop(iByHgt: 2 * (iIndent1 - iIndent2));
522 pBtnLogin->SetBounds(caLogin.GetFromTop(C4GUI_ButtonHgt, C4GUI_DefButtonWdt));
523}
524
525void C4ChatControl::OnShown()
526{
527 UpdateShownPage();
528}
529
530C4GUI::Control *C4ChatControl::GetDefaultControl()
531{
532 // only return a default control if no control is selected to prevent deselection of other controls
533 if (GetDlg()->GetFocus()) return nullptr;
534 ChatSheet *pActiveSheet = GetActiveChatSheet();
535 if (pActiveSheet) return pActiveSheet->GetInputEdit();
536 if (pBtnLogin->IsVisible()) return pBtnLogin;
537 return nullptr;
538}
539
540C4ChatControl::ChatSheet *C4ChatControl::GetActiveChatSheet()
541{
542 if (pTabChats->IsVisible())
543 {
544 C4GUI::Tabular::Sheet *pSheet = pTabChats->GetActiveSheet();
545 if (pSheet) return static_cast<ChatSheet *>(pSheet);
546 }
547 return nullptr;
548}
549
550C4ChatControl::ChatSheet *C4ChatControl::GetSheetByIdent(const char *szIdent, C4ChatControl::SheetType eType)
551{
552 int32_t i = 0; C4GUI::Tabular::Sheet *pSheet; const char *szCheckIdent;
553 while (pSheet = pTabChats->GetSheet(iIndex: i++))
554 {
555 ChatSheet *pChatSheet = static_cast<ChatSheet *>(pSheet);
556 if (szCheckIdent = pChatSheet->GetIdent())
557 if (SEqualNoCase(szStr1: szCheckIdent, szStr2: szIdent))
558 if (eType == pChatSheet->GetSheetType())
559 return pChatSheet;
560 }
561 return nullptr;
562}
563
564C4ChatControl::ChatSheet *C4ChatControl::GetSheetByTitle(const char *szTitle, C4ChatControl::SheetType eType)
565{
566 int32_t i = 0; C4GUI::Tabular::Sheet *pSheet; const char *szCheckTitle;
567 while (pSheet = pTabChats->GetSheet(iIndex: i++))
568 if (szCheckTitle = pSheet->GetTitle())
569 if (SEqualNoCase(szStr1: szCheckTitle, szStr2: szTitle))
570 {
571 ChatSheet *pChatSheet = static_cast<ChatSheet *>(pSheet);
572 if (eType == pChatSheet->GetSheetType())
573 return pChatSheet;
574 }
575 return nullptr;
576}
577
578C4ChatControl::ChatSheet *C4ChatControl::GetServerSheet()
579{
580 // server sheet is always the first
581 return static_cast<ChatSheet *>(pTabChats->GetSheet(iIndex: 0));
582}
583
584C4GUI::InputResult C4ChatControl::OnLoginDataEnter(C4GUI::Edit *edt, bool fPasting, bool fPastingMore)
585{
586 // advance focus when user presses enter in one of the login edits
587 GetDlg()->AdvanceFocus(fBackwards: false);
588 // no more pasting
589 return C4GUI::IR_Abort;
590}
591
592void C4ChatControl::OnConnectBtn(C4GUI::Control *btn)
593{
594 // check parameters
595 StdStrBuf sNick(pEdtLoginNick->GetText());
596 StdStrBuf sPass(pEdtLoginPass->GetText());
597 StdStrBuf sRealName(pEdtLoginRealName->GetText());
598 StdStrBuf sChannel(pEdtLoginChannel->GetText());
599 StdStrBuf sServer(Config.IRC.Server);
600 if (C4InVal::ValidateString(rsString&: sNick, eOption: C4InVal::VAL_IRCName))
601 {
602 GetScreen()->ShowErrorMessage(szMessage: LoadResStr(id: C4ResStrTableKey::IDS_ERR_INVALIDNICKNAME));
603 GetDlg()->SetFocus(pCtrl: pEdtLoginNick, fByMouse: false);
604 return;
605 }
606 if (sPass.getLength() && C4InVal::ValidateString(rsString&: sPass, eOption: C4InVal::VAL_IRCPass))
607 {
608 GetScreen()->ShowErrorMessage(szMessage: LoadResStr(id: C4ResStrTableKey::IDS_ERR_INVALIDPASSWORDMAX31CHARA));
609 GetDlg()->SetFocus(pCtrl: pEdtLoginPass, fByMouse: false);
610 return;
611 }
612 if (sChannel.getLength() && C4InVal::ValidateString(rsString&: sChannel, eOption: C4InVal::VAL_IRCChannel))
613 {
614 GetScreen()->ShowErrorMessage(szMessage: LoadResStr(id: C4ResStrTableKey::IDS_ERR_INVALIDCHANNELNAME));
615 GetDlg()->SetFocus(pCtrl: pEdtLoginChannel, fByMouse: false);
616 return;
617 }
618 // store back config values
619 SCopy(szSource: sNick.getData(), sTarget: Config.IRC.Nick, iMaxL: CFG_MaxString);
620 SCopy(szSource: sRealName.getData(), sTarget: Config.IRC.RealName, iMaxL: CFG_MaxString);
621 SCopy(szSource: sChannel.getData(), sTarget: Config.IRC.Channel, iMaxL: CFG_MaxString);
622 // show chat warning
623 const std::string warnMsg{LoadResStr(id: C4ResStrTableKey::IDS_MSG_YOUAREABOUTTOCONNECTTOAPU, args: sServer.getData())};
624 if (!GetScreen()->ShowMessageModal(szMessage: warnMsg.c_str(), szCaption: LoadResStr(id: C4ResStrTableKey::IDS_MSG_CHATDISCLAIMER), dwButtons: C4GUI::MessageDialog::btnOKAbort, icoIcon: C4GUI::Ico_Notify, pbConfigDontShowAgainSetting: &Config.Startup.HideMsgIRCDangerous))
625 return;
626 // set up IRC callback
627 pIRCClient->SetNotify(&Application.InteractiveThread);
628 // initiate connection
629 if (!pIRCClient->Connect(szServer: sServer.getData(), szNick: sNick.getData(), szRealName: sRealName.getData(), szPassword: sPass.getData(), szChannel: sChannel.getData()))
630 {
631 GetScreen()->ShowErrorMessage(szMessage: LoadResStr(id: C4ResStrTableKey::IDS_ERR_IRCCONNECTIONFAILED, args: pIRCClient->GetError()).c_str());
632 return;
633 }
634 // enable client execution
635 Application.InteractiveThread.AddProc(pProc: pIRCClient);
636 // reset chat sheets (close queries, etc.)
637 ClearChatSheets();
638 // connection message
639 ChatSheet *pServerSheet = GetServerSheet();
640 if (pServerSheet)
641 {
642 pServerSheet->SetChatTitle(sServer.getData());
643 pServerSheet->AddTextLine(szText: LoadResStr(id: C4ResStrTableKey::IDS_NET_CONNECTING, args: sServer.getData(), args: "").c_str(), C4GUI_MessageFontClr);
644 }
645 // switch to server window
646 UpdateShownPage();
647}
648
649void C4ChatControl::UpdateShownPage()
650{
651 if (pIRCClient->IsActive())
652 {
653 // connected to a server: Show chat window
654 pTabMain->SelectSheet(iIndex: 1, fByUser: false);
655 Update();
656 }
657 else
658 {
659 // not connected: Login stuff
660 pTabMain->SelectSheet(iIndex: 0, fByUser: false);
661 UpdateTitle();
662 }
663}
664
665bool C4ChatControl::IsServiceName(const char *szName)
666{
667 // return true for some hardcoded list of service names
668 if (!szName) return false;
669 const char *szServiceNames[] = { "NickServ", "ChanServ", "MemoServ", "HelpServ", "Global", nullptr }, *szServiceName;
670 int32_t i = 0;
671 while (szServiceName = szServiceNames[i++])
672 if (SEqualNoCase(szStr1: szName, szStr2: szServiceName))
673 return true;
674 return false;
675}
676
677void C4ChatControl::Update()
678{
679 CStdLock Lock(pIRCClient->getCSec());
680 // update channels
681 for (C4Network2IRCChannel *pChan = pIRCClient->getFirstChannel(); pChan; pChan = pIRCClient->getNextChannel(pPrevChan: pChan))
682 {
683 ChatSheet *pChanSheet = GetSheetByIdent(szIdent: pChan->getName(), eType: CS_Channel);
684 if (!pChanSheet)
685 {
686 // new channel! Create sheet for it
687 pTabChats->AddCustomSheet(pAddSheet: pChanSheet = new ChatSheet(this, pChan->getName(), pChan->getName(), CS_Channel));
688 // and show immediately
689 pTabChats->SelectSheet(pSelSheet: pChanSheet, fByUser: false);
690 }
691 }
692 // remove parted channels
693 int32_t i = 0; C4GUI::Tabular::Sheet *pSheet;
694 while (pSheet = pTabChats->GetSheet(iIndex: i++))
695 {
696 C4Network2IRCChannel *pIRCChan;
697 ChatSheet *pChatSheet = static_cast<ChatSheet *>(pSheet);
698 if (pChatSheet->GetSheetType() == CS_Channel)
699 if (!(pIRCChan = pIRCClient->getChannel(szName: pChatSheet->GetTitle())))
700 {
701 delete pChatSheet;
702 --i;
703 }
704 }
705 // retrieve messages: All messages in initial update; only unread in subsequent calls
706 C4Network2IRCMessage *pMsg;
707 if (fInitialMessagesReceived)
708 {
709 pMsg = pIRCClient->getUnreadMessageLog();
710 }
711 else
712 {
713 pMsg = pIRCClient->getMessageLog();
714 fInitialMessagesReceived = true;
715 }
716 // update messages
717 for (; pMsg; pMsg = pMsg->getNext())
718 {
719 // get target sheet to put message into
720 ChatSheet *pChatSheet; StdStrBuf sUser, sIdent;
721 bool fMsgToService = false;
722 if (pMsg->getType() == MSG_Server)
723 {
724 // server messages in server sheet
725 pChatSheet = GetServerSheet();
726 }
727 else
728 {
729 if (pMsg->getType() != MSG_Status) sUser.Copy(pnData: pMsg->getSource());
730 if (!sUser.SplitAtChar(cSplit: '!', psSplit: &sIdent)) sIdent.Ref(Buf2: sUser);
731 // message: Either channel or user message (or channel notify). Get correct sheet.
732 if (pMsg->isChannel())
733 {
734 // if no sheet is found, don't create - assume it's an outdated message with the cahnnel window already closed
735 pChatSheet = GetSheetByIdent(szIdent: pMsg->getTarget(), eType: CS_Channel);
736 }
737 else if (IsServiceName(szName: sUser.getData()))
738 {
739 // notices and messages by services always in server sheet
740 pChatSheet = GetServerSheet();
741 }
742 else if (pMsg->getType() == MSG_Notice)
743 {
744 // notifies in current sheet; default to server sheet
745 pChatSheet = GetActiveChatSheet();
746 if (!pChatSheet) pChatSheet = GetServerSheet();
747 }
748 else if (pMsg->getType() == MSG_Status || !sUser.getLength())
749 {
750 // server message
751 pChatSheet = GetServerSheet();
752 }
753 else if (sUser == pIRCClient->getUserName())
754 {
755 // private message by myself
756 // message to a service into service window; otherwise (new) query
757 if (IsServiceName(szName: pMsg->getTarget()))
758 {
759 pChatSheet = GetServerSheet();
760 fMsgToService = true;
761 }
762 else
763 {
764 pChatSheet = OpenQuery(szForNick: pMsg->getTarget(), fSelect: true, szIdentFallback: nullptr);
765 if (pChatSheet) pChatSheet->SetChatTitle(pMsg->getTarget());
766 }
767 }
768 else
769 {
770 // private message
771 pChatSheet = OpenQuery(szForNick: sUser.getData(), fSelect: false, szIdentFallback: sIdent.getData());
772 if (pChatSheet) pChatSheet->SetChatTitle(pMsg->getSource());
773 }
774 }
775 if (pChatSheet)
776 {
777 // get message formatting and color
778 std::string msg; uint32_t dwClr = C4GUI_MessageFontClr;
779 switch (pMsg->getType())
780 {
781 case MSG_Server:
782 msg = std::format(fmt: "- {}", args: pMsg->getData());
783 break;
784
785 case MSG_Status:
786 msg = std::format(fmt: "- {}", args: pMsg->getData());
787 dwClr = C4GUI_InactMessageFontClr;
788 break;
789
790 case MSG_Notice:
791 if (sUser.getLength())
792 if (sUser != pIRCClient->getUserName())
793 msg = std::format(fmt: "-{}- {}", args: sUser.getData(), args: pMsg->getData());
794 else
795 msg = std::format(fmt: "-> -{}- {}", args: pMsg->getTarget(), args: pMsg->getData());
796 else
797 msg = std::format(fmt: "* {}", args: pMsg->getData());
798 dwClr = C4GUI_NotifyFontClr;
799 break;
800
801 case MSG_Message:
802 if (fMsgToService)
803 msg = std::format(fmt: "-> *{}* {}", args: pMsg->getTarget(), args: pMsg->getData());
804 else if (sUser.getLength())
805 msg = std::format(fmt: "<{}> {}", args: sUser.getData(), args: pMsg->getData());
806 else
807 msg = std::format(fmt: "* {}", args: pMsg->getData());
808 break;
809
810 case MSG_Action:
811 if (sUser.getLength())
812 msg = std::format(fmt: "* {} {}", args: sUser.getData(), args: pMsg->getData());
813 else
814 msg = std::format(fmt: "* {}", args: pMsg->getData());
815 break;
816
817 default:
818 msg = std::format(fmt: "??? {}", args: pMsg->getData());
819 dwClr = C4GUI_ErrorFontClr;
820 break;
821 }
822 pChatSheet->AddTextLine(szText: msg.c_str(), dwClr);
823 }
824 }
825 // OK; all messages processed. Delete overflow messages.
826 pIRCClient->MarkMessageLogRead();
827 // update selected channel (users, topic)
828 ChatSheet *pActiveSheet = GetActiveChatSheet();
829 if (pActiveSheet) pActiveSheet->Update(fLock: false);
830 // update title
831 UpdateTitle();
832}
833
834C4ChatControl::ChatSheet *C4ChatControl::OpenQuery(const char *szForNick, bool fSelect, const char *szIdentFallback)
835{
836 // search existing query first
837 ChatSheet *pChatSheet = GetSheetByTitle(szTitle: szForNick, eType: CS_Query);
838 // not found but ident given? Then search for ident as well
839 if (!pChatSheet && szIdentFallback) pChatSheet = GetSheetByIdent(szIdent: szIdentFallback, eType: CS_Query);
840 // auto-open query if not found
841 if (!pChatSheet)
842 {
843 pTabChats->AddCustomSheet(pAddSheet: pChatSheet = new ChatSheet(this, szForNick, szIdentFallback, CS_Query));
844 // initial chat title just user name; changed to user name+ident if a message from the nick arrives
845 pChatSheet->SetChatTitle(szForNick);
846 }
847 else
848 {
849 // query already open: Update user name if necessary
850 pChatSheet->SetTitle(szForNick);
851 if (szIdentFallback) pChatSheet->SetIdent(szIdentFallback);
852 }
853 if (fSelect) pTabChats->SelectSheet(pSelSheet: pChatSheet, fByUser: true);
854 return pChatSheet;
855}
856
857void C4ChatControl::UpdateTitle()
858{
859 StdStrBuf sNewTitle;
860 if (pTabMain->GetActiveSheetIndex() == 0)
861 {
862 // login title
863 const std::string notConnected{LoadResStr(id: C4ResStrTableKey::IDS_CHAT_NOTCONNECTED)};
864 sNewTitle.Copy(pnData: notConnected.c_str(), iChars: notConnected.size());
865 }
866 else
867 {
868 // login by active sheet
869 ChatSheet *pActiveSheet = GetActiveChatSheet();
870 if (pActiveSheet)
871 {
872 sNewTitle = pActiveSheet->GetChatTitle();
873 }
874 else
875 sNewTitle = "";
876 }
877 // call update proc only if title changed
878 if (sTitle != sNewTitle)
879 {
880 sTitle.Take(Buf2&: sNewTitle);
881 if (pTitleChangeBC) pTitleChangeBC->OnOK(sText: sTitle);
882 }
883}
884
885bool C4ChatControl::DlgEnter()
886{
887 // enter on connect button connects
888 if (GetDlg()->GetFocus() == pBtnLogin) { OnConnectBtn(btn: pBtnLogin); return true; }
889 return false;
890}
891
892void C4ChatControl::ClearChatSheets()
893{
894 pTabChats->ClearSheets();
895 // add server sheet
896 pTabChats->AddCustomSheet(pAddSheet: new ChatSheet(this, LoadResStr(id: C4ResStrTableKey::IDS_CHAT_SERVER), Config.IRC.Server, CS_Server));
897}
898
899bool C4ChatControl::ProcessInput(const char *szInput, ChatSheet *pChatSheet)
900{
901 CStdLock Lock(pIRCClient->getCSec());
902 // process chat input - return false if no more pasting is to be done (i.e., on /quit or /part in channel)
903 bool fResult = true;
904 bool fIRCSuccess = true;
905 // not connected?
906 if (!pIRCClient->IsConnected())
907 {
908 pChatSheet->DoError(szError: LoadResStr(id: C4ResStrTableKey::IDS_ERR_NOTCONNECTEDTOSERVER));
909 return fResult;
910 }
911 // safety
912 if (!szInput || !*szInput || !pChatSheet) return fResult;
913 // command?
914 if (*szInput == '/' && !SEqual2NoCase(szStr1: szInput + 1, szStr2: "me "))
915 {
916 StdStrBuf sCommand, sParam(""); sCommand.Copy(pnData: szInput + 1);
917 sCommand.SplitAtChar(cSplit: ' ', psSplit: &sParam);
918 if (SEqualNoCase(szStr1: sCommand.getData(), szStr2: "quit"))
919 {
920 // query disconnect from IRC server
921 fIRCSuccess = pIRCClient->Quit(szReason: sParam.getData());
922 fResult = false;
923 }
924 else if (SEqualNoCase(szStr1: sCommand.getData(), szStr2: "part"))
925 {
926 // part channel. Default to current channel if typed within a channel
927 if (!sParam.getLength() && pChatSheet->GetSheetType() == CS_Channel)
928 {
929 sParam.Copy(pnData: pChatSheet->GetIdent());
930 fResult = false;
931 }
932 fIRCSuccess = pIRCClient->Part(szChannel: sParam.getData());
933 }
934 else if (SEqualNoCase(szStr1: sCommand.getData(), szStr2: "join") || SEqualNoCase(szStr1: sCommand.getData(), szStr2: "j"))
935 {
936 if (!sParam.getLength()) sParam.Copy(pnData: Config.IRC.Channel);
937 fIRCSuccess = pIRCClient->Join(szChannel: sParam.getData());
938 }
939 else if (SEqualNoCase(szStr1: sCommand.getData(), szStr2: "notice") || SEqualNoCase(szStr1: sCommand.getData(), szStr2: "msg"))
940 {
941 bool fMsg = SEqualNoCase(szStr1: sCommand.getData(), szStr2: "msg");
942 StdStrBuf sMsg;
943 if (!sParam.SplitAtChar(cSplit: ' ', psSplit: &sMsg) || !sMsg.getLength())
944 {
945 pChatSheet->DoError(szError: LoadResStr(id: C4ResStrTableKey::IDS_ERR_INSUFFICIENTPARAMETERS, args: sCommand.getData()).c_str());
946 }
947 else
948 {
949 if (fMsg)
950 fIRCSuccess = pIRCClient->Message(szTarget: sParam.getData(), szText: sMsg.getData());
951 else
952 fIRCSuccess = pIRCClient->Notice(szTarget: sParam.getData(), szText: sMsg.getData());
953 }
954 }
955 else if (SEqualNoCase(szStr1: sCommand.getData(), szStr2: "raw"))
956 {
957 if (!sParam.getLength())
958 pChatSheet->DoError(szError: LoadResStr(id: C4ResStrTableKey::IDS_ERR_INSUFFICIENTPARAMETERS, args: sCommand.getData()).c_str());
959 else
960 fIRCSuccess = pIRCClient->Send(szCommand: sParam.getData());
961 }
962 else if (SEqualNoCase(szStr1: sCommand.getData(), szStr2: "ns") || SEqualNoCase(szStr1: sCommand.getData(), szStr2: "cs") || SEqualNoCase(szStr1: sCommand.getData(), szStr2: "ms"))
963 {
964 if (!sParam.getLength())
965 pChatSheet->DoError(szError: LoadResStr(id: C4ResStrTableKey::IDS_ERR_INSUFFICIENTPARAMETERS, args: sCommand.getData()).c_str());
966 else
967 {
968 const char *szMsgTarget;
969 if (SEqualNoCase(szStr1: sCommand.getData(), szStr2: "ns")) szMsgTarget = "NickServ";
970 else if (SEqualNoCase(szStr1: sCommand.getData(), szStr2: "cs")) szMsgTarget = "ChanServ";
971 else szMsgTarget = "MemoServ";
972 fIRCSuccess = pIRCClient->Message(szTarget: szMsgTarget, szText: sParam.getData());
973 }
974 }
975 else if (SEqualNoCase(szStr1: sCommand.getData(), szStr2: "query") || SEqualNoCase(szStr1: sCommand.getData(), szStr2: "q"))
976 {
977 if (!sParam.getLength())
978 pChatSheet->DoError(szError: LoadResStr(id: C4ResStrTableKey::IDS_ERR_INSUFFICIENTPARAMETERS, args: sCommand.getData()).c_str());
979 else
980 OpenQuery(szForNick: sParam.getData(), fSelect: true, szIdentFallback: nullptr);
981 }
982 else if (SEqualNoCase(szStr1: sCommand.getData(), szStr2: "nick"))
983 {
984 if (C4InVal::ValidateString(rsString&: sParam, eOption: C4InVal::VAL_IRCName))
985 pChatSheet->DoError(szError: LoadResStr(id: C4ResStrTableKey::IDS_ERR_INVALIDNICKNAME2, args: sCommand.getData()).c_str());
986 else
987 fIRCSuccess = pIRCClient->ChangeNick(szNewNick: sParam.getData());
988 }
989 else
990 {
991 // unknown command
992 pChatSheet->DoError(szError: LoadResStr(id: C4ResStrTableKey::IDS_ERR_UNKNOWNCMD, args: sCommand.getData()).c_str());
993 }
994 }
995 else
996 {
997 // regular chat input: Send as message to current channel/user
998 const char *szMsgTarget;
999 SheetType eSheetType = pChatSheet->GetSheetType();
1000 if (eSheetType == CS_Server)
1001 {
1002 pChatSheet->DoError(szError: LoadResStr(id: C4ResStrTableKey::IDS_ERR_NOTONACHANNEL));
1003 }
1004 else
1005 {
1006 szMsgTarget = pChatSheet->GetTitle();
1007 if (*szInput == '/') // action
1008 fIRCSuccess = pIRCClient->Action(szTarget: szMsgTarget, szText: szInput + 4);
1009 else
1010 fIRCSuccess = pIRCClient->Message(szTarget: szMsgTarget, szText: szInput);
1011 }
1012 }
1013 // IRC sending error? log it
1014 if (!fIRCSuccess)
1015 {
1016 pChatSheet->DoError(szError: pIRCClient->GetError());
1017 }
1018 return fResult;
1019}
1020
1021void C4ChatControl::UserQueryQuit()
1022{
1023 // still connected? Then confirm first
1024 if (pIRCClient->IsActive())
1025 {
1026 if (!GetScreen()->ShowMessageModal(szMessage: LoadResStr(id: C4ResStrTableKey::IDS_MSG_DISCONNECTFROMSERVER), szCaption: LoadResStr(id: C4ResStrTableKey::IDS_DLG_CHAT), dwButtons: C4GUI::MessageDialog::btnOKAbort, icoIcon: C4GUI::Ico_Confirm, pbConfigDontShowAgainSetting: nullptr))
1027 return;
1028 }
1029 // disconnect from server
1030 pIRCClient->Close();
1031 // change back to login page
1032 UpdateShownPage();
1033}
1034
1035/* C4ChatDlg */
1036
1037C4ChatDlg *C4ChatDlg::pInstance = nullptr;
1038
1039C4ChatDlg::C4ChatDlg() : C4GUI::Dialog(100, 100, "IRC", false)
1040{
1041 // child elements - positioned later
1042 pChatCtrl = new C4ChatControl(&Application.IRCClient);
1043 pChatCtrl->SetTitleChangeCB(new C4GUI::InputCallback<C4ChatDlg>(this, &C4ChatDlg::OnChatTitleChange));
1044 AddElement(pChild: pChatCtrl);
1045 C4Rect rcDefault(0, 0, 10, 10);
1046 // del dlg when closed
1047 SetDelOnClose();
1048 // set initial element positions
1049 UpdateSize();
1050 // intial focus
1051 SetFocus(pCtrl: GetDefaultControl(), fByMouse: false);
1052}
1053
1054C4ChatDlg::~C4ChatDlg() {}
1055
1056C4ChatDlg *C4ChatDlg::ShowChat()
1057{
1058 if (!Game.pGUI) return nullptr;
1059 if (!pInstance)
1060 {
1061 pInstance = new C4ChatDlg();
1062 pInstance->Show(pOnScreen: Game.pGUI, fCB: true);
1063 }
1064 else
1065 {
1066 Game.pGUI->ActivateDialog(pDlg: pInstance);
1067 }
1068 return pInstance;
1069}
1070
1071void C4ChatDlg::StopChat()
1072{
1073 if (!pInstance) return;
1074 pInstance->Close(fOK: false);
1075 // 2do: Quit IRC
1076}
1077
1078bool C4ChatDlg::ToggleChat()
1079{
1080 if (pInstance && pInstance->IsShown())
1081 StopChat();
1082 else
1083 ShowChat();
1084 return true;
1085}
1086
1087bool C4ChatDlg::IsChatActive()
1088{
1089 // not if chat is disabled
1090 if (!IsChatEnabled()) return false;
1091 // check whether IRC is connected
1092 return Application.IRCClient.IsActive();
1093}
1094
1095bool C4ChatDlg::IsChatEnabled()
1096{
1097 return true;
1098}
1099
1100C4GUI::Control *C4ChatDlg::GetDefaultControl()
1101{
1102 return pChatCtrl->GetDefaultControl();
1103}
1104
1105bool C4ChatDlg::DoPlacement(C4GUI::Screen *pOnScreen, const C4Rect &rPreferredDlgRect)
1106{
1107 // ignore preferred rect; place over complete screen
1108 C4Rect rcPos = pOnScreen->GetContainedClientRect();
1109 rcPos.x += rcPos.Wdt / 10; rcPos.Wdt -= rcPos.Wdt / 5;
1110 rcPos.y += rcPos.Hgt / 10; rcPos.Hgt -= rcPos.Hgt / 5;
1111 SetBounds(rcPos);
1112 return true;
1113}
1114
1115void C4ChatDlg::OnClosed(bool fOK)
1116{
1117 // callback when dlg got closed
1118 pInstance = nullptr;
1119 typedef C4GUI::Dialog ParentClass;
1120 ParentClass::OnClosed(fOK);
1121}
1122
1123void C4ChatDlg::OnShown()
1124{
1125 // callback when shown - should not delete the dialog
1126 typedef C4GUI::Dialog ParentClass;
1127 ParentClass::OnShown();
1128 pChatCtrl->OnShown();
1129}
1130
1131void C4ChatDlg::UpdateSize()
1132{
1133 // parent update
1134 typedef C4GUI::Dialog ParentClass;
1135 ParentClass::UpdateSize();
1136 // position child elements
1137 C4GUI::ComponentAligner caMain(GetContainedClientRect(), 0, 0);
1138 pChatCtrl->SetBounds(caMain.GetAll());
1139}
1140
1141void C4ChatDlg::OnChatTitleChange(const StdStrBuf &sNewTitle)
1142{
1143 SetTitle(szToTitle: std::format(fmt: "{} - {}", args: LoadResStr(id: C4ResStrTableKey::IDS_DLG_CHAT), args: sNewTitle.isNull() ? "(null)" : sNewTitle.getData()).c_str());
1144}
1145