//----------------------------------------------------------------------------- // Torque Game Engine // Copyright (C) GarageGames.com, Inc. //----------------------------------------------------------------------------- #include "gui/game/guiMessageVectorCtrl.h" #include "gui/utility/messageVector.h" #include "dgl/dgl.h" #include "console/consoleTypes.h" #include "gui/containers/guiScrollCtrl.h" IMPLEMENT_CONOBJECT(GuiMessageVectorCtrl); //-------------------------------------- Console functions ConsoleMethod(GuiMessageVectorCtrl, attach, bool, 3, 3, "(MessageVector item)" "Make this gui control display messages from the specified MessageVector") { MessageVector* pMV = NULL; Sim::findObject(argv[2], pMV); if (pMV == NULL) { Con::errorf(ConsoleLogEntry::General, "Could not find MessageVector: %s", argv[2]); return false; } return object->attach(pMV); } ConsoleMethod(GuiMessageVectorCtrl, detach, void, 2, 2, "()" "Stop listing messages from the MessageVector previously attached to, if any.") { if (object->isAttached() == false) { Con::warnf(ConsoleLogEntry::General, "GuiMessageVectorCtrl: double detach"); return; } object->detach(); } struct TempLineBreak { S32 start; S32 end; }; //-------------------------------------------------------------------------- // Callback for messageVector void sMVCtrlCallback(void * spectatorKey, const MessageVector::MessageCode code, const U32 argument) { GuiMessageVectorCtrl* pMVC = reinterpret_cast(spectatorKey); pMVC->callbackRouter(code, argument); } //-------------------------------------------------------------------------- GuiMessageVectorCtrl::GuiMessageVectorCtrl() { VECTOR_SET_ASSOCIATION(mLineWrappings); VECTOR_SET_ASSOCIATION(mSpecialMarkers); VECTOR_SET_ASSOCIATION(mLineElements); mMessageVector = NULL; mLineSpacingPixels = 0; mLineContinuationIndent = 10; mMouseDown = false; mMouseSpecialLine = -1; mMouseSpecialRef = -1; for (U32 i = 0; i < 16; i++) mAllowedMatches[i] = ""; mSpecialColor.set(0, 0, 255); mMaxColorIndex = 9; } //-------------------------------------------------------------------------- GuiMessageVectorCtrl::~GuiMessageVectorCtrl() { AssertFatal(mLineWrappings.size() == 0, "Error, line wrappings not properly cleared!"); AssertFatal(mSpecialMarkers.size() == 0, "Error, special markers not properly cleared!"); AssertFatal(mLineElements.size() == 0, "Error, line elements not properly cleared!"); } //-------------------------------------------------------------------------- void GuiMessageVectorCtrl::initPersistFields() { Parent::initPersistFields(); addField("lineSpacing", TypeS32, Offset(mLineSpacingPixels, GuiMessageVectorCtrl)); addField("lineContinuedIndex", TypeS32, Offset(mLineContinuationIndent, GuiMessageVectorCtrl)); addField("allowedMatches", TypeString, Offset(mAllowedMatches, GuiMessageVectorCtrl), 16); addField("matchColor", TypeColorI, Offset(mSpecialColor, GuiMessageVectorCtrl)); addField("maxColorIndex", TypeS32, Offset(mMaxColorIndex, GuiMessageVectorCtrl)); } bool GuiMessageVectorCtrl::onAdd() { return Parent::onAdd(); } void GuiMessageVectorCtrl::onRemove() { Parent::onRemove(); } //-------------------------------------------------------------------------- bool GuiMessageVectorCtrl::isAttached() const { return (mMessageVector != NULL); } //-------------------------------------------------------------------------- bool GuiMessageVectorCtrl::attach(MessageVector* newAttachment) { AssertFatal(newAttachment, "No attachment!"); if (newAttachment == NULL || !isAwake()) return false; if (isAttached()) { Con::warnf(ConsoleLogEntry::General, "GuiMessageVectorCtrl::attach: overriding attachment"); detach(); } AssertFatal(mLineWrappings.size() == 0, "Error, line wrappings not properly cleared!"); mMessageVector = newAttachment; mMessageVector->registerSpectator(sMVCtrlCallback, this); return true; } //-------------------------------------------------------------------------- void GuiMessageVectorCtrl::detach() { if (isAttached() == false) { Con::warnf(ConsoleLogEntry::General, "GuiMessageVectorCtrl::detach: not attached!"); return; } mMessageVector->unregisterSpectator(this); mMessageVector = NULL; AssertFatal(mLineWrappings.size() == 0, "Error, line wrappings not properly cleared!"); } //-------------------------------------------------------------------------- void GuiMessageVectorCtrl::lineInserted(const U32 arg) { AssertFatal(mMessageVector != NULL, "Should not be here unless we're attached!"); GuiScrollCtrl* pScroll = dynamic_cast(getParent()); bool fullyScrolled = pScroll->isScrolledToBottom(); mSpecialMarkers.insert(arg); createSpecialMarkers(mSpecialMarkers[arg], mMessageVector->getLine(arg).message); mLineWrappings.insert(arg); createLineWrapping(mLineWrappings[arg], mMessageVector->getLine(arg).message); mLineElements.insert(arg); createLineElement(mLineElements[arg], mLineWrappings[arg], mSpecialMarkers[arg]); U32 numLines = 0; for (U32 i = 0; i < mLineWrappings.size(); i++) { // We need to rebuild the physicalLineStart markers at the same time as // we find out how many of them are left... mLineElements[i].physicalLineStart = numLines; numLines += mLineWrappings[i].numLines; } U32 newHeight = (mProfile->mFont->getHeight() + mLineSpacingPixels) * getMax(numLines, U32(1)); resize(mBounds.point, Point2I(mBounds.extent.x, newHeight)); if(fullyScrolled) pScroll->scrollTo(0, 0x7FFFFFFF); } void GuiMessageVectorCtrl::lineDeleted(const U32 arg) { AssertFatal(mMessageVector != NULL, "Should not be here unless we're attached!"); AssertFatal(arg < mLineWrappings.size(), "Error, out of bounds line deleted!"); // It's a somewhat involved process to delete the lineelements... LineElement& rElement = mLineElements[arg]; TextElement* walk = rElement.headLineElements; while (walk != NULL) { TextElement* lineWalk = walk->nextInLine; while (lineWalk != NULL) { TextElement* temp = lineWalk; lineWalk = lineWalk->nextPhysicalLine; delete temp; } TextElement* temp = walk; walk = walk->nextPhysicalLine; delete temp; } rElement.headLineElements = NULL; mLineElements.erase(arg); delete [] mLineWrappings[arg].startEndPairs; mLineWrappings.erase(arg); delete [] mSpecialMarkers[arg].specials; mSpecialMarkers.erase(arg); U32 numLines = 0; for (U32 i = 0; i < mLineWrappings.size(); i++) { // We need to rebuild the physicalLineStart markers at the same time as // we find out how many of them are left... mLineElements[i].physicalLineStart = numLines; numLines += mLineWrappings[i].numLines; } U32 newHeight = (mProfile->mFont->getHeight() + mLineSpacingPixels) * getMax(numLines, U32(1)); resize(mBounds.point, Point2I(mBounds.extent.x, newHeight)); } void GuiMessageVectorCtrl::vectorDeleted() { AssertFatal(mMessageVector != NULL, "Should not be here unless we're attached!"); AssertFatal(mLineWrappings.size() == 0, "Error, line wrappings not properly cleared out!"); mMessageVector = NULL; U32 newHeight = mProfile->mFont->getHeight() + mLineSpacingPixels; resize(mBounds.point, Point2I(mBounds.extent.x, newHeight)); } void GuiMessageVectorCtrl::callbackRouter(const MessageVector::MessageCode code, const U32 arg) { switch (code) { case MessageVector::LineInserted: lineInserted(arg); break; case MessageVector::LineDeleted: lineDeleted(arg); break; case MessageVector::VectorDeletion: vectorDeleted(); break; } } //-------------------------------------------------------------------------- void GuiMessageVectorCtrl::createSpecialMarkers(SpecialMarkers& rSpecial, const char* string) { // The first thing we need to do is create a version of the string with no uppercase // chars for matching... char* pLCCopy = new char[dStrlen(string) + 1]; for (U32 i = 0; string[i] != '\0'; i++) pLCCopy[i] = dTolower(string[i]); pLCCopy[dStrlen(string)] = '\0'; Vector tempSpecials(__FILE__, __LINE__); Vector tempTypes(__FILE__, __LINE__); const char* pCurr = pLCCopy; while (pCurr[0] != '\0') { const char* pMinMatch = &pLCCopy[dStrlen(string)]; U32 minMatchType = 0xFFFFFFFF; AssertFatal(pMinMatch[0] == '\0', "Error, bad positioning of sentry..."); for (U32 i = 0; i < 16; i++) { if (mAllowedMatches[i][0] == '\0') continue; // Not the most efficient... char matchBuffer[512]; dStrncpy(matchBuffer, mAllowedMatches[i], 500); matchBuffer[499] = '\0'; dStrcat(matchBuffer, "://"); const char* pMatch = dStrstr(pCurr, (const char*)matchBuffer); if (pMatch != NULL && pMatch < pMinMatch) { pMinMatch = pMatch; minMatchType = i; } } if (pMinMatch[0] != '\0') { AssertFatal(minMatchType != 0xFFFFFFFF, "Hm, that's bad"); // Found a match... U32 start = pMinMatch - pLCCopy; U32 j; for (j = 0; pLCCopy[start + j] != '\0'; j++) { if (pLCCopy[start + j] == '\n' || pLCCopy[start + j] == ' ' || pLCCopy[start + j] == '\t') break; } AssertFatal(j > 0, "Error, j must be > 0 at this point!"); U32 end = start + j - 1; tempSpecials.increment(); tempSpecials.last().start = start; tempSpecials.last().end = end; tempTypes.push_back(minMatchType); pCurr = &pLCCopy[end + 1]; } else { // No match. This will cause the while loop to terminate... pCurr = pMinMatch; } } if ((rSpecial.numSpecials = tempSpecials.size()) != 0) { rSpecial.specials = new SpecialMarkers::Special[tempSpecials.size()]; for (U32 i = 0; i < tempSpecials.size(); i++) { rSpecial.specials[i].start = tempSpecials[i].start; rSpecial.specials[i].end = tempSpecials[i].end; rSpecial.specials[i].specialType = tempTypes[i]; } } else { rSpecial.specials = NULL; } } //-------------------------------------------------------------------------- void GuiMessageVectorCtrl::createLineWrapping(LineWrapping& rWrapping, const char* string) { Vector tempBreaks(__FILE__, __LINE__); U32 i; U32 currStart = 0; if (dStrlen(string) != 0) { for (i = 0; i < dStrlen(string); i++) { if (string[i] == '\n') { tempBreaks.increment(); tempBreaks.last().start = currStart; tempBreaks.last().end = i-1; currStart = i+1; } else if (i == dStrlen(string) - 1) { tempBreaks.increment(); tempBreaks.last().start = currStart; tempBreaks.last().end = i; currStart = i+1; } } } else { tempBreaks.increment(); tempBreaks.last().start = 0; tempBreaks.last().end = -1; } bool used = false; U32 splitWidth = mBounds.extent.x; U32 currLine = 0; while (currLine < tempBreaks.size()) { TempLineBreak& rLine = tempBreaks[currLine]; if (rLine.start >= rLine.end) { if (currLine == 0) splitWidth -= mLineContinuationIndent; currLine++; continue; } // Ok, there's some actual text in this line. How long is it? U32 baseLength = mProfile->mFont->getStrNWidthPrecise((const UTF8 *)&string[rLine.start], rLine.end-rLine.start+1); if (baseLength > splitWidth) { // DMMNOTE: Replace with bin search eventually U32 currPos; U32 breakPos; for (currPos = 0; currPos < rLine.end-rLine.start+1; currPos++) { U32 currLength = mProfile->mFont->getStrNWidthPrecise((const UTF8 *)&string[rLine.start], currPos+1); if (currLength > splitWidth) { // Make sure that the currPos has advanced, then set the breakPoint. breakPos = currPos != 0 ? currPos - 1 : 0; break; } } if (currPos == rLine.end-rLine.start+1) { AssertFatal(false, "Error, if the line must be broken, the position must be before this point!"); currLine++; continue; } // Ok, the character at breakPos is the last valid char we can render. We // want to scan back to the first whitespace character (which, in the bounds // of the line, is guaranteed to be a space or a tab). U32 originalBreak = breakPos; while (true) { if (string[rLine.start + breakPos] == ' ' || string[rLine.start + breakPos] == '\t') { break; } else { AssertFatal(string[rLine.start + breakPos] != '\n', "Bad characters in line range..."); if (breakPos == 0) { breakPos = originalBreak; break; } breakPos--; } } // Ok, everything up to and including breakPos is in the currentLine. Insert // a new line at this point, and put everything after in that line. S32 oldStart = rLine.start; S32 oldEnd = rLine.end; rLine.end = rLine.start + breakPos; // Note that rLine is NOTNOTNOTNOT valid after this point! tempBreaks.insert(currLine+1); tempBreaks[currLine+1].start = oldStart + breakPos + 1; tempBreaks[currLine+1].end = oldEnd; } if (currLine == 0) splitWidth -= mLineContinuationIndent; currLine++; } rWrapping.numLines = tempBreaks.size(); rWrapping.startEndPairs = new LineWrapping::SEPair[tempBreaks.size()]; for (i = 0; i < tempBreaks.size(); i++) { rWrapping.startEndPairs[i].start = tempBreaks[i].start; rWrapping.startEndPairs[i].end = tempBreaks[i].end; } } //-------------------------------------------------------------------------- void GuiMessageVectorCtrl::createLineElement(LineElement& rElement, LineWrapping& rWrapping, SpecialMarkers& rSpecial) { // First, do a straighforward translation of the wrapping... TextElement** ppWalk = &rElement.headLineElements; for (U32 i = 0; i < rWrapping.numLines; i++) { *ppWalk = new TextElement; (*ppWalk)->nextInLine = NULL; (*ppWalk)->nextPhysicalLine = NULL; (*ppWalk)->specialReference = -1; (*ppWalk)->start = rWrapping.startEndPairs[i].start; (*ppWalk)->end = rWrapping.startEndPairs[i].end; ppWalk = &((*ppWalk)->nextPhysicalLine); } if (rSpecial.numSpecials != 0) { // Ok. Now, walk down the lines, and split the elements into their contiuent parts... // TextElement* walk = rElement.headLineElements; while (walk) { TextElement* walkAcross = walk; while (walkAcross) { S32 specialMatch = -1; for (U32 i = 0; i < rSpecial.numSpecials; i++) { if (walkAcross->start <= rSpecial.specials[i].end && walkAcross->end >= rSpecial.specials[i].start) { specialMatch = i; break; } } if (specialMatch != -1) { // We have a match here. Break it into the appropriate number of segments. // this will vary depending on how the overlap is occuring... if (walkAcross->start >= rSpecial.specials[specialMatch].start) { if (walkAcross->end <= rSpecial.specials[specialMatch].end) { // The whole thing is part of the special AssertFatal(walkAcross->nextInLine == NULL, "Bad assumption!"); walkAcross->specialReference = specialMatch; } else { // The first part is in the special, the tail is out AssertFatal(walkAcross->nextInLine == NULL, "Bad assumption!"); walkAcross->nextInLine = new TextElement; walkAcross->nextInLine->nextInLine = NULL; walkAcross->nextInLine->nextPhysicalLine = NULL; walkAcross->nextInLine->specialReference = -1; walkAcross->specialReference = specialMatch; walkAcross->nextInLine->end = walkAcross->end; walkAcross->end = rSpecial.specials[specialMatch].end; walkAcross->nextInLine->start = rSpecial.specials[specialMatch].end + 1; AssertFatal(walkAcross->end >= walkAcross->start && walkAcross->nextInLine->end >= walkAcross->nextInLine->start, "Bad textelements generated!"); } walkAcross = walkAcross->nextInLine; } else { if (walkAcross->end <= rSpecial.specials[specialMatch].end) { // The first part is out of the special, the second part in. AssertFatal(walkAcross->nextInLine == NULL, "Bad assumption!"); walkAcross->nextInLine = new TextElement; walkAcross->nextInLine->nextInLine = NULL; walkAcross->nextInLine->nextPhysicalLine = NULL; walkAcross->nextInLine->specialReference = specialMatch; walkAcross->specialReference = -1; walkAcross->nextInLine->end = walkAcross->end; walkAcross->end = rSpecial.specials[specialMatch].start - 1; walkAcross->nextInLine->start = rSpecial.specials[specialMatch].start; AssertFatal(walkAcross->end >= walkAcross->start && walkAcross->nextInLine->end >= walkAcross->nextInLine->start, "Bad textelements generated!"); walkAcross = walkAcross->nextInLine; } else { // First out, middle in, last out. Oy. AssertFatal(walkAcross->nextInLine == NULL, "Bad assumption!"); walkAcross->nextInLine = new TextElement; walkAcross->nextInLine->nextInLine = NULL; walkAcross->nextInLine->nextPhysicalLine = NULL; walkAcross->nextInLine->specialReference = specialMatch; walkAcross->nextInLine->nextInLine = new TextElement; walkAcross->nextInLine->nextInLine->nextInLine = NULL; walkAcross->nextInLine->nextInLine->nextPhysicalLine = NULL; walkAcross->nextInLine->nextInLine->specialReference = -1; walkAcross->nextInLine->start = rSpecial.specials[specialMatch].start; walkAcross->nextInLine->end = rSpecial.specials[specialMatch].end; walkAcross->nextInLine->nextInLine->start = rSpecial.specials[specialMatch].end+1; walkAcross->nextInLine->nextInLine->end = walkAcross->end; walkAcross->end = walkAcross->nextInLine->start - 1; AssertFatal((walkAcross->end >= walkAcross->start && walkAcross->nextInLine->end >= walkAcross->nextInLine->start && walkAcross->nextInLine->nextInLine->end >= walkAcross->nextInLine->nextInLine->start), "Bad textelements generated!"); walkAcross = walkAcross->nextInLine->nextInLine; } } } else { walkAcross = walkAcross->nextInLine; } } walk = walk->nextPhysicalLine; } } } //-------------------------------------------------------------------------- bool GuiMessageVectorCtrl::onWake() { if (Parent::onWake() == false) return false; if (bool(mProfile->mFont) == false) return false; mMinSensibleWidth = 1; for (U32 i = 0; i < 256; i++) { if (mProfile->mFont->isValidChar(U8(i))) { if (mProfile->mFont->getCharWidth(U8(i)) > mMinSensibleWidth) mMinSensibleWidth = mProfile->mFont->getCharWidth(U8(i)); } } AssertFatal(mLineWrappings.size() == 0, "Error, line wrappings not properly cleared!"); return true; } //-------------------------------------------------------------------------- void GuiMessageVectorCtrl::onSleep() { if (isAttached()) detach(); Parent::onSleep(); } //-------------------------------------------------------------------------- void GuiMessageVectorCtrl::onRender(Point2I offset, const RectI& updateRect) { Parent::onRender(offset, updateRect); if (isAttached()) { U32 linePixels = mProfile->mFont->getHeight() + mLineSpacingPixels; U32 currLine = 0; for (U32 i = 0; i < mMessageVector->getNumLines(); i++) { TextElement* pElement = mLineElements[i].headLineElements; ColorI lastColor = mProfile->mFontColor; while (pElement != NULL) { Point2I localStart(pElement == mLineElements[i].headLineElements ? 0 : mLineContinuationIndent, currLine * linePixels); Point2I globalCheck = localToGlobalCoord(localStart); U32 globalRangeStart = globalCheck.y; U32 globalRangeEnd = globalCheck.y + mProfile->mFont->getHeight(); if (globalRangeStart > updateRect.point.y + updateRect.extent.y || globalRangeEnd < updateRect.point.y) { currLine++; pElement = pElement->nextPhysicalLine; continue; } TextElement* walkAcross = pElement; while (walkAcross) { if (walkAcross->start > walkAcross->end) break; Point2I globalStart = localToGlobalCoord(localStart); U32 strWidth; if (walkAcross->specialReference == -1) { dglSetBitmapModulation(lastColor); dglSetTextAnchorColor(mProfile->mFontColor); strWidth = dglDrawTextN(mProfile->mFont, globalStart, &mMessageVector->getLine(i).message[walkAcross->start], walkAcross->end - walkAcross->start + 1, mProfile->mFontColors, mMaxColorIndex); dglGetBitmapModulation(&lastColor); } else { dglGetBitmapModulation(&lastColor); dglSetBitmapModulation(mSpecialColor); dglSetTextAnchorColor(mProfile->mFontColor); strWidth = dglDrawTextN(mProfile->mFont, globalStart, &mMessageVector->getLine(i).message[walkAcross->start], walkAcross->end - walkAcross->start + 1); // in case we have 2 in a row... dglSetBitmapModulation(lastColor); } if (walkAcross->specialReference != -1) { Point2I lineStart = localStart; Point2I lineEnd = localStart; lineStart.y += mProfile->mFont->getBaseline() + 1; lineEnd.x += strWidth; lineEnd.y += mProfile->mFont->getBaseline() + 1; dglDrawLine(localToGlobalCoord(lineStart), localToGlobalCoord(lineEnd), mSpecialColor); } localStart.x += strWidth; walkAcross = walkAcross->nextInLine; } currLine++; pElement = pElement->nextPhysicalLine; } } dglClearBitmapModulation(); } } //-------------------------------------------------------------------------- void GuiMessageVectorCtrl::inspectPostApply() { Parent::inspectPostApply(); } //-------------------------------------------------------------------------- void GuiMessageVectorCtrl::parentResized(const Point2I& oldSize, const Point2I& newSize) { Parent::parentResized(oldSize, newSize); // If we have a MesssageVector, detach/reattach so we can reflow the text. if (mMessageVector) { MessageVector *reflowme = mMessageVector; detach(); attach(reflowme); } } //-------------------------------------------------------------------------- void GuiMessageVectorCtrl::findSpecialFromCoord(const Point2I& point, S32* specialLine, S32* specialRef) { if (mLineElements.size() == 0) { *specialLine = -1; *specialRef = -1; return; } U32 linePixels = mProfile->mFont->getHeight() + mLineSpacingPixels; if ((point.x < 0 || point.x >= mBounds.extent.x) || (point.y < 0 || point.y >= mBounds.extent.y)) { *specialLine = -1; *specialRef = -1; return; } // Ok, we have real work to do here. Let's determine the physical line that it's on... U32 physLine = point.y / linePixels; AssertFatal(physLine >= 0, "Bad physical line!"); // And now we find the LineElement that contains that physicalLine... U32 elemIndex; for (elemIndex = 0; elemIndex < mLineElements.size(); elemIndex++) { if (mLineElements[elemIndex].physicalLineStart > physLine) { // We've passed it. AssertFatal(elemIndex != 0, "Error, bad elemIndex, check assumptions."); elemIndex--; break; } } if (elemIndex == mLineElements.size()) { // On the last line... elemIndex = mLineElements.size() - 1; } TextElement* line = mLineElements[elemIndex].headLineElements; for (U32 i = 0; i < physLine - mLineElements[elemIndex].physicalLineStart; i++) { if (line->nextPhysicalLine == NULL) { *specialLine = -1; *specialRef = -1; return; } line = line->nextPhysicalLine; AssertFatal(line != NULL, "Error, moved too far!"); } // Ok, line represents the current line. We now need to find out which textelement // the points x coord falls in. U32 currX = 0; if ((physLine - mLineElements[elemIndex].physicalLineStart) != 0) { currX = mLineContinuationIndent; // First, if this isn't the first line in this wrapping, we have to make sure // that the coord isn't in the margin... if (point.x < mLineContinuationIndent) { *specialLine = -1; *specialRef = -1; return; } } if (line->start > line->end) { // Empty line special case... *specialLine = -1; *specialRef = -1; return; } U32 currElem = 0; while (line) { U32 newX = currX + mProfile->mFont->getStrNWidth((const UTF8 *)mMessageVector->getLine(elemIndex).message, line->end - line->start + 1); if (point.x < newX) { // That's the one! *specialLine = elemIndex; *specialRef = line->specialReference; return; } currX = newX; line = line->nextInLine; } // Off to the right. Aw... *specialLine = -1; *specialRef = -1; } void GuiMessageVectorCtrl::onMouseDown(const GuiEvent& event) { Parent::onMouseDown(event); mouseUnlock(); mMouseDown = true; // Find the special we are in, if any... findSpecialFromCoord(globalToLocalCoord(event.mousePoint), &mMouseSpecialLine, &mMouseSpecialRef); } void GuiMessageVectorCtrl::onMouseUp(const GuiEvent& event) { Parent::onMouseUp(event); mouseUnlock(); // Is this an up from a dragged click? if (mMouseDown == false) return; // Find the special we are in, if any... S32 currSpecialLine; S32 currSpecialRef; findSpecialFromCoord(globalToLocalCoord(event.mousePoint), &currSpecialLine, &currSpecialRef); if (currSpecialRef != -1 && (currSpecialLine == mMouseSpecialLine && currSpecialRef == mMouseSpecialRef)) { // Execute the callback SpecialMarkers& rSpecial = mSpecialMarkers[currSpecialLine]; S32 specialStart = rSpecial.specials[currSpecialRef].start; S32 specialEnd = rSpecial.specials[currSpecialRef].end; char* copyURL = new char[specialEnd - specialStart + 2]; dStrncpy(copyURL, &mMessageVector->getLine(currSpecialLine).message[specialStart], specialEnd - specialStart + 1); copyURL[specialEnd - specialStart + 1] = '\0'; Con::executef(this, 2, "urlClickCallback", copyURL); delete [] copyURL; } mMouseDown = false; mMouseSpecialLine = -1; mMouseSpecialRef = -1; }