diff options
Diffstat (limited to 'indra/newview/llimpanel.cpp')
-rw-r--r-- | indra/newview/llimpanel.cpp | 793 |
1 files changed, 793 insertions, 0 deletions
diff --git a/indra/newview/llimpanel.cpp b/indra/newview/llimpanel.cpp new file mode 100644 index 0000000000..718ea894aa --- /dev/null +++ b/indra/newview/llimpanel.cpp @@ -0,0 +1,793 @@ +/** + * @file llimpanel.cpp + * @brief LLIMPanel class definition + * + * Copyright (c) 2001-$CurrentYear$, Linden Research, Inc. + * $License$ + */ + +#include "llviewerprecompiledheaders.h" + +#include "llimpanel.h" + +#include "indra_constants.h" +#include "llfocusmgr.h" +#include "llfontgl.h" +#include "llrect.h" +#include "llerror.h" +#include "llstring.h" +#include "message.h" +#include "lltextbox.h" + +#include "llagent.h" +#include "llbutton.h" +#include "llcallingcard.h" +#include "llconsole.h" +#include "llfloater.h" +#include "llinventory.h" +#include "llinventorymodel.h" +#include "llinventoryview.h" +#include "llfloateravatarinfo.h" +#include "llkeyboard.h" +#include "lllineeditor.h" +#include "llresmgr.h" +#include "lltabcontainer.h" +#include "llimview.h" +#include "llviewertexteditor.h" +#include "llviewermessage.h" +#include "llviewerstats.h" +#include "viewer.h" +#include "llvieweruictrlfactory.h" +#include "lllogchat.h" +#include "llfloaterhtml.h" +#include "llweb.h" + +// +// Constants +// +const S32 LINE_HEIGHT = 16; +const S32 MIN_WIDTH = 200; +const S32 MIN_HEIGHT = 130; + +// +// Statics +// +// +static LLString sTitleString = "Instant Message with [NAME]"; +static LLString sTypingStartString = "[NAME]: ..."; + +// Member Functions +// + +LLFloaterIMPanel::LLFloaterIMPanel(const std::string& name, const LLRect& rect, + const std::string& session_label, + const LLUUID& session_id, + const LLUUID& other_participant_id, + EInstantMessage dialog) : + LLFloater(name, rect, session_label), + mInputEditor(NULL), + mHistoryEditor(NULL), + mSessionLabel(session_label), + mSessionUUID(session_id), + mOtherParticipantUUID(other_participant_id), + mLureID(), + mDialog(dialog), + mTyping(FALSE), + mOtherTyping(FALSE), + mTypingLineStartIndex(0), + mSentTypingState(TRUE), + mFirstKeystrokeTimer(), + mLastKeystrokeTimer() +{ + init(); + setLabel(session_label); + setTitle(session_label); + mInputEditor->setMaxTextLength(1023); +} + + +void LLFloaterIMPanel::init() +{ + gUICtrlFactory->buildFloater(this, "floater_instant_message.xml", NULL, FALSE); +} + + +BOOL LLFloaterIMPanel::postBuild() +{ + requires("chat_editor", WIDGET_TYPE_LINE_EDITOR); + requires("profile_btn", WIDGET_TYPE_BUTTON); + requires("close_btn", WIDGET_TYPE_BUTTON); + requires("im_history", WIDGET_TYPE_TEXT_EDITOR); + requires("live_help_dialog", WIDGET_TYPE_TEXT_BOX); + requires("title_string", WIDGET_TYPE_TEXT_BOX); + requires("typing_start_string", WIDGET_TYPE_TEXT_BOX); + + if (checkRequirements()) + { + mInputEditor = LLUICtrlFactory::getLineEditorByName(this, "chat_editor"); + mInputEditor->setFocusReceivedCallback( onInputEditorFocusReceived ); + mInputEditor->setFocusLostCallback( onInputEditorFocusLost ); + mInputEditor->setKeystrokeCallback( onInputEditorKeystroke ); + mInputEditor->setCallbackUserData(this); + mInputEditor->setCommitOnFocusLost( FALSE ); + + LLButton* profile_btn = LLUICtrlFactory::getButtonByName(this, "profile_btn"); + profile_btn->setClickedCallback(&LLFloaterIMPanel::onClickProfile, this); + + LLButton* close_btn = LLUICtrlFactory::getButtonByName(this, "close_btn"); + close_btn->setClickedCallback(&LLFloaterIMPanel::onClickClose, this); + + mHistoryEditor = LLViewerUICtrlFactory::getViewerTextEditorByName(this, "im_history"); + mHistoryEditor->setParseHTML(TRUE); + if ( gSavedPerAccountSettings.getBOOL("LogShowHistory") ) + { + LLLogChat::loadHistory(mSessionLabel, &chatFromLogFile, (void *)this); + } + + if (IM_SESSION_GROUP_START == mDialog + || IM_SESSION_911_START == mDialog) + { + profile_btn->setEnabled(FALSE); + if(IM_SESSION_911_START == mDialog) + { + LLTextBox* live_help_text = LLUICtrlFactory::getTextBoxByName(this, "live_help_dialog"); + addHistoryLine(live_help_text->getText()); + } + } + + LLTextBox* title = LLUICtrlFactory::getTextBoxByName(this, "title_string"); + sTitleString = title->getText(); + + LLTextBox* typing_start = LLUICtrlFactory::getTextBoxByName(this, "typing_start_string"); + + sTypingStartString = typing_start->getText(); + + return TRUE; + } + + return FALSE; +} + +// virtual +void LLFloaterIMPanel::draw() +{ + if (mTyping) + { + // Time out if user hasn't typed for a while. + if (mLastKeystrokeTimer.getElapsedTimeF32() > LLAgent::TYPING_TIMEOUT_SECS) + { + setTyping(FALSE); + } + + // If we are typing, and it's been a little while, send the + // typing indicator + if (!mSentTypingState + && mFirstKeystrokeTimer.getElapsedTimeF32() > 1.f) + { + sendTypingState(TRUE); + mSentTypingState = TRUE; + } + } + + LLFloater::draw(); +} + + +BOOL LLFloaterIMPanel::addParticipants(const LLDynamicArray<LLUUID>& ids) +{ + if(isAddAllowed()) + { + llinfos << "LLFloaterIMPanel::addParticipants() - adding participants" << llendl; + const S32 MAX_AGENTS = 50; + S32 count = ids.count(); + if(count > MAX_AGENTS) return FALSE; + LLMessageSystem *msg = gMessageSystem; + msg->newMessageFast(_PREHASH_ImprovedInstantMessage); + msg->nextBlockFast(_PREHASH_AgentData); + msg->addUUIDFast(_PREHASH_AgentID, gAgent.getID()); + msg->addUUIDFast(_PREHASH_SessionID, gAgent.getSessionID()); + msg->nextBlockFast(_PREHASH_MessageBlock); + msg->addBOOLFast(_PREHASH_FromGroup, FALSE); + msg->addUUIDFast(_PREHASH_ToAgentID, mOtherParticipantUUID); + msg->addU8Fast(_PREHASH_Offline, IM_ONLINE); + msg->addU8Fast(_PREHASH_Dialog, mDialog); + msg->addUUIDFast(_PREHASH_ID, mSessionUUID); + msg->addU32Fast(_PREHASH_Timestamp, NO_TIMESTAMP); // no timestamp necessary + std::string name; + gAgent.buildFullname(name); + msg->addStringFast(_PREHASH_FromAgentName, name); + msg->addStringFast(_PREHASH_Message, LLString::null); + msg->addU32Fast(_PREHASH_ParentEstateID, 0); + msg->addUUIDFast(_PREHASH_RegionID, LLUUID::null); + msg->addVector3Fast(_PREHASH_Position, gAgent.getPositionAgent()); + if (IM_SESSION_GROUP_START == mDialog) + { + // *HACK: binary bucket contains session label - the server + // will actually add agents. + llinfos << "Group IM session name '" << mSessionLabel + << "'" << llendl; + msg->addStringFast(_PREHASH_BinaryBucket, mSessionLabel); + gAgent.sendReliableMessage(); + } + else if (IM_SESSION_911_START == mDialog) + { + // HACK -- we modify the name of the session going out to + // the helpers to help them easily identify "Help" + // sessions in their collection of IM panels. + LLString name; + gAgent.getName(name); + LLString buffer = LLString("HELP ") + name; + llinfos << "LiveHelp IM session '" << buffer << "'." << llendl; + msg->addStringFast(_PREHASH_BinaryBucket, buffer.c_str()); + + // automaticaly open a wormhole when this reliable message gets through + msg->sendReliable( + gAgent.getRegionHost(), + 3, // retries + TRUE, // ping-based + 5.0f, // timeout + send_lure_911, + (void**)&mSessionUUID); + } + else + { + if (mDialog != IM_SESSION_ADD + && mDialog != IM_SESSION_OFFLINE_ADD) + { + llwarns << "LLFloaterIMPanel::addParticipants() - dialog type " << mDialog + << " is not an ADD" << llendl; + } + // *FIX: this could suffer from endian issues + S32 bucket_size = UUID_BYTES * count; + U8* bucket = new U8[bucket_size]; + U8* pos = bucket; + for(S32 i = 0; i < count; ++i) + { + memcpy(pos, &(ids.get(i)), UUID_BYTES); + pos += UUID_BYTES; + } + msg->addBinaryDataFast(_PREHASH_BinaryBucket, bucket, bucket_size); + delete[] bucket; + gAgent.sendReliableMessage(); + } + + } + else + { + llinfos << "LLFloaterIMPanel::addParticipants() - no need to add agents for " + << mDialog << llendl; + // successful add, because everyone that needed to get added + // was added. + } + return TRUE; +} + +void LLFloaterIMPanel::addHistoryLine(const std::string &utf8msg, const LLColor4& color, bool log_to_file) +{ + LLMultiFloater* hostp = getHost(); + if( !getVisible() && hostp && log_to_file) + { + // Only flash for logged ("real") messages + LLTabContainer* parent = (LLTabContainer*) getParent(); + parent->setTabPanelFlashing( this, TRUE ); + } + + // Now we're adding the actual line of text, so erase the + // "Foo is typing..." text segment, and the optional timestamp + // if it was present. JC + removeTypingIndicator(); + + // Actually add the line + LLString timestring; + bool prepend_newline = true; + if (gSavedSettings.getBOOL("IMShowTimestamps")) + { + timestring = mHistoryEditor->appendTime(prepend_newline); + prepend_newline = false; + } + mHistoryEditor->appendColoredText(utf8msg, false, prepend_newline, color); + + if (log_to_file + && gSavedPerAccountSettings.getBOOL("LogInstantMessages") ) + { + LLString histstr = timestring + utf8msg; + + LLLogChat::saveHistory(mSessionLabel,histstr); + } +} + + +void LLFloaterIMPanel::setVisible(BOOL b) +{ + LLPanel::setVisible(b); + + LLMultiFloater* hostp = getHost(); + if( b && hostp ) + { + LLTabContainer* parent = (LLTabContainer*) getParent(); + + // When this tab is displayed, you can stop flashing. + parent->setTabPanelFlashing( this, FALSE ); + + /* Don't change containing floater title - leave it "Instant Message" JC + LLUIString title = sTitleString; + title.setArg("[NAME]", mSessionLabel); + hostp->setTitle( title ); + */ + } +} + + +void LLFloaterIMPanel::setInputFocus( BOOL b ) +{ + mInputEditor->setFocus( b ); +} + + +void LLFloaterIMPanel::selectAll() +{ + mInputEditor->selectAll(); +} + + +void LLFloaterIMPanel::selectNone() +{ + mInputEditor->deselect(); +} + + +BOOL LLFloaterIMPanel::handleKeyHere( KEY key, MASK mask, BOOL called_from_parent ) +{ + BOOL handled = FALSE; + if( getVisible() && mEnabled && !called_from_parent && gFocusMgr.childHasKeyboardFocus(this)) + { + if( KEY_RETURN == key && mask == MASK_NONE) + { + sendMsg(); + handled = TRUE; + + // Close talk panels on hitting return + // but not shift-return or control-return + if ( !gSavedSettings.getBOOL("PinTalkViewOpen") && !(mask & MASK_CONTROL) && !(mask & MASK_SHIFT) ) + { + gIMView->toggle(NULL); + } + } + else if ( KEY_ESCAPE == key ) + { + handled = TRUE; + gFocusMgr.setKeyboardFocus(NULL, NULL); + + // Close talk panel with escape + if( !gSavedSettings.getBOOL("PinTalkViewOpen") ) + { + gIMView->toggle(NULL); + } + } + } + + // May need to call base class LLPanel::handleKeyHere if not handled + // in order to tab between buttons. JNC 1.2.2002 + return handled; +} + +BOOL LLFloaterIMPanel::handleDragAndDrop(S32 x, S32 y, MASK mask, BOOL drop, + EDragAndDropType cargo_type, + void* cargo_data, + EAcceptance* accept, + LLString& tooltip_msg) +{ + BOOL accepted = FALSE; + switch(cargo_type) + { + case DAD_CALLINGCARD: + accepted = dropCallingCard((LLInventoryItem*)cargo_data, drop); + break; + case DAD_CATEGORY: + accepted = dropCategory((LLInventoryCategory*)cargo_data, drop); + break; + default: + break; + } + if (accepted) + { + *accept = ACCEPT_YES_MULTI; + } + else + { + *accept = ACCEPT_NO; + } + return TRUE; +} + +BOOL LLFloaterIMPanel::dropCallingCard(LLInventoryItem* item, BOOL drop) +{ + BOOL rv = isAddAllowed(); + if(rv && item && item->getCreatorUUID().notNull()) + { + if(drop) + { + LLDynamicArray<LLUUID> ids; + ids.put(item->getCreatorUUID()); + addParticipants(ids); + } + } + else + { + // set to false if creator uuid is null. + rv = FALSE; + } + return rv; +} + +BOOL LLFloaterIMPanel::dropCategory(LLInventoryCategory* category, BOOL drop) +{ + BOOL rv = isAddAllowed(); + if(rv && category) + { + LLInventoryModel::cat_array_t cats; + LLInventoryModel::item_array_t items; + LLUniqueBuddyCollector buddies; + gInventory.collectDescendentsIf(category->getUUID(), + cats, + items, + LLInventoryModel::EXCLUDE_TRASH, + buddies); + S32 count = items.count(); + if(count == 0) + { + rv = FALSE; + } + else if(drop) + { + LLDynamicArray<LLUUID> ids; + for(S32 i = 0; i < count; ++i) + { + ids.put(items.get(i)->getCreatorUUID()); + } + addParticipants(ids); + } + } + return rv; +} + +BOOL LLFloaterIMPanel::isAddAllowed() const +{ + + return ((IM_SESSION_ADD == mDialog) + || (IM_SESSION_OFFLINE_ADD == mDialog) + || (IM_SESSION_GROUP_START == mDialog) + || (IM_SESSION_911_START == mDialog) + || (IM_SESSION_CARDLESS_START == mDialog)); +} + + +// static +void LLFloaterIMPanel::onTabClick(void* userdata) +{ + LLFloaterIMPanel* self = (LLFloaterIMPanel*) userdata; + self->setInputFocus(TRUE); +} + + +// static +void LLFloaterIMPanel::onClickProfile( void* userdata ) +{ + // Bring up the Profile window + LLFloaterIMPanel* self = (LLFloaterIMPanel*) userdata; + + if (self->mOtherParticipantUUID.notNull()) + { + LLFloaterAvatarInfo::showFromDirectory(self->getOtherParticipantID()); + } +} + +// static +void LLFloaterIMPanel::onClickClose( void* userdata ) +{ + LLFloaterIMPanel* self = (LLFloaterIMPanel*) userdata; + if(self) + { + self->close(); + } +} + +void LLFloaterIMPanel::addTeleportButton(const LLUUID& lure_id) +{ + LLButton* btn = LLViewerUICtrlFactory::getButtonByName(this, "Teleport Btn"); + if (!btn) + { + S32 BTN_VPAD = 2; + S32 BTN_HPAD = 2; + + const char* teleport_label = "Teleport"; + + const LLFontGL* font = gResMgr->getRes( LLFONT_SANSSERIF ); + S32 p_btn_width = 75; + S32 c_btn_width = 60; + S32 t_btn_width = 75; + + // adjust the size of the editor to make room for the new button + LLRect rect = mInputEditor->getRect(); + S32 editor_right = rect.mRight - t_btn_width; + rect.mRight = editor_right; + mInputEditor->reshape(rect.getWidth(), rect.getHeight(), FALSE); + mInputEditor->setRect(rect); + + const S32 IMPANEL_PAD = 1 + LLPANEL_BORDER_WIDTH; + const S32 IMPANEL_INPUT_HEIGHT = 20; + + rect.setLeftTopAndSize( + mRect.getWidth() - IMPANEL_PAD - p_btn_width - c_btn_width - t_btn_width - BTN_HPAD - RESIZE_HANDLE_WIDTH, + IMPANEL_INPUT_HEIGHT + IMPANEL_PAD - BTN_VPAD, + t_btn_width, + BTN_HEIGHT); + + btn = new LLButton( + "Teleport Btn", rect, + "","", "", + &LLFloaterIMPanel::onTeleport, this, + font, teleport_label, teleport_label ); + + btn->setFollowsBottom(); + btn->setFollowsRight(); + addChild( btn ); + } + btn->setEnabled(TRUE); + mLureID = lure_id; +} + +void LLFloaterIMPanel::removeTeleportButton() +{ + // TODO -- purge the button + LLButton* btn = LLViewerUICtrlFactory::getButtonByName(this, "Teleport Btn"); + if (btn) + { + btn->setEnabled(FALSE); + } +} + +// static +void LLFloaterIMPanel::onTeleport(void* userdata) +{ + LLFloaterIMPanel* self = (LLFloaterIMPanel*) userdata; + if(self) + { + send_simple_im(self->mLureID, + "", + IM_LURE_911, + LLUUID::null); + self->removeTeleportButton(); + } +} + +// static +void LLFloaterIMPanel::onInputEditorFocusReceived( LLUICtrl* caller, void* userdata ) +{ + LLFloaterIMPanel* self= (LLFloaterIMPanel*) userdata; + self->mHistoryEditor->setCursorAndScrollToEnd(); +} + +// static +void LLFloaterIMPanel::onInputEditorFocusLost(LLLineEditor* caller, void* userdata) +{ + LLFloaterIMPanel* self = (LLFloaterIMPanel*) userdata; + self->setTyping(FALSE); +} + +// static +void LLFloaterIMPanel::onInputEditorKeystroke(LLLineEditor* caller, void* userdata) +{ + LLFloaterIMPanel* self = (LLFloaterIMPanel*)userdata; + LLString text = self->mInputEditor->getText(); + if (!text.empty()) + { + self->setTyping(TRUE); + } + else + { + // Deleting all text counts as stopping typing. + self->setTyping(FALSE); + } +} + +void LLFloaterIMPanel::close(bool app_quitting) +{ + setTyping(FALSE); + + if(mSessionUUID.notNull()) + { + std::string name; + gAgent.buildFullname(name); + pack_instant_message( + gMessageSystem, + gAgent.getID(), + FALSE, + gAgent.getSessionID(), + mOtherParticipantUUID, + name.c_str(), + "", + IM_ONLINE, + IM_SESSION_DROP, + mSessionUUID); + gAgent.sendReliableMessage(); + } + gIMView->removeSession(mSessionUUID); + + destroy(); +} + + +void LLFloaterIMPanel::sendMsg() +{ + LLWString text = mInputEditor->getWText(); + LLWString::trim(text); + if (!gAgent.isGodlike() + && (mDialog == IM_NOTHING_SPECIAL) + && mOtherParticipantUUID.isNull()) + { + llinfos << "Cannot send IM to everyone unless you're a god." << llendl; + return; + } + if(text.length() > 0) + { + // Truncate and convert to UTF8 for transport + std::string utf8_text = wstring_to_utf8str(text); + utf8_text = utf8str_truncate(utf8_text, MAX_MSG_BUF_SIZE - 1); + std::string name; + gAgent.buildFullname(name); + + const LLRelationship* info = NULL; + info = LLAvatarTracker::instance().getBuddyInfo(mOtherParticipantUUID); + U8 offline = (!info || info->isOnline()) ? IM_ONLINE : IM_OFFLINE; + + // default to IM_SESSION_SEND unless it's nothing special - in + // which case it's probably an IM to everyone. + U8 dialog = (mDialog == IM_NOTHING_SPECIAL) + ? (U8)IM_NOTHING_SPECIAL : (U8)IM_SESSION_SEND; + pack_instant_message( + gMessageSystem, + gAgent.getID(), + FALSE, + gAgent.getSessionID(), + mOtherParticipantUUID, + name.c_str(), + utf8_text.c_str(), + offline, + (EInstantMessage)dialog, + mSessionUUID); + gAgent.sendReliableMessage(); + + // local echo + if((mDialog == IM_NOTHING_SPECIAL) && (mOtherParticipantUUID.notNull())) + { + std::string history_echo; + gAgent.buildFullname(history_echo); + + // Look for IRC-style emotes here. + char tmpstr[5]; + strcpy(tmpstr,utf8_text.substr(0,4).c_str()); + if (!strncmp(tmpstr, "/me ", 4) || !strncmp(tmpstr, "/me'", 4)) + { + utf8_text.replace(0,3,""); + } + else + { + history_echo += ": "; + } + history_echo += utf8_text; + addHistoryLine(history_echo); + } + + gViewerStats->incStat(LLViewerStats::ST_IM_COUNT); + } + mInputEditor->setText(""); + + // Don't need to actually send the typing stop message, the other + // client will infer it from receiving the message. + mTyping = FALSE; + mSentTypingState = TRUE; +} + + +void LLFloaterIMPanel::setTyping(BOOL typing) +{ + if (typing) + { + // Every time you type something, reset this timer + mLastKeystrokeTimer.reset(); + + if (!mTyping) + { + // You just started typing. + mFirstKeystrokeTimer.reset(); + + // Will send typing state after a short delay. + mSentTypingState = FALSE; + } + } + else + { + if (mTyping) + { + // you just stopped typing, send state immediately + sendTypingState(FALSE); + mSentTypingState = TRUE; + } + } + + mTyping = typing; +} + +void LLFloaterIMPanel::sendTypingState(BOOL typing) +{ + // Don't want to send typing indicators to multiple people, potentially too + // much network traffic. Only send in person-to-person IMs. + if (mDialog != IM_NOTHING_SPECIAL) return; + + std::string name; + gAgent.buildFullname(name); + + pack_instant_message( + gMessageSystem, + gAgent.getID(), + FALSE, + gAgent.getSessionID(), + mOtherParticipantUUID, + name.c_str(), + "typing", + IM_ONLINE, + (typing ? IM_TYPING_START : IM_TYPING_STOP), + mSessionUUID); + gAgent.sendReliableMessage(); +} + + +void LLFloaterIMPanel::processIMTyping(const LLIMInfo* im_info, BOOL typing) +{ + if (typing) + { + // other user started typing + addTypingIndicator(im_info); + } + else + { + // other user stopped typing + removeTypingIndicator(); + } +} + + +void LLFloaterIMPanel::addTypingIndicator(const LLIMInfo* im_info) +{ + mTypingLineStartIndex = mHistoryEditor->getText().length(); + + LLUIString typing_start = sTypingStartString; + typing_start.setArg("[NAME]", im_info->mName); + bool log_to_file = false; + addHistoryLine(typing_start, LLColor4::grey, log_to_file); + mOtherTyping = TRUE; +} + + +void LLFloaterIMPanel::removeTypingIndicator() +{ + if (mOtherTyping) + { + // Must do this first, otherwise addHistoryLine calls us again. + mOtherTyping = FALSE; + + S32 chars_to_remove = mHistoryEditor->getText().length() - mTypingLineStartIndex; + mHistoryEditor->removeTextFromEnd(chars_to_remove); + } +} + +//static +void LLFloaterIMPanel::chatFromLogFile(LLString line, void* userdata) +{ + LLFloaterIMPanel* self = (LLFloaterIMPanel*)userdata; + + //self->addHistoryLine(line, LLColor4::grey, FALSE); + self->mHistoryEditor->appendColoredText(line, false, true, LLColor4::grey); + +} |