TweetFollow Us on Twitter

Undo
Volume Number:3
Issue Number:5
Column Tag:Programmer's Workshop

Implementing Undo For Text Edit

By Melvyn D. Magree, (M)agreeable software, inc.

Undo (Cmd-Z) It!

Honored more in the breech than not is the standard Edit Menu Apple published in Inside Macintosh (page I-58). Even if a program does contain an Edit Menu with the recommended items, Undo might not really be available.

An Undo command is a very helpful feature, especially if you just did Clear when you meant to do Cut or if you backspaced one word too many or whatever! It is remarkable that Undo is not included in all editing type programs because it is not that difficult to program. I designed and coded a text-file version for my own development system in less that two weeks. I modeled my Undo by observing the behavior of MacWrite.

We want to give the user the following Undo capabilities:

Undo Cut:

• Reinsert the cut text from the clipboard.

• Restore the previous contents of the clipboard.

Undo Copy:

• Restore the previous contents of the clipboard.

Undo Paste:

• Remove pasted text.

• Reinsert the text overlaid by Paste.

Undo Clear:

• Reinsert the text removed by Clear.

Undo Typing:

• Remove all characters typed since the user made the last selection.

• Reinsert the selection overlaid by the typing including any removed by backspacing.

For each Undo operation except copy, we also must reset the selection as it was before the user requested the "undid" operation.

Redo Cut, Copy, Paste, and Clear are straightforward. We merely perform a Cut, Copy, Paste, or Clear. However, for Redo Typing, we must:

• Remove the current selection.

• Remove any characters that the user had backspaced over.

• Reinsert the "effective" typing sequence that the user Undid.

The effective typing sequence is the string of characters remaining after the user backspaced. That is, if the text and selection had been as follows:

and the user had typed:

 {backspace}{backspace}the most  user-
dis{backspace}{backspace}{backspace}un

resulting in

then the effective typing sequence is

 the most user-un

If the user requested Undo Typing, the text and selection would revert to

and if the user followed the request with Redo Typing, the text and selection would once again become:

To make sure that we can undo an operation, we have to save any information that the operation destroys. So, our edit operations are:

Cut

• Save current contents of clipboard.

• Cut current selection to clipboard.

• Set menu to Undo Cut.

Copy

• Save current contents of clipboard,

• Copy current selection to clipboard, and

• Set menu to Undo Copy.

Paste

• Save current selection,

• Paste clipboard to current selection, and

• Set menu to Undo Paste.

Clear

• Save current selection,

• Delete current selection, and

• Set menu to Undo Clear.

Typing

• If first character for selection:

Save current selection.

• If backspace over previously unsaved character:

Insert unsaved character at beginning of saved selection.

• Insert character in text at insertion point.

• Set menu to Undo Typing.

Note that I use the term clipboard to refer to the TextEdit scrap, not the clipboard file. To actually put the TextEdit scrap in the clipboard file (also called the desk scrap) you must call TEtoScrap. To read the clipboard to the TextEdit scrap you must call TEfromScrap.

The key design elements for undo (and redo) are a state variable for the next possible undo operation and a second Text Edit record. We use the state variable to reset the Undo item of the Edit Menu and to call the proper procedure when the user requests Undo. We use the second Text Edit record as a private scrap area to save the text removed by the user's last editing operation. We can then use the various TextEdit procedures to move text from the clipboard to the private scrap or vice versa.

To try out some simple versions of these undoable editing routines, we will write a very simple editing program. The program allows the user to select a file, opens a window, reads the contents of the file into the window, and then allows the user to change the contents of the window by typing, by using the mouse, and by selecting from the Edit menu. The program allows the user to close the window and open another from the File menu. The program's File menu also contains a Quit entry.

If you are familiar with writing Macintosh stand-alone applications, you might want to skip over the next section to Editing Functions.

To keep the program simple, we will ignore many expected Macintosh features such as desk accessories, moving the window on the screen, scrolling the text, and making the program easily translatable from English. We will not check for many possible error situations.

Thus, our main program is:

Initialize tool box,

Initialize program's global variables,

Initialize menus, and

Start the main event loop.

Our main event loop checks only for four events:

activate:

If window open calls TEActivate.

mouse down:

If in menu, calls menu selector

Else if window open calls TE selector.

key down and auto key:

If window open calls typing routine

Our main event loop continues to look for these events until the menu selector sets the global variable Quit to TRUE. When Quit is TRUE, the main event loop returns to our main program which terminates.

Our menu selector:

Determines which menu item was selected,

If Apple menu, ignores it,

If File Menu, calls File Manager with item, or

If Edit Menu, calls Edit Manager with the item.

Removes highlighting from menu bar.

Our File Manager calls FileOpen or FileClose according to the item selected. That is,

If Open, calls Open_File,

If Close, calls Close_File, and

If Quit, then

If a file is open, calls Close_File,

Sets QuitFlag to TRUE.

Open_File

Asks the user to select a file from a list;

Opens the requested file;

Opens a window to display the text of the file;

Opens a TextEdit record to control display of the text;

Opens a TextEdit record for the scrap;

Reads text of the file into TextEdit record;

Disables the Open item and enables Close item;

The only error condition that we will check is if the user clicked the cancel button in the file dialog box. Note that if the file contains more than 32,767 characters, then our program may hang. TEInsert does not check for this limit and may never return. [The 32K limit is notorious throughout Text Edit, especially in TEScroll. This has not been changed in the SE & Mac II ROMS unfortunately. -Ed]

Close_File does almost the reverse of Open_File. It:

Closes the display TextEdit record,

Closes the scrap TextEdit record,

Closes the window,

Closes the file, and

Enables the Open item and disables Close item.

