1 /*
2  *  The ManaPlus Client
3  *  Copyright (C) 2004-2009  The Mana World Development Team
4  *  Copyright (C) 2009-2010  The Mana Developers
5  *  Copyright (C) 2011-2019  The ManaPlus Developers
6  *  Copyright (C) 2019-2021  Andrei Karas
7  *
8  *  This file is part of The ManaPlus Client.
9  *
10  *  This program is free software; you can redistribute it and/or modify
11  *  it under the terms of the GNU General Public License as published by
12  *  the Free Software Foundation; either version 2 of the License, or
13  *  any later version.
14  *
15  *  This program is distributed in the hope that it will be useful,
16  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
17  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18  *  GNU General Public License for more details.
19  *
20  *  You should have received a copy of the GNU General Public License
21  *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
22  */
23 
24 #include "gui/windows/chatwindow.h"
25 
26 #include "actormanager.h"
27 #include "game.h"
28 #include "guild.h"
29 #include "party.h"
30 #include "settings.h"
31 #include "spellmanager.h"
32 
33 #include "being/localplayer.h"
34 #include "being/playerinfo.h"
35 #include "being/playerrelations.h"
36 
37 #include "const/gui/chat.h"
38 
39 #include "fs/virtfs/tools.h"
40 
41 #include "input/inputmanager.h"
42 
43 #include "gui/focushandler.h"
44 #include "gui/gui.h"
45 #include "gui/skin.h"
46 #include "gui/viewport.h"
47 
48 #include "gui/models/colorlistmodel.h"
49 
50 #include "gui/popups/popupmenu.h"
51 
52 #include "gui/windows/setupwindow.h"
53 #include "gui/windows/whoisonline.h"
54 
55 #include "gui/widgets/button.h"
56 #include "gui/widgets/chatinput.h"
57 #include "gui/widgets/createwidget.h"
58 #include "gui/widgets/dropdown.h"
59 #include "gui/widgets/itemlinkhandler.h"
60 #include "gui/widgets/scrollarea.h"
61 #include "gui/widgets/tabbedarea.h"
62 
63 #include "gui/widgets/tabs/chat/battletab.h"
64 #include "gui/widgets/tabs/chat/channeltab.h"
65 #include "gui/widgets/tabs/chat/gmtab.h"
66 #include "gui/widgets/tabs/chat/langtab.h"
67 #include "gui/widgets/tabs/chat/tradetab.h"
68 #include "gui/widgets/tabs/chat/whispertab.h"
69 
70 #include "render/opengl/opengldebug.h"
71 
72 #include "resources/db/textdb.h"
73 
74 #include "net/chathandler.h"
75 #include "net/net.h"
76 
77 #include "utils/copynpaste.h"
78 #include "utils/delete2.h"
79 #include "utils/foreach.h"
80 
81 #include "utils/translation/podict.h"
82 
83 #include <sys/stat.h>
84 
85 #include <fstream>
86 #include <sstream>
87 
88 #ifdef __SWITCH__
89 #include "enums/input/keyvalue.h"
90 #endif
91 
92 #include "debug.h"
93 
94 ChatWindow *chatWindow = nullptr;
95 
96 static const char *const ACTION_COLOR_PICKER = "color picker";
97 
ChatWindow(const std::string & name)98 ChatWindow::ChatWindow(const std::string &name) :
99     // TRANSLATORS: chat window name
100     Window(_("Chat"), Modal_false, nullptr, "chat.xml"),
101     ActionListener(),
102     KeyListener(),
103     AttributeListener(),
104     mItemLinkHandler(new ItemLinkHandler),
105     mChatTabs(CREATEWIDGETR(TabbedArea, this)),
106     mChatInput(new ChatInput(this)),
107     mRainbowColor(0U),
108     mWhispers(),
109     mChannels(),
110     mHistory(),
111     mCurHist(),
112     mCommands(),
113     mCustomWords(),
114     mTradeFilter(),
115     mColorListModel(new ColorListModel),
116     mColorPicker(new DropDown(this, mColorListModel,
117         false, Modal_false, nullptr, std::string())),
118     mChatButton(new Button(this, ":)", "openemote", BUTTON_SKIN, this)),
119     mAwayLog(),
120     mHighlights(),
121     mGlobalsFilter(),
122     mChatColor(config.getIntValue("chatColor")),
123     mEmoteButtonSpacing(mSkin != nullptr ?
124                         mSkin->getOption("emoteButtonSpacing", 2) : 2),
125     mEmoteButtonY(mSkin != nullptr ?
126                   mSkin->getOption("emoteButtonY", -2) : -2),
127     mChatHistoryIndex(0),
128     mReturnToggles(config.getBoolValue("ReturnToggles")),
129     mGMLoaded(false),
130     mHaveMouse(false),
131     mAutoHide(config.getBoolValue("autohideChat")),
132     mShowBattleEvents(config.getBoolValue("showBattleEvents")),
133     mShowAllLang(serverConfig.getValue("showAllLang", 0) != 0),
134     mEnableTradeFilter(config.getBoolValue("enableTradeFilter")),
135     mTmpVisible(false)
136 {
137     setWindowName(name);
138 
139     if (setupWindow != nullptr)
140         setupWindow->registerWindowForReset(this);
141 
142     setShowTitle(false);
143     setResizable(true);
144     setDefaultVisible(true);
145     setSaveVisible(true);
146     setStickyButtonLock(true);
147 
148     int w = 600;
149 #ifdef ANDROID
150     if (mainGraphics->getWidth() < 710)
151         w = mainGraphics->getWidth() - 110;
152     if (w < 100)
153         w = 100;
154     if (mainGraphics->getHeight() < 480)
155         setDefaultSize(w, 90, ImagePosition::UPPER_LEFT, -110, -35);
156     else
157         setDefaultSize(w, 123, ImagePosition::UPPER_LEFT, -110, -35);
158 #else  // ANDROID
159 
160     if (mainGraphics->getWidth() < 600)
161         w = mainGraphics->getWidth() - 10;
162     if (w < 100)
163         w = 100;
164     setDefaultSize(w, 123, ImagePosition::LOWER_LEFT, 0, 0);
165 #endif  // ANDROID
166 
167     setMinWidth(150);
168     setMinHeight(90);
169 
170     setTitleBarHeight(getPadding() + getTitlePadding());
171 
172     if (emoteWindow != nullptr)
173         emoteWindow->addListeners(this);
174 
175     mChatButton->adjustSize();
176 
177     mChatTabs->enableScrollButtons(true);
178     mChatTabs->setFollowDownScroll(true);
179     mChatTabs->setResizeHeight(false);
180 
181     mChatInput->setActionEventId("chatinput");
182     mChatInput->addActionListener(this);
183     mChatInput->setAllowSpecialActions(false);
184 
185     mColorPicker->setActionEventId(ACTION_COLOR_PICKER);
186     mColorPicker->addActionListener(this);
187     mColorPicker->setSelected(mChatColor);
188 
189     mItemLinkHandler->setAllowCommands(false);
190 
191     loadWindowState();
192 
193     mColorPicker->setPosition(this->getWidth() - mColorPicker->getWidth()
194         - 2 * mPadding - 8 - 16, mPadding);
195 
196     // Add key listener to chat input to be able to respond to up/down
197     mChatInput->addKeyListener(this);
198     mCurHist = mHistory.end();
199     mColorPicker->setVisible(fromBool(config.getBoolValue(
200         "showChatColorsList"), Visible));
201     updateTabsMargin();
202 
203     fillCommands();
204     if ((localPlayer != nullptr) && localPlayer->isGM())
205         loadGMCommands();
206     initTradeFilter();
207     loadCustomList();
208     parseHighlights();
209     parseGlobalsFilter();
210 
211     config.addListener("autohideChat", this);
212     config.addListener("showBattleEvents", this);
213     config.addListener("globalsFilter", this);
214     config.addListener("enableTradeFilter", this);
215 
216     enableVisibleSound(true);
217 }
218 
~ChatWindow()219 ChatWindow::~ChatWindow()
220 {
221     config.removeListeners(this);
222     CHECKLISTENERS
223     saveState();
224     config.setValue("ReturnToggles", mReturnToggles);
225     removeAllWhispers();
226     removeAllChannels();
227     delete2(mItemLinkHandler)
228     delete2(mColorPicker)
229     delete2(mColorListModel)
230 }
231 
postInit()232 void ChatWindow::postInit()
233 {
234     Window::postInit();
235     add(mChatTabs);
236     add(mChatInput);
237     add(mColorPicker);
238     add(mChatButton);
239     updateVisibility();
240 }
241 
loadCommandsFile(const std::string & name)242 void ChatWindow::loadCommandsFile(const std::string &name)
243 {
244     StringVect list;
245     VirtFs::loadTextFile(name, list);
246     StringVectCIter it = list.begin();
247     const StringVectCIter it_end = list.end();
248 
249     while (it != it_end)
250     {
251         const std::string str = *it;
252         if (!str.empty())
253             mCommands.push_back(str);
254         ++ it;
255     }
256 }
257 
fillCommands()258 void ChatWindow::fillCommands()
259 {
260     loadCommandsFile("chatcommands.txt");
261     InputManager::addChatCommands(mCommands);
262 }
263 
loadGMCommands()264 void ChatWindow::loadGMCommands()
265 {
266     if (mGMLoaded)
267         return;
268 
269     loadCommandsFile("gmcommands.txt");
270     mGMLoaded = true;
271 }
272 
updateTabsMargin()273 void ChatWindow::updateTabsMargin()
274 {
275     if (mColorPicker->mVisible == Visible_true)
276         mChatTabs->setRightMargin(mColorPicker->getWidth() + 16 + 8);
277     else
278         mChatTabs->setRightMargin(8);
279 }
280 
adjustTabSize()281 void ChatWindow::adjustTabSize()
282 {
283     const Rect area = getChildrenArea();
284 
285     const int aw = area.width;
286     const int ah = area.height;
287     const int frame = mChatInput->getFrameSize();
288     const int inputHeight = mChatInput->getHeight();
289     const bool showEmotes = config.getBoolValue("showEmotesButton");
290     int maxHeight = inputHeight;
291     if (showEmotes)
292     {
293         const int buttonHeight = mChatButton->getHeight();
294         if (buttonHeight > maxHeight)
295             maxHeight = buttonHeight;
296     }
297     const int frame2 = 2 * frame;
298     const int awFrame2 = aw - frame2;
299     int y = ah - maxHeight - frame;
300     mChatInput->setPosition(frame, y);
301     mChatTabs->setWidth(awFrame2);
302     const int height = ah - frame2 - (maxHeight + frame2);
303     if (mChatInput->mVisible == Visible_true ||
304         !config.getBoolValue("hideChatInput"))
305     {
306         mChatTabs->setHeight(height);
307     }
308     else
309     {
310         mChatTabs->setHeight(height + maxHeight);
311     }
312     updateTabsMargin();
313 
314     if (showEmotes)
315     {
316         const int chatButtonSize = mChatButton->getWidth();
317         int w = awFrame2 - chatButtonSize;
318         const int x = aw - frame - chatButtonSize;
319         w -= mEmoteButtonSpacing;
320         y += mEmoteButtonY;
321         mChatInput->setWidth(w);
322         mChatButton->setVisible(mChatInput->mVisible);
323         mChatButton->setPosition(x, y);
324     }
325     else
326     {
327         mChatInput->setWidth(awFrame2);
328         mChatButton->setVisible(Visible_false);
329     }
330 
331     const ChatTab *const tab = getFocused();
332     if (tab != nullptr)
333     {
334         Widget *const content = tab->mScrollArea;
335         if (content != nullptr)
336         {
337             const int contentFrame2 = 2 * content->getFrameSize();
338             content->setSize(mChatTabs->getWidth() - contentFrame2,
339                 mChatTabs->getContainerHeight() - contentFrame2);
340             content->logic();
341         }
342     }
343 
344     mColorPicker->setPosition(this->getWidth() - mColorPicker->getWidth()
345         - 2 * mPadding - 8 - 16, mPadding);
346 
347     mChatTabs->adjustSize();
348 }
349 
widgetResized(const Event & event)350 void ChatWindow::widgetResized(const Event &event)
351 {
352     Window::widgetResized(event);
353 
354     adjustTabSize();
355 }
356 
getFocused() const357 ChatTab *ChatWindow::getFocused() const
358 {
359     return static_cast<ChatTab*>(mChatTabs->getSelectedTab());
360 }
361 
clearTab(ChatTab * const tab)362 void ChatWindow::clearTab(ChatTab *const tab)
363 {
364     if (tab != nullptr)
365         tab->clearText();
366 }
367 
clearTab() const368 void ChatWindow::clearTab() const
369 {
370     clearTab(getFocused());
371 }
372 
prevTab()373 void ChatWindow::prevTab()
374 {
375     if (mChatTabs == nullptr)
376         return;
377 
378     int tab = mChatTabs->getSelectedTabIndex();
379 
380     if (tab <= 0)
381         tab = mChatTabs->getNumberOfTabs();
382     tab--;
383 
384     mChatTabs->setSelectedTabByIndex(tab);
385 }
386 
nextTab()387 void ChatWindow::nextTab()
388 {
389     if (mChatTabs == nullptr)
390         return;
391 
392     int tab = mChatTabs->getSelectedTabIndex();
393 
394     tab++;
395     if (tab == mChatTabs->getNumberOfTabs())
396         tab = 0;
397 
398     mChatTabs->setSelectedTabByIndex(tab);
399 }
400 
selectTabByType(const ChatTabTypeT & type)401 void ChatWindow::selectTabByType(const ChatTabTypeT &type)
402 {
403     if (mChatTabs == nullptr)
404         return;
405 
406     int sz = mChatTabs->getNumberOfTabs();
407     for (int f = 0; f < sz; f ++)
408     {
409         ChatTab *const tab = dynamic_cast<ChatTab*>(
410             mChatTabs->getTabByIndex(f));
411         if ((tab != nullptr) && tab->getType() == type)
412         {
413             mChatTabs->setSelectedTab(tab);
414             break;
415         }
416     }
417 }
418 
closeTab() const419 void ChatWindow::closeTab() const
420 {
421     if (mChatTabs == nullptr)
422         return;
423 
424     ChatTab *const tab = dynamic_cast<ChatTab*>(mChatTabs->getTabByIndex(
425         mChatTabs->getSelectedTabIndex()));
426     if (tab == nullptr)
427         return;
428     const ChatTabTypeT &type = tab->getType();
429     if (type == ChatTabType::WHISPER || type == ChatTabType::CHANNEL)
430         tab->handleCommand("close", "");
431 }
432 
defaultTab()433 void ChatWindow::defaultTab()
434 {
435     if (mChatTabs != nullptr)
436         mChatTabs->setSelectedTabByIndex(CAST_U32(0));
437 }
438 
action(const ActionEvent & event)439 void ChatWindow::action(const ActionEvent &event)
440 {
441     const std::string &eventId = event.getId();
442     if (eventId == "chatinput")
443     {
444         std::string message = mChatInput->getText();
445 
446         if (!message.empty())
447         {
448             // If message different from previous, put it in the history
449             if (mHistory.empty() || message != mHistory.back())
450                 mHistory.push_back(message);
451 
452             // Reset history iterator
453             mCurHist = mHistory.end();
454 
455             // Send the message to the server
456             chatInput(addColors(message));
457 
458             // Clear the text from the chat input
459             mChatInput->setText("");
460         }
461 
462         if (message.empty() || !mReturnToggles)
463         {
464             // Remove focus and hide input
465             mChatInput->unprotectFocus();
466             if (mFocusHandler != nullptr)
467                 mFocusHandler->focusNone();
468 
469             // If the chatWindow is shown up because you want to send a message
470             // It should hide now
471             if (mTmpVisible)
472                 setVisible(Visible_false);
473         }
474     }
475     else if (eventId == "emote")
476     {
477         if (emoteWindow != nullptr)
478         {
479             const std::string str = emoteWindow->getSelectedEmote();
480             if (!str.empty())
481             {
482                 addInputText(str, false);
483                 emoteWindow->clearEmote();
484             }
485         }
486     }
487     else if (eventId == "openemote")
488     {
489         if (emoteWindow != nullptr)
490         {
491             if (emoteWindow->mVisible == Visible_true)
492                 emoteWindow->hide();
493             else
494                 emoteWindow->show();
495         }
496     }
497     else if (eventId == "color")
498     {
499         if (emoteWindow != nullptr)
500         {
501             const std::string str = emoteWindow->getSelectedColor();
502             if (!str.empty())
503             {
504                 addInputText(str, false);
505                 emoteWindow->clearColor();
506             }
507         }
508     }
509     else if (eventId == "font")
510     {
511         if (emoteWindow != nullptr)
512         {
513             const std::string str = emoteWindow->getSelectedFont();
514             if (!str.empty())
515             {
516                 addInputText(str, false);
517                 emoteWindow->clearFont();
518             }
519         }
520     }
521     else if (eventId == "text")
522     {
523         if ((emoteWindow != nullptr) && (reverseDictionary != nullptr))
524         {
525             const int idx = emoteWindow->getSelectedTextIndex();
526             if (idx >= 0)
527             {
528                 const std::string str = TextDb::getByIndex(idx);
529                 const std::string enStr = reverseDictionary->getStr(str);
530                 addInputText(enStr, false);
531             }
532             emoteWindow->clearText();
533         }
534     }
535     else if (eventId == ACTION_COLOR_PICKER)
536     {
537         if (mColorPicker != nullptr)
538         {
539             mChatColor = mColorPicker->getSelected();
540             config.setValue("chatColor", mChatColor);
541         }
542     }
543 
544     if (mColorPicker != nullptr)
545     {
546         const Visible vis = fromBool(config.getBoolValue(
547             "showChatColorsList"), Visible);
548         if (mColorPicker->mVisible != vis)
549             mColorPicker->setVisible(vis);
550     }
551 }
552 
requestChatFocus()553 bool ChatWindow::requestChatFocus()
554 {
555     // Make sure chatWindow is visible
556     if (!isWindowVisible())
557     {
558         setVisible(Visible_true);
559 
560         /*
561          * This is used to hide chatWindow after sending the message. There is
562          * a trick here, because setVisible will set mTmpVisible to false, you
563          * have to put this sentence *after* setVisible, not before it
564          */
565         mTmpVisible = true;
566     }
567 
568     // Don't do anything else if the input is already visible and has focus
569     if (mChatInput->isVisible() && mChatInput->isFocused())
570         return false;
571 
572     // Give focus to the chat input
573     mChatInput->processVisible(Visible_true);
574     unHideWindow();
575     mChatInput->requestFocus();
576     return true;
577 }
578 
isInputFocused() const579 bool ChatWindow::isInputFocused() const
580 {
581     return mChatInput->isFocused();
582 }
583 
removeTab(ChatTab * const tab)584 void ChatWindow::removeTab(ChatTab *const tab)
585 {
586     mChatTabs->removeTab(tab);
587 }
588 
addTab(ChatTab * const tab)589 void ChatWindow::addTab(ChatTab *const tab)
590 {
591     if (tab == nullptr)
592         return;
593 
594     mChatTabs->addTab(tab, tab->mScrollArea);
595     logic();
596 }
597 
removeWhisper(const std::string & nick)598 void ChatWindow::removeWhisper(const std::string &nick)
599 {
600     std::string tempNick = nick;
601     toLower(tempNick);
602     mWhispers.erase(tempNick);
603 }
604 
removeChannel(const std::string & name)605 void ChatWindow::removeChannel(const std::string &name)
606 {
607     std::string tempName = name;
608     toLower(tempName);
609     mChannels.erase(tempName);
610     chatHandler->partChannel(name);
611 }
612 
removeAllWhispers()613 void ChatWindow::removeAllWhispers()
614 {
615     std::list<ChatTab*> tabs;
616 
617     FOR_EACH (TabMap::iterator, iter, mWhispers)
618         tabs.push_back(iter->second);
619 
620     for (std::list<ChatTab*>::iterator it = tabs.begin();
621          it != tabs.end(); ++it)
622     {
623         delete *it;
624     }
625 
626     mWhispers.clear();
627 }
628 
removeAllChannels()629 void ChatWindow::removeAllChannels()
630 {
631     std::list<ChatTab*> tabs;
632 
633     FOR_EACH (ChannelMap::iterator, iter, mChannels)
634         tabs.push_back(iter->second);
635 
636     for (std::list<ChatTab*>::iterator it = tabs.begin();
637          it != tabs.end(); ++it)
638     {
639         delete *it;
640     }
641 
642     mChannels.clear();
643 }
644 
ignoreAllWhispers()645 void ChatWindow::ignoreAllWhispers()
646 {
647     for (TabMap::iterator iter = mWhispers.begin();
648          iter != mWhispers.end();
649          ++ iter)
650     {
651         WhisperTab *const tab = iter->second;
652         if (tab != nullptr)
653         {
654             if (playerRelations.getRelation(tab->getNick())
655                 != Relation::IGNORED)
656             {
657                 playerRelations.setRelation(tab->getNick(),
658                     Relation::IGNORED);
659             }
660             tab->handleCommand("close", "");
661         }
662     }
663 }
664 
chatInput(const std::string & message) const665 void ChatWindow::chatInput(const std::string &message) const
666 {
667     ChatTab *tab = nullptr;
668     std::string msg = message;
669     trim(msg);
670 
671     if (config.getBoolValue("allowCommandsInChatTabs")
672         && msg.length() > 1
673         && ((msg.at(0) == '#' && msg.at(1) != '#') || msg.at(0) == '@')
674         && (localChatTab != nullptr))
675     {
676         tab = localChatTab;
677     }
678     else
679     {
680         tab = getFocused();
681         if (tab == nullptr)
682             tab = localChatTab;
683     }
684     if (tab != nullptr)
685         tab->chatInput(msg);
686     Game *const game = Game::instance();
687     if (game != nullptr)
688         game->setValidSpeed();
689 }
690 
localChatInput(const std::string & msg) const691 void ChatWindow::localChatInput(const std::string &msg) const
692 {
693     if (localChatTab != nullptr)
694         localChatTab->chatInput(msg);
695     else
696         chatInput(msg);
697 }
698 
doPresent() const699 void ChatWindow::doPresent() const
700 {
701     if (actorManager == nullptr)
702         return;
703 
704     const ActorSprites &actors = actorManager->getAll();
705     std::string response;
706     int playercount = 0;
707 
708     FOR_EACH (ActorSpritesIterator, it, actors)
709     {
710         if ((*it)->getType() == ActorType::Player)
711         {
712             if (!response.empty())
713                 response.append(", ");
714             response.append(static_cast<Being*>(*it)->getName());
715             playercount ++;
716         }
717     }
718 
719     ChatTab *const tab = getFocused();
720     if (tab != nullptr)
721     {
722         const std::string log = strprintf(
723             // TRANSLATORS: chat message
724             _("Present: %s; %d players are present."),
725             response.c_str(), playercount);
726         tab->chatLog(log,
727             ChatMsgType::BY_SERVER,
728             IgnoreRecord_false,
729             TryRemoveColors_true);
730     }
731 }
732 
scroll(const int amount) const733 void ChatWindow::scroll(const int amount) const
734 {
735     if (!isWindowVisible())
736         return;
737 
738     ChatTab *const tab = getFocused();
739     if (tab != nullptr)
740         tab->scroll(amount);
741 }
742 
mousePressed(MouseEvent & event)743 void ChatWindow::mousePressed(MouseEvent &event)
744 {
745     if (event.isConsumed())
746         return;
747 
748     if (event.getButton() == MouseButton::RIGHT)
749     {
750         if (popupMenu != nullptr)
751         {
752             ChatTab *const cTab = dynamic_cast<ChatTab*>(
753                 mChatTabs->getSelectedTab());
754             if (cTab != nullptr)
755             {
756                 event.consume();
757                 if (inputManager.isActionActive(InputAction::CHAT_MOD))
758                 {
759                     inputManager.executeChatCommand(
760                         InputAction::CLOSE_CHAT_TAB,
761                         std::string(), cTab);
762                 }
763                 else
764                 {
765                     popupMenu->showChatPopup(viewport->mMouseX,
766                         viewport->mMouseY,
767                         cTab);
768                 }
769             }
770         }
771     }
772 
773     Window::mousePressed(event);
774 
775     if (event.isConsumed())
776         return;
777 
778     if (event.getButton() == MouseButton::LEFT)
779     {
780         const int clicks = event.getClickCount();
781         if (clicks == 2)
782         {
783             toggleChatFocus();
784             if (gui != nullptr)
785                 gui->resetClickCount();
786         }
787         else if (clicks == 1)
788         {
789             const ChatTab *const tab = getFocused();
790             if (tab != nullptr)
791                 mMoved = !isResizeAllowed(event);
792         }
793     }
794 
795     mDragOffsetX = event.getX();
796     mDragOffsetY = event.getY();
797 }
798 
mouseDragged(MouseEvent & event)799 void ChatWindow::mouseDragged(MouseEvent &event)
800 {
801     Window::mouseDragged(event);
802 
803     if (event.isConsumed())
804         return;
805 
806     if (canMove() && isMovable() && mMoved)
807     {
808         int newX = std::max(0, getX() + event.getX() - mDragOffsetX);
809         int newY = std::max(0, getY() + event.getY() - mDragOffsetY);
810         newX = std::min(mainGraphics->mWidth - getWidth(), newX);
811         newY = std::min(mainGraphics->mHeight - getHeight(), newY);
812         setPosition(newX, newY);
813     }
814 }
815 
816 #define ifKey(key, str) \
817     else if (actionId == (key)) \
818     { \
819         temp = str; \
820     }
821 
keyPressed(KeyEvent & event)822 void ChatWindow::keyPressed(KeyEvent &event)
823 {
824     const InputActionT actionId = event.getActionId();
825     std::string temp;
826 #ifdef __SWITCH__
827     if (event.getKey().getValue() == KeyValue::TEXTINPUT)
828     {
829         action(ActionEvent(this, "chatinput"));
830         return;
831     }
832     else
833 #endif
834     if (actionId == InputAction::GUI_DOWN)
835     {
836         if (mCurHist != mHistory.end())
837         {
838             // Move forward through the history
839             const HistoryIterator prevHist = mCurHist++;
840             addCurrentToHistory();
841 
842             if (mCurHist != mHistory.end())
843             {
844                 mChatInput->setText(*mCurHist);
845                 mChatInput->setCaretPosition(CAST_U32(
846                         mChatInput->getText().length()));
847             }
848             else
849             {
850                 mChatInput->setText("");
851                 mCurHist = prevHist;
852             }
853         }
854         else if (!mChatInput->getText().empty())
855         {
856             if (addCurrentToHistory())
857                 mCurHist = mHistory.end();
858             mChatInput->setText("");
859         }
860     }
861     else if (actionId == InputAction::GUI_UP &&
862              mCurHist != mHistory.begin() &&
863              !mHistory.empty())
864     {
865         // Move backward through the history
866         --mCurHist;
867         addCurrentToHistory();
868         mChatInput->setText(*mCurHist);
869         mChatInput->setCaretPosition(CAST_U32(
870                 mChatInput->getText().length()));
871     }
872     else if (actionId == InputAction::GUI_INSERT &&
873              !mChatInput->getText().empty())
874     {
875         const std::string str = mChatInput->getText();
876         // Add the current message to the history and clear the text
877         if (mHistory.empty() || str != mHistory.back())
878             mHistory.push_back(str);
879         mCurHist = mHistory.end();
880         mChatInput->setText(std::string());
881     }
882     else if (actionId == InputAction::GUI_TAB &&
883              !mChatInput->getText().empty())
884     {
885         autoComplete();
886         return;
887     }
888     else if (actionId == InputAction::GUI_CANCEL &&
889              mChatInput->mVisible == Visible_true)
890     {
891         mChatInput->processVisible(Visible_false);
892     }
893     else if (actionId == InputAction::CHAT_PREV_HISTORY &&
894              mChatInput->mVisible == Visible_true)
895     {
896         const ChatTab *const tab = getFocused();
897         if ((tab != nullptr) && tab->hasRows())
898         {
899             if (mChatHistoryIndex == 0U)
900             {
901                 mChatHistoryIndex = CAST_U32(
902                     tab->getRows().size());
903 
904                 mChatInput->setText("");
905                 mChatInput->setCaretPosition(0);
906                 return;
907             }
908 
909             mChatHistoryIndex --;
910 
911             unsigned int f = 0;
912             const std::list<std::string> &rows = tab->getRows();
913             for (std::list<std::string>::const_iterator it = rows.begin(),
914                  it_end = rows.end(); it != it_end; ++ it, f ++)
915             {
916                 if (f == mChatHistoryIndex)
917                     mChatInput->setText(*it);
918             }
919             mChatInput->setCaretPosition(CAST_U32(
920                 mChatInput->getText().length()));
921         }
922     }
923     else if (actionId == InputAction::CHAT_NEXT_HISTORY &&
924              mChatInput->mVisible == Visible_true)
925     {
926         const ChatTab *const tab = getFocused();
927         if ((tab != nullptr) && tab->hasRows())
928         {
929             const std::list<std::string> &rows = tab->getRows();
930             const size_t &tabSize = rows.size();
931             if (CAST_SIZE(mChatHistoryIndex) + 1 < tabSize)
932             {
933                 mChatHistoryIndex ++;
934             }
935             else if (CAST_SIZE(mChatHistoryIndex) < tabSize)
936             {
937                 mChatHistoryIndex ++;
938                 mChatInput->setText("");
939                 mChatInput->setCaretPosition(0);
940                 return;
941             }
942             else
943             {
944                 mChatHistoryIndex = 0;
945             }
946 
947             unsigned int f = 0;
948             for (std::list<std::string>::const_iterator
949                  it = rows.begin(), it_end = rows.end();
950                  it != it_end; ++it, f++)
951             {
952                 if (f == mChatHistoryIndex)
953                     mChatInput->setText(*it);
954             }
955             mChatInput->setCaretPosition(CAST_U32(
956                     mChatInput->getText().length()));
957         }
958     }
959     else if (actionId == InputAction::GUI_F1)
960     {
961         if (emoteWindow != nullptr)
962         {
963             if (emoteWindow->mVisible == Visible_true)
964                 emoteWindow->hide();
965             else
966                 emoteWindow->show();
967         }
968     }
969     ifKey(InputAction::GUI_F2, "\u2318")
970     ifKey(InputAction::GUI_F3, "\u263A")
971     ifKey(InputAction::GUI_F4, "\u2665")
972     ifKey(InputAction::GUI_F5, "\u266A")
973     ifKey(InputAction::GUI_F6, "\u266B")
974     ifKey(InputAction::GUI_F7, "\u26A0")
975     ifKey(InputAction::GUI_F8, "\u2622")
976     ifKey(InputAction::GUI_F9, "\u262E")
977     ifKey(InputAction::GUI_F10, "\u2605")
978     ifKey(InputAction::GUI_F11, "\u2618")
979     ifKey(InputAction::GUI_F12, "\u2592")
980 
981     if (inputManager.isActionActive(InputAction::GUI_CTRL))
982     {
983         if (actionId == InputAction::GUI_B)
984         {
985             std::string inputText = mChatInput->getTextBeforeCaret();
986             toLower(inputText);
987             const size_t idx = inputText.rfind("##b");
988             if (idx == std::string::npos
989                 || mChatInput->getTextBeforeCaret().substr(idx, 3) == "##b")
990             {
991                 temp = "##B";
992             }
993             else
994             {
995                 temp = "##b";
996             }
997         }
998     }
999 
1000     if (!temp.empty())
1001         addInputText(temp, false);
1002 }
1003 
1004 #undef ifKey
1005 
addCurrentToHistory()1006 bool ChatWindow::addCurrentToHistory()
1007 {
1008     const std::string &str = mChatInput->getText();
1009     if (str.empty())
1010         return false;
1011     FOR_EACH (HistoryIterator, it, mHistory)
1012     {
1013         if (*it == str)
1014             return false;
1015     }
1016     mHistory.push_back(str);
1017     return true;
1018 }
1019 
attributeChanged(const AttributesT id,const int64_t oldVal,const int64_t newVal)1020 void ChatWindow::attributeChanged(const AttributesT id,
1021                                   const int64_t oldVal,
1022                                   const int64_t newVal)
1023 {
1024     if (!mShowBattleEvents)
1025         return;
1026     PRAGMA45(GCC diagnostic push)
1027     PRAGMA45(GCC diagnostic ignored "-Wswitch-enum")
1028     switch (id)
1029     {
1030         case Attributes::PLAYER_EXP:
1031         {
1032             if (oldVal > newVal)
1033                 break;
1034             const int64_t change = newVal - oldVal;
1035             if (change != 0)
1036             {
1037                 battleChatLog(std::string("+").append(toString(
1038                     CAST_U64(change))).append(" xp"),
1039                     ChatMsgType::BY_SERVER,
1040                     IgnoreRecord_false,
1041                     TryRemoveColors_true);
1042             }
1043             break;
1044         }
1045         case Attributes::PLAYER_BASE_LEVEL:
1046             battleChatLog(std::string(
1047                 "Level: ").append(toString(CAST_S32(
1048                 newVal))),
1049                 ChatMsgType::BY_SERVER,
1050                 IgnoreRecord_false,
1051                 TryRemoveColors_true);
1052             break;
1053         case Attributes::PLAYER_JOB_EXP:
1054         {
1055             if (!config.getBoolValue("showJobExp"))
1056                 return;
1057             if (oldVal > newVal ||
1058                 PlayerInfo::getAttribute(
1059                 Attributes::PLAYER_JOB_EXP_NEEDED) == 0)
1060             {
1061                 return;
1062             }
1063             const int64_t change = newVal - oldVal;
1064             if (change != 0)
1065             {
1066                 battleChatLog(std::string("+").append(toString(CAST_U64(
1067                     change))).append(" job"),
1068                     ChatMsgType::BY_SERVER,
1069                     IgnoreRecord_false,
1070                     TryRemoveColors_true);
1071             }
1072             break;
1073         }
1074         default:
1075             break;
1076     }
1077     PRAGMA45(GCC diagnostic pop)
1078 }
1079 
addInputText(const std::string & text,const bool space)1080 void ChatWindow::addInputText(const std::string &text,
1081                               const bool space)
1082 {
1083     const int caretPos = mChatInput->getCaretPosition();
1084     const std::string &inputText = mChatInput->getText();
1085 
1086     std::ostringstream ss;
1087     ss << inputText.substr(0, caretPos) << text;
1088     if (space)
1089         ss << " ";
1090 
1091     ss << inputText.substr(caretPos);
1092     mChatInput->setText(ss.str());
1093     mChatInput->setCaretPosition(caretPos + CAST_S32(
1094         text.length()) + CAST_S32(space));
1095     requestChatFocus();
1096 }
1097 
addItemText(const std::string & item)1098 void ChatWindow::addItemText(const std::string &item)
1099 {
1100     std::ostringstream text;
1101     text << "[" << item << "]";
1102     addInputText(text.str(),
1103         true);
1104 }
1105 
setVisible(Visible visible)1106 void ChatWindow::setVisible(Visible visible)
1107 {
1108     Window::setVisible(visible);
1109 
1110     /*
1111      * For whatever reason, if setVisible is called, the mTmpVisible effect
1112      * should be disabled.
1113      */
1114     mTmpVisible = false;
1115 }
1116 
addWhisper(const std::string & restrict nick,const std::string & restrict mes,const ChatMsgTypeT own)1117 void ChatWindow::addWhisper(const std::string &restrict nick,
1118                             const std::string &restrict mes,
1119                             const ChatMsgTypeT own)
1120 {
1121     if (mes.empty() || (localPlayer == nullptr))
1122         return;
1123 
1124     std::string playerName = localPlayer->getName();
1125     std::string tempNick = nick;
1126 
1127     toLower(playerName);
1128     toLower(tempNick);
1129 
1130     if (tempNick == playerName)
1131         return;
1132 
1133     WhisperTab *tab = nullptr;
1134     const TabMap::const_iterator i = mWhispers.find(tempNick);
1135 
1136     if (i != mWhispers.end())
1137     {
1138         tab = i->second;
1139     }
1140     else if (config.getBoolValue("whispertab"))
1141     {
1142         tab = addWhisperTab(nick, nick, false);
1143         if (tab != nullptr)
1144             saveState();
1145     }
1146 
1147     if (tab != nullptr)
1148     {
1149         if (own == ChatMsgType::BY_PLAYER)
1150         {
1151             tab->chatInput(mes);
1152         }
1153         else if (own == ChatMsgType::BY_SERVER)
1154         {
1155             tab->chatLog(mes,
1156                 ChatMsgType::BY_SERVER,
1157                 IgnoreRecord_false,
1158                 TryRemoveColors_true);
1159         }
1160         else
1161         {
1162             if (tab->getRemoveNames())
1163             {
1164                 std::string msg = mes;
1165                 const size_t idx = mes.find(':');
1166                 if (idx != std::string::npos && idx > 0)
1167                 {
1168                     std::string nick2 = msg.substr(0, idx);
1169                     msg = msg.substr(idx + 1);
1170                     nick2 = removeColors(nick2);
1171                     nick2 = trim(nick2);
1172                     if (config.getBoolValue("removeColors"))
1173                         msg = removeColors(msg);
1174                     msg = trim(msg);
1175                     tab->chatLog(nick2, msg);
1176                 }
1177                 else
1178                 {
1179                     if (config.getBoolValue("removeColors"))
1180                         msg = removeColors(msg);
1181                     tab->chatLog(msg,
1182                         ChatMsgType::BY_SERVER,
1183                         IgnoreRecord_false,
1184                         TryRemoveColors_true);
1185                 }
1186             }
1187             else
1188             {
1189                 tab->chatLog(nick, mes);
1190             }
1191             localPlayer->afkRespond(tab, nick);
1192         }
1193     }
1194     else if (localChatTab != nullptr)
1195     {
1196         if (own == ChatMsgType::BY_PLAYER)
1197         {
1198             chatHandler->privateMessage(nick, mes);
1199 
1200             // TRANSLATORS: chat message
1201             localChatTab->chatLog(strprintf(_("Whispering to %s: %s"),
1202                 nick.c_str(), mes.c_str()),
1203                 ChatMsgType::BY_PLAYER,
1204                 IgnoreRecord_false,
1205                 TryRemoveColors_true);
1206         }
1207         else
1208         {
1209             localChatTab->chatLog(std::string(nick).append(
1210                 " : ").append(mes),
1211                 ChatMsgType::ACT_WHISPER,
1212                 IgnoreRecord_false,
1213                 TryRemoveColors_true);
1214             if (localPlayer != nullptr)
1215                 localPlayer->afkRespond(nullptr, nick);
1216         }
1217     }
1218 }
1219 
addWhisperTab(const std::string & caption,const std::string & nick,const bool switchTo)1220 WhisperTab *ChatWindow::addWhisperTab(const std::string &caption,
1221                                       const std::string &nick,
1222                                       const bool switchTo)
1223 {
1224     if (localPlayer == nullptr)
1225         return nullptr;
1226 
1227     std::string playerName = localPlayer->getName();
1228     std::string tempNick = nick;
1229 
1230     toLower(playerName);
1231     toLower(tempNick);
1232 
1233     const TabMap::const_iterator i = mWhispers.find(tempNick);
1234     WhisperTab *ret = nullptr;
1235 
1236     if (tempNick == playerName)
1237         return nullptr;
1238 
1239     if (i != mWhispers.end())
1240     {
1241         ret = i->second;
1242     }
1243     else
1244     {
1245         ret = new WhisperTab(this, caption, nick);
1246         if ((gui != nullptr) && !playerRelations.isGoodName(nick))
1247             ret->setLabelFont(gui->getSecureFont());
1248         mWhispers[tempNick] = ret;
1249         if (config.getBoolValue("showChatHistory"))
1250             ret->loadFromLogFile(nick);
1251     }
1252 
1253     if (switchTo)
1254         mChatTabs->setSelectedTab(ret);
1255 
1256     return ret;
1257 }
1258 
getWhisperTab(const std::string & nick) const1259 WhisperTab *ChatWindow::getWhisperTab(const std::string &nick) const
1260 {
1261     if (localPlayer == nullptr)
1262         return nullptr;
1263 
1264     std::string playerName = localPlayer->getName();
1265     std::string tempNick = nick;
1266 
1267     toLower(playerName);
1268     toLower(tempNick);
1269 
1270     const TabMap::const_iterator i = mWhispers.find(tempNick);
1271     WhisperTab *ret = nullptr;
1272 
1273     if (tempNick == playerName)
1274         return nullptr;
1275 
1276     if (i != mWhispers.end())
1277         ret = i->second;
1278 
1279     return ret;
1280 }
1281 
addSpecialChannelTab(const std::string & name,const bool switchTo)1282 ChatTab *ChatWindow::addSpecialChannelTab(const std::string &name,
1283                                           const bool switchTo)
1284 {
1285     ChatTab *ret = nullptr;
1286     if (name == TRADE_CHANNEL)
1287     {
1288         if (tradeChatTab == nullptr)
1289         {
1290             tradeChatTab = new TradeTab(chatWindow);
1291             tradeChatTab->setAllowHighlight(false);
1292             chatHandler->joinChannel(tradeChatTab->getChannelName());
1293         }
1294         ret = tradeChatTab;
1295     }
1296     else if (name == GM_CHANNEL)
1297     {
1298         if (gmChatTab == nullptr)
1299         {
1300             gmChatTab = new GmTab(chatWindow);
1301             chatHandler->joinChannel(gmChatTab->getChannelName());
1302         }
1303         ret = gmChatTab;
1304     }
1305     if (switchTo)
1306         mChatTabs->setSelectedTab(ret);
1307 
1308     return ret;
1309 }
1310 
addChannelTab(const std::string & name,const bool switchTo)1311 ChatTab *ChatWindow::addChannelTab(const std::string &name,
1312                                    const bool switchTo)
1313 {
1314     std::string tempName = name;
1315     toLower(tempName);
1316 
1317     ChatTab *const tab = addSpecialChannelTab(name, switchTo);
1318     if (tab != nullptr)
1319         return tab;
1320 
1321     const ChannelMap::const_iterator i = mChannels.find(tempName);
1322     ChannelTab *ret = nullptr;
1323 
1324     if (i != mChannels.end())
1325     {
1326         ret = i->second;
1327     }
1328     else
1329     {
1330         ret = new ChannelTab(this, name);
1331         mChannels[tempName] = ret;
1332         ret->setAllowHighlight(false);
1333         if (config.getBoolValue("showChatHistory"))
1334             ret->loadFromLogFile(name);
1335     }
1336 
1337     if (switchTo)
1338         mChatTabs->setSelectedTab(ret);
1339 
1340     return ret;
1341 }
1342 
addChatTab(const std::string & name,const bool switchTo,const bool join)1343 ChatTab *ChatWindow::addChatTab(const std::string &name,
1344                                 const bool switchTo,
1345                                 const bool join)
1346 {
1347     if (Net::getNetworkType() != ServerType::TMWATHENA &&
1348         name.size() > 1 &&
1349         name[0] == '#')
1350     {
1351         ChatTab *const tab = addChannelTab(name, switchTo);
1352         if ((tab != nullptr) && join)
1353             chatHandler->joinChannel(name);
1354         return tab;
1355     }
1356     return addWhisperTab(name, name, switchTo);
1357 }
1358 
postConnection()1359 void ChatWindow::postConnection()
1360 {
1361     FOR_EACH (ChannelMap::const_iterator, iter, mChannels)
1362     {
1363         ChatTab *const tab = iter->second;
1364         if (tab == nullptr)
1365             return;
1366         chatHandler->joinChannel(tab->getChannelName());
1367     }
1368     if (langChatTab != nullptr)
1369         chatHandler->joinChannel(langChatTab->getChannelName());
1370 }
1371 
1372 #define changeColor(fun) \
1373     { \
1374         msg = removeColors(msg); \
1375         int skip = 0; \
1376         const size_t sz = msg.length(); \
1377         for (size_t f = 0; f < sz; f ++) \
1378         { \
1379             if (skip > 0) \
1380             { \
1381                 newMsg += msg.at(f); \
1382                 skip --; \
1383                 continue; \
1384             } \
1385             const unsigned char ch = CAST_U8(msg.at(f)); \
1386             if (f + 2 < sz && msg.substr(f, 2) == "%%") \
1387             { \
1388                 newMsg += msg.at(f); \
1389                 skip = 2; \
1390             } \
1391             else if (ch > 0xc0 || ch < 0x80) \
1392             { \
1393                 newMsg += "##" + toString(fun) + msg.at(f); \
1394                 if (mRainbowColor > 9U) \
1395                     mRainbowColor = 0U; \
1396             } \
1397             else \
1398             { \
1399                 newMsg += msg.at(f); \
1400             } \
1401         } \
1402     }
1403 
addColors(std::string & msg)1404 std::string ChatWindow::addColors(std::string &msg)
1405 {
1406     // default color or chat command
1407     if (mChatColor == 0 || msg.length() == 0 || msg.at(0) == '#'
1408         || msg.at(0) == '/' || msg.at(0) == '@' || msg.at(0) == '!')
1409     {
1410         return msg;
1411     }
1412 
1413     std::string newMsg;
1414     const int cMap[] = {1, 4, 5, 2, 3, 6, 7, 9, 0, 8};
1415 
1416     // rainbow
1417     switch (mChatColor)
1418     {
1419         case 11:
1420             changeColor(mRainbowColor++)
1421             return newMsg;
1422         case 12:
1423             changeColor(cMap[mRainbowColor++])
1424             return newMsg;
1425         case 13:
1426             changeColor(cMap[9-mRainbowColor++])
1427             return newMsg;
1428         default:
1429             break;
1430     }
1431 
1432     // simple colors
1433     return std::string("##").append(toString(mChatColor - 1)).append(msg);
1434 }
1435 
1436 #undef changeColor
1437 
autoComplete()1438 void ChatWindow::autoComplete()
1439 {
1440     const int caretPos = mChatInput->getCaretPosition();
1441     int startName = 0;
1442     const std::string &inputText = mChatInput->getText();
1443     std::string name = inputText.substr(0, caretPos);
1444 
1445     for (int f = caretPos - 1; f > -1; f --)
1446     {
1447         if (isWordSeparator(inputText[f]))
1448         {
1449             startName = f + 1;
1450             name = inputText.substr(f + 1, caretPos - f);
1451             break;
1452         }
1453     }
1454 
1455     if (caretPos - 1 + 1 == startName)
1456         return;
1457 
1458     const ChatTab *const cTab = static_cast<ChatTab*>(
1459         mChatTabs->getSelectedTab());
1460     StringVect nameList;
1461 
1462     if (cTab != nullptr)
1463         cTab->getAutoCompleteList(nameList);
1464     std::string newName = autoComplete(nameList, name);
1465     if (!newName.empty() && (startName == 0))
1466         secureChatCommand(newName);
1467 
1468     if ((cTab != nullptr) && newName.empty())
1469     {
1470         cTab->getAutoCompleteCommands(nameList);
1471         newName = autoComplete(nameList, name);
1472     }
1473 
1474     if (newName.empty() && (actorManager != nullptr))
1475     {
1476         actorManager->getPlayerNames(nameList, NpcNames_true);
1477         newName = autoComplete(nameList, name);
1478         if (!newName.empty() && (startName == 0))
1479             secureChatCommand(newName);
1480     }
1481     if (newName.empty())
1482         newName = autoCompleteHistory(name);
1483     if (newName.empty() && (spellManager != nullptr))
1484         newName = spellManager->autoComplete(name);
1485     if (newName.empty())
1486         newName = autoComplete(name, &mCommands);
1487     if (newName.empty() && (actorManager != nullptr))
1488     {
1489         actorManager->getMobNames(nameList);
1490         newName = autoComplete(nameList, name);
1491     }
1492     if (newName.empty())
1493         newName = autoComplete(name, &mCustomWords);
1494     if (newName.empty())
1495     {
1496         whoIsOnline->getPlayerNames(nameList);
1497         newName = autoComplete(nameList, name);
1498     }
1499 
1500     if (!newName.empty())
1501     {
1502         mChatInput->setText(inputText.substr(0, startName).append(newName)
1503             .append(inputText.substr(caretPos,
1504             inputText.length() - caretPos)));
1505 
1506         const int len = caretPos - CAST_S32(name.length())
1507             + CAST_S32(newName.length());
1508 
1509         if (startName > 0)
1510             mChatInput->setCaretPosition(len + 1);
1511         else
1512             mChatInput->setCaretPosition(len);
1513     }
1514 }
1515 
autoComplete(const StringVect & names,std::string partName)1516 std::string ChatWindow::autoComplete(const StringVect &names,
1517                                      std::string partName)
1518 {
1519     StringVectCIter i = names.begin();
1520     const StringVectCIter i_end = names.end();
1521     toLower(partName);
1522     std::string newName;
1523 
1524     while (i != i_end)
1525     {
1526         if (!i->empty())
1527         {
1528             std::string name = *i;
1529             toLower(name);
1530 
1531             const size_t pos = name.find(partName, 0);
1532             if (pos == 0)
1533             {
1534                 if (!newName.empty())
1535                     newName = findSameSubstringI(*i, newName);
1536                 else
1537                     newName = *i;
1538             }
1539         }
1540         ++i;
1541     }
1542 
1543     return newName;
1544 }
1545 
autoComplete(const std::string & partName,const History * const words)1546 std::string ChatWindow::autoComplete(const std::string &partName,
1547                                      const History *const words)
1548 {
1549     if (words == nullptr)
1550         return "";
1551 
1552     ChatCommands::const_iterator i = words->begin();
1553     const ChatCommands::const_iterator i_end = words->end();
1554     StringVect nameList;
1555 
1556     while (i != i_end)
1557     {
1558         const std::string line = *i;
1559         if (line.find(partName, 0) == 0)
1560             nameList.push_back(line);
1561         ++i;
1562     }
1563     return autoComplete(nameList, partName);
1564 }
1565 
1566 /*
1567 void ChatWindow::moveTabLeft(ChatTab *tab)
1568 {
1569     mChatTabs->moveLeft(tab);
1570 }
1571 
1572 void ChatWindow::moveTabRight(ChatTab *tab)
1573 {
1574     mChatTabs->moveRight(tab);
1575 }
1576 */
1577 
autoCompleteHistory(const std::string & partName) const1578 std::string ChatWindow::autoCompleteHistory(const std::string &partName) const
1579 {
1580     History::const_iterator i = mHistory.begin();
1581     const History::const_iterator i_end = mHistory.end();
1582     StringVect nameList;
1583 
1584     while (i != i_end)
1585     {
1586         std::string line = *i;
1587         unsigned int f = 0;
1588         while (f < line.length() && !isWordSeparator(line.at(f)))
1589             f++;
1590 
1591         line = line.substr(0, f);
1592         if (!line.empty())
1593             nameList.push_back(line);
1594 
1595         ++i;
1596     }
1597     return autoComplete(nameList, partName);
1598 }
1599 
resortChatLog(std::string line,ChatMsgTypeT own,const std::string & channel,const IgnoreRecord ignoreRecord,const TryRemoveColors tryRemoveColors)1600 bool ChatWindow::resortChatLog(std::string line,
1601                                ChatMsgTypeT own,
1602                                const std::string &channel,
1603                                const IgnoreRecord ignoreRecord,
1604                                const TryRemoveColors tryRemoveColors)
1605 {
1606     if (own == ChatMsgType::BY_UNKNOWN)
1607     {
1608         const size_t pos = line.find(" : ");
1609         if (pos != std::string::npos)
1610         {
1611             if (line.length() <= pos + 3)
1612                 own = ChatMsgType::BY_SERVER;
1613             else if (line.substr(0, pos) == localPlayer->getName())
1614                 own = ChatMsgType::BY_PLAYER;
1615             else
1616                 own = ChatMsgType::BY_OTHER;
1617         }
1618         else
1619         {
1620             own = ChatMsgType::BY_SERVER;
1621         }
1622     }
1623 
1624     std::string prefix;
1625     if (!channel.empty())
1626     {
1627         prefix = std::string("##3").append(channel).append("##0");
1628     }
1629     else if (mEnableTradeFilter &&
1630              (tradeChatTab != nullptr) &&
1631              findI(line, mTradeFilter) != std::string::npos)
1632     {
1633         // TRANSLATORS: prefix for moved message to trade tab.
1634         tradeChatTab->chatLog(std::string("##S") +  _("Moved: ") + line,
1635             own,
1636             ignoreRecord,
1637             tryRemoveColors);
1638         if (own == ChatMsgType::BY_PLAYER)
1639         {
1640             own = ChatMsgType::BY_SERVER;
1641             // TRANSLATORS: moved message to trade tab warning.
1642             line = _("Your message was moved to trade tab");
1643         }
1644         else
1645         {
1646             return false;
1647         }
1648     }
1649 
1650     size_t idx2 = line.find(": ");
1651     if (idx2 != std::string::npos)
1652     {
1653         std::string tmpNick = line.substr(0, idx2);
1654         if (tmpNick.find('#') != std::string::npos ||
1655             tmpNick.find(':') != std::string::npos ||
1656             tmpNick.find('%') != std::string::npos ||
1657             tmpNick.find('@') != std::string::npos ||
1658             tmpNick.size() < 5 ||
1659             tmpNick[0] == '@' ||
1660             tmpNick[0] == '/' ||
1661             tmpNick[0] == '!'
1662             )
1663         {
1664             replaceAll(tmpNick, "#", "_");
1665             replaceAll(tmpNick, "%", "_");
1666             // TRANSLATORS: error message
1667             line = _("Broken nick detected: ") + line;
1668             own = ChatMsgType::BY_SERVER;
1669         }
1670         const size_t idx = line.find(": \302\202");
1671         if (idx == idx2)
1672         {
1673             if (line.find(": \302\202\302") != std::string::npos)
1674             {
1675                 if (line.find(": \302\202\302e") != std::string::npos)
1676                 {
1677                     // Do nothing. Before was local pet emote
1678                 }
1679                 else if (line.find(": \302\202\302m") != std::string::npos)
1680                 {
1681                     // Do nothing. Before was local pet move
1682                 }
1683                 else if (line.find(": \302\202\302d") != std::string::npos)
1684                 {
1685                     // Do nothing. Before was local pet direction
1686                 }
1687                 else if (line.find(": \302\202\302a") != std::string::npos)
1688                 {
1689                     // Do nothing. Before was local pet ai enable/disable
1690                 }
1691                 // ignore other special message formats.
1692                 return false;
1693             }
1694 
1695             // pet talk message detected
1696             if (line.find(": \302\202\303 ") != std::string::npos)
1697             {
1698                 // Do nothing. Before was local pet talk
1699                 return false;
1700             }
1701             if (line.find(": \302\202\304") != std::string::npos)
1702             {
1703                 replaceAll(line, ": \302\202\304", ": ");
1704             }
1705 
1706             if (tradeChatTab != nullptr)
1707             {
1708                 line = line.erase(idx + 2, 2);
1709                 tradeChatTab->chatLog(prefix + line,
1710                     own,
1711                     ignoreRecord,
1712                     tryRemoveColors);
1713             }
1714             else if (localChatTab != nullptr)
1715             {
1716                 line = line.erase(idx + 2, 2);
1717                 localChatTab->chatLog(prefix + line,
1718                     own,
1719                     ignoreRecord,
1720                     tryRemoveColors);
1721             }
1722             return false;
1723         }
1724     }
1725 
1726     if (!channel.empty())
1727     {
1728         if (langChatTab != nullptr)
1729         {
1730             if (langChatTab->getChannelName() == channel)
1731             {
1732                 langChatTab->chatLog(line,
1733                     own,
1734                     ignoreRecord,
1735                     tryRemoveColors);
1736             }
1737             else if (mShowAllLang)
1738             {
1739                 langChatTab->chatLog(prefix + line,
1740                     own,
1741                     ignoreRecord,
1742                     tryRemoveColors);
1743             }
1744         }
1745         else if (Net::getNetworkType() != ServerType::TMWATHENA)
1746         {
1747             channelChatLog(channel, line, own, ignoreRecord, tryRemoveColors);
1748             return false;
1749         }
1750         else if (mShowAllLang)
1751         {
1752             localChatTab->chatLog(prefix + line,
1753                 own,
1754                 ignoreRecord,
1755                 tryRemoveColors);
1756         }
1757     }
1758     else if (localChatTab != nullptr)
1759     {
1760         localChatTab->chatLog(line, own, ignoreRecord, tryRemoveColors);
1761     }
1762     return true;
1763 }
1764 
battleChatLog(const std::string & line,ChatMsgTypeT own,const IgnoreRecord ignoreRecord,const TryRemoveColors tryRemoveColors)1765 void ChatWindow::battleChatLog(const std::string &line, ChatMsgTypeT own,
1766                                const IgnoreRecord ignoreRecord,
1767                                const TryRemoveColors tryRemoveColors)
1768 {
1769     if (own == ChatMsgType::BY_UNKNOWN)
1770         own = ChatMsgType::BY_SERVER;
1771     if (battleChatTab != nullptr)
1772         battleChatTab->chatLog(line, own, ignoreRecord, tryRemoveColors);
1773     else if (debugChatTab != nullptr)
1774         debugChatTab->chatLog(line, own, ignoreRecord, tryRemoveColors);
1775 }
1776 
channelChatLog(const std::string & channel,const std::string & line,ChatMsgTypeT own,const IgnoreRecord ignoreRecord,const TryRemoveColors tryRemoveColors)1777 void ChatWindow::channelChatLog(const std::string &channel,
1778                                 const std::string &line,
1779                                 ChatMsgTypeT own,
1780                                 const IgnoreRecord ignoreRecord,
1781                                 const TryRemoveColors tryRemoveColors)
1782 {
1783     std::string tempChannel = channel;
1784     toLower(tempChannel);
1785 
1786     ChatTab *tab = nullptr;
1787     const ChannelMap::const_iterator i = mChannels.find(tempChannel);
1788 
1789     if (i != mChannels.end())
1790     {
1791         tab = i->second;
1792     }
1793     else
1794     {
1795         tab = addChannelTab(channel, false);
1796         if (tab != nullptr)
1797             saveState();
1798     }
1799 
1800     if (tab != nullptr)
1801         tab->chatLog(line, own, ignoreRecord, tryRemoveColors);
1802 }
1803 
initTradeFilter()1804 void ChatWindow::initTradeFilter()
1805 {
1806     const std::string tradeListName = settings.serverConfigDir
1807         + "/tradefilter.txt";
1808 
1809     std::ifstream tradeFile;
1810     struct stat statbuf;
1811 
1812     if (stat(tradeListName.c_str(), &statbuf) == 0 &&
1813         S_ISREG(statbuf.st_mode))
1814     {
1815         tradeFile.open(tradeListName.c_str(), std::ios::in);
1816         if (tradeFile.is_open())
1817         {
1818             char line[100];
1819             while (tradeFile.getline(line, 100))
1820             {
1821                 const std::string str = line;
1822                 if (!str.empty())
1823                     mTradeFilter.push_back(str);
1824             }
1825         }
1826         tradeFile.close();
1827     }
1828 }
1829 
updateOnline(const std::set<std::string> & onlinePlayers) const1830 void ChatWindow::updateOnline(const std::set<std::string> &onlinePlayers) const
1831 {
1832     const Party *party = nullptr;
1833     const Guild *guild = nullptr;
1834     if (localPlayer != nullptr)
1835     {
1836         party = localPlayer->getParty();
1837         guild = localPlayer->getGuild();
1838     }
1839     FOR_EACH (TabMap::const_iterator, iter, mWhispers)
1840     {
1841         if (iter->second == nullptr)
1842             return;
1843 
1844         WhisperTab *const tab = static_cast<WhisperTab*>(iter->second);
1845         if (tab == nullptr)
1846             continue;
1847 
1848         if (onlinePlayers.find(tab->getNick()) != onlinePlayers.end())
1849         {
1850             tab->setWhisperTabColors();
1851         }
1852         else
1853         {
1854             const std::string &nick = tab->getNick();
1855             if (actorManager != nullptr)
1856             {
1857                 const Being *const being = actorManager->findBeingByName(
1858                     nick, ActorType::Player);
1859                 if (being != nullptr)
1860                 {
1861                     tab->setWhisperTabColors();
1862                     continue;
1863                 }
1864             }
1865             if (party != nullptr)
1866             {
1867                 const PartyMember *const pm = party->getMember(nick);
1868                 if ((pm != nullptr) && pm->getOnline())
1869                 {
1870                     tab->setWhisperTabColors();
1871                     continue;
1872                 }
1873             }
1874             if (guild != nullptr)
1875             {
1876                 const GuildMember *const gm = guild->getMember(nick);
1877                 if ((gm != nullptr) && gm->getOnline())
1878                 {
1879                     tab->setWhisperTabColors();
1880                     continue;
1881                 }
1882             }
1883             tab->setWhisperTabOfflineColors();
1884         }
1885     }
1886 }
1887 
loadState()1888 void ChatWindow::loadState()
1889 {
1890     int num = 0;
1891     while (num < 50)
1892     {
1893         const std::string nick = serverConfig.getValue(
1894             "chatWhisper" + toString(num), "");
1895 
1896         if (nick.empty())
1897             break;
1898 
1899         ChatTab *const tab = addChatTab(nick, false, false);
1900         if (tab != nullptr)
1901         {
1902             const int flags = serverConfig.getValue(
1903                 "chatWhisperFlags" + toString(num), 1);
1904 
1905             tab->setAllowHighlight((flags & 1) != 0);
1906             tab->setRemoveNames(((flags & 2) / 2) != 0);
1907             tab->setNoAway(((flags & 4) / 4) != 0);
1908         }
1909         num ++;
1910     }
1911 }
1912 
saveState() const1913 void ChatWindow::saveState() const
1914 {
1915     int num = 0;
1916     for (ChannelMap::const_iterator iter = mChannels.begin(),
1917          iter_end = mChannels.end(); iter != iter_end && num < 50; ++iter)
1918     {
1919         if (iter->second == nullptr)
1920             return;
1921         if (!saveTab(num, iter->second))
1922             continue;
1923         num ++;
1924     }
1925 
1926     for (TabMap::const_iterator iter = mWhispers.begin(),
1927          iter_end = mWhispers.end(); iter != iter_end && num < 50; ++iter)
1928     {
1929         if (iter->second == nullptr)
1930             return;
1931         if (!saveTab(num, iter->second))
1932             continue;
1933         num ++;
1934     }
1935 
1936     while (num < 50)
1937     {
1938         serverConfig.deleteKey("chatWhisper" + toString(num));
1939         serverConfig.deleteKey("chatWhisperFlags" + toString(num));
1940         num ++;
1941     }
1942 }
1943 
saveTab(const int num,const ChatTab * const tab)1944 bool ChatWindow::saveTab(const int num,
1945                          const ChatTab *const tab)
1946 {
1947     if (tab == nullptr)
1948         return false;
1949 
1950     serverConfig.setValue("chatWhisper" + toString(num),
1951         tab->getChannelName());
1952 
1953     serverConfig.setValue("chatWhisperFlags" + toString(num),
1954         CAST_S32(tab->getAllowHighlight())
1955         + (2 * CAST_S32(tab->getRemoveNames()))
1956         + (4 * CAST_S32(tab->getNoAway())));
1957 
1958     return true;
1959 }
1960 
doReplace(const std::string & msg)1961 std::string ChatWindow::doReplace(const std::string &msg)
1962 {
1963     std::string str = msg;
1964     replaceSpecialChars(str);
1965     return str;
1966 }
1967 
loadCustomList()1968 void ChatWindow::loadCustomList()
1969 {
1970     std::ifstream listFile;
1971     struct stat statbuf;
1972 
1973     std::string listName = settings.serverConfigDir
1974         + "/customwords.txt";
1975 
1976     if ((stat(listName.c_str(), &statbuf) == 0) && S_ISREG(statbuf.st_mode))
1977     {
1978         listFile.open(listName.c_str(), std::ios::in);
1979         if (listFile.is_open())
1980         {
1981             char line[101];
1982             while (listFile.getline(line, 100))
1983             {
1984                 std::string str = line;
1985                 if (!str.empty())
1986                     mCustomWords.push_back(str);
1987             }
1988         }
1989         listFile.close();
1990     }
1991 }
1992 
addToAwayLog(const std::string & line)1993 void ChatWindow::addToAwayLog(const std::string &line)
1994 {
1995     if (!settings.awayMode)
1996         return;
1997 
1998     if (mAwayLog.size() > 20)
1999         mAwayLog.pop_front();
2000 
2001     if (findI(line, mHighlights) != std::string::npos)
2002         mAwayLog.push_back("##aaway:" + line);
2003 }
2004 
displayAwayLog() const2005 void ChatWindow::displayAwayLog() const
2006 {
2007     if (localChatTab == nullptr)
2008         return;
2009 
2010     std::list<std::string>::const_iterator i = mAwayLog.begin();
2011     const std::list<std::string>::const_iterator i_end = mAwayLog.end();
2012 
2013     while (i != i_end)
2014     {
2015         std::string str = *i;
2016         localChatTab->addNewRow(str);
2017         ++i;
2018     }
2019 }
2020 
parseHighlights()2021 void ChatWindow::parseHighlights()
2022 {
2023     mHighlights.clear();
2024     if (localPlayer == nullptr)
2025         return;
2026 
2027     splitToStringVector(mHighlights, config.getStringValue(
2028         "highlightWords"), ',');
2029 
2030     mHighlights.push_back(localPlayer->getName());
2031 }
2032 
parseGlobalsFilter()2033 void ChatWindow::parseGlobalsFilter()
2034 {
2035     mGlobalsFilter.clear();
2036     if (localPlayer == nullptr)
2037         return;
2038 
2039     splitToStringVector(mGlobalsFilter, config.getStringValue(
2040         "globalsFilter"), ',');
2041 
2042     mHighlights.push_back(localPlayer->getName());
2043 }
2044 
findHighlight(const std::string & str)2045 bool ChatWindow::findHighlight(const std::string &str)
2046 {
2047     return findI(str, mHighlights) != std::string::npos;
2048 }
2049 
copyToClipboard(const int x,const int y) const2050 void ChatWindow::copyToClipboard(const int x, const int y) const
2051 {
2052     const ChatTab *const tab = getFocused();
2053     if (tab == nullptr)
2054         return;
2055 
2056     const BrowserBox *const text = tab->mTextOutput;
2057     if (text == nullptr)
2058         return;
2059 
2060     std::string str = text->getTextAtPos(x, y);
2061     sendBuffer(str);
2062 }
2063 
optionChanged(const std::string & name)2064 void ChatWindow::optionChanged(const std::string &name)
2065 {
2066     if (name == "autohideChat")
2067         mAutoHide = config.getBoolValue("autohideChat");
2068     else if (name == "showBattleEvents")
2069         mShowBattleEvents = config.getBoolValue("showBattleEvents");
2070     else if (name == "globalsFilter")
2071         parseGlobalsFilter();
2072     else if (name == "enableTradeFilter")
2073         mEnableTradeFilter = config.getBoolValue("enableTradeFilter");
2074 }
2075 
mouseMoved(MouseEvent & event)2076 void ChatWindow::mouseMoved(MouseEvent &event)
2077 {
2078     mHaveMouse = true;
2079     Window::mouseMoved(event);
2080 }
2081 
mouseEntered(MouseEvent & event)2082 void ChatWindow::mouseEntered(MouseEvent& event)
2083 {
2084     mHaveMouse = true;
2085     Window::mouseEntered(event);
2086 }
2087 
mouseExited(MouseEvent & event)2088 void ChatWindow::mouseExited(MouseEvent& event)
2089 {
2090     updateVisibility();
2091     Window::mouseExited(event);
2092 }
2093 
draw(Graphics * const graphics)2094 void ChatWindow::draw(Graphics *const graphics)
2095 {
2096     BLOCK_START("ChatWindow::draw")
2097     if (!mAutoHide || mHaveMouse)
2098     {
2099         GLDEBUG_START("ChatWindow::draw");
2100         Window::draw(graphics);
2101         GLDEBUG_END();
2102     }
2103     BLOCK_END("ChatWindow::draw")
2104 }
2105 
safeDraw(Graphics * const graphics)2106 void ChatWindow::safeDraw(Graphics *const graphics)
2107 {
2108     BLOCK_START("ChatWindow::draw")
2109     if (!mAutoHide || mHaveMouse)
2110     {
2111         GLDEBUG_START("ChatWindow::draw");
2112         Window::safeDraw(graphics);
2113         GLDEBUG_END();
2114     }
2115     BLOCK_END("ChatWindow::draw")
2116 }
2117 
updateVisibility()2118 void ChatWindow::updateVisibility()
2119 {
2120     if (gui == nullptr)
2121         return;
2122 
2123     int mouseX = 0;
2124     int mouseY = 0;
2125     int x = 0;
2126     int y = 0;
2127     Gui::getMouseState(mouseX, mouseY);
2128     getAbsolutePosition(x, y);
2129     if (mChatInput->isVisible())
2130     {
2131         mHaveMouse = true;
2132     }
2133     else
2134     {
2135         mHaveMouse = mouseX >= x && mouseX <= x + getWidth()
2136             && mouseY >= y && mouseY <= y + getHeight();
2137     }
2138 }
2139 
unHideWindow()2140 void ChatWindow::unHideWindow()
2141 {
2142     mHaveMouse = true;
2143 }
2144 
2145 #ifdef USE_PROFILER
logicChildren()2146 void ChatWindow::logicChildren()
2147 {
2148     BLOCK_START("ChatWindow::logicChildren")
2149     BasicContainer::logicChildren();
2150     BLOCK_END("ChatWindow::logicChildren")
2151 }
2152 #endif  // USE_PROFILER
2153 
addGlobalMessage(const std::string & line)2154 void ChatWindow::addGlobalMessage(const std::string &line)
2155 {
2156     if (debugChatTab != nullptr &&
2157         findI(line, mGlobalsFilter) != std::string::npos)
2158     {
2159         debugChatTab->chatLog(line,
2160             ChatMsgType::BY_OTHER,
2161             IgnoreRecord_false,
2162             TryRemoveColors_true);
2163     }
2164     else
2165     {
2166         localChatTab->chatLog(line,
2167             ChatMsgType::BY_GM,
2168             IgnoreRecord_false,
2169             TryRemoveColors_true);
2170     }
2171 }
2172 
isTabPresent(const ChatTab * const tab) const2173 bool ChatWindow::isTabPresent(const ChatTab *const tab) const
2174 {
2175     return mChatTabs->isTabPresent(tab);
2176 }
2177 
debugMessage(const std::string & msg)2178 void ChatWindow::debugMessage(const std::string &msg)
2179 {
2180     if (debugChatTab != nullptr)
2181     {
2182         debugChatTab->chatLog(msg,
2183             ChatMsgType::BY_SERVER,
2184             IgnoreRecord_false,
2185             TryRemoveColors_true);
2186     }
2187 }
2188 
showGMTab()2189 void ChatWindow::showGMTab()
2190 {
2191     if ((gmChatTab == nullptr) &&
2192         config.getBoolValue("enableGmTab") &&
2193         localPlayer->getGroupId() >= paths.getIntValue("gmTabMinimalLevel"))
2194     {
2195         addSpecialChannelTab(GM_CHANNEL, false);
2196     }
2197 }
2198 
toggleChatFocus()2199 void ChatWindow::toggleChatFocus()
2200 {
2201     if (mChatInput->isVisible() && mChatInput->isFocused())
2202         mChatInput->processVisible(Visible_false);
2203     else
2204         requestChatFocus();
2205 }
2206 
joinRoom(const bool isJoin)2207 void ChatWindow::joinRoom(const bool isJoin)
2208 {
2209     Tab *const tab = mChatTabs->getTabByIndex(0);
2210     if (tab != nullptr)
2211     {
2212         std::string name;
2213         if (isJoin)
2214         {
2215             name = PlayerInfo::getRoomName();
2216         }
2217         else
2218         {
2219             // TRANSLATORS: chat tab name
2220             name = _("General");
2221         }
2222         tab->setCaption(name);
2223     }
2224 }
2225 
scheduleDelete()2226 void ChatWindow::scheduleDelete()
2227 {
2228     DebugMessageListener::removeListener(this);
2229     Window::scheduleDelete();
2230 }
2231