| 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 | |
| 32 | void 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 |
| 45 | class C4ChatControl::ChatSheet : public C4GUI::Tabular::Sheet |
| 46 | { |
| 47 | public: |
| 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 | |
| 74 | private: |
| 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 | |
| 88 | public: |
| 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 | |
| 105 | protected: |
| 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 | |
| 114 | private: |
| 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 | |
| 122 | C4ChatControl::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 | |
| 135 | void 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 | |
| 148 | void 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 | |
| 170 | int32_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 | |
| 181 | C4ChatControl::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 | |
| 220 | C4ChatControl::ChatSheet::~ChatSheet() |
| 221 | { |
| 222 | delete pKeyHistoryUp; |
| 223 | delete pKeyHistoryDown; |
| 224 | } |
| 225 | |
| 226 | void 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 | |
| 240 | void C4ChatControl::ChatSheet::OnShown(bool fByUser) |
| 241 | { |
| 242 | ResetUnread(); |
| 243 | if (fByUser) |
| 244 | { |
| 245 | Update(fLock: true); |
| 246 | pChatControl->UpdateTitle(); |
| 247 | } |
| 248 | } |
| 249 | |
| 250 | C4GUI::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 | |
| 276 | bool 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 | |
| 292 | void 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 | |
| 299 | void 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 | |
| 319 | void C4ChatControl::ChatSheet::ResetUnread() |
| 320 | { |
| 321 | // mark messages as read |
| 322 | if (fHasUnread) |
| 323 | { |
| 324 | fHasUnread = false; |
| 325 | SetCaptionColor(); |
| 326 | } |
| 327 | } |
| 328 | |
| 329 | void 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 | |
| 338 | void 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 | |
| 368 | void 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 | |
| 406 | void 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 | |
| 428 | C4ChatControl::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 | |
| 440 | C4ChatControl::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 | |
| 481 | C4ChatControl::~C4ChatControl() |
| 482 | { |
| 483 | Application.InteractiveThread.ClearCallback(eEvent: Ev_IRC_Message, pnNetworkCallback: this); |
| 484 | delete pTitleChangeBC; |
| 485 | } |
| 486 | |
| 487 | void C4ChatControl::SetTitleChangeCB(C4GUI::BaseInputCallback *pNewCB) |
| 488 | { |
| 489 | delete pTitleChangeBC; |
| 490 | pTitleChangeBC = pNewCB; |
| 491 | // initial title |
| 492 | if (pTitleChangeBC) pTitleChangeBC->OnOK(sText: sTitle); |
| 493 | } |
| 494 | |
| 495 | void 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 | |
| 525 | void C4ChatControl::OnShown() |
| 526 | { |
| 527 | UpdateShownPage(); |
| 528 | } |
| 529 | |
| 530 | C4GUI::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 | |
| 540 | C4ChatControl::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 | |
| 550 | C4ChatControl::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 | |
| 564 | C4ChatControl::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 | |
| 578 | C4ChatControl::ChatSheet *C4ChatControl::GetServerSheet() |
| 579 | { |
| 580 | // server sheet is always the first |
| 581 | return static_cast<ChatSheet *>(pTabChats->GetSheet(iIndex: 0)); |
| 582 | } |
| 583 | |
| 584 | C4GUI::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 | |
| 592 | void 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 | |
| 649 | void 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 | |
| 665 | bool 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 | |
| 677 | void 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 | |
| 834 | C4ChatControl::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 | |
| 857 | void 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 | |
| 885 | bool C4ChatControl::DlgEnter() |
| 886 | { |
| 887 | // enter on connect button connects |
| 888 | if (GetDlg()->GetFocus() == pBtnLogin) { OnConnectBtn(btn: pBtnLogin); return true; } |
| 889 | return false; |
| 890 | } |
| 891 | |
| 892 | void 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 | |
| 899 | bool 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 | |
| 1021 | void 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 | |
| 1037 | C4ChatDlg *C4ChatDlg::pInstance = nullptr; |
| 1038 | |
| 1039 | C4ChatDlg::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 | |
| 1054 | C4ChatDlg::~C4ChatDlg() {} |
| 1055 | |
| 1056 | C4ChatDlg *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 | |
| 1071 | void C4ChatDlg::StopChat() |
| 1072 | { |
| 1073 | if (!pInstance) return; |
| 1074 | pInstance->Close(fOK: false); |
| 1075 | // 2do: Quit IRC |
| 1076 | } |
| 1077 | |
| 1078 | bool C4ChatDlg::ToggleChat() |
| 1079 | { |
| 1080 | if (pInstance && pInstance->IsShown()) |
| 1081 | StopChat(); |
| 1082 | else |
| 1083 | ShowChat(); |
| 1084 | return true; |
| 1085 | } |
| 1086 | |
| 1087 | bool 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 | |
| 1095 | bool C4ChatDlg::IsChatEnabled() |
| 1096 | { |
| 1097 | return true; |
| 1098 | } |
| 1099 | |
| 1100 | C4GUI::Control *C4ChatDlg::GetDefaultControl() |
| 1101 | { |
| 1102 | return pChatCtrl->GetDefaultControl(); |
| 1103 | } |
| 1104 | |
| 1105 | bool 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 | |
| 1115 | void 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 | |
| 1123 | void 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 | |
| 1131 | void 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 | |
| 1141 | void 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 | |