If we allowed the user to actually change the file, then we would have to rewrite the text and flush the volume also. Without considering the editing portion of our program, we need the following global variables:

Pointer for the window,

Handle for the TextEdit record,

Handle for the scrap TextEdit record,

Integer for the file reference number, and

Boolean for the Quit flag.

Our first program in TML Pascal is then shown in listing one.

PROGRAM UndoIt;

{Program to test Edit menu including Undo/Redo of previous operation}

{$L UndoIt/Rsrc}


{$I Memtypes.Ipas }
{$I QuickDraw.Ipas}
{$I OSIntf.Ipas   }
{$I ToolIntf.Ipas }
{$I PackIntf.Ipas }

CONST MenuBarID = 200;
      FileMenu  = 200;
      OpenItem  =   1;
      CloseItem =   2;
      QuitItem  =   4;
      EditMenu  = 201;
      UndoItem  =   1;
      CutItem   =   3;
      CopyItem  =   4;
      PasteItem =   5;
      ClearItem =   6;
      WindowID  = 200;

{Global variables}

VAR theWindow  : WindowPtr; {Main window}
    DisplayTE  : TEHandle;  {TextEdit record for display}
    ScrapTE    : TEHandle;  {TextEdit record for scrap}
    fNum       : integer;   {Ref number for current file}
    QuitFlag   : Boolean;   {Main event loop exits when TRUE}
    FileHandle : MenuHandle;
    EditHandle : MenuHandle;

PROCEDURE Init_MyGlobals;

 BEGIN
  QuitFlag:=FALSE;
  theWindow:=NIL; {=NIL if no window opened}
 END; {Init_MyGlobals}

PROCEDURE Open_File;
 
 CONST hCorner   =    90;
       vCorner   =    90;
       MaxTEText = 32767;  {Should be smaller for 128K Mac}
       
 VAR OpenReply  : SFReply;
     GetWhere   : Point;
     fTypes     : SFTypeList;
     OpenErr    : OSErr;
     TextRect   : Rect;
     TextLength : LongInt;
     TextDest   : Ptr;
 
 BEGIN
  GetWhere.h:=hCorner;
  GetWhere.v:=vCorner;
  fTypes[0]:='TEXT';
  OpenErr:=-1;           {Set to other than noErr}
  SFGetFile(GetWhere,'',NIL,1,fTypes,NIL,OpenReply);
  WITH OpenReply DO
   IF Good THEN
    OpenErr:=FSOpen(fName,vRefNum,fNum);
  IF OpenErr=noErr THEN
   BEGIN
    theWindow:=GetNewWindow(windowID,NIL,WindowPtr(-1));
    SetPort(theWindow);
    TextRect:=theWindow^.portRect;
    DisplayTE:=TENew(TextRect,TextRect);
    WITH TextRect DO    {Make ScrapTE "invisible"}
     BEGIN
      top:=-bottom;
      left:=-right;
      bottom:=0;
      right:=0;
     END;
    ScrapTE:=TENew(TextRect,TextRect);
    OpenErr:=GetEof(fNum,TextLength);
    IF TextLength>MaxTEText THEN  {Ensure "not too much"}
     TextLength:=MaxTEText;
    TextDest:=NewPtr(TextLength);
    OpenErr:=SetFPos(fNum,fsFromStart,0); {read text}
    OpenErr:=FSRead(fNum,TextLength,TextDest);
    TEInsert(TextDest,TextLength,DisplayTE);
    DisposPtr(TextDest);
    EnableItem(FileHandle,CloseItem);
    DisableItem(FileHandle,OpenItem);
   END; {IF OpenErr=noErr}
 END; {Open_File}

PROCEDURE Close_File;
 
 VAR CloseErr   : OSErr;
 
 BEGIN
  HideWindow(theWindow);
  TEDispose(DisplayTE);
  TEDispose(ScrapTE);
  DisposeWindow(theWindow);
  theWindow:=NIL;
  CloseErr:=FSClose(fNum);
  EnableItem(FileHandle,OpenItem);
  DisableItem(FileHandle,CloseItem);
 END; {Close_File}

PROCEDURE File_Manager(MenuItem:integer;VAR QuitFlag:Boolean);

 BEGIN
  CASE MenuItem OF
   OpenItem:Open_File;
   CloseItem:Close_File;
   QuitItem:
    BEGIN
     IF theWindow<>NIL THEN
      Close_File;
     QuitFlag:=TRUE;
    END;
  OTHERWISE
  END; {CASE MenuItem}
 END; {File_Manager}

PROCEDURE Cut;
 
 BEGIN
 END; {Cut}

PROCEDURE Copy;
 
 BEGIN
 END; {Copy}

PROCEDURE Paste;
 
 BEGIN
 END; {Paste}

PROCEDURE Clear;
 
 BEGIN
 END; {Clear}

PROCEDURE Undo;
 
 BEGIN
 END; {Undo}

PROCEDURE Edit_Manager (MenuItem:integer);

 BEGIN
  CASE MenuItem OF
   UndoItem:
    Undo;
   CutItem:
    Cut;
   CopyItem:
    Copy;
   PasteItem:
    Paste;
  END; {CASE MenuItem}
 END; {Edit_Manager}

PROCEDURE Menu_Selector(where:Point;VAR QuitFlag:boolean);
 
 VAR theCode          : LongInt;
     MenuNum,MenuItem : integer;

 BEGIN
  theCode:=MenuSelect(where);
  MenuNum:=HiWord(theCode);
  MenuItem:=LoWord(theCode);
  Case MenuNum OF
   FileMenu:File_Manager(MenuItem,QuitFlag);
   EditMenu:Edit_Manager(MenuItem);
   OTHERWISE
  END; {CASE OF MenuNum}
  HiliteMenu(0);
 END; {Menu_Selector}

PROCEDURE TE_Selector(where:Point;extend:Boolean);

 BEGIN
 END; {TE_Selector}

PROCEDURE Typist(EventMessage:LongInt);

 BEGIN
 END; {Typist}

PROCEDURE MainEventLoop;
 
 MainEvent : EventRecord;
     theCode   : integer;
     extend    : Boolean;
     anyWindow : WindowPtr;

 BEGIN {MainEventLoop}
  REPEAT
   IF GetNextEvent(everyEvent,MainEvent) THEN
    CASE MainEvent.what OF
     activateEvt:
      IF theWindow<>NIL THEN
       TEActivate(DisplayTE);
     mouseDown:
      BEGIN
       theCode:=FindWindow(MainEvent.where,anyWindow);
       CASE theCode OF
        inMenuBar:
         Menu_Selector(MainEvent.where,QuitFlag);
        inContent:         {Assume only one window}
         BEGIN
          extend:=(BitAnd(MainEvent.modifiers,ShiftKey)<>0);
                  {If user holding shift key, then extended
                   selection}
          TE_Selector(MainEvent.where,extend);
         END;
        OTHERWISE           {Ignore}
       END; {CASE theCode}
      END; {mouseDown}
   keyDown,autoKey:  {ignores command key}
      IF theWindow<>NIL THEN
       Typist(MainEvent.message);
     OTHERWISE
    END; {IF GetNextEvent, CASE MainEvent.what}
  UNTIL QuitFlag;
 END; {MainEventLoop}

FUNCTION Init_MyMenus:Boolean;
 
 CONST MenuBarId = 200;

 VAR theMenuBar : Handle; {MBAR resource points to menus}

 BEGIN
  Init_MyMenus:=FALSE;     {Assume menus not initialized}
  theMenuBar:=GetNewMBar(MenuBarId);
  IF theMenuBar<>NIL THEN
   BEGIN
    SetMenuBar(theMenuBar);
    DrawMenuBar;
    FileHandle:=GetMHandle(FileMenu);
    EditHandle:=GetMHandle(EditMenu);
    Init_MyMenus:=TRUE;
   END;
 END; {Init_MyMenus}

BEGIN {Main Program}
 InitGraf(@thePort);
 InitFonts;
 InitWindows;
 InitMenus;
 TEInit;
 InitDialogs(NIL);
 InitCursor;
 Init_MyGlobals;
 FlushEvents(EveryEvent,0);
 IF Init_MyMenus THEN
  MainEventLoop;
END. {Main Program}

The resource file for this program is shown in listing two.

*  UndoIt/Rsrc.R - Resource definition of UndoIt

UndoIt/Rsrc.rel

TYPE MBAR=GNRL
,200    ;; Resource ID
.I ;; Integers follow
3;; Three menu items;
1;; Apple Menu
200;; File Menu
201;; Edit Menu

TYPE MENU
  ,1    ;; Apple Menu
\14

 ,200 ;; File Menu
File
 Open
 (Close ;; Initially disabled
 (-
 Quit

 ,201   ;; Edit Menu
Edit
 (Can't Undo   ;; Disable at beginning
 (-
 (Cut
 (Copy
 (Paste
 (Clear ;; Full standard edit menu should also include
 ;; Select All and Show Clipboard

TYPE WINDOW
   ,200
No Title;; If title is only blanks, RMaker can crash
   40 20 300 480
   Visible NoGoAway
   2                 ;; Plain Box
   0

Editing Functions

We now continue by doing the traditional editing functions: Cut, Copy, Paste, and Clear. We need a state variable to let us know what the next possible Undo or Redo operation is. For this we define:

TYPE EditType = (CantUndo,UndoTyping,UndoCut,UndoCopy,
 UndoPaste,UndoClear,
 RedoTyping,RedoCut,RedoCopy,
 RedoPaste,RedoClear);

and add the variable EditStatus of EditType.

To switch the text of the menu according to the current value of EditStatus, we need several string constants. These really should be in the resource file, but for brevity we put them directly in the program. These strings are:

CONST CantUndoStr= 'Can''t Undo'; {Note double apostrophe}
 UndoStr= 'Undo';
 RedoStr= 'Redo';
 TypingStr= 'Typing';

The remainder, Cut, Copy, Paste, and Clear we can take from the menu itself.

We need two global variables to save the start and end of the selection to be redone. Because the user can backspace over text in front of the previous selection, we also need a global variable to save the farthest point which the user backspaced to. These variables are:

VAR
 UndoStart  : integer;  {Start previous/current selection}
 UndoEnd: integer; {End previous/current selection}
 CurrentStart  : integer; {Backspace point before previous 
 selection}

With these variables defined, we can write our TE_Selector procedure for when the user clicks the mouse in the window.

TE_Selector

Calls TEClick to set the selection in the TextEdit record, and
Calls Reset_EditMenu to set Undo to Can't Undo.
Reset_EditMenu is called with an EditType parameter and
 If the Clipboard contains text then
 Enables the Paste item
 Else
 Disables the Paste item;
 If the selection is more than the insertion point,
 Enables the Cut, Copy, and Clear items
 Else
 Disables the Cut, Copy, and Clear items;
 Sets the Edit status to the value of the parameter;
 If the parameter is CantUndo
 Sets Undo item to Can't Undo
   Else if Undo Typing or Redo Typing
 Sets Undo item appropriately,
 Else
 Get the text of the corresponding menu item, and
 Sets Undo item appropriately;
If the paramter is CantUndo
 Disable Undo item
Else
 Enable Undo item;

So, we fill out TE_Selector in our program above as:

PROCEDURE TE_Selector(where:Point;extend:Boolean);

 BEGIN
  SetPort(theWindow);      {Ensure port is text window}
  GlobaltoLocal(where);    {Make mouse local to window}
  TEClick(where,extend,DisplayTE);
  Reset_EditMenu(CantUndo);
 END; {TE_Selector}

and we add Reset_EditMenu, placing it before Cut.

PROCEDURE Reset_EditMenu(UndoState:EditType);
 
 VAR theStr     : Str255;
     theItem    : integer;

 BEGIN
  IF TEGetScrapLen>0 THEN    {Set Paste according to scrap}
   Enable(EditHandle,PasteItem)
  ELSE
   Disable(EditHandle,PasteItem);
  WITH DisplayTE^^ DO
   IF SelStart<SelEnd THEN   {Set Cut,Copy,Clear according}
    BEGIN                    {to selection size     }
     Enable(EditHandle,CutItem);
     Enable(EditHandle,CopyItem);
     Enable(EditHandle,ClearItem);
    END
   ELSE
    BEGIN
     Disable(EditHandle,CutItem);
     Disable(EditHandle,CopyItem);
     Disable(EditHandle,ClearItem);
    END;
  EditStatus:=UndoState;
  IF EditStatus=CantUndo THEN
   theStr:=CantUndoStr
  ELSE IF EditStatus IN [UndoTyping,RedoTyping] THEN
   theStr:=TypingStr
  ELSE
   BEGIN
    CASE EditStatus OF     {Get item number to Undo/Redo}
     UndoCut,RedoCut:
      theItem:=CutItem;
     UndoCopy,RedoCopy:
      theItem:=CopyItem;
     UndoPaste,RedoPaste:
      theItem:=PasteItem;
     UndoClear,RedoClear:
      theItem:=ClearItem;
     OTHERWISE
    END; {CASE EditStatus}
    GetItem(EditHandle,theItem,theStr;
   END; {IF EditStatus}
  IF EditStatus IN [UndoTyping..UndoClear] THEN
   theStr:=Concat(UndoStr,' ',theStr)
  ELSE IF EditStatus IN [RedoTyping..RedoClear] THEN
   theStr:=Concat(RedoStr,' ',theStr);
  SetItem(EditHandle,UndoItem,theStr);   {Reset Undo item}
  IF EditStatus=CantUndo THEN
   Disable(EditHandle,UndoItem)      {Disable Can't Undo or}
  ELSE
   Enable(EditHandle,UndoItem);      {Enable Undo/Redo  }
 END; {Reset_EditMenu}

Have you often thought that "These writers dash off programs so easily, how do they do it?" Well, in many cases they don't. They just don't bother telling you all their struggles to find the typo or the minus sign that should have been a plus sign. In this particular case, I spent an hour trying to figure out why no caret appeared and why clicking on the mouse highlighted a new area, but did not unhighlight the old area.

I read and reread about TEClick in both Inside Macintosh and Macintosh Revealed. I could not understand what I was doing wrong. Then I remembered a similar problem from long ago; to make the TextEdit procedure I was using work, I had to call TEActivate. For this program, I could have called TEActivate right after the call to TENew and that would have been sufficient, but I went back and added a new event test in MainEventLoop. So what you see now as the first try at the program was really the n-th try. Most programs don't have this problem because, whenever a window is activated, the program also activates any associated TextEdit record.

After I fixed the program, I saw the note on the middle of page I-383 of Inside Macintosh. [This is a common frustration; you spend hours finally fixing the problem, only to discover it later in Inside Macintosh, AFTER you know what to look for! -Ed]

Other problems that I encountered while writing this program are summarized at the end of this article under "What Can Go Wrong".

Continuing, we now write write the procedures Cut, Copy, Paste, and Clear. These are:

Cut

Save selection points,

If selection greater than an insertion point

• Paste Clipboard to ScrapTE,

• Cut current selection to Clipboard,

• Set Undo item to Undo Cut,

• Enable Undo and Paste items, and

• Disable Cut item.

Copy

Save selection points,

If selection greater than an insertion point

• Paste Clipboard to ScrapTE,

• Copy current selection to Clipboard,

• Set Undo item to Undo Copy, and

• Enable Undo item.

Paste

• Save selection points,

• Delete ScrapTE text,

• Copy current selection to ScrapTE,

• Paste Clipboard to current selection,

• Set Undo item to Undo Paste, and

• Enable Undo item.

Clear

Save selection points,

If selection greater than an insertion point

• Delete ScrapTE text,

• Copy current selection to ScrapTE,

• Delete current selection,

• Set Undo item to Undo Copy, and

• Enable Undo item.

Writing these in Pascal, we add before Cut:

PROCEDURE Save_EndPoints;  {Save selection points}

 BEGIN
  WITH DisplayTE^^ DO
   BEGIN
    UndoStart:=SelStart;
    UndoEnd:=SelEnd;
    CurrentStart:=UndoStart;
   END;
 END {Save_Selection}

PROCEDURE Save_Clipboard;

 BEGIN
  TESetSelect(0,ScrapTE^^.TELength,ScrapTE);
  TEPaste(ScrapTE);
 END; {Save_Clipboard}

PROCEDURE Delete_ScrapTE;

 BEGIN
  TESetSelect(0,ScrapTE^^.TELength,ScrapTE);
  TEDelete(ScrapTE);
 END; {Delete_ScrapTE}

PROCEDURE Save_Selection(theStart,theEnd);

 BEGIN
  IF theStart<theEnd THEN
   BEGIN
    HLock(Handle(DisplayTE));
    WITH DisplayTE^^ DO
     BEGIN
      HLock(hText);
     TEInsert(Ptr(Ord4(hText^)+theStart,                 theEnd-theStart,ScrapTE);
      HUnlock(hText);
     END;
    HUnlock(Handle(DisplayTE));
   END;
 END; {Save_Selection}

and replace Cut, Copy, Paste, and Clear with:

PROCEDURE Cut;

 BEGIN
  Save_EndPoints;
  Save_Clipboard;        {Save old clipboard}
  TECut(DisplayTE);      {Cut selection to clipboard}
  Reset_EditMenu(UndoCut);
 END; {Cut}

PROCEDURE Copy;

 BEGIN
  Save_EndPoints;
  Save_Clipboard;        {Save old clipboard}
  TECopy(DisplayTE);     {Copy selection to clipboard}
  Reset_EditMenu(UndoCopy);
 END; {Copy}

PROCEDURE Paste;

 BEGIN
  Save_EndPoints;
  Delete_ScrapTE;
  Save_Selection(UndoStart,UndoEnd);
  TEPaste(DisplayTE);     {Paste selection from clipboard}
  Reset_EditMenu(UndoPaste);
 END; {Paste}

PROCEDURE Clear;

 BEGIN
  Save_EndPoints;
  Delete_ScrapTE;
  Save_Selection(UndoStart,UndoEnd);
  TEDelete(DisplayTE);            {Delete current selection}
  Reset_EditMenu(UndoClear);
 END; {Clear}

Undo Operations

Now we have most of the tools in place to undo cut, copy, paste, or clear. With Undo we can now restore both the text and the clipboard as they were before the user requested the operation to be undone. For the undo operations except Undo Typing, we need to:

Undo Cut:

• Paste clipboard to insertion point,

• Reset selection,

• Copy ScrapTE to clipboard,

• Set Undo item to Redo Cut, and

• Enable all edit items.

Undo Copy:

• Cut ScrapTE to clipboard,

• Set Undo item to Redo Copy, and

• Enable all edit items.

Undo Paste:

• Reset selection to pasted text,

• Delete selection,

• Copy ScrapTE to selection

• Reset selection to previous text,

• Set Undo item to Redo Paste, and

• If selection greater than insertion point

Enable all edit items

Else

Enable Undo and Paste items.

Undo Clear:

• Copy ScrapTE to selection

• Reset selection,

• Set Undo item to Redo Clear, and

• Enable all edit items.

For common subroutines for undo operations, we add to the group of Save Procedures:

PROCEDURE Restore_Clipboard;

 BEGIN
  TESetSelect(0,ScrapTE^^.TELength,ScrapTE);
  TECut(ScrapTE);              {Also clears ScrapTE}
 END; {Restore_Clipboard}
 
PROCEDURE Restore_Selection(theLength:integer);

 BEGIN
  IF theLength>0 THEN
   BEGIN
    HLock(Handle(ScrapTE));
    WITH ScrapTE^^ DO
     BEGIN
      HLock(hText);            {ScrapTE to insertion point}
      TEInsert(Ptr(Ord4(hText^)),theLength,DisplayTE);
      HUnlock(hText);
     END;
    HUnlock(Handle(ScrapTE));
 END; {Restore_Selection}

and replace Undo with all of the following:

PROCEDURE Undo_Cut;
 
 BEGIN
  TEPaste(DisplayTE);                        {Restore text}
  TESetSelect(UndoStart,UndoEnd,DisplayTE);  {Reset selection}
  Restore_Clipboard;
  Reset_EditMenu(RedoCut);
 END; {Undo_Cut}
 
PROCEDURE Undo_Copy;
 
 BEGIN
  Restore_Clipboard;
  Reset_EditMenu(RedoCopy);
 END; {Undo_Copy}
 
PROCEDURE Undo_Paste;
 
 BEGIN
  TESetSelect(UndoStart,DisplayTE^^.SelEnd,DisplayTE);
  {Delete pasted text}
  {UndoStart is also beginning of pasted text}
  TEDelete(DisplayTE);
  Restore_Selection(ScrapTE^^.TELength);
  TESetSelect(UndoStart,UndoEnd,DisplayTE);  {Reset selection}
  Delete_ScrapTE;
  Reset_EditMenu(RedoPaste);
 END; {Undo_Paste}
 
PROCEDURE Undo_Clear;
 
 BEGIN
  Restore_Selection(ScrapTE^^.TELength);
  TESetSelect(UndoStart,UndoEnd,DisplayTE);  {Reset selection}
  Delete_ScrapTE;
  Reset_EditMenu(RedoClear);
 END; {Undo_Clear}
 
PROCEDURE Undo_Typing;
 
 BEGIN
 END; {Undo_Typing}
 
PROCEDURE Redo_Typing;
 
 BEGIN
 END; {Redo_Typing}
 
PROCEDURE Undo;

 BEGIN
  CASE EditStatus OF
   UndoCut:
    Undo_Cut;
   UndoCopy:
    Undo_Copy;
   UndoPaste:
    Undo_Paste;
   UndoClear:
    Undo_Clear;
   UndoTyping:
    Undo_Typing;
   RedoCut:
    Cut;
   RedoCopy:
    Copy;
   RedoPaste:
    Paste;
   RedoClear:
    Clear;
   RedoTyping:
    Redo_Typing;
   OTHERWISE
  END; {CASE EditStatus}
 END; {Undo}

Typing

Finally, we get to the most difficult, handling typing so that it is undoable. One would think that it is no more difficult than undoing any of the other editing operations. However, the backspace key causes a problem. If the user backs over newly entered text, we have no problem. But when the user backs over previous text we must save the newly deleted character and the new beginning point.

See the example at the beginning of the article for an example of backspacing over the previous text.

For typing, we need to:

• Check if user entered a meaningful character;

• If EditStatus is not UndoTyping, then

Save selection points;

Delete ScrapTE text;

Copy selection to ScrapTE;

• If user entered backspace, then

If not beginning of text and

If all newly typed characters deleted, then

Copy character preceding insertion point to beginning of ScrapTE;

Decrement "current insertion point";

• Insert character entered by user in text;

• If EditStatus is not UndoTyping, then

Set Undo item to Undo Typing;

Enable Undo item.

Note that we cannot set Undo Typing under the first test of EditStatus because Reset_EditMenu enables or disables the other item according to the size of the selection. If the selection is more than the insertion point, the first character inserted with TEKey into the text will delete the selection.

Thus, we rewrite Typist as:

PROCEDURE Typist;
 
 CONST Return    = $0D;
       Enter     = $03;
       Backspace = $08;
       Tab       = $09;
  
  TYPE Codes = 0..255;

  VAR KeyCode      : integer;
      CharIn       : Char;
      CharH        : CharsHandle;
      AllowedCodes : SET OF Codes;

 BEGIN
  KeyCode:=BitAnd(EventMessage,charCodeMask);
  AllowedCodes:=[$20..$7F,Return,Enter,Backspace,Tab];
  {May be erroneous if used directly in TML Pascal 1.11}
  {See letter from Christopher Dunn in July 1986 MacTutor}
  IF KeyCode IN AllowedCodes THEN
   CharIn:=chr(KeyCode)
  ELSE
   KeyCode:=0;  {Use KeyCode=0} as test to bypass sections}
  IF EditStatus<>UndoTyping THEN
   IF KeyCode<>0 THEN
    BEGIN
     Save_EndPoints;
     Delete_ScrapTE;
  Save_Selection(UndoStart,UndoEnd);
    END; {IF EditStatus<>UndoTyping,IF KeyCode<>0}
  IF KeyCode=Backspace THEN
   WITH DisplayTE^^ DO
    IF SelStart>0 THEN
     IF SelEnd<=CurrentStart THEN
      BEGIN
       CharH:=TEGetText(DisplayTE);
    {Get the text as a character array}
       CurrentStart:=CurrentStart-1;
       TESetSelect(0,0,ScrapTE);
       TEKey(CharH^^[SelStart-1],ScrapTE);
      END;
    IF KeyCode<>0 THEN
     BEGIN
     TEKey(CharIn,DisplayTE);
  IF EditStatus<>UndoTyping THEN
   Reset_EditMenu(UndoTyping);
     END;
 END; {Typist}

We now have the pieces in place to Undo or Redo any typing by the user. For Undo Typing we neeed to:

• Move new typing to end of ScrapTE,

• Delete new typing from DisplayTE,

• Move previous selection to DisplayTE,

• Set selection points to previous selection,

• Delete previous selection from ScrapTE, and

• Set Undo item to Undo Typing.

For Redo Typing we need to:

• Move previous selection to end of ScrapTE,

• Delete previous selection from DisplayTE,

• Move new typing to DisplayTE,

• Delete new typing from ScrapTE, and

• Set Undo item to Redo Typing.

Hmm! Undo Typing and Redo Typing look very similar! They are, and in fact, we could merge them into one procedure. For clarity, we won't but will leave that as an exercise for . . .

Our final piece of code is to expand these by replacing Undo_Typing and Redo_Typing. For Undo_Typing we have:

PROCEDURE Undo_Typing;

 VAR scrapLength : integer;
  TypingEnd   : integer;
  theText     : Handle;

 BEGIN
  scrapLength:=ScrapTE^^.TELength;{Put new type at scrap end}
  TESetSelect(scrapLength,scrapLength,ScrapTE);
  TypingEnd:=DisplayTE^^.selEnd;
  IF CurrentStart<TypingEnd THEN
   BEGIN
    Save_Selection(CurrentStart,TypingEnd);
    TESetSelect(CurrentStart,TypingEnd,DisplayTE);
    TEDelete(DisplayTE);       {Delete new typing}
   END; {IF CurrentStart<TypingEnd}
  Restore_Selection(scrapLength); {Put orig selection back}
  TESetSelect(UndoStart,UndoEnd,DisplayTE);
  TESetSelect(0,scrapLength,ScrapTE); {Delete prev from scrap}
  TEDelete(ScrapTE);
  Reset_EditMenu(RedoTyping);
 END; {Undo_Typing}

For Redo_Typing we have:

PROCEDURE Redo_Typing;

 VAR scrapLength : integer;
  TypingEnd   : integer;
  theText     : Handle;

 BEGIN
  scrapLength:=ScrapTE^^.TELength; {Put old select at end of}  
  TESetSelect(scrapLength,scrapLength,ScrapTE);  {ScrapTE}
  TypingEnd:= DisplayTE^^.selEnd;
  IF CurrentStart<TypingEnd THEN
   BEGIN
    Save_Selection(CurrentStart,TypingEnd);
    TESetSelect(CurrentStart,TypingEnd,DisplayTE);
    TEDelete(DisplayTE);       {Delete old selection}
   END; {IF CurrentStart<TypingEnd}
  Restore_Selection(scrapLength); {Move new typing to          
                           DisplayTE}
  TESetSelect(0,scrapLength,ScrapTE);
  TEDelete(ScrapTE);     {Delete new typing from ScrapTE}
  Reset_EditMenu(RedoTyping);
 END; {Redo_Typing}

That's it! Now you have an editor for very small files that allows the user to Undo each editing operation immediately after requesting the operation. But this is only the beginning of your work for an editor. Now you have to add:

• Scrolling,

• Window resizing,

• Multiple windows,

• Search and replace commands,

• Additional error checking, and

• Much more.

[Note: Some of this capability was published in the first article in this series on Text Edit in the January 1987 issue of MacTutor. -Ed]

What Can Go Wrong

This article is a bit deceptive. Because, I wrote it as a step-by-step approach, programming this small editor seems easy. However, I should admit that what you see is the third attempt. My first editor was the text editing support in DevHELPER®. My second editor was the first draft of this article, and I did it pretty much in the order given. My third editor is the program of this article with all the bugs out (or most obvious bugs), with little reorganizations to improve readability, and with common code put into separate procedures.

When you write your editor, you may make some of the same mistakes that I did. To save you some of the grief of figuring out what went wrong, here are some mistakes that I recorded in my notes.

• Immediate termination of program

Did not call Init_MyGlobals, thus did not set QuitFlag.

• System Error 01,02

Used theWindow in MainEventLoop as local variable, but did not declare it so.

• Watch cursor shown before first menu selection

Did not call InitCursor before or at beginning of MainEventLoop.

• Selection is "checkerboard" of highlighting

Did not call GlobaltoLocal for mousepoint.

• No blinking caret

Did not call TEActivate after TENew.

• Hang on Cut

viewRect and destRect for ScrapTE were "illegal" rectangles, that is, top>bottom or right>left.

• Undo Paste using wrong text:

ScrapTE not cleared by earlier operation

• Undo Paste not undoing:

Scrap TE not cleared by earlier operation.

• Slow typing

Slow typoing can happen on a MacXL or a Mac with 128K. TEKey on these systems is too slow if it has to move several thousand characters which would happen at the beginning of large documents. TEKey on a Mac+ was not a problem on the same document in the same place. If you plan to write your editor for earlier machines, you may need to work with only a portion of the text. Now that we've begun to understand Text Edit, we get to start all over again, with the new text edit on the SE and Macintosh II! Look for more articles in this series in the coming months.

 

Community Search:
MacTech Search:

Software Updates via MacUpdate

Vivaldi 1.10.867.48 - An advanced browse...
Vivaldi is a browser for our friends. In 1994, two programmers started working on a web browser. Our idea was to make a really fast browser, capable of running on limited hardware, keeping in mind... Read more
EarthDesk 7.2 - Striking real-time anima...
EarthDesk replaces your static desktop picture with a rendered image of Earth showing correct sun, moon, and city illumination. With an Internet connection, EarthDesk displays near-real-time global... Read more
Fission 2.3.2 - Streamlined audio editor...
Fission can crop and trim audio, paste in or join files, or just rapidly split one long file into many. It's streamlined for fast editing. Plus, it works without the quality loss caused by other... Read more
Drive Genius 5.0.3 - Powerful system uti...
Drive Genius gives you faster performance from your Mac while also protecting it. The award-winning and improved DrivePulse feature alerts you to hard drive issues before they become major problems.... Read more
iDefrag 5.2.0 - Disk defragmentation and...
iDefrag helps defragment and optimize your disk for improved performance. iDefrag Features Supports HFS and HFS+ (Mac OS Extended). Supports case sensitive and journaled filesystems. Supports... Read more
Things 3.1.1 - Elegant personal task man...
Things is a task management solution that helps to organize your tasks in an elegant and intuitive way. Things combines powerful features with simplicity through the use of tags and its intelligent... Read more
GraphicConverter 10.4.3 - $39.95
GraphicConverter is an all-purpose image-editing program that can import 200 different graphic-based formats, edit the image, and export it to any of 80 available file formats. The high-end editing... Read more
Google Chrome 60.0.3112.78 - Modern and...
Google Chrome is a Web browser by Google, created to be a modern platform for Web pages and applications. It utilizes very fast loading of Web pages and has a V8 engine, which is a custom built... Read more
PDFpenPro 9.1 - Advanced PDF toolkit for...
PDFpenPro allows users to edit PDF's easily. Add text, images and signatures. Fill out PDF forms. Merge or split PDF documents. Reorder and delete pages. Create fillable forms and tables of content... Read more
PDFpen 9.1 - $74.95
PDFpen allows users to easily edit PDF's. Add text, images and signatures. Fill out PDF forms. Merge or split PDF documents. Reorder and delete pages. Even correct text and edit graphics! Features... Read more

Latest Forum Discussions

See All

The 5 best life-saving apps for dog owne...
While it's true that dogs are man's best friend, they're also a pretty big responsibility. We want to give our dogs the best lives, but with busy schedules that's not always easy. Luckily, though, there are a bunch of quality apps out there that... | Read more »
Mix and match magical brews in Miracle M...
Miracle Merchant, the charming fantasy card game by Tiny Touch Tales, is arriving next week. The development team, which also brought you Card Crawl and Card Thief, announced the game's launch with a pleasant little trailer that showcases the game'... | Read more »
Last Day on Earth: Zombie Survival guide...
Last Day on Earth: Zombie Survival is the latest big hit in the survival game craze. The gist of the game is pretty cut and dry -- try your best to survive in a world overrun by flesh-eating zombies. But Last Day on Earth justifies the hype... | Read more »
Eden: Renaissance (Games)
Eden: Renaissance 1.0 Device: iOS Universal Category: Games Price: $4.99, Version: 1.0 (iTunes) Description: Eden: Renaissance is a thrilling turn-based puzzle adventure set in a luxurious world, offering a deep and moving... | Read more »
Glyph Quest Chronicles guide - how to ma...
Glyph Quest returns with a new free-to-play game, Glyph Quest Chronicles. Chronicles offers up more of the light-hearted, good humored fantasy fun that previous games featured, but with a few more refined tricks up its sleeve. It's a clever mix of... | Read more »
Catch yourself a Lugia and Articuno in P...
Pokémon Go Fest may have been a bit of a disaster, with Niantic offering fans full refunds and $100 worth of in-game curency to apologize for the failed event, but that hasn't ruined trainers' chances of catching new legendary Pokémon. Lugia nad... | Read more »
The best deals on the App Store this wee...
There are quite a few truly superb games on sale on the App Store this week. If you haven't played some of these, many of which are true classics, now's the time to jump on the bandwagon. Here are the deals you need to know about. [Read more] | Read more »
Realpolitiks Mobile (Games)
Realpolitiks Mobile 1.0 Device: iOS Universal Category: Games Price: $5.99, Version: 1.0 (iTunes) Description: PLEASE NOTE: The game might not work properly on discontinued 1GB of RAM devices (iPhone 5s, iPhone 6, iPhone 6 Plus, iPad... | Read more »
Layton’s Mystery Journey (Games)
Layton’s Mystery Journey 1.0.0 Device: iOS Universal Category: Games Price: $15.99, Version: 1.0.0 (iTunes) Description: THE MUCH-LOVED LAYTON SERIES IS BACK WITH A 10TH ANNIVERSARY INSTALLMENT! Developed by LEVEL-5, LAYTON’S... | Read more »
Full Throttle Remastered (Games)
Full Throttle Remastered 1.0 Device: iOS Universal Category: Games Price: $4.99, Version: 1.0 (iTunes) Description: Originally released by LucasArts in 1995, Full Throttle is a classic graphic adventure game from industry legend Tim... | Read more »

Price Scanner via MacPrices.net

Photographer Explains Choosing Dell Laptop Ov...
Last week photographer and video blogger Manny Ortiz posted a video explaining the five most important reasons he settled on a Dell XPS 15 laptop instead of a MacBook Pro for his latest portable... Read more
Sale! 10-inch iPad Pros for $50 off MSRP, no...
B&H Photo has 64GB and 256GB 10.5″ iPad Pros in stock today and on sale for $50 off MSRP. Each iPad includes free shipping, and B&H charges sales tax in NY & NJ only: – 10.5″ 64GB iPad... Read more
WaterField Designs Upgrades TSA-friendly Zip...
San Francisco based designer and manufacturer Waterfield Designs has unveiled an upgraded and refined Zip Brief. Ideal for the minimalist professional, the ultra-slim Zip laptop bag actually holds a... Read more
USB 3.0 Promoter Group Announces USB 3.2 Upda...
The USB 3.0 Promoter Group has announced the pending release of the USB 3.2 specification, an incremental update that defines multi-lane operation for new USB 3.2 hosts and devices. USB Developer... Read more
Save on MacBook Pros with Apple Refurbished 2...
Apple recently dropped prices on Certified Refurbished 2016 15″ and 13″ MacBook Pros with models now as much as $590 off original MSRP. An Apple one-year warranty is included with each model, and... Read more
13-inch 2.3GHz/256GB MacBook Pros on sale for...
B&H Photo has 13″ 2.3GHz/256GB MacBook Pros in stock today and on sale for $100 off MSRP including free shipping plus NY & NJ sales tax only: – 13-inch 2.3GHz/256GB Space Gray MacBook Pro (... Read more
Clearance 2016 13-inch MacBook Airs, Apple re...
Apple has Certified Refurbished 2016 13″ MacBook Airs available starting at $809. An Apple one-year warranty is included with each MacBook, and shipping is free: – 13″ 1.6GHz/8GB/128GB MacBook Air: $... Read more
PHOOZY World’s First Thermal Capsules to Summ...
Summer days spent soaking up the sun can be tough on smartphones, causing higher battery consumption and overheating. To solve this problem, eXclaim IP, LLC has introduced the PHOOZY Thermal Capsule... Read more
2018 Honda Ridgeline with Android Auto and Ap...
The 2018 Honda Ridgeline is arriving in dealerships nationwide with a Manufacturer’s Suggested Retail Price (MSRP1) starting at $29,630. The 2017 Honda Ridgeline was named North American Truck of the... Read more
comScore Ranks Top 15 U.S. Smartphone Apps fo...
comScore, Inc. recently released data from comScore Mobile Metrix, reporting the top smartphone apps in the U.S. by audience reach for June 2017. * “Apple Music,” as it appears in comScore’s monthly... Read more

Jobs Board

Frameworks Engineering Manager, *Apple* Wat...
Frameworks Engineering Manager, Apple Watch Job Number: 41632321 Santa Clara Valley, California, United States Posted: Jun. 15, 2017 Weekly Hours: 40.00 Job Summary Read more
*Apple* Solutions Consultant (ASC) - Poole -...
Job Summary The people here at Apple don't just create products - they create the kind of wonder that's revolutionised entire industries. It's the diversity of those Read more
SW Engineer *Apple* TV - Apple Inc. (United...
Changing the world is all in a day's work at Apple . If you love innovation, here's your chance to make a career of it. You'll work hard. But the job comes with more Read more
Frameworks Engineering Manager, *Apple* Wat...
Frameworks Engineering Manager, Apple Watch Job Number: 41632321 Santa Clara Valley, California, United States Posted: Jun. 15, 2017 Weekly Hours: 40.00 Job Summary Read more
Product Manager - *Apple* Pay on the *Appl...
Job Summary Apple is looking for a talented product manager to drive the expansion of Apple Pay on the Apple Online Store. This position includes a unique Read more
All contents are Copyright 1984-2011 by Xplain Corporation. All rights reserved. Theme designed by Icreon.