Torque3D/Engine/source/gui/controls/guiTextEditCtrl.cpp
Areloch abf5a09bc3 Removed duplicate checkbox image
Added asset import conflict resolution option to prepend folder name
Cleanups of Project Importer and fixed importing of material and terrain materials, specifically, fallbacks in the event they are unnamed to utilize the mapTo and internalName fields, respectively
Fixed typos in guiTerrainMaterialDlg
Added AssetBrowser button to GuiEditor
Improved FileObject's PeekLine function to be able to peek forward further via an optional lineOffset argument
2021-07-28 09:26:13 -05:00

1825 lines
49 KiB
C++

//-----------------------------------------------------------------------------
// 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; count<mTextBuffer.length(); count++)
{
UTF16 c = mTextBuffer.getChar(count);
if(!mPasswordText && !mProfile->mFont->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)
{
#if defined(TORQUE_OS_MAC)
// Added UNIX emacs key bindings - just a little hack here...
// Ctrl-B - move one character back
case KEY_B:
{
GuiEvent new_event;
new_event.modifier = 0;
new_event.keyCode = KEY_LEFT;
return(onKeyDown(new_event));
}
// Ctrl-F - move one character forward
case KEY_F:
{
GuiEvent new_event;
new_event.modifier = 0;
new_event.keyCode = KEY_RIGHT;
return(onKeyDown(new_event));
}
// Ctrl-A - move to the beginning of the line
case KEY_A:
{
GuiEvent new_event;
new_event.modifier = 0;
new_event.keyCode = KEY_HOME;
return(onKeyDown(new_event));
}
// Ctrl-E - move to the end of the line
case KEY_E:
{
GuiEvent new_event;
new_event.modifier = 0;
new_event.keyCode = KEY_END;
return(onKeyDown(new_event));
}
// Ctrl-P - move backward in history
case KEY_P:
{
GuiEvent new_event;
new_event.modifier = 0;
new_event.keyCode = KEY_UP;
return(onKeyDown(new_event));
}
// Ctrl-N - move forward in history
case KEY_N:
{
GuiEvent new_event;
new_event.modifier = 0;
new_event.keyCode = KEY_DOWN;
return(onKeyDown(new_event));
}
// Ctrl-D - delete under cursor
case KEY_D:
{
GuiEvent new_event;
new_event.modifier = 0;
new_event.keyCode = KEY_DELETE;
return(onKeyDown(new_event));
}
case KEY_U:
{
GuiEvent new_event;
new_event.modifier = SI_CTRL;
new_event.keyCode = KEY_DELETE;
return(onKeyDown(new_event));
}
// End added UNIX emacs key bindings
#endif
// 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;
}
#if !defined(TORQUE_OS_MAC)
// 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;
}
#endif
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();
}