Game Development Community

dev|Pro Game Development Curriculum

GuiMLTextEditCtrl improvements

by Orion Elenzil · 12/19/2008 (2:12 pm) · 6 comments

This resource brings several improvements to the text-editing behaviour of the GuiMLTextEditCtrl
as implemented in TGE 1.3.5 and 1.4, with the exception of 1.4's unicode support.

Frankly, the original 1.3.5 / 1.4 control was not usable as a user-facing widget,
and the 1.5 version seems to have introduced a few more problems.

The code here addresses the 1.3.5 / 1.4 issues,
and shouldn't be difficult to integrate into those codebases.
I can't speak to 1.5 or TGB or TGEA, but i suspect it would
be easy to incorporate into those as well.



1.3.5 / 1.4 GuiMLTextEditCtrl shortcomings:
-------------------------------------------

* text could only be selected with mouse, not with arrow keys.

* poor cursor movement support (no ctrl-home, ctrl-end,
to say nothing of shift-ctrl-home).

* no undo.

* non-blinking cursor.

* up- and down-arrow keys move cursor to beginning of line.
(this seems fixed in TGE 1.5.2)



So here are the improvements:
-----------------------------

* text can now be selected by holding shift and using the arrow keys.
also works with home, end, and shift-home and shift-end.

* added an undo and redo system, with 500-step history buffer.
(this is a more sophisticated undo/redo than in GuiTextEditCtrl)

* added ctrl-A = select all.

* added ctrl-insert = copy and shift-insert = paste.

* improved arrow-key navigation.

* improved cursor alignment with text characters.

* cursor now blinks.

* changed TAB and shift-TAB to do the regular focus-switching thing
instead of actually inserting spaces into the text.

* refactored away some code duplication,
as well as allowing more flexible key-modifiers like ctrl+shift.

* use proper modifier key on Mac.
(this may not compile in stock TGE, pls. let me know)



ToDo:
-----

* Confirm that this compiles with stock torque!
I unfortunately don't currently have a build environment for stock TGE set up,
and haven't taken the hour or so it takes to make one.

* Confirm it compiles with stock torque on OSX!
See "#ifdef __MACOSX__".

* Include unicode support from TGE 1.4 (looks like this shouldn't be hard)

* Determine why the selection state sometimes becomes invalid,
see note "validate selection" below.

* Confirm memory conservation.





GuiMLTextCtrl.h
change this section:
// SBM: Begin - Added for Script Editor
  public:
   void setSelectionStart( U32 start ) { clearSelection(); mSelectionStart = start; };
   void setSelectionEnd( U32 end ) { mSelectionEnd = end;};
   void setSelectionActive(bool active) { mSelectionActive = active; };
// SBM:  End - Added for Script Editor
// DRSJR:  Added for TGE Script Editor
   S32 getCursorPosition()  { return( mCursorPosition ); }
to this:
// SBM: Begin - Added for Script Editor
  public:
   void selectAll();
   void setSelection           (U32 start, U32 end);
   void setSelectionStart      (U32 start         );
   void setSelectionEnd        (U32 end           );
   void setSelectionActive     (bool active       );
   void validateSelectionBounds(                  );
// SBM:  End - Added for Script Editor
// DRSJR:  Added for TGE Script Editor
   S32 getCursorPosition()  { return( mCursorPosition ); }


GuiMLTextCtrl.cc
at the bottom of GuiMLTextCtrl::getCursorPositionAndColor(), change this:
cursorTop.set(x, y);
   cursorBottom.set(x, y + height);
to this:
cursorTop   .set(x, y          + 1);
   cursorBottom.set(x, y + height - 1);

at the bottom of the file, add this:
// oxe 20081216 - extend text-editing capabilities
void GuiMLTextCtrl::selectAll()
{
   setSelectionStart (0                );
   setSelectionEnd   (mCurrTextSize - 1);
   setSelectionActive(true             );
}

void GuiMLTextCtrl::setSelectionStart(U32 start)
{
   mSelectionStart = start;
}

void GuiMLTextCtrl::setSelectionEnd(U32 end)
{
   mSelectionEnd   = end;
}

void GuiMLTextCtrl::setSelection(U32 start, U32 end)
{
   mSelectionStart = start;
   mSelectionEnd   = end;
   validateSelectionBounds();
}

void GuiMLTextCtrl::validateSelectionBounds()
{
   if (mSelectionEnd >= mSelectionStart)
      return;

   U32 tmp         = mSelectionEnd;
   mSelectionEnd   = mSelectionStart;
   mSelectionStart = tmp;
}

void GuiMLTextCtrl::setSelectionActive(bool active)
{
   mSelectionActive = active;
}


GuiMLTextEditCtrl.h and GuiMLTextEditCtrl.cc both have rather extensive changes,
so it's probably simplest just to take them in toto.

GuiMLTextEditCtrl.h:
//-----------------------------------------------------------------------------
// Torque Game Engine 
// Copyright (C) GarageGames.com, Inc.
//-----------------------------------------------------------------------------

#ifndef _GUIMLTEXTEDITCTRL_H_
#define _GUIMLTEXTEDITCTRL_H_

#ifndef _GUIMLTEXTCTRL_H_
#include "gui/guiMLTextCtrl.h"
#endif

class GuiMLTextEditCtrl : public GuiMLTextCtrl
{
   typedef GuiMLTextCtrl Parent;

   //-------------------------------------- Overrides
  protected:
   StringTableEntry mEscapeCommand;

   // Events
   bool onKeyDown (const GuiEvent&event);
   bool processKey(const GuiEvent&event);

   // Event forwards
   void handleMoveKeys(const GuiEvent&);
   void handleDeleteKeys(const GuiEvent&);

   // rendering
   void onPreRender();
   void onRender   (Point2I offset, const RectI &updateRect);

  public:
   GuiMLTextEditCtrl();
   ~GuiMLTextEditCtrl();

   void resize(const Point2I &newPosition, const Point2I &newExtent);

   DECLARE_CONOBJECT(GuiMLTextEditCtrl);
   static void initPersistFields();

  protected:
   void doCopy  ();
   void doCut   ();
   void doPaste ();
   void doDelete();

  protected:
   enum {
     maxUndoDepth = 500
   };

   typedef struct {
      char* textBuffer;
      U32  cursorPosition;
      U32  selectionStart;
      U32  selectionEnd;
      S32  selectionAnchor;
      bool selectionActive;
   } undoStateT ;

   undoStateT mUndoStack[maxUndoDepth];
   S32        mUndoStackCurFrame;
 
   void saveUndoState ();
   void doUndo        ();
   void doRedo        ();
   void storeUndoState(      undoStateT& undoState);
   void clearUndoState(      undoStateT& undoState);
   void applyUndoState(const undoStateT& undoState);
   void dumpUndoStack (                           );
   void dumpUndoState (const undoStateT& undoState, const char* prefix = NULL);

  private:
   bool mSaveUndo;   // whether or not to save an undo state as a result of a particular keystroke.



  private:
   // cursor blinkin'. borrowed from GuiTextEditCtrl.
   bool mCursorOn;
   U32  mTimeLastCursorFlipped;


};

#endif  // _H_GUIMLTEXTEDITCTRL_


GuiMLTextEditCtrl.cc:
//-----------------------------------------------------------------------------
// Torque Game Engine 
// Copyright (C) GarageGames.com, Inc.
//-----------------------------------------------------------------------------

#include "gui/guiMLTextEditCtrl.h"
#include "gui/guiScrollCtrl.h"
#include "dgl/dgl.h"
#include "console/consoleTypes.h"
#include "platform/event.h"

IMPLEMENT_CONOBJECT(GuiMLTextEditCtrl);


//--------------------------------------------------------------------------
GuiMLTextEditCtrl::GuiMLTextEditCtrl()
{
   mEscapeCommand = StringTable->insert( "" );

   mIsEditCtrl = true;

   mActive = true;

   mVertMoveAnchorValid = false;

   // undo system:
   mUndoStackCurFrame     = 0;
   for (S32 n = 0; n < maxUndoDepth; n++)
   {
      mUndoStack[n].textBuffer = NULL;
   }

   // cursor blinkin':
   mTimeLastCursorFlipped = 0;
   mCursorOn              = 0;
}


//--------------------------------------------------------------------------
GuiMLTextEditCtrl::~GuiMLTextEditCtrl()
{
   for (S32 n = 0; n < maxUndoDepth; n++)
   {
      if (mUndoStack[n].textBuffer != NULL)
      {
         delete [] mUndoStack[n].textBuffer;
         mUndoStack[n].textBuffer = NULL;
      }
   }
}


//--------------------------------------------------------------------------
void GuiMLTextEditCtrl::resize(const Point2I &newPosition, const Point2I &newExtent)
{
   // We don't want to get any smaller than our parent:
   Point2I newExt = newExtent;
   GuiControl* parent = getParent();
   if ( parent )
      newExt.y = getMax( parent->mBounds.extent.y, newExt.y );

   Parent::resize( newPosition, newExt );
}


//--------------------------------------------------------------------------
void GuiMLTextEditCtrl::initPersistFields()
{
   Parent::initPersistFields();
   addField( "escapeCommand", TypeString, Offset( mEscapeCommand, GuiMLTextEditCtrl ) ); 
}

// ETS-4103:  Use Command instead of Control on the mac
#ifdef __MACOSX__
#define KEY_MODIFIER SI_MAC_OPT
#else
#define KEY_MODIFIER SI_CTRL
#endif

//--------------------------------------------------------------------------
// Key events...
bool GuiMLTextEditCtrl::onKeyDown(const GuiEvent& event)
{
   bool ret = processKey(event);

   if (mSaveUndo)
      saveUndoState();

   // force the cursor to flip to On in next render:
   mCursorOn              = false;
   mTimeLastCursorFlipped = 0;

   // validate selection.
   // todo: track down how these are possible. they should never happen.
   if (mSelectionActive && mSelectionStart >= mCurrTextSize)
   {
      Con::errorf("%s() - bad selection state: mSelectionStart too large. %d, %d"                , __FUNCTION__, mSelectionStart, mCurrTextSize);
      mSelectionStart = mCurrTextSize - 1;
      dumpUndoStack();
   }

   if (mSelectionActive && mSelectionEnd   >= mCurrTextSize)
   {
      Con::errorf("%s() - bad selection state: mSelectionEnd   too large. %d, %d"                , __FUNCTION__, mSelectionEnd  , mCurrTextSize);
      mSelectionEnd = mCurrTextSize - 1;
      dumpUndoStack();
   }

   if (mSelectionActive && mSelectionStart >  mSelectionEnd)
   {
      Con::errorf("%s() - bad selection state: mSelectionStart larger than mSelectionEnd. %d, %d", __FUNCTION__, mSelectionStart, mSelectionEnd);
      mSelectionStart = mCurrTextSize - 1;
      dumpUndoStack();
   }

   return ret;
}

bool GuiMLTextEditCtrl::processKey(const GuiEvent& event)
{
   setUpdate();

   mSaveUndo = false;

   switch (event.keyCode) 
   {
      case KEY_Z:
      {
         if (event.modifier & KEY_MODIFIER)
         {
            doUndo();
            return true;
         }
         break;
      }

      case KEY_Y:
      {
         if (event.modifier & KEY_MODIFIER)
         {
            doRedo();
            return true;
         }
         break;
      }

      case KEY_INSERT:
      {
         if (event.modifier & KEY_MODIFIER)
         {
            doCopy();
            return true;
         }
         if (event.modifier & SI_SHIFT    )
         {
            doPaste();
            return true;
         }
         break;
      }


      case KEY_C:
      {
         if (event.modifier & KEY_MODIFIER)
         {
            doCopy();
            return true;
         }
         break;
      }

      case KEY_X:
      {
         if (event.modifier & KEY_MODIFIER)
         {
            doCut();
            return true;
         }
         break;
      }

      case KEY_V:
      {
         if (event.modifier & KEY_MODIFIER)
         {
            doPaste();
            return true;
         }
         break;
      }

      case KEY_A:
      {
         if (event.modifier & KEY_MODIFIER)
         {
            selectAll();
            return true;
         }
         break;
      }

      case KEY_ESCAPE:
      {
         if ( mEscapeCommand[0] )
         {
            Con::evaluate(mEscapeCommand);
            return true ;
         }
         else
         {
            return(Parent::onKeyDown(event));
         }
         break;
      }

      // Deletion
      case KEY_BACKSPACE:
      case KEY_DELETE:
      {
         handleDeleteKeys(event);
         return true;
         break;
      }

      // Cursor movement
      case KEY_LEFT:
      case KEY_RIGHT:
      case KEY_UP:
      case KEY_DOWN:
      case KEY_HOME:
      case KEY_END:
      {
         handleMoveKeys(event);
         return true;
         break;
      }

      case KEY_TAB:
      {
         // don't insert tab characters
         return Parent::onKeyDown(event);
         break;
      }

      case KEY_RETURN:
      {
         // insert carriage return
         if (mSelectionActive == true) 
         {
            mSelectionActive = false;
            deleteChars(mSelectionStart, mSelectionEnd);
            mCursorPosition = mSelectionStart;
         }
         insertChars( "\n", 1, mCursorPosition );
         mSaveUndo        = true;
         return true;
         break;
      }
   }



   if (event.ascii != 0) 
   {
      // oxe 20081217 - would like to do something like "if mFont->isValidChar()" here,
      //                but don't have a particular mFont in question..

      // Normal ascii keypress.  Go ahead and add the chars...

      // first delete any current selection
      doDelete();

      char ascii = char(event.ascii);
      insertChars(&ascii, 1, mCursorPosition);
      mVertMoveAnchorValid = false;
      mSelectionAnchor     = mCursorPosition;
      mSelectionStart      = mCursorPosition;
      mSelectionEnd        = mCursorPosition;

      mSaveUndo        = true;

      return true;
   }

   // Otherwise, let the parent have the event...
   return Parent::onKeyDown(event);
}


//--------------------------------------
void GuiMLTextEditCtrl::handleDeleteKeys(const GuiEvent& event)
{
   if ( isSelectionActive() ) 
   {
      doDelete();
   } 
   else 
   {
      switch ( event.keyCode ) 
      {
         case KEY_BACKSPACE:
            if (mCursorPosition != 0) 
            {
               // delete one character left
               deleteChars(mCursorPosition-1, mCursorPosition-1);
               setUpdate();
               mSaveUndo        = true;
            }
            break;

         case KEY_DELETE:
            if (mCursorPosition != mCurrTextSize) 
            {
               // delete one character right
               deleteChars(mCursorPosition, mCursorPosition);
               setUpdate();
               mSaveUndo        = true;
            }
            break;

        default:
            AssertFatal(false, "Unknown key code received!");
      }
   }
}


//--------------------------------------
void GuiMLTextEditCtrl::handleMoveKeys(const GuiEvent& event)
{
   U32 prevCursorPosition = mCursorPosition;

   switch ( event.keyCode ) 
   {
      case KEY_LEFT:
         mVertMoveAnchorValid = false;
         // move one left
         if ( mCursorPosition != 0 ) 
         {
            mCursorPosition--;
            setUpdate();
         }
         break;

      case KEY_RIGHT:
         mVertMoveAnchorValid = false;
         // move one right
         if ( mCursorPosition != mCurrTextSize ) 
         {
            mCursorPosition++;
            setUpdate();
         }
         break;

      case KEY_UP:
      case KEY_DOWN:
      {
         Line* walk;
         for ( walk = mLineList; walk->next; walk = walk->next )
         {
            if ( mCursorPosition <= ( walk->textStart + walk->len ) )
               break;
         }

         if ( !walk )
            return;

         if ( event.keyCode == KEY_UP )
         {
            if ( walk == mLineList )
               return;
         }
         else if ( walk->next == NULL )
         {
            return;
         }

         Point2I newPos;
         newPos.set( 0, walk->y );
         
         // Find the x-position:
         if ( !mVertMoveAnchorValid )
         {
            Point2I cursorTopP, cursorBottomP;
            ColorI color;
            getCursorPositionAndColor(cursorTopP, cursorBottomP, color);
            mVertMoveAnchor = cursorTopP.x;
            mVertMoveAnchorValid = true;
         }

         newPos.x = mVertMoveAnchor;

         // Set the new y-position:
         if (event.keyCode == KEY_UP)
            newPos.y -= (walk->height + 1);
         else
            newPos.y += (walk->height + 1);

         if (setCursorPosition(getTextPosition(newPos)))
            mVertMoveAnchorValid = false;
         break;
      }

      case KEY_HOME:
      case KEY_END:
      {
         if (event.modifier & KEY_MODIFIER)
         {
            if (event.keyCode == KEY_HOME)
            {
               mCursorPosition = 0;
            }
            else
            {
               mCursorPosition  = mCurrTextSize;
            }
         }
         else
         {
            mVertMoveAnchorValid = false;
            Line* walk;
            for (walk = mLineList; walk->next; walk = walk->next)
            {
               if (mCursorPosition <= (walk->textStart + walk->len))
                  break;
            }

            if (walk)
            {
               if (event.keyCode == KEY_HOME)
               {
                  //place the cursor at the beginning of the first atom if there is one
                  if (walk->atomList)
                     mCursorPosition = walk->atomList->textStart;
                  else
                     mCursorPosition = walk->textStart;
               }
               else
               {
                  mCursorPosition = walk->textStart;
                  mCursorPosition += walk->len;
               }
               setUpdate();
            }
         }
         break;
      }

      default:
         AssertFatal(false, "Unknown move key code was received!");
   }

   if (event.modifier & SI_SHIFT)
   {
      if      (mSelectionAnchor == mCursorPosition)
      {
         setSelectionActive(false);
      }
      else if (mSelectionAnchor < mCursorPosition)
      {
         setSelection(mSelectionAnchor, mCursorPosition  - 1);
         setSelectionActive(true);
      }
      else // (mSelectionAnchor > mCursorPosition)
      {
         setSelection(mCursorPosition , mSelectionAnchor - 1);
         setSelectionActive(true);
      }
   }
   else
   {
      mSelectionAnchor = mCursorPosition;
      mSelectionStart  = mCursorPosition;
      mSelectionEnd    = mCursorPosition;
      setSelectionActive(false);
   }

   ensureCursorOnScreen();
}

//--------------------------------------------------------------------------
// oxe 20081217

void GuiMLTextEditCtrl::doCopy()
{
   if (!mSelectionActive)
      return;

   copyToClipboard(mSelectionStart, mSelectionEnd);
}

void GuiMLTextEditCtrl::doCut()
{
   doCopy  ();
   doDelete();
}

void GuiMLTextEditCtrl::doPaste()
{
   const char *clipBuf = Platform::getClipboard();
   if (dStrlen(clipBuf) < 1)
      return;

   doDelete();       // saves undo state

   insertChars(clipBuf, dStrlen(clipBuf), mCursorPosition);
   mSelectionAnchor = mCursorPosition;
   mSelectionStart  = mCursorPosition;
   mSelectionEnd    = mCursorPosition;

   mSaveUndo        = true;
}

void GuiMLTextEditCtrl::doDelete()
{
   if (!mSelectionActive)
      return;

   setSelectionActive(false);
   deleteChars(mSelectionStart, mSelectionEnd);
   mCursorPosition  = mSelectionStart;
   mSelectionAnchor = mSelectionStart;
   mSelectionEnd    = mSelectionStart;

   mSaveUndo        = true;
}

//--------------------------------------------------------------------------
// oxe 20081217 - undo system
//
// we have an undo stack of N frames,
// and an index to the current frame,
// so the index is initially zero.
// when we save a state, we:
//   * check if we've filled all the frames already;
//     if so, "shift" them all down to open one up at the top
//   * copy the state into the new frame
//   * increment the current frame index.
// when we undo, we:
//   * decrement the current frame index.
//   * re-apply the (new) current frame.
// when we redo, we:
//   * increment the current frame index.
//   * re-apply the (new) current frame.

void GuiMLTextEditCtrl::saveUndoState()
{
   AssertFatal(mUndoStackCurFrame < maxUndoDepth, "invalid undo state!");
   AssertFatal(maxUndoDepth       > 0           , "need at least one undo level!");

   if (mUndoStackCurFrame == maxUndoDepth - 1)
   {
      // shift undo stack down
      delete [] mUndoStack[0].textBuffer;

      for (S32 n = 1; n < maxUndoDepth; n++)
      {
         mUndoStack[n - 1] = mUndoStack[n];
      }

      // nullify the topmost frame because it's now duplicated in the topmost-1.
      mUndoStack[maxUndoDepth - 1].textBuffer = NULL;
   }
   else
   {
      mUndoStackCurFrame += 1;
   }

   // save into next undo frame
   storeUndoState(mUndoStack[mUndoStackCurFrame]);

   // erase history "above" the present
   if (mUndoStackCurFrame < maxUndoDepth - 1)
      clearUndoState(mUndoStack[mUndoStackCurFrame + 1]);
}

void GuiMLTextEditCtrl::doUndo()
{
   if (mUndoStackCurFrame < 1)
      return;

   applyUndoState(mUndoStack[mUndoStackCurFrame - 1]);

   mUndoStackCurFrame -= 1;
}

void GuiMLTextEditCtrl::doRedo()
{
   // no more frames
   if (mUndoStackCurFrame >= maxUndoDepth - 1)
      return;

   // next frame is empty
   if (mUndoStack[mUndoStackCurFrame + 1].textBuffer == NULL)
      return;

   mUndoStackCurFrame += 1;

   applyUndoState(mUndoStack[mUndoStackCurFrame]);
}

void GuiMLTextEditCtrl::applyUndoState(const undoStateT& undoState)
{
   if (undoState.textBuffer == NULL)
   {
      setText("", 0);
      mCursorPosition  = 0;
      mSelectionStart  = 0;
      mSelectionEnd    = 0;
      mSelectionAnchor = 0;
      mSelectionActive = 0;
   }
   else
   {
      setText(undoState.textBuffer, dStrlen(undoState.textBuffer));
      mCursorPosition  = undoState.cursorPosition ;
      mSelectionStart  = undoState.selectionStart ;
      mSelectionEnd    = undoState.selectionEnd   ;
      mSelectionAnchor = undoState.selectionAnchor;
      mSelectionActive = undoState.selectionActive;
   }
}

void GuiMLTextEditCtrl::storeUndoState(undoStateT& undoState)
{
   clearUndoState(undoState);

   // .. and save the frame
   undoState.textBuffer                = new char[mCurrTextSize + 1];
   dStrncpy(undoState.textBuffer, mTextBuffer, mCurrTextSize);
   undoState.textBuffer[mCurrTextSize] = '[[6287ce0f8ba04]]';
   undoState.cursorPosition            = mCursorPosition;
   undoState.selectionStart            = mSelectionStart;
   undoState.selectionEnd              = mSelectionEnd;
   undoState.selectionAnchor           = mSelectionAnchor;
   undoState.selectionActive           = mSelectionActive;
}

void GuiMLTextEditCtrl::clearUndoState(undoStateT& undoState)
{
   if (undoState.textBuffer != NULL)
   {
      delete [] undoState.textBuffer;
      undoState.textBuffer = NULL;
   }
}

void GuiMLTextEditCtrl::dumpUndoStack()
{
   char buf[11];

   for (S32 n = maxUndoDepth - 1; n >= 0; n--)
   {
      if (n == 0 || mUndoStack[n].textBuffer != NULL)
      {
         dSprintf(buf, sizeof(buf), "%2d %s", n, n == mUndoStackCurFrame ? "-->" : "   ");
         dumpUndoState(mUndoStack[n], buf);
      }
   }
}

void GuiMLTextEditCtrl::dumpUndoState(const undoStateT& undoState, const char* prefix)
{
   if (prefix == NULL)
      prefix = "";

   if (undoState.textBuffer == NULL)
   {
      Con::printf("%-10s%s", prefix, "<empty>");
   }
   else
   {
      Con::printf("%-10s%s", prefix, undoState.textBuffer     );
      Con::printf("%-10s%d", prefix, undoState.cursorPosition );
      Con::printf("%-10s%d", prefix, undoState.selectionStart );
      Con::printf("%-10s%d", prefix, undoState.selectionEnd   );
      Con::printf("%-10s%d", prefix, undoState.selectionAnchor);
      Con::printf("%-10s%d", prefix, undoState.selectionActive);
   }
}


//--------------------------------------------------------------------------

void GuiMLTextEditCtrl::onPreRender()
{
   if (isFirstResponder())
   {
      U32 curTime     = Platform::getVirtualMilliseconds();
      U32 timeElapsed = curTime - mTimeLastCursorFlipped;
      if (timeElapsed > 500)
      {
         mCursorOn              = !mCursorOn;
         mTimeLastCursorFlipped =  curTime;
         setUpdate();
      }
   }
   else
   {
      mCursorOn              = false;
      mTimeLastCursorFlipped = 0;
   }

   Parent::onPreRender();
}

void GuiMLTextEditCtrl::onRender(Point2I offset, const RectI& updateRect)
{
   Parent::onRender(offset, updateRect);
   
   // cursor
   if (mCursorOn)
   {
      Point2I top, bottom;
      ColorI color;
      getCursorPositionAndColor(top, bottom, color);
      if (top.x > 0)
      {
         top   .x -= 1;
         bottom.x -= 1;
      }
      dglDrawLine(top + offset, bottom + offset, mProfile->mCursorColor);
   }
}

#1
12/29/2008 (11:38 pm)
Couple of things regarding this resource, Torque 1.5, and Linux:

1. mCurrTextSize is not valid in 1.5. Replace this with mTextBuffer.length(). This is pretty pervasive.
2. Your include directives are wrong for a stock setup. Need to be gui/Containers/whatever.h and gui/Controls/whatever.h.
3. For 1.5, dStrncpy doesn't work on line 631 in GuiMLTextEditCtrl::storeUndoState. Replace this:
dStrncpy(undoState.textBuffer, mTextBuffer, mTextBuffer.length());
with:
mTextBuffer.get(undoState.textBuffer, mTextBuffer.length());

It compiled and ran with these changes for me. Select all (control-A) works, shift-arrow select works, but undo doesn't work, so it's probably change #3 at fault. No time to figure this out right now, though, but it's a step in the right direction.
#2
12/30/2008 (12:42 am)
hey Charlie,
many many thanks for trying this out and finding the problems you did.

it sounds like the underlying framework has changed from 1.3 to 1.5,
which unfortunately means any resource needs to come in various flavors.
i'll include your notes up at the top of it.

i don't have the 1.5 source, so i'm not sure what's going on with step 3.
perhaps i can get a license for that to look into this stuff.

let me modify it to include some optional debug prints,
which might help tracking down what's wrong with undo.
#3
12/30/2008 (8:49 am)
One thing it's letting me do is unlimited undos even if I haven't done anything to the text. It should ignore a ctrl-z if there's nothing on the undo stack, right? I'll try and do some digging tomorrow (tonight is totally booked) and maybe we can work out the kinks in this for 1.5 on my side and save you a license fee and some time.
#4
12/30/2008 (9:04 am)
that would be awesome.
one thing which might help is a debugging function already in there: dumpUndoStack().
you could either expose it to torquescript or just call it on every keystroke.
happy new years!
#5
08/18/2009 (12:48 am)
Hello again... I follow the steps but in guiMLTextEditCtrl.cc I have this error on line 631 (step 3)...

error: 'class StringBuffer' has no member named 'get'

on line...
mTextBuffer.get(undoState.textBuffer, mTextBuffer.length());

Thanks for your help.
#6
09/05/2011 (2:59 pm)
Thanks Orion, this is great!