//----------------------------------------------------------------------------- // Copyright (c) 2012 GarageGames, LLC // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to // deal in the Software without restriction, including without limitation the // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or // sell copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS // IN THE SOFTWARE. //----------------------------------------------------------------------------- #include "platform/platform.h" #include "gui/controls/guiTextEditCtrl.h" #include "console/consoleTypes.h" #include "console/console.h" #include "gui/core/guiCanvas.h" #include "gui/controls/guiMLTextCtrl.h" #include "gui/core/guiDefaultControlRender.h" #include "gfx/gfxDevice.h" #include "gfx/gfxDrawUtil.h" #include "core/frameAllocator.h" #include "sfx/sfxTrack.h" #include "sfx/sfxTypes.h" #include "sfx/sfxSystem.h" #include "core/strings/unicode.h" #include "console/engineAPI.h" IMPLEMENT_CONOBJECT(GuiTextEditCtrl); ConsoleDocClass( GuiTextEditCtrl, "@brief A component that places a text entry box on the screen.\n\n" "Fonts and sizes are changed using profiles. The text value can be set or entered by a user.\n\n" "@tsexample\n" " new GuiTextEditCtrl(MessageHud_Edit)\n" " {\n" " text = \"Hello World\";\n" " validate = \"validateCommand();\"\n" " escapeCommand = \"escapeCommand();\";\n" " historySize = \"5\";\n" " tabComplete = \"true\";\n" " deniedSound = \"DeniedSoundProfile\";\n" " sinkAllKeyEvents = \"true\";\n" " password = \"true\";\n" " passwordMask = \"*\";\n" " //Properties not specific to this control have been omitted from this example.\n" " };\n" "@endtsexample\n\n" "@see GuiTextCtrl\n" "@see GuiControl\n\n" "@ingroup GuiControls\n" ); IMPLEMENT_CALLBACK( GuiTextEditCtrl, onTabComplete, void, (const char* val),( val ), "@brief Called if tabComplete is true, and the 'tab' key is pressed.\n\n" "@param val Input to mimick the '1' sent by the actual tab key button press.\n" "@tsexample\n" "// Tab key has been pressed, causing the callback to occur.\n" "GuiTextEditCtrl::onTabComplete(%this,%val)\n" " {\n" " //Code to run when the onTabComplete callback occurs\n" " }\n" "@endtsexample\n\n" "@see GuiTextCtrl\n" "@see GuiControl\n\n" ); IMPLEMENT_CALLBACK( GuiTextEditCtrl, onReturn, void, (),(), "@brief Called when the 'Return' or 'Enter' key is pressed.\n\n" "@tsexample\n" "// Return or Enter key was pressed, causing the callback to occur.\n" "GuiTextEditCtrl::onReturn(%this)\n" " {\n" " // Code to run when the onReturn callback occurs\n" " }\n" "@endtsexample\n\n" "@see GuiTextCtrl\n" "@see GuiControl\n\n" ); IMPLEMENT_CALLBACK( GuiTextEditCtrl, onValidate, void, (),(), "@brief Called whenever the control is validated.\n\n" "@tsexample\n" "// The control gets validated, causing the callback to occur\n" "GuiTextEditCtrl::onValidated(%this)\n" " {\n" " // Code to run when the control is validated\n" " }\n" "@endtsexample\n\n" "@see GuiTextCtrl\n" "@see GuiControl\n\n" ); GuiTextEditCtrl::GuiTextEditCtrl() { mInsertOn = true; mBlockStart = 0; mBlockEnd = 0; mCursorPos = 0; mCursorOn = false; mNumFramesElapsed = 0; mDragHit = false; mTabComplete = false; mScrollDir = 0; mUndoBlockStart = 0; mUndoBlockEnd = 0; mUndoCursorPos = 0; mPasswordText = false; mSinkAllKeyEvents = false; mActive = true; mTextValid = true; mTextOffsetReset = true; mHistoryDirty = false; mHistorySize = 0; mHistoryLast = -1; mHistoryIndex = 0; mHistoryBuf = NULL; mDoubleClickTimeMS = 50; mMouseUpTime = 0; mPlaceholderText = StringTable->EmptyString(); #if defined(__MACOSX__) UTF8 bullet[4] = { UTF8(0xE2), UTF8(0x80), UTF8(0xA2), 0 }; mPasswordMask = StringTable->insert( bullet ); #else mPasswordMask = StringTable->insert( "*" ); #endif Sim::findObject( "InputDeniedSound", mDeniedSound ); mValidateCommand = ""; } GuiTextEditCtrl::~GuiTextEditCtrl() { //delete the history buffer if it exists if (mHistoryBuf) { for (S32 i = 0; i < mHistorySize; i++) delete [] mHistoryBuf[i]; delete [] mHistoryBuf; } } void GuiTextEditCtrl::initPersistFields() { addProtectedField("placeholderText", TypeCaseString, Offset(mPlaceholderText, GuiTextEditCtrl), setPlaceholderText, getPlaceholderText, "The text to show on the control."); addGroup( "Text Input" ); addField("validate", TypeRealString,Offset(mValidateCommand, GuiTextEditCtrl), "Script command to be called when the first validater is lost.\n"); addField("escapeCommand", TypeRealString,Offset(mEscapeCommand, GuiTextEditCtrl), "Script command to be called when the Escape key is pressed.\n"); addField("historySize", TypeS32, Offset(mHistorySize, GuiTextEditCtrl), "How large of a history buffer to maintain.\n"); addField("tabComplete", TypeBool, Offset(mTabComplete, GuiTextEditCtrl), "If true, when the 'tab' key is pressed, it will act as if the Enter key was pressed on the control.\n"); addField("deniedSound", TypeSFXTrackName, Offset(mDeniedSound, GuiTextEditCtrl), "If the attempted text cannot be entered, this sound effect will be played.\n"); addField("sinkAllKeyEvents", TypeBool, Offset(mSinkAllKeyEvents, GuiTextEditCtrl), "If true, every key event will act as if the Enter key was pressed.\n"); addField("password", TypeBool, Offset(mPasswordText, GuiTextEditCtrl), "If true, all characters entered will be stored in the control, however will display as the character stored in passwordMask.\n"); addField("passwordMask", TypeString, Offset(mPasswordMask, GuiTextEditCtrl), "If 'password' is true, this is the character that will be used to mask the characters in the control.\n"); endGroup( "Text Input" ); Parent::initPersistFields(); } bool GuiTextEditCtrl::onAdd() { if ( ! Parent::onAdd() ) return false; //create the history buffer if ( mHistorySize > 0 ) { mHistoryBuf = new UTF16*[mHistorySize]; for ( S32 i = 0; i < mHistorySize; i++ ) { mHistoryBuf[i] = new UTF16[GuiTextCtrl::MAX_STRING_LENGTH + 1]; mHistoryBuf[i][0] = '\0'; } } if( mText && mText[0] ) { setText(mText); } return true; } void GuiTextEditCtrl::onStaticModified(const char* slotName, const char* newValue) { if(!dStricmp(slotName, "text")) setText(mText); } void GuiTextEditCtrl::execConsoleCallback() { // Execute the console command! Parent::execConsoleCallback(); // Update the console variable: if ( mConsoleVariable[0] ) Con::setVariable(mConsoleVariable, mTextBuffer.getPtr8()); } void GuiTextEditCtrl::updateHistory( StringBuffer *inTxt, bool moveIndex ) { if(!mHistorySize) return; const UTF16* txt = inTxt->getPtr(); // Reject empty strings. if( !txt || !txt[ 0 ] ) return; // see if it's already in if(mHistoryLast == -1 || String::compare(txt, mHistoryBuf[mHistoryLast])) { if(mHistoryLast == mHistorySize-1) // we're at the history limit... shuffle the pointers around: { UTF16 *first = mHistoryBuf[0]; for(U32 i = 0; i < mHistorySize - 1; i++) mHistoryBuf[i] = mHistoryBuf[i+1]; mHistoryBuf[mHistorySize-1] = first; if(mHistoryIndex > 0) mHistoryIndex--; } else mHistoryLast++; inTxt->getCopy(mHistoryBuf[mHistoryLast], GuiTextCtrl::MAX_STRING_LENGTH); mHistoryBuf[mHistoryLast][GuiTextCtrl::MAX_STRING_LENGTH] = '\0'; } if(moveIndex) mHistoryIndex = mHistoryLast + 1; } void GuiTextEditCtrl::getText( char *dest ) { if ( dest ) mTextBuffer.getCopy8((UTF8*)dest, GuiTextCtrl::MAX_STRING_LENGTH+1); } void GuiTextEditCtrl::getRenderText(char *dest) { getText( dest ); } void GuiTextEditCtrl::setText( const UTF8 *txt ) { if(txt && txt[0] != 0) { Parent::setText(txt); mTextBuffer.set( txt ); } else mTextBuffer.set( "" ); mCursorPos = mTextBuffer.length(); } void GuiTextEditCtrl::setText( const UTF16* txt) { if(txt && txt[0] != 0) { UTF8* txt8 = createUTF8string( txt ); Parent::setText( txt8 ); delete[] txt8; mTextBuffer.set( txt ); } else { Parent::setText(""); mTextBuffer.set(""); } mCursorPos = mTextBuffer.length(); } bool GuiTextEditCtrl::isAllTextSelected() { if( mBlockStart == 0 && mBlockEnd == mTextBuffer.length() ) return true; else return false; } void GuiTextEditCtrl::selectAllText() { mBlockStart = 0; mBlockEnd = mTextBuffer.length(); setUpdate(); } void GuiTextEditCtrl::clearSelectedText() { mBlockStart = 0; mBlockEnd = 0; setUpdate(); } void GuiTextEditCtrl::forceValidateText() { if( mValidateCommand.isNotEmpty() ) evaluate( mValidateCommand ); } void GuiTextEditCtrl::setCursorPos( const S32 newPos ) { S32 charCount = mTextBuffer.length(); S32 realPos = newPos > charCount ? charCount : newPos < 0 ? 0 : newPos; if ( realPos != mCursorPos ) { mCursorPos = realPos; setUpdate(); } } S32 GuiTextEditCtrl::calculateCursorPos( const Point2I &globalPos ) { Point2I ctrlOffset = localToGlobalCoord( Point2I( 0, 0 ) ); S32 charLength = 0; S32 curX; curX = globalPos.x - ctrlOffset.x; setUpdate(); //if the cursor is too far to the left if ( curX < 0 ) return -1; //if the cursor is too far to the right if ( curX >= ctrlOffset.x + getExtent().x ) return -2; curX = globalPos.x - mTextOffset.x; S32 count=0; if(mTextBuffer.length() == 0) return 0; for(count=0; countmFont->isValidChar(c)) continue; if(mPasswordText) charLength += mProfile->mFont->getCharXIncrement( mPasswordMask[0] ); else charLength += mProfile->mFont->getCharXIncrement( c ); if ( charLength > curX ) break; } return count; } void GuiTextEditCtrl::onMouseDown( const GuiEvent &event ) { if(!isActive()) return; mDragHit = false; // If we have a double click, select all text. Otherwise // act as before by clearing any selection. bool doubleClick = (event.mouseClickCount > 1 && Platform::getRealMilliseconds() - mMouseUpTime > mDoubleClickTimeMS); if(doubleClick) { selectAllText(); } else { //undo any block function mBlockStart = 0; mBlockEnd = 0; } //find out where the cursor should be S32 pos = calculateCursorPos( event.mousePoint ); // if the position is to the left if ( pos == -1 ) mCursorPos = 0; else if ( pos == -2 ) //else if the position is to the right mCursorPos = mTextBuffer.length(); else //else set the mCursorPos mCursorPos = pos; //save the mouseDragPos mMouseDragStart = mCursorPos; // lock the mouse mouseLock(); //set the drag var mDragHit = true; //let the parent get the event setFirstResponder(); } void GuiTextEditCtrl::onMouseDragged( const GuiEvent &event ) { S32 pos = calculateCursorPos( event.mousePoint ); // if the position is to the left if ( pos == -1 ) mScrollDir = -1; else if ( pos == -2 ) // the position is to the right mScrollDir = 1; else // set the new cursor position { mScrollDir = 0; mCursorPos = pos; } // update the block: mBlockStart = getMin( mCursorPos, mMouseDragStart ); mBlockEnd = getMax( mCursorPos, mMouseDragStart ); if ( mBlockStart < 0 ) mBlockStart = 0; if ( mBlockStart == mBlockEnd ) mBlockStart = mBlockEnd = 0; //let the parent get the event Parent::onMouseDragged(event); } void GuiTextEditCtrl::onMouseUp(const GuiEvent &event) { TORQUE_UNUSED(event); mDragHit = false; mScrollDir = 0; mMouseUpTime = Platform::getRealMilliseconds(); mouseUnlock(); } void GuiTextEditCtrl::saveUndoState() { //save the current state mUndoText.set(&mTextBuffer); mUndoBlockStart = mBlockStart; mUndoBlockEnd = mBlockEnd; mUndoCursorPos = mCursorPos; } void GuiTextEditCtrl::onCopy(bool andCut) { // Don't copy/cut password field! if(mPasswordText) return; if (mBlockEnd > 0) { //save the current state saveUndoState(); //copy the text to the clipboard UTF8* clipBuff = mTextBuffer.createSubstring8(mBlockStart, mBlockEnd - mBlockStart); Platform::setClipboard(clipBuff); delete[] clipBuff; //if we pressed the cut shortcut, we need to cut the selected text from the control... if (andCut) { mTextBuffer.cut(mBlockStart, mBlockEnd - mBlockStart); mCursorPos = mBlockStart; } mBlockStart = 0; mBlockEnd = 0; } } void GuiTextEditCtrl::onPaste() { //first, make sure there's something in the clipboard to copy... const UTF8 *clipboard = Platform::getClipboard(); if(dStrlen(clipboard) <= 0) return; //save the current state saveUndoState(); //delete anything hilited if (mBlockEnd > 0) { mTextBuffer.cut(mBlockStart, mBlockEnd - mBlockStart); mCursorPos = mBlockStart; mBlockStart = 0; mBlockEnd = 0; } // We'll be converting to UTF16, and maybe trimming the string, // so let's use a StringBuffer, for convinience. StringBuffer pasteText(clipboard); // Space left after we remove the highlighted text S32 stringLen = mTextBuffer.length(); // Trim down to fit in a buffer of size mMaxStrLen S32 pasteLen = pasteText.length(); if(stringLen + pasteLen > mMaxStrLen) { pasteLen = mMaxStrLen - stringLen; pasteText.cut(pasteLen, pasteText.length() - pasteLen); } if (mCursorPos == stringLen) { mTextBuffer.append(pasteText); } else { mTextBuffer.insert(mCursorPos, pasteText); } mCursorPos += pasteLen; } void GuiTextEditCtrl::onUndo() { StringBuffer tempBuffer; S32 tempBlockStart; S32 tempBlockEnd; S32 tempCursorPos; //save the current tempBuffer.set(&mTextBuffer); tempBlockStart = mBlockStart; tempBlockEnd = mBlockEnd; tempCursorPos = mCursorPos; //restore the prev mTextBuffer.set(&mUndoText); mBlockStart = mUndoBlockStart; mBlockEnd = mUndoBlockEnd; mCursorPos = mUndoCursorPos; //update the undo mUndoText.set(&tempBuffer); mUndoBlockStart = tempBlockStart; mUndoBlockEnd = tempBlockEnd; mUndoCursorPos = tempCursorPos; } bool GuiTextEditCtrl::onKeyDown(const GuiEvent &event) { if ( !isActive() || !isAwake() ) return false; S32 stringLen = mTextBuffer.length(); setUpdate(); // Ugly, but now I'm cool like MarkF. if(event.keyCode == KEY_BACKSPACE) goto dealWithBackspace; if ( event.modifier & SI_SHIFT ) { // Added support for word jump selection. if ( event.modifier & SI_CTRL ) { switch ( event.keyCode ) { case KEY_LEFT: { S32 newpos = findPrevWord(); if ( mBlockStart == mBlockEnd ) { // There was not already a selection so start a new one. mBlockStart = newpos; mBlockEnd = mCursorPos; } else { // There was a selection already... // In this case the cursor MUST be at either the // start or end of that selection. if ( mCursorPos == mBlockStart ) { // We are at the start block and traveling left so // just extend the start block farther left. mBlockStart = newpos; } else { // We are at the end block BUT traveling left // back towards the start block... if ( newpos > mBlockStart ) { // We haven't overpassed the existing start block // so just trim back the end block. mBlockEnd = newpos; } else if ( newpos == mBlockStart ) { // We are back at the start, so no more selection. mBlockEnd = mBlockStart = 0; } else { // Only other option, we just backtracked PAST // our original start block. // So the new position becomes the start block // and the old start block becomes the end block. mBlockEnd = mBlockStart; mBlockStart = newpos; } } } mCursorPos = newpos; return true; } case KEY_RIGHT: { S32 newpos = findNextWord(); if ( mBlockStart == mBlockEnd ) { // There was not already a selection so start a new one. mBlockStart = mCursorPos; mBlockEnd = newpos; } else { // There was a selection already... // In this case the cursor MUST be at either the // start or end of that selection. if ( mCursorPos == mBlockEnd ) { // We are at the end block and traveling right so // just extend the end block farther right. mBlockEnd = newpos; } else { // We are at the start block BUT traveling right // back towards the end block... if ( newpos < mBlockEnd ) { // We haven't overpassed the existing end block // so just trim back the start block. mBlockStart = newpos; } else if ( newpos == mBlockEnd ) { // We are back at the end, so no more selection. mBlockEnd = mBlockStart = 0; } else { // Only other option, we just backtracked PAST // our original end block. // So the new position becomes the end block // and the old end block becomes the start block. mBlockStart = mBlockEnd; mBlockEnd = newpos; } } } mCursorPos = newpos; return true; } default: break; } } // End support for word jump selection. switch ( event.keyCode ) { case KEY_TAB: if ( mTabComplete ) { onTabComplete_callback("1"); return true; } break; // We don't want to fall through if we don't handle the TAB here. case KEY_HOME: mBlockStart = 0; mBlockEnd = mCursorPos; mCursorPos = 0; return true; case KEY_END: mBlockStart = mCursorPos; mBlockEnd = stringLen; mCursorPos = stringLen; return true; case KEY_LEFT: if ((mCursorPos > 0) & (stringLen > 0)) { //if we already have a selected block if (mCursorPos == mBlockEnd) { mCursorPos--; mBlockEnd--; if (mBlockEnd == mBlockStart) { mBlockStart = 0; mBlockEnd = 0; } } else { mCursorPos--; mBlockStart = mCursorPos; if (mBlockEnd == 0) { mBlockEnd = mCursorPos + 1; } } } return true; case KEY_RIGHT: if (mCursorPos < stringLen) { if ((mCursorPos == mBlockStart) && (mBlockEnd > 0)) { mCursorPos++; mBlockStart++; if (mBlockStart == mBlockEnd) { mBlockStart = 0; mBlockEnd = 0; } } else { if (mBlockEnd == 0) { mBlockStart = mCursorPos; mBlockEnd = mCursorPos; } mCursorPos++; mBlockEnd++; } } return true; case KEY_RETURN: case KEY_NUMPADENTER: return dealWithEnter(false); default: break; } } else if (event.modifier & SI_CTRL) { switch(event.keyCode) { // Adding word jump navigation. case KEY_LEFT: { mCursorPos = findPrevWord(); mBlockStart = 0; mBlockEnd = 0; return true; } case KEY_RIGHT: { mCursorPos = findNextWord(); mBlockStart = 0; mBlockEnd = 0; return true; } // Select all case KEY_A: { selectAllText(); return true; } // windows style cut / copy / paste / undo keybinds case KEY_C: case KEY_X: { // copy, and cut the text if we hit ctrl-x onCopy( event.keyCode==KEY_X ); return true; } case KEY_V: { onPaste(); // Execute the console command! execConsoleCallback(); return true; } case KEY_Z: if (! mDragHit) { onUndo(); return true; } case KEY_DELETE: case KEY_BACKSPACE: //save the current state saveUndoState(); //delete everything in the field mTextBuffer.set(""); mCursorPos = 0; mBlockStart = 0; mBlockEnd = 0; execConsoleCallback(); return true; default: break; } } #if defined(TORQUE_OS_MAC) // mac style cut / copy / paste / undo keybinds else if (event.modifier & SI_ALT) { // Mac command key maps to alt in torque. // Added Mac cut/copy/paste/undo keys switch(event.keyCode) { // Select all case KEY_A: { selectAllText(); return true; } case KEY_C: case KEY_X: { // copy, and cut the text if we hit cmd-x onCopy( event.keyCode==KEY_X ); return true; } case KEY_V: { onPaste(); // Execute the console command! execConsoleCallback(); return true; } case KEY_Z: if (! mDragHit) { onUndo(); return true; } default: break; } } #endif else { switch(event.keyCode) { case KEY_ESCAPE: if( mEscapeCommand.isNotEmpty() ) { evaluate( mEscapeCommand ); return( true ); } return( Parent::onKeyDown( event ) ); case KEY_RETURN: case KEY_NUMPADENTER: return dealWithEnter(true); case KEY_UP: { if( mHistorySize > 0 ) { if(mHistoryDirty) { updateHistory(&mTextBuffer, false); mHistoryDirty = false; } mHistoryIndex--; if(mHistoryIndex >= 0 && mHistoryIndex <= mHistoryLast) setText(mHistoryBuf[mHistoryIndex]); else if(mHistoryIndex < 0) mHistoryIndex = 0; } return true; } case KEY_DOWN: { if( mHistorySize > 0 ) { if(mHistoryDirty) { updateHistory(&mTextBuffer, false); mHistoryDirty = false; } mHistoryIndex++; if(mHistoryIndex > mHistoryLast) { mHistoryIndex = mHistoryLast + 1; setText(""); } else setText(mHistoryBuf[mHistoryIndex]); } return true; } case KEY_LEFT: // If we have a selection put the cursor to the left side of it. if ( mBlockStart != mBlockEnd ) { mCursorPos = mBlockStart; mBlockStart = mBlockEnd = 0; } else { mBlockStart = mBlockEnd = 0; mCursorPos = getMax( mCursorPos - 1, 0 ); } return true; case KEY_RIGHT: // If we have a selection put the cursor to the right side of it. if ( mBlockStart != mBlockEnd ) { mCursorPos = mBlockEnd; mBlockStart = mBlockEnd = 0; } else { mBlockStart = mBlockEnd = 0; mCursorPos = getMin( mCursorPos + 1, stringLen ); } return true; case KEY_BACKSPACE: dealWithBackspace: //save the current state saveUndoState(); if (mBlockEnd > 0) { mTextBuffer.cut(mBlockStart, mBlockEnd-mBlockStart); mCursorPos = mBlockStart; mBlockStart = 0; mBlockEnd = 0; mHistoryDirty = true; // Execute the console command! execConsoleCallback(); } else if (mCursorPos > 0) { mTextBuffer.cut(mCursorPos-1, 1); mCursorPos--; mHistoryDirty = true; // Execute the console command! execConsoleCallback(); } return true; case KEY_DELETE: //save the current state saveUndoState(); if (mBlockEnd > 0) { mHistoryDirty = true; mTextBuffer.cut(mBlockStart, mBlockEnd-mBlockStart); mCursorPos = mBlockStart; mBlockStart = 0; mBlockEnd = 0; // Execute the console command! execConsoleCallback(); } else if (mCursorPos < stringLen) { mHistoryDirty = true; mTextBuffer.cut(mCursorPos, 1); // Execute the console command! execConsoleCallback(); } return true; case KEY_INSERT: mInsertOn = !mInsertOn; return true; case KEY_HOME: mBlockStart = 0; mBlockEnd = 0; mCursorPos = 0; return true; case KEY_END: mBlockStart = 0; mBlockEnd = 0; mCursorPos = stringLen; return true; default: break; } } switch ( event.keyCode ) { case KEY_TAB: if ( mTabComplete ) { onTabComplete_callback("0"); return( true ); } case KEY_UP: case KEY_DOWN: case KEY_ESCAPE: return Parent::onKeyDown( event ); default: break; } // Handle character input events. if( mProfile->mFont->isValidChar( event.ascii ) ) { handleCharInput( event.ascii ); return true; } // Or eat it if that's appropriate. if( mSinkAllKeyEvents ) return true; // Not handled - pass the event to it's parent. return Parent::onKeyDown( event ); } bool GuiTextEditCtrl::dealWithEnter( bool clearResponder ) { //first validate if (mProfile->mReturnTab) { onLoseFirstResponder(); } updateHistory(&mTextBuffer, true); mHistoryDirty = false; //next exec the alt console command execAltConsoleCallback(); // Notify of Return onReturn_callback(); if (mProfile->mReturnTab) { GuiCanvas *root = getRoot(); if (root) { root->tabNext(); return true; } } if( clearResponder ) clearFirstResponder(); return true; } void GuiTextEditCtrl::setFirstResponder() { Parent::setFirstResponder(); GuiCanvas *root = getRoot(); if (root != NULL) { root->enableKeyboardTranslation(); // If the native OS accelerator keys are not disabled // then some key events like Delete, ctrl+V, etc may // not make it down to us. root->setNativeAcceleratorsEnabled( false ); } } void GuiTextEditCtrl::onLoseFirstResponder() { GuiCanvas *root = getRoot(); if( root ) { root->setNativeAcceleratorsEnabled( true ); root->disableKeyboardTranslation(); } updateHistory(&mTextBuffer, true); mHistoryDirty = false; //execute the validate command if( mValidateCommand.isNotEmpty() ) evaluate( mValidateCommand.c_str() ); onValidate_callback(); // Redraw the control: setUpdate(); // Lost Responder Parent::onLoseFirstResponder(); } void GuiTextEditCtrl::onRender( Point2I offset, const RectI &updateRect ) { RectI ctrlRect( offset, getExtent() ); //if opaque, fill the update rect with the fill color if ( mProfile->mOpaque ) { if ( !mTextValid ) GFX->getDrawUtil()->drawRectFill( ctrlRect, mProfile->mFillColorERR ); else if ( isFirstResponder() ) GFX->getDrawUtil()->drawRectFill( ctrlRect, mProfile->mFillColorHL ); else GFX->getDrawUtil()->drawRectFill( ctrlRect, mProfile->mFillColor ); } //if there's a border, draw the border if ( mProfile->mBorder ) { renderBorder( ctrlRect, mProfile ); if ( !mTextValid ) GFX->getDrawUtil()->drawRectFill( ctrlRect, mProfile->mFillColorERR ); } drawText( ctrlRect, isFirstResponder() ); } void GuiTextEditCtrl::onPreRender() { if ( isFirstResponder() ) { U32 timeElapsed = Platform::getVirtualMilliseconds() - mTimeLastCursorFlipped; mNumFramesElapsed++; if ( ( timeElapsed > 500 ) && ( mNumFramesElapsed > 3 ) ) { mCursorOn = !mCursorOn; mTimeLastCursorFlipped = Platform::getVirtualMilliseconds(); mNumFramesElapsed = 0; setUpdate(); } //update the cursor if the text is scrolling if ( mDragHit ) { if ( ( mScrollDir < 0 ) && ( mCursorPos > 0 ) ) mCursorPos--; else if ( ( mScrollDir > 0 ) && ( mCursorPos < (S32) mTextBuffer.length() ) ) mCursorPos++; } } } void GuiTextEditCtrl::drawText( const RectI &drawRect, bool isFocused ) { StringBuffer textBuffer; Point2I drawPoint = drawRect.point; Point2I paddingLeftTop, paddingRightBottom; // Or else just copy it over. char *renderText = Con::getReturnBuffer( GuiTextEditCtrl::MAX_STRING_LENGTH ); getRenderText( renderText ); // Apply password masking (make the masking char optional perhaps?) if(mPasswordText) { const U32 renderLen = dStrlen( renderText ); for( U32 i = 0; i < renderLen; i++ ) textBuffer.append(mPasswordMask); } else { textBuffer.set( renderText ); } bool usePlaceholder = false; if (textBuffer.length() == 0 && !isFocused) { textBuffer.set(mPlaceholderText); usePlaceholder = true; } // Just a little sanity. if(mCursorPos > textBuffer.length()) mCursorPos = textBuffer.length(); paddingLeftTop.set(( mProfile->mTextOffset.x != 0 ? mProfile->mTextOffset.x : 3 ), mProfile->mTextOffset.y); paddingRightBottom = paddingLeftTop; // Center vertically: drawPoint.y += ( ( drawRect.extent.y - paddingLeftTop.y - paddingRightBottom.y - S32( mProfile->mFont->getHeight() ) ) / 2 ) + paddingLeftTop.y; // Align horizontally: S32 textWidth = mProfile->mFont->getStrNWidth(textBuffer.getPtr(), textBuffer.length()); switch( mProfile->mAlignment ) { case GuiControlProfile::RightJustify: drawPoint.x += ( drawRect.extent.x - textWidth - paddingRightBottom.x ); break; case GuiControlProfile::CenterJustify: drawPoint.x += ( ( drawRect.extent.x - textWidth ) / 2 ); break; default: case GuiControlProfile::LeftJustify : drawPoint.x += paddingLeftTop.x; break; } ColorI fontColor = mActive ? mProfile->mFontColor : mProfile->mFontColorNA; if (usePlaceholder) fontColor = mProfile->mFontColorNA; // now draw the text Point2I cursorStart, cursorEnd; mTextOffset.y = drawPoint.y; mTextOffset.x = drawPoint.x; if ( drawRect.extent.x - paddingLeftTop.x > textWidth ) mTextOffset.x = drawPoint.x; else { // Alignment affects large text if ( mProfile->mAlignment == GuiControlProfile::RightJustify || mProfile->mAlignment == GuiControlProfile::CenterJustify ) { if ( mTextOffset.x + textWidth < (drawRect.point.x + drawRect.extent.x) - paddingRightBottom.x) mTextOffset.x = (drawRect.point.x + drawRect.extent.x) - paddingRightBottom.x - textWidth; } } // calculate the cursor if( isFocused && mActive ) { // Where in the string are we? S32 cursorOffset=0, charWidth=0; UTF16 tempChar = textBuffer.getChar(mCursorPos); // Alright, we want to terminate things momentarily. if(mCursorPos > 0) { cursorOffset = mProfile->mFont->getStrNWidth(textBuffer.getPtr(), mCursorPos); } else cursorOffset = 0; if( tempChar && mProfile->mFont->isValidChar( tempChar ) ) charWidth = mProfile->mFont->getCharWidth( tempChar ); else charWidth = paddingRightBottom.x; if( mTextOffset.x + cursorOffset + 1 >= (drawRect.point.x + drawRect.extent.x) ) // +1 is for the cursor width { // Cursor somewhere beyond the textcontrol, // skip forward roughly 25% of the total width (if possible) S32 skipForward = drawRect.extent.x / 4 * 3; if ( cursorOffset + skipForward > textWidth ) mTextOffset.x = (drawRect.point.x + drawRect.extent.x) - paddingRightBottom.x - textWidth; else { //mTextOffset.x -= skipForward; S32 mul = (S32)( mFloor( (cursorOffset-drawRect.extent.x) / skipForward ) ); mTextOffset.x -= skipForward * mul + drawRect.extent.x - 1; // -1 is for the cursor width } } else if( mTextOffset.x + cursorOffset < drawRect.point.x + paddingLeftTop.x ) { // Cursor somewhere before the textcontrol // skip backward roughly 25% of the total width (if possible) S32 skipBackward = drawRect.extent.x / 4 * 3; if ( cursorOffset - skipBackward < 0 ) mTextOffset.x = drawRect.point.x + paddingLeftTop.x; else { S32 mul = (S32)( mFloor( cursorOffset / skipBackward ) ); mTextOffset.x += drawRect.point.x - mTextOffset.x - skipBackward * mul; } } cursorStart.x = mTextOffset.x + cursorOffset; #ifdef TORQUE_OS_MAC cursorStart.x += charWidth/2; #endif cursorEnd.x = cursorStart.x; S32 cursorHeight = mProfile->mFont->getHeight(); if ( cursorHeight < drawRect.extent.y ) { cursorStart.y = drawPoint.y; cursorEnd.y = cursorStart.y + cursorHeight; } else { cursorStart.y = drawRect.point.y; cursorEnd.y = cursorStart.y + drawRect.extent.y; } } //draw the text if ( !isFocused ) mBlockStart = mBlockEnd = 0; //also verify the block start/end if ((mBlockStart > textBuffer.length() || (mBlockEnd > textBuffer.length()) || (mBlockStart > mBlockEnd))) mBlockStart = mBlockEnd = 0; Point2I tempOffset = mTextOffset; //draw the portion before the highlight if ( mBlockStart > 0 ) { GFX->getDrawUtil()->setBitmapModulation( fontColor ); const UTF16* preString2 = textBuffer.getPtr(); GFX->getDrawUtil()->drawText( mProfile->mFont, tempOffset, preString2, mProfile->mFontColors ); tempOffset.x += mProfile->mFont->getStrNWidth(preString2, mBlockStart); } //draw the highlighted portion if ( mBlockEnd > 0 ) { const UTF16* highlightBuff = textBuffer.getPtr() + mBlockStart; U32 highlightBuffLen = mBlockEnd-mBlockStart; S32 highlightWidth = mProfile->mFont->getStrNWidth(highlightBuff, highlightBuffLen); GFX->getDrawUtil()->drawRectFill( Point2I( tempOffset.x, drawRect.point.y ), Point2I( tempOffset.x + highlightWidth, drawRect.point.y + drawRect.extent.y - 1), mProfile->mFontColorSEL ); GFX->getDrawUtil()->setBitmapModulation( mProfile->mFontColorHL ); GFX->getDrawUtil()->drawTextN( mProfile->mFont, tempOffset, highlightBuff, highlightBuffLen, mProfile->mFontColors ); tempOffset.x += highlightWidth; } //draw the portion after the highlight if(mBlockEnd < textBuffer.length()) { const UTF16* finalBuff = textBuffer.getPtr() + mBlockEnd; U32 finalBuffLen = textBuffer.length() - mBlockEnd; GFX->getDrawUtil()->setBitmapModulation( fontColor ); GFX->getDrawUtil()->drawTextN( mProfile->mFont, tempOffset, finalBuff, finalBuffLen, mProfile->mFontColors ); } //draw the cursor if ( isFocused && mCursorOn ) GFX->getDrawUtil()->drawLine( cursorStart, cursorEnd, mProfile->mCursorColor ); } bool GuiTextEditCtrl::hasText() { return ( mTextBuffer.length() ); } void GuiTextEditCtrl::invalidText(bool playSound) { mTextValid = false; if ( playSound ) playDeniedSound(); } void GuiTextEditCtrl::validText() { mTextValid = true; } bool GuiTextEditCtrl::isValidText() { return mTextValid; } void GuiTextEditCtrl::playDeniedSound() { if ( mDeniedSound ) SFX->playOnce( mDeniedSound ); } const char *GuiTextEditCtrl::getScriptValue() { return StringTable->insert(mTextBuffer.getPtr8()); } void GuiTextEditCtrl::setScriptValue(const char *value) { mTextBuffer.set(value); mCursorPos = mTextBuffer.length(); } void GuiTextEditCtrl::handleCharInput( U16 ascii ) { S32 stringLen = mTextBuffer.length(); // Get the character ready to add to a UTF8 string. UTF16 convertedChar[2] = { ascii, 0 }; //see if it's a number field if ( mProfile->mNumbersOnly ) { if (ascii == '-') { //a minus sign only exists at the beginning, and only a single minus sign if (mCursorPos != 0 && !isAllTextSelected()) { invalidText(); return; } if (mInsertOn && (mTextBuffer.getChar(0) == '-')) { invalidText(); return; } } // BJTODO: This is probably not unicode safe. else if (ascii != '.' && (ascii < '0' || ascii > '9')) { invalidText(); return; } else validText(); } //save the current state saveUndoState(); bool alreadyCut = false; //delete anything highlighted if ( mBlockEnd > 0 ) { mTextBuffer.cut(mBlockStart, mBlockEnd-mBlockStart); mCursorPos = mBlockStart; mBlockStart = 0; mBlockEnd = 0; // We just changed the string length! // Get its new value. stringLen = mTextBuffer.length(); // If we already had text highlighted, we just want to cut that text. // Don't cut the next character even if insert is not on. alreadyCut = true; } if ( ( mInsertOn && ( stringLen < mMaxStrLen ) ) || ( !mInsertOn && ( mCursorPos < mMaxStrLen ) ) ) { if ( mCursorPos == stringLen ) { mTextBuffer.append(convertedChar); mCursorPos++; } else { if ( mInsertOn || alreadyCut ) { mTextBuffer.insert(mCursorPos, convertedChar); mCursorPos++; } else { mTextBuffer.cut(mCursorPos, 1); mTextBuffer.insert(mCursorPos, convertedChar); mCursorPos++; } } } else playDeniedSound(); //reset the history index mHistoryDirty = true; //execute the console command if it exists execConsoleCallback(); } S32 GuiTextEditCtrl::findPrevWord() { // First the first word to the left of the current cursor position // and return the positional index of its starting character. // We define the first character of a word as any non-whitespace // character which has a non-alpha-numeric character to its immediate left. const UTF8* text = mTextBuffer.getPtr8(); for ( S32 i = mCursorPos - 1; i > 0; i-- ) { if ( !dIsspace( text[i] ) ) { if ( !dIsalnum( text[i-1] ) ) { return i; } } } return 0; } S32 GuiTextEditCtrl::findNextWord() { // First the first word to the right of the current cursor position // and return the positional index of its starting character. // We define the first character of a word as any non-whitespace // character which has a non-alpha-numeric character to its immediate left. const UTF8* text = mTextBuffer.getPtr8(); for ( S32 i = mCursorPos + 1; i < mTextBuffer.length(); i++ ) { if ( !dIsspace( text[i] ) ) { if ( !dIsalnum( text[i-1] ) ) { return i; } } } return mTextBuffer.length(); } DefineEngineMethod( GuiTextEditCtrl, getText, const char*, (),, "@brief Acquires the current text displayed in this control.\n\n" "@tsexample\n" "// Acquire the value of the text control.\n" "%text = %thisGuiTextEditCtrl.getText();\n" "@endtsexample\n\n" "@return The current text within the control.\n\n" "@see GuiControl") { if( !object->hasText() ) return StringTable->EmptyString(); char *retBuffer = Con::getReturnBuffer( GuiTextEditCtrl::MAX_STRING_LENGTH ); object->getText( retBuffer ); return retBuffer; } DefineEngineMethod( GuiTextEditCtrl, setText, void, (const char* text),, "@brief Sets the text in the control.\n\n" "@param text Text to place in the control.\n" "@tsexample\n" "// Define the text to display\n" "%text = \"Text!\"\n\n" "// Inform the GuiTextEditCtrl to display the defined text\n" "%thisGuiTextEditCtrl.setText(%text);\n" "@endtsexample\n\n" "@see GuiControl") { object->setText( text ); } DefineEngineMethod( GuiTextEditCtrl, getCursorPos, S32, (),, "@brief Returns the current position of the text cursor in the control.\n\n" "@tsexample\n" "// Acquire the cursor position in the control\n" "%position = %thisGuiTextEditCtrl.getCursorPost();\n" "@endtsexample\n\n" "@return Text cursor position within the control.\n\n" "@see GuiControl") { return( object->getCursorPos() ); } DefineEngineMethod( GuiTextEditCtrl, setCursorPos, void, (S32 position),, "@brief Sets the text cursor at the defined position within the control.\n\n" "@param position Text position to set the text cursor.\n" "@tsexample\n" "// Define the cursor position\n" "%position = \"12\";\n\n" "// Inform the GuiTextEditCtrl control to place the text cursor at the defined position\n" "%thisGuiTextEditCtrl.setCursorPos(%position);\n" "@endtsexample\n\n" "@see GuiControl") { object->setCursorPos( position ); } DefineEngineMethod( GuiTextEditCtrl, isAllTextSelected, bool, (),, "@brief Checks to see if all text in the control has been selected.\n\n" "@tsexample\n" "// Check to see if all text has been selected or not.\n" "%allSelected = %thisGuiTextEditCtrl.isAllTextSelected();\n" "@endtsexample\n\n" "@return True if all text in the control is selected, otherwise false.\n\n" "@see GuiControl") { return object->isAllTextSelected(); } DefineEngineMethod( GuiTextEditCtrl, selectAllText, void, (),, "@brief Selects all text within the control.\n\n" "@tsexample\n" "// Inform the control to select all of its text.\n" "%thisGuiTextEditCtrl.selectAllText();\n" "@endtsexample\n\n" "@see GuiControl") { object->selectAllText(); } DefineEngineMethod( GuiTextEditCtrl, clearSelectedText, void, (),, "@brief Unselects all selected text in the control.\n\n" "@tsexample\n" "// Inform the control to unselect all of its selected text\n" "%thisGuiTextEditCtrl.clearSelectedText();\n" "@endtsexample\n\n" "@see GuiControl") { object->clearSelectedText(); } DefineEngineMethod( GuiTextEditCtrl, forceValidateText, void, (),, "@brief Force a validation to occur.\n\n" "@tsexample\n" "// Inform the control to force a validation of its text.\n" "%thisGuiTextEditCtrl.forceValidateText();\n" "@endtsexample\n\n" "@see GuiControl") { object->forceValidateText(); } DefineEngineMethod(GuiTextEditCtrl, invalidText, void, (bool playSound), (true), "@brief Trigger the invalid sound and make the box red.nn" "@param playSound Play the invalid text sound or not.n") { object->invalidText(playSound); } DefineEngineMethod(GuiTextEditCtrl, validText, void, (), , "@brief Restores the box to normal color.nn") { object->validText(); } DefineEngineMethod(GuiTextEditCtrl, isValidText, bool, (), , "@brief Returns if the text is set to valid or not.n" "@Return true if text is set to valid, false if not.nn") { return object->isValidText(); }