TweetFollow Us on Twitter

Volume Number:2
Issue Number:11
Column Tag:Pascal Procedures

Extending TextEdit to Handle Tabs

By Bradley W. Nedrud, Nedrud Data Systems, Las Vegas, NV

Bradley W. Nedrud has a PhD from the University of Illinois in low-temperature solid-state physics. He worked for four years at Hughes Aircraft Company, designing and building microwave circuits for communication satelites and managing the C-band receiver section. In 1985 he decided to write a microwave circuit CAD program, because a). he was very impressed with the Macintosh, b). he was disillusioned with the CAD programs currently available, c). he wanted to spend more time with his family, and d). he didn't know any better.

A Simple (?) Way to Implement Tabs in TextEdit Windows

In this column, I present a simple TML Pascal editor of very little interest since it does not allow scrolling, resizing, saving, or printing. It does, however, allow me to demonstrate the implementation of tabs in a textEdit window, which in itself is an extremely useful feature. And in the process, I will show how to manipulate the low-level QuickDraw routines via the QDprocs field of a GrafPort and how to customize the intrinsic miniEditor, TEDoText, and the intrinsic lineStart recalculator.

Simple window with tabs in the text

In scientific program development, it's often desirable to arrange data in neat tables. This allows the user to quickly find what he wants without that feeling of panic one gets when confronted with a windowful of jumbled numbers. After all, the Macintosh is based on the principle that neatness counts (grossly simplified). Of course, scientific programs are not the only ones that use a table format. Database managers, editors, even language output routines all need to produce tables, and tables means TABS!

When I turned to IM, I read that famous line, "Although TextEdit is useful for many standard text editing operations, there are some additional features that it doesn't support. TextEdit does not support... tabs." At that time I was more naive then now and I felt something as intuitive and useful as tabs should be easy to implement. I started by writing a routine that measured the text from the first character on a line (following the CR of the previous line) up to a tab using TextWidth, subtracting that from the calculated pixel distance to the next larger tab and then dividing that by the width of a space. I then TEKeyed in that number of spaces. Simple as that was, the routine actually sort of worked, with two major drawbacks. First, the window didn't edit at all like it should. For example, TEClick would not treat the tab as an entity, so you could select positions between any of the spaces. Also, as soon as text was added or subtracted, the table reverted to a jumble. The second drawback was that the entries in columns just wouldn't line up exactly. In proportional fonts, letters are all different widths, and adding spaces can only align text to the nearest half space-width. This gave ragged looking columns (much like in MicroSoft Basic's output windows) and just didn't project the kind of polished image I wanted to with my program.

I started to wonder how the real programmers made tabs work. After all, both Edit and MacWrite do an admirable job of lining up columns of text. When I looked at Edit with a disassembler, however, my budding hopes were crushed. Someone had rewritten most of the TextEdit routines! I don't know if this gargantuan task was motivated principally by the need for tabs, but I was getting the idea that I might have a long road before me.

The biggest reason tabs are so hard to implement is that they are variable-length characters. Sometimes a tab is only a character long, sometimes many. Its length depends on where it is located in a line of text (from the last CR). Widths of characters are normally looked up in a special table that is a part of every font record. Every TextEdit routine (minus TEInit, TENew, and TEDispose) makes use of character widths (e.g. TEActivate must calculate the selection rectangles between the selStart and selEnd character positions to highlight text properly). I half-heartedly started to code a custom implementation of TEClick, but I gave up. It's very complicated: calculating justification for each line, getting the clipRgns right, using the wordBreak routine, and trying to make sense of a lot of ROM code that just... doesn't seem to make sense. I'm not knocking Apple or their ROM code - far from it. After all, their thing was compactness, not logical layout to make code easy to read by hackers. Also they had to "get-it-done-NOW", a motivating factor I've learned to have a lot of sympathy for since I've tried my hand at program development. Enough editorializing (I'll leave that to Ed). Suffice it to say that I felt that if there wasn't a way to use the standard TextEdit routines and still use tabs, my program wasn't going to have tabs. Somewhere, there must exist the Elegant Solution (the programmer's elusive Holy Grail). Somehow, I had to intercept the routine that looked up character widths in the font record and modify it. That reminded me of something and I turned to page I-197 of IM.

The grafProcs field of any grafPort can contain a pointer to a table of ProcPtrs that specify the low level routines which QuickDraw uses to (among other things) draw text and measure text widths. The standard routines to do these are called StdText and StdTxMeas, but their entries in the QDProcs record can be replaced with custom routines with the same arguments -- exactly what I needed. I wrote custom routines (first in TML Pascal, then in assembly for speed) which I call tabTxWrite and tabTxMeas. They work GREAT. Text lines up perfectly in columns. Any font (including proportional) works. Some windows can support tabs and others can use the standard QDProcs (since this is specified in each window's record).

However, it isn't quite as simple is that. I didn't want the tabs to be equally spaced and I wanted each window to have different tabs. So I set up a tabRecord (see Type declaration in Pascal main program) and put a handle to it in the refCon field of each window. It contained mainly an integer specifying the number of tabs and an array showing where those tabs were (the pixel distances of the tabs from the left side of the destination rect). I made the tabs a resource. In the resource, I stored the tabs as a numbers of characters instead of pixel distances (they are converted when the window is set up), so that different size fonts would work the same.

However, it isn't quite as simple as that. In addition to being variable-length, tabs have another peculiarity. Usually, when one tabs after the position of the last set tab in a line, the input caret skips to the beginning of the next line. In other words, such a tab is treated like a carriage return. I call such a tab, a pseudoCR. I cast around until I found a very good solution. There is a routine, scantily described on page I-391 of IM, called TERecal. All it does is recalculate the entries in the lineStarts array (the last field in the TErecord). Its address is stored in a low-memory system global (at $A74). It is called by many TE routines, but always indirectly through the address stored in $A74. I figure that the reason Apple used this scheme was so that we programmers could replace the address of the standard routine with a custom one. So that is what I did. Actually, I dug around until I found 3 completely undocumented routines (see Table 1) which are called by TERecal and which are also accessed through low-memory system globals (and therefore, I feel, are fair game to replace). I wrote a replacement (tabLineStart) for the one in $7FC so that entries are inserted into the lineStarts array after every pseudoCR. So everything worked great.

The subroutines below are always called by TE through the indicated low-memory global addresses. All of them expect to receive a pointer to a locked TErecord in A3.

Table 1

global Parameters

$7F8 input: D0 = a character position

output: D0 = char pos after 1st wordbreak char < D0

D1 = char pos before 1st wordbreak char > D0

$7FC input: D6 = a character position

output: D0 = char position of next lineStart > D6

$7F4 input: D6,D7 = character positions

output: D0 = length of text from D6 to char pos just after 1st non-wordbreak char < D7

Not quite. Unfortunately, it isn't even as simple as that. There are two problems. The low level text routines get passed only a pointer to text, and a count of the number of characters to measure or write. They know nothing about the TErecord they are writing into. In particular, they do not know where the beginnings of the lines are. Both of my custom routines ASSUME that the first character in the textbuffer is the beginning of a line. This is only a problem if the TE routines call the QuickDraw routines with textPointers to a character which is not a lineStart. That is the case, folks, at least on the 128/512K Macs (this has apparently been corrected in the new ROMs shipped with the Mac Pluses). For example, TEKey calls StdTxMeas three times: once for beginning of line to selStart - 8 (that's OK), once from beginning of line to selEnd (that's OK) and once from selStart - 8 to selStart (not OK). The extra characters that were erased and written were probably so that fonts with overlap between letters (kerning) would be written correctly (on the MacPlus, the -8 was changed to -1). So much for problem #1. The second problem is that some of the TE routines take action based on the presence of a CR, not just a lineStart, so they would not work when a pseudoCR was detected. The only solution is to replace every routine that causes problem #1 or problem #2.

Fortunately, it turns out that the TextEdit routines call the low-level QuickDraw routines only indirectly via a built-in miniEditor (described briefly on page I-391 of IM) whose address is stored in the low-memory system global, TEDoText ($A70). This miniEditor is composed of four subroutines which variously hitTest (figure out which character position a click is closest to), highlight a selection range, display some text, or position the pen to draw the blinking caret. Since these routines receive a pointer to the locked TErecord, it is easy to find the beginnings of each line of text from the lineStarts field. The actions of the standard miniEditor can be changed by storing the address of a different miniEditor in the global, TEDoText.

Either two or three of the miniEditor subroutines need to be modified to make our tabs scheme work, depending on whether the machine is a MacPlus or an earlier model. They are summarized in the accompanying Table 2. Once this is done, everything works correctly, from double-click text selection to un-backspacing tabs. It is that simple.

The following subroutines in TEDoText need to be changed for the indicated reasons (Note the differences between the 512 and Mac+ ROMs)

Table 2

calls QD text routines w/ refers explicitly

TEDoText textPtr in middle of line to CR

Routines (Problem #1) (Problem #2)

DrawSomeText ----- -----

setCaret ----- 512/Mac+

HiLite 512 -----

HitTest 512 512/Mac+

A Few Intricacies (for those that like that sort of stuff)

First of all, there are characters and there are inter-character positions. These are used rather loosely (such as IM talking about TEDoText hit-testing a character when actually it is looking for the inter-character position closest to the click). Characters are numbered from one to teLength. Character positions are numbered from zero (before the first character in the record) to teLength (after the last character in the record). LineStarts happen at character positions. They may be after regular characters (if the line was wrapped around) or after CRs (or pseudoCRs). In the first case, a click past the last character on one line or before the first character on the next line actually causes the HitTest routine to return the same character position. That is why there is an extra field (called clickStuff) in the TErecord, which must be set by the hitTest routine to tell the caret-placing routine whether to put the caret at the end of one line or at the beginning of the next (more about that below). Secondly, CRs (and pseudoCRs) are the last characters in their lines, not the 1st characters of the next line. This is actually quite significant. They have zero length, so that a click after them on the same line or at the beginning of the next line should, according to the above rule, cause HitTest to return the same character position. Of course, that would be wrong, since if the caret is at the beginning of the line, a backspace removes the CR (or pseudoCR) whereas if the caret is at the end of a line, a backspace removes the last (non-control) character. That means that, in this case, HitTest must specifically check for a CR (or pseudoCR) in front of the lineStart and return one less than it would if there was none. That is why the TEDoText hittesting routine had to be rewritten.

Also, through some quirk, if a click occurs below the last line of the text, TEClick does not call the HitTest routine at all, but calls the setCaret routine with D3 equal to teLength. Poor setCaret cannot tell whether to put the caret at the end of the line or the beginning of the next line (since the clickStuff field was not set by HitTest). Therefore, setCaret must check to see if the last character in the record actually is a CR (or pseudoCR) and if it isn't, move the character to the end of the previous line. That is why the setCaret routine had to be rewritten.

Here is some more information on the ClickStuff field, among others. In IM, eight fields of the TextEdit record are marked {used internally} with no further explanation except their names and the warning, "Don't change any of the fields marked "used internally". Although I don't claim these to be definitive, here is some idea of what they do:

active -- High byte: set if window active. Low byte always 0.

clickTime -- Time (in tics from startUp) when last click happened (used to check for double clicks).

clickLoc -- Result of last call to HitTest subroutine = character position of click (used to check for double clicks and when click-drag is specifying a range).

caretTime -- Time when next caret toggle should take place.

caretState -- High byte: set if caret visible (alternates as caret blinks). Low byte: set if caret should blink (would be zero if selStart selEnd, i.e. a selection range). Note: these are called teCarOn and teCarAct in the new versions of the Apple MDS equate files.

recalBack -- Absolutely nothing.

recalLines -- Absolutely nothing.

clickStuff -- High byte: set by HitTest, if last click was at first character position of a line (as opposed to last character of previous line). Low byte: set if caret should be shown in front of 1st char of line (as opposed to after last character of previous line). Note: these are called teLftClick and teLftCaret in the new versions of the Apple MDS equate files.

Description of Pascal tabEditor Program

The editor program presented has been stripped of most of its features to emphasize the tab feature and to save room. Therefore, it does NOT save, print, resize, scroll, or allow multiple windows. Most of these features have been described before or could be more conveniently (and clearly) described in a separate column and all of them can be added modularly on top of the tabEditor program without rewriting any existing code. TabEditor DOES handle desk accessories (including cut/paste) and puts up an About. . . dialog. It allows one to exit the program, via the File/Quit menu. It opens a textEdit window into which text can be typed, cut, copied, pasted, or cleared and which furthermore, has tabs set every 8th character position. If the window is closed with its close box, a New window can be opened using the File/New menu.

The code in tabEditor's Pascal listing which deals explicitly with the tab feature is boldfaced and consists of a few lines in the Initialize routine which are executed once, a few lines in the Activates and Updates subroutines, and the entire subroutine SetUpForTabs, which is called once for each window supporting tabs at the time of its creation with GetNewWindow or NewWindow.

Initialization Code. TabEditor also requires six global variables, which are all set by the Initialize. myQDProcs is a QDProcs record as described on page I-197 of IM, and is filled with pointers to all of the standard low-level QD routines by SetStdProcs. Then two of the pointers are changed to point to the custom text routines, tabTxMeas and tabTxWrite. nowTabs contains a handle to the tabRecord (defined under Type) of the current window or NIL if the current window does not support tabs. globalA70 is a long integer pointer: i.e., it is set to point to the long integer at absolute address $A70. Note that the standard pointer type would not work since it points to a byte and we need to address the whole long integer (i.e. ptr^ is length 1 byte, while LIptr^ is length 4). We change the address of the miniEditor used by ROM routines through this global variable. The address originally stored in $A70 is saved in stdEDoText during the initialization process, for two reasons: 1) This routine is called via this application global from the tabTEDoText assembly routine. 2) I also restore the default miniEditor address to $A70 when leaving the program, although this is unnecessary since ExitToShell restores it anyway. global7FC and stdLineStart function in the same way as globalA70 and stdTEDoText. However, it is absolutely necessary to restore the default address (in stdLineStart) to global $7FC upon exiting the program since ExitToShell does not (otherwise the next program to call this routine will crash).

Activates Code. Whenever a window deactivates, the nowTabs application global must be set equal to NIL (so that a desk accessory, for example, will function correctly). It must be set to the handle of the tabRecord (if there is one) when a window activates. Both tabTEDoText and tabLineStart check if this global is NIL and passes control directly to the default routine if it is.

UpDates Code. Whenever a window updates, the nowTabs application global must be set equal to the tabHandle of the window being updated, since it may not be the same as the active window. That means that the nowTabs handle of the active window must be saved and restored after the update is done.

SetUpForTabs. This subroutine gets passed a windowPointer and a resource ID. The window must already have been created, and must have a handle stored in its refCon, which points to a block containing only the handle of the window's TErecord. The resource ID must be for a resource of type 'bTAB' (I use the same resource ID as for the window itself) which contains the tab information. The block containing the TEhandle is enlarged with SetHandleSize so that there is enough room for the Tabs array. Then the resource information is copied to the tabRecord with a BlockMove. Note that although the tabRecord type definition allows up to 100 tabs per line, an actual tabRecord is a dynamic structure with only enough space allocated to hold the array of tabs contained in the resource. Finally, the character position of each tab in the tabRecord is multiplied by the width of the zero character to convert it to a pixel length. Also, as a time-saver, the standard pixel width of the tab character is stored in the tabRecord to be used by the tabTxMeas routine (this width is taken directly from the font character-width table and usually equals the width of a space).

Description of tab Resource

The 'bTAB' (arbitrary and non-registered) resource contains integers: the number of tabs followed by the character position of each tab. Note that it can be edited by any resource editor to change the position of the tabs or to add/remove tabs.

Description of Low-level QuickDraw Text Routines

TabTxMeas is my replacement for the standard QD text measuring routine, StdTxMeas. It starts by measuring the given text using StdTxMeas which gives a pixel length (D7), which we will have to modify only if there are some tabs in the text. Then it checks each character to see if it is a carriage return (CR) or tab. If it is a CR, it sets a pointer to point to the character after the CR (A3), and zeros a character counter (D6) and zeros D5, which is the pixel length from the beginning of the line to A3. If it is a tab, it first subtracts off the standard tabwidth (from the nowTabs record). Then StdTxMeas is used to measure the pixel length of the text from A3 for D6 characters (this text contains by definition no CRs or tabs) and this length is added to D5 (which now makes it the pixel length from the beginning of the line up to the tab). If D5 is greater than the last tab position or if lastTab is zero, then the tab is treated exactly like a CR. Otherwise, the tab position just larger than D5 is added to D7 and D5 is subtracted from D7 (i.e. D7 increases by the width of the tab character alone). Then D5 is set equal to the tab position, A3 is set to point to the character after the tab, and D6 is zeroed (making everything consistent).

TabTxWrite looks up destRect.left for the TErecord whose handle is stored in nowTabs and stores it in D7. It then checks each character to see if it is a tab. If it encounters a tab, it writes all characters (counted by D6) since the character just after last tab (pointed to by A3) using StdText. Note that the pen is positioned by StdText just after the last character written. tabTxWrite puts the horizontal pen position (via GetPen) into D5, and subtracts off D7 to get the pixel width of the characters since the beginning of the line. If D5 is greater than the last tab position or if lastTab is zero, nothing is done. Otherwise, D5 is subtracted from the tab position next larger than D5 and the pen is moved by that amount. When all characters have been checked, StdText is called one more time to write all remaining characters.

Description of LineStart Calculating Routine

tabLineStart receives a character position in D6 and must return the next larger lineStart position in D0. It initializes D4 equal to the width of the destRect and D7 equal to D6. Inside the Loop, the routine whose address is in $7F8 is called with D0 equal to D7. It returns the position before the next wordbreak character (i.e. D7 is incremented by one word). If the CR only field of the TErecord is zero, the text width from D6 to D7 is calculated (via routine whose address is in $7F4) and compared to D4. If the text has exceeded the end of the destination rectangle, a lineStart is placed one larger than D5, which is what D7 was last time through the loop (one word back). If this is the first time through the loop (i.e. we're still working on our first word), D5 is undefined, but that's OK because A2 (the word counter) = 0, so we are detoured through oneWord, which is a code fragment that backs up the end of the single word, one character at a time, until it has enough characters to just fill the width of the destRect. There it inserts a lineStart. If the text has not exceeded the end of the destRect (or CRonly is nonzero), the wordbreak character at D7 is checked to see if it is a CR (or pseudoCR) and, if so, a lineStart is returned. Note that a lineStart is also returned if we reach teLength before anything else.

An aside I found interesting. IM states that TEDoText and TERecal receive a pointer to a locked TErecord in A3 and that is true. However, whenever more lineStarts need to be added to that TErecord, its size must increase, as necessary, to accommodate them. This means that a subroutine called by TERecal can unlock the TErecord, move it elsewhere, and relock it. Therefore one must be careful in making copies of A3 or pointers to other fields in the TErecord. A2 (used as a pointer to somewhere in the lineStarts array) is however adjusted by the subroutine to point to the same position in the moved record.

Description of MiniEditor Text Routines

TabTEDoText is called by the TextEdit routines to do basic editing. If nowTabs is NIL or D7 = -1, control is passed immediately to the default miniEditor. Otherwise, if D7 is 0 or -2, tabTEDoText calls the custom routines, HitTest or setCaret, respectively. Since the Hilite routine is needed on 128K/512K machines and not on MacPluses, it is conditionally compiled depending on the state of the ROM128K flag. On MacPlus, if D7 = 1, tabTEDoText calls the default miniEditor. For earlier machines it calls HiLite. (Note that Hilite will work on the MacPlus also).

HitTest receives the point where the mouse was clicked in local coordinates in the selPoint field of the TErecord. TEClick has already processed the vertical component of that point by the time that the miniEditor is called, so that the 1st character position of the line containing selPoint is in D3, and the 1st character of the next line (or teLength) is in D4. All that remains for HitTest to do is to find out which character position between D3 and D4 is closest to selPoint.h. First, selPoint.h minus destRect.left is moved to D7. If this is less than zero (i.e. selPoint is to left of first character in the line) then D3 is returned in D0 and clickStuff is set. Otherwise D4 is adjusted to point to the last character in the line (rather than the 1st character in the next line). This is the only part of the program which had to be different from the default hit-testing routine so that tabs (actually pseudoCRs) would work. D6 is set equal to D4 minus D3 (number of characters in the line). The width of the D6 characters is calculated (D5), compared to D7, and D6 is decremented. If D7 is greater than D5 the first time through the loop (i.e. the selPoint was beyond the last character in the line) the position of the last character in the line is returned in D0. Otherwise the loop continues until D5 ¾ D7 < D4 where D5 = D3+D6 and D4 = D5+1. D4 minus D7 is compared to D7 minus D5 to see which character position is closest.

A few words about speed. The standard hit-testing routine in the default TEDoText on the 128/512K Macs, uses TextWidth on each single character in the line (creating problem #1 -- see above), adding them up to compare to selPoint.h. This is probably faster than the method used in HitTest and the new 128K ROMs, which call TextWidth the same number of times, but for strings of characters rather than single characters. Both the intrinsic 512K and MacPlus hit-testing routines use PtInRect to see if selPoint is in selRect, which is set using the same routines used for highlighting. I think this was done to use existing code, rather than for speed. HitTest isn't appreciably slower than the standard routine, although I haven't tested it on very large text files. HitTest could probably be speeded up by changing it to check character positions from the beginning of the line to the end, or by calculating single character widths and adding, unless a tab character is detected.

HiLite receives the character positions of the start/end select range in D3/D4. If D3 equals D4, HiLite does absolutely nothing (except set A0 equal to thePort as specified by the description of TEDoText in IM. I don't know why this is necessary, and the more adventuresome of you might want to leave it out). If D3 is larger than D4, the registers are exchanged because the default miniEditor does that, although I doubt that HiLite is ever called with D3 > D4 (TEClick makes the adjustment before calling the miniEditor if you click-drag select text from a high character position to a low one), so this could probably be left out. HiLite proceeds to calculate rectangles one line at a time, which are in turn processed by subHilite. The first line rectangle must have a left side equal to the destRect.left plus the textWidth of all the characters from the first character of that line to D3. The last line rectangle gets its right side set in a similar fashion using D4. Any in-between rectangles have to be as wide as the destRect. Note that HiLite sets these lefts and rights to $8002 and $7FFE, which are one short of minus and plus machine infinity. I do this because the default routine does it, and because I ran across at least one place elsewhere where these values were checked for explicitly. If D3 and D4 are in the same line, only one rectangle needs to be inverted, a combination of the first and last rectangles.

A loop in HiLite gives (A2) ¾ D3 < 2(A2). Note that D3 cannot equal teLength. Then each rectangle described above is calculated in the selRect field of the TErecord. subHilite either calls InvertRect or the HiHook routine (see IM page I-379), if there is one.

I left out a routine (at $41668E on the MacPlus) that changes D3/D4 so that they correspond to lines actually inside the viewRect. I don't think that this makes for an upDate problem since the text is clipped to the viewRect anyway, but it would speed up the HiLite routine if there was a LOT of selected text not in the viewRect. Adding this routine is left as an exercise for those who need it.

SetCaret receives the character position of the caret in D3 and sets selRect to be one pixel wide and lineHeight tall in the proper location. It is very straightforward unless D3 happens to be equal to a lineStart. In that case, the clickStuff field is checked to see if the caret should be put at the end of the line or at the beginning of the next line UNLESS D3 is equal to teLength. In that case, as explained previously, the last character in the record is checked to see if it is a CR (or pseudoCR) and, if not, the caret is put end of the previous line.

Once the mysteries of text handling on the Mac are understood, implementing tabs is pretty straightforward. Making tabs setable from the program is not too difficult. Just add Print, Save, Scroll, Size, a few menus to change the overall TErecord font and textSize, and a search/replace routine, and you have a fullblown text editor.

Program TabEditor;

{ Pascal source: tabEditor.Pas > tabEditor.rel
  assembly source: tabGlue.asm > tabGlue.rel
  Resources:tabEditor.R > tabEditor/RSRC.rel}

{$B+ set bundle bit}
{$I MemTypes.ipas}
{$I QuickDraw.ipas }
{$I OSIntf.ipas  }
{$I ToolIntf.ipas}
{$U tabGlue }
{$L tabEditor/RSRC }

  applemenu =  301;
  filemenu= 302;
  editmenu= 303;
  windID= 300; {our text window}
  aboutID = 300; {modal dialog}

  tabRecord =  RECORD
   tabTE: TEHandle;
   lastTab: integer;
   Tabs:array [1..100] of integer;
  tabPtr =^tabRecord;
  tabHandle =  ^tabPtr;
  LIptr = ^LongInt;

  done: boolean;
  myWindow: WindowPtr;
  textCursor:  cursHandle;
  DragArea: Rect;
  stdTEDoText: LongInt;
  stdLineStart:  LongInt;

FUNCTION tabTxMeas(byteCount: integer; textAddr: Ptr; VAR numer,denom: 
Point; VAR info: FontInfo): integer; EXTERNAL;
PROCEDURE tabTxWrite(byteCount: integer; textBuf: Ptr; numer,denom: Point); 

{*********** initialization Procedures ***************}

PROCEDURE SetUpForTabs(resID:integer; wPtr: windowPtr);
    aHndl,resHndl: handle;
    tabH: tabHandle;
    i,widthZeroChar: integer;
    bTABsize:    longInt;
  aHndl:= Handle(GetWRefCon(wPtr));
  resHndl:= GetResource('bTAB',resID);
  bTABsize:= GetHandleSize(resHndl);
{make rel block large enough to hold rest of tabRecord}
  SetHandleSize(aHndl,bTABsize + 6);
  tabH:= tabHandle(aHndl);
{set tabWidth field of tabRecord}
  tabH^^.tabWidth:= CharWidth(Chr($9));
{transfer rest of tabRecord}

  widthZeroChar:= CharWidth(Chr($30));   {width of a zero }
  WITH tabH^^ DO
    if lastTab <> 0 then
      for i:= 1 to lastTab DO {transform tabs from # chars }
        Tabs[i]:= tabs[i]*widthZeroChar;  {to pixel lengths}

  wPtr^.grafProcs:= @myQDProcs;


VarMenuTopic: MenuHandle;
   MenuTopic := GetMenu(AppleMenu);  {get the apple menu}
   AddResMenu(MenuTopic,'DRVR');     {adds all 'DRVR's}
   InsertMenu(MenuTopic,0);          {put in menuBar}
   MenuTopic := GetMenu(FileMenu);   {Quit & New}
   MenuTopic := GetMenu(EditMenu);


FUNCTION SetUpTextWindow(ID_No: integer): WindowPtr;

  Hndl: Handle;
  r:  Rect;
  li: LIptr;
  myW:  windowPtr;
  aTE:  TEhandle;

  myW := GetNewWindow(ID_No, NIL, POINTER(-1));

  r:= myW^.portRect;
  WITH r DO begin top:= top + 4; left:= left + 4; end;
  aTE:= TENew(r,r);

  Hndl:= NewHandle(ord4(4));
  li:= LIPtr(Hndl^);
  li^:= ord4(aTE);
{myW refCon has handle to a relocatable block }
{containing only a TEhandle for the moment}

{should be called once for every new window supporting tabs}
  SetUpTextWindow:= myW;


PROCEDURE Initialize;

  i:  integer;
  r:  Rect;

  InitGraf(@thePort);     {create a grafport for the screen}

  r:= ScreenBits.Bounds;
  done:= FALSE;  {set by QUIT command to signal end}

  myWindow := SetUpTextWindow(windID);
  nowTabs:= tabHandle(GetWRefCon(myWindow));
  nowTE:= nowTabs^^.tabTE;

  textCursor := GetCursor(ibeamCursor);
  InitCursor;    {show the Arrow cursor}
  globalA70:= LIptr($A70);{global variable points to TEDoText}
  stdTEDoText:= globalA70^;
{save pointer to default miniEdit routine}
  globalA70^:= ord4(@tabTEDoText);
{set so calls to miniEdit go to our modified tab routine}

  global7FC:= LIptr($7FC); {global var points to lineStart}
  stdlineStart:= global7FC^;
{save pointer to nonTab lineStart routine}
  global7FC^:= ord4(@tablineStart);
{set so calls to lineStart routine go to modified tab routine}

{set up a special QDprocs record for use with tab windows}
  myQDProcs.txMeasProc:= @tabTxMeas;
  myQDProcs.textProc:= @tabTxWrite;


{************ Menu Command Processing ********}

PROCEDURE ProcessMenu(CodeWord:longint);
  i,Menu_No,{menu number selected}
  Item_No:integer; {item in selected  menu}
  NameHolder:  Str255;    {for desk accessory}
  DNA:  integer; {dummy return}
  ourDlg: dialogPtr;
  If CodeWord <> 0 then
  BEGIN {process the command}
    Menu_No := HiWord(CodeWord);
    Item_no := LoWord(CodeWord);

    CASE Menu_No of
    AppleMenu: if Item_no = 1
       then begin{About...}
             ourDlg:= GetNewDialog(AboutID,NIL,POINTER(-1));
       else begin{Desk Accessories}
            DNA := OpenDeskAcc(NameHolder);
    FileMenu: CASE Item_No OF
              1: begin
                  myWindow:= SetUpTextWindow(windID);  {NEW}
                  nowTabs:= tabHandle(GetWRefCon(myWindow));
                  nowTE:= nowTabs^^.tabTE;
              2: done:= TRUE; {QUIT}
            end  {CASE};
      If Not SystemEdit(Item_no - 1) then    {for DAs}
 CASE Item_No OF
 1:;  {undo}
 { 2:   line divider}
 3: TECut(nowTE);
 4: TECopy(nowTE);
 5: TEPaste(nowTE);
 6: TEDelete(nowTE);
 end    {Item_No CASE};
    End {Menu_No CASE};
  END   {if};

  HiliteMenu(0);       {unhilite after processing menu}
End;  {ProcessMenu}

{******** Event Processing Routines ***********}

PROCEDURE MouseDowns(Event:EventRecord);
  WindowPointedTo: WindowPtr;
  MouseLoc: Point;
  WindoLoc: integer;
  MouseLoc := Event.Where;
  WindoLoc := FindWindow(MouseLoc, WindowPointedTo);
  CASE WindoLoc OF

  inDesk: {empty statement};
  inMenuBar: ProcessMenu(MenuSelect(MouseLoc));
  inSysWindow: SystemClick(Event,WindowPointedTo); {desk accessories}
  otherwise if WindowPointedTo <> FrontWindow
    THEN SelectWindow(WindowPointedTo) 
    ELSE CASE WindoLoc OF
 TEClick(MouseLoc,(BitAnd(Event.modifiers,shiftKey)      = shiftKey),nowTE);
       inGrow: {empty statement};
        If TrackGoAway(WindowPointedTo,MouseLoc) then
             nowTabs:= NIL;
      END {CASE};
  End  {CASE};


PROCEDURE KeyDowns(Event:EventRecord);
  CharCode   := chr(bitAnd(Event.message,charCodeMask));
  If BitAnd(Event.modifiers,CmdKey) = CmdKey
    then ProcessMenu(MenuKey(CharCode))
    else TEKey(CharCode,nowTE);     {regular keyboard entry}

End  {KeyDowns};


PROCEDURE Activates(Event: EventRecord);
  TargetWindow:  WindowPtr;
  active: boolean;

  TargetWindow := WindowPtr(Event.message);
  active:= Odd(Event.modifiers);
{true = the window is becoming active}

  IF active THEN
      nowTabs:= tabHandle(GetWRefCon(TargetWindow));
      nowTE:= nowTabs^^.tabTE;
    end ELSE
      nowTabs:= NIL;



PROCEDURE Updates(Event:EventRecord);
  savePort: WindowPtr;
tempTabs: TabHandle;
  UpDateWindow := WindowPtr(Event.message);
  GetPort(savePort); {Save the current port}
  tempTabs:= nowTabs;{Save current tabsHandle}
  SetPort(UpDateWindow);  {set the port to one in Evt.msg}
  nowTabs:= tabHandle(GetWRefCon(UpDateWindow));
  with nowTabs^^  DO TEUpdate(tabTE^^.viewRect,tabTE);
  SetPort(savePort);             {restore to the previous port}
  nowTabs:= tempTabs;     {restore to current tabHandle}

{************ End of Event Processing ************}

PROCEDURE MainEventLoop;
  mousePt: Point;
    SystemTask;             {run Desk Accessories}
    if myWindow = FrontWindow then
          if PtinRect(MousePt,nowTE^^.viewRect)
              then SetCursor(textCursor^^)
             else SetCursor(arrow);

    If  GetNextEvent(EveryEvent,Event) then
      CASE Event.what OF

      mouseDown: MouseDowns(Event);
      KeyDown,autoKey:  KeyDowns(Event);
      ActivateEvt: Activates(Event);
      UpDateEvt: Updates(Event);
      END  {CASE};
  Until done;   {terminate the program}


BEGIN {Main Program}
  globalA70^:= stdTEDoText;
  global7FC^:= stdlineStart;
END  {TabEditor}.

; Note: Hilite is not needed for the Mac Plus because some
;shortcomings of the TEDoText routines have been rectified in
;the new 128K roms.  If you have a MacPlus, set the constant
;ROM128K equal to 1.  If you have a 128K/512K Mac,
; set it equal to 0.  The appropriate parts will be assembled.
;----------- INCLUDES --------------------

Include MacTraps.D ; Use System and ToolBox traps
Include ToolEqu.D; Use ToolBox equates

;---------- XDEFs & XREFs ---------

 XDEF tabTxMeas  ; replaces stdTxMeas
 XDEF tabTxWrite ; replaces stdText
 XDEF tabTEdoText; replaces TEDoText in $A70
 XDEF tabLineStart ; replaces lineStart in $7FC

;------------ global variables -----------------
; the address of the default TEDoText routine was saved
; in this application global during main program init

 XREF stdTEdoText; address of standard routine
 XREF stdLineStart ; address of standard routine
 XREF nowTE ; currently active TEhandle
 XREF nowTabs  ; current tabRecord handle
 ;( or 0 if not tab Window)

;------------- other Equates ---------------

;  conditional assembly of Hilite routine if not set
; so I don't have to load the QuickDraw Equates
 left EQU 2
 bottom EQU 4
 right  EQU 6
; offsets into tab record
 tabWidth EQU  4
 lastTabEQU 6
; Note: clickStuff (1 word) is now teLftClick & teLftCaret 
; (bytes)

;-------------- Other XDEFs ---------------------
; XDEF all labels to be symbolically displayed by debugger.
; these are the names of custom replacements for 3 of the 4 
;routines called by TEDoText.
 XDEF   setCaret
 XDEF   HitTest
 IF ROM128K <> 1
 XDEF   HiLite

;------------- tabTEdoText -------------------
; on entry:
;A3>> pointer to locked edit record
;A4>> handle to edit record
;D3>> position of 1st char to be drawn or selected
;D4>> position of last char to be drawn or selected
;D7>>  0 to hit-test a character
; 1 to highlight the selection range
;-1 to display the text
;-2 to position the pen to draw the caret
; on exit:
;A0>> pointer to current grafPort
;D0>> if hit-testing, character position (-1 for none)
 TST.L  nowTabs(A5); if not a tabWindow
 BEQ    @0; go to default routine
 TST    D7
 BEQ    HitTest  ; D7 = 0
 IF ROM128K <> 1
 BPL    HiLite   ; D7 = 1
 BPL    @0
 CMP    #-1,D7
 BLT    setCaret ; D7 = -2
@0 MOVE.L stdTEdoText(A5),A0; D7 = -1
 JMP    (A0)

;------------- setCaret ---------------------
; on entry: see above
; other variables
;A2>> pointer to lineStart ¾ D3
;A0 = thePort on exit as mandated by Inside Macintosh
; selRect encloses caret
 LEA    teLines(A3),A2
 TST    D3
 BEQ    E2
@0 CMP  2(A2),D3
 BLS    @1
 ADDQ   #2,A2
 BRA    @0
@1 CMP  2(A2),D3 ; (A2) < D3 ¾ 2(A2)
 BNE    E2
 ADDQ   #2,A2    ; now D3 = (A2)
 MOVE   teLength(A3),D0
 BEQ    E2
 CMP    D0,D3
 BCS    E0

; from here to E0 executed only if D3 = teLength(A3)
 MOVE.L teTextH(A3),A0
 MOVE.L (A0),A0
 ; if D3 = CR, don't put caret on previous line
 CMP.B  #$D,-1(A0,D3)
 BEQ    E2
 MOVE   -2(A2),D6
 MOVE   D3,D7
 SUBQ   #1,D7
 JSR    tstTab ;treat pseudoCR (tab) like CR
 BEQ    E2
 BRA    E1

 ; teLftCaret is set if caret is at beginning of line 
E0 TST.BteLftCaret(A3)
 BNE    E2
E1 SUB  #2,A2
 ; set top and bottom of selRect
E2 LEA  teLines(A3),A0
 SUB.L  A2,A0
 MOVE   A0,D0
 NEG    D0
 ASR    #1,D0  ; divide by 2
 MOVE   teLineHite(A3),D1
 MULU   D1,D0
 MOVE.L teDestRect(A3),teSelRect(A3); top and left
 ADD    D0,teSelRect(A3)
 MOVE   teSelRect(A3),teSelRect+bottom(A3)
 ADD    D1,teSelRect+bottom(A3)

; set left and right of selRect
 MOVE.L teTextH(A3),A0
 MOVE.L (A0),A0
 CLR    -(SP)
 MOVE.L A0,-(SP) ; textBuf
 MOVE   (A2),D0
 MOVE   D0,-(SP) ; firstChar
 MOVE   D3,-(SP)
 SUB    D0,(SP)
 MOVE   (SP)+,D0 ; length of text from (A2) to D3
 ADD    D0,teSelRect+left(A3)
 MOVE   teSelRect+left(A3),teSelRect+right(A3)
 ADDQ   #1,teSelRect+right(A3)

; return
 MOVE.L (A5),A0
 MOVE.L (A0),A0  ; A0 = thePort (I don't know why)

;---------------- HitTest -----------------------
; on entry: see above
;teSelPoint field has mousePt in local coord
;D3>> first char in line
;D4>> first char in next line
; other variables
;D4>> flag & hiPosition
;D5>> loPosition
;D6>> counter of char position in line
;D7>> selPoint.h - destRect.left
;A2>> pointer to locked hText
; on exit:
;A0 = thePort on exit as mandated by Inside Macintosh
;D0>> char position of "hit" character

 CLR.L  D6; clear bit 31 to save Lock Bit
 MOVE.L teTextH(A3),A2
 BSET   #7,(A2)
 BEQ    @0
 BSET   #31,D6   ; save Lock Bit
@0 MOVE.L (A2),A2
 CMP.B  #$D,-1(A2,D4)
 BEQ    @1
 MOVE   D3,D6
 MOVE   D4,D7
 SUBQ   #1,D7
 JSR    TstTab   ; check if pseudoCR
 BNE    @2
@1 SUBQ #1,D4    ; for CR or pseudoCR (tab)

@2 MOVE.L teSelPoint(A3),D7 ; point.h
 SUB    teDestRect+left(A3),D7; relative xPosition
 BGT    @3
 MOVE   D3,D0
 BRA    StopHT

@3 MOVE D4,D6
 SUB    D3,D6
 CLR    D5; flag for 1st time thru loop
 MOVE   D5,D4  ; save high position
 CLR    -(SP)
 MOVE.L A2,-(SP) ; teTextH pointer
 MOVE   D3,-(SP) ; firstChar
 MOVE   D6,-(SP)
 MOVE   (SP)+,D5
 CMP    D5,D7
 DBGE   D6,loop  ; drops thru when D5 <= D7 < D4

 ADD    D3,D6  ; convert to absolute char position
 MOVE   D6,D0
; if = 0 (i.e. selPoint.h > end of line) then you're done
 TST    D4
 BEQ    StopHT
 SUB    D7,D4
 SUB    D5,D7
 CMP    D4,D7
 BLE    StopHT
 ADDQ   #1,D0

 TST.L  D6; restore Lock Bit
 BMI    @0
 MOVE.L teTextH(A3),A2
 BCLR   #7,(A2)  ; clear Lock bit

@0 CMP  D3,D0
 SEQ    teLftClick(A3)  ; click at beginning of line
 MOVE.L (A5),A0
 MOVE.L (A0),A0  ; thePort

 IF ROM128K <> 1 ; assemble if 128/512K Mac

;-------------- HiLite ---------------------------
; on entry: see above
; other variables:
;D5>> lineHeight(A3)
;A2>> pointer to lineStarts
; exit:
;A0 = thePort on exit as mandated by Inside Macintosh
; preserves all but A0,D0

 CMP    D3,D4
 BEQ    StopHL
 BGT    @0
 EXG    D3,D4

@0 LEA  teLines(A3),A2
@1 CMP  2(A2),D3
 BLT    @2
 ADDQ   #2,A2
 BRA    @1

@2 MOVE.L A2,D1  ; (A2) <= D3 < 2(A2)
 SUB    D0,D1
 ; D1 is offset of line containing D3 from lineStarts
 LSR    #1,D1  ; divide by 2 to give # lines
 MOVE   teLineHite(A3),D5
 MULU   D5,D1

 MOVE.L teDestRect(A3),teSelRect(A3)
 ADD    D1,teSelRect(A3)  ;TOP of 1st rect
 MOVE   teSelRect(A3),teSelRect+bottom(A3)
 ADD    D5,teSelRect+bottom(A3)    ; BOTTOM
 MOVE   #$7FFE,teSelRect+right(A3) ; RIGHT

 CLR    -(SP)
 MOVE.L teTextH(A3),A0
 MOVE.L (A0),-(SP)
 MOVE   (A2),D0
 MOVE   D0,-(SP)
 MOVE   D3,-(SP)
 SUB    D0,(SP)
 MOVE   (SP)+,D0
 ADD    D0,teSelRect+left(A3) ; LEFT of 1st rect
 CMP    2(A2),D4
 BLE    LastRect
 JSR    SubHilite; INVERT 1st rect
 ; LEFT of subsequent rects
 MOVE #$8002,teSelRect+left(A3)

@5 ADD  D5,teSelRect(A3)  ; TOP of subsequent rects
 ADD    D5,teSelRect+bottom(A3)  ; BOTTOM 
 ADDQ #2,A2
 CMP    2(A2),D4
 BLE    LastRect
 JSR    SubHilite; INVERT middle rects
 BRA    @5

 BEQ    @0
 CLR    -(SP)
 MOVE.L teTextH(A3),A0
 MOVE.L (A0),-(SP)
 MOVE   (A2),D0
 MOVE   D0,-(SP)
 MOVE   D4,-(SP)
 SUB    D0,(SP)
 MOVE   (SP)+,D0
 ADD    teDestRect+left(A3),D0
 MOVE   D0,teSelRect+right(A3); RIGHT of last rect
@0 JSR  SubHilite; INVERT last rect

 MOVE.L (A5),A0
 MOVE.L (A0),A0  ; thePort

;----------------- subHilite -------------------------
; if hiHook(A3)   0, jumps to it.  Otherwise inverts selRect(A3)

 PEA    teSelRect(A3)
 MOVE.L teHiHook(A3),A0
 CMP.L  #0,A0
 BEQ    @0
 JMP    (A0)
@0 _InverRect

 ENDIF  ; conditional assembly of HiLite routine

;---------- tablineStart -----------------------
; on entry:
;A3>> pointer to locked edit record
;A4,A6>> used by subroutines (don't change)
;D6>> char position (usually lineStart)
; on exit:
;D0>> next lineStart > D6
;others variables:
;A2>> # words in line
;D5>> pixel length of last tab
;D6>> 1st char of current line
;D7>> last char of current line
; subroutines:
;in $7F4returns D0 = length of text from D6 up to last 
;nonWordBreak char < D7
;in $7F8returns D1 = char position before 1st 
;wordBreak char > D0

 TST.L  nowTabs(A5); if not a tab Window
 BNE    @0;execute default routine
 MOVE.L stdLineStart(A5),A0
 JMP    (A0)

@0 MOVEM.LD1-D7/A1/A2,-(SP)
 MOVE   teDestRect+right(A3),D4
 SUB    teDestRect+left(A3),D4
 SUBQ   #1,D4
 SUB.L  A2,A2  ; clear A2 to use as counter
 MOVE   D6,D7

 MOVE   D7,D0
 MOVEQ  #$C,D2
 MOVE.L $7F8,A0  ; jumps to routine
 JSR    (A0);whose address is in global 7F8
 MOVE   D1,D7  ; increment D7 by one word
 TST.B  teCrOnly(A3)
 BNE    @0
 MOVE.L $7F4,A0  ; jumps to routine whose
 JSR    (A0);address is in global 7F4
 CMP    D4,D0  ; if   width destRect, put lineStart
 BGE    @2
@0 MOVE D7,D5
 ADDQ   #1,A2  ; increment word counter
 CMP    teLength(A3),D7
 BEQ    endLS  ; if at text end, put in lineStart
 MOVE.L teTextH(A3),A0
 MOVE.L (A0),A0
 CMP.B  #$D,(A0,D7.L); if = CR, put in a lineStart
 BEQ    @2
 JSR    tstTab ; if = pseudoCR, lineStart
 BEQ    @2
 ADDQ   #1,D7  ; otherwise look at next char
 BRA    LSloop
@2 MOVE A2,D0  ; if > than 1 word fits in destRect
 BEQ    oneWord  ; branch to fitting routine
 ADDQ   #1,D5

 MOVE   D5,D0
oneWord ; if one word > than the line, break it anyway
 SUBQ   #1,D7
 MOVE.L $7F4,A0  ; jumps to routine
 JSR    (A0); whose address is in global 7F4
 CMP    D0,D4  ; take off 1 char at a time until
 BLE    oneWord  ; word fragment fits in destRect
 MOVE   D7,D5
 BRA    endLS

;---------------- tabTxMeas -------------------------
;FUNCTION tabTxMeas(byteCount: integer; textAddr: Ptr; VAR 
;numer,denom: Point; VAR info: FontInfo): integer;
;CLR    -(SP)    22+24  room for result
;MOVE   byteCount,-(SP) 20+24
;PEA    text16+24
;PEA    numer,-(SP)12+24
;PEA    denom,-(SP) 8+24
;PEA    fontinfo   4+24
;JSR    tabTxMeas
;A2>> ptr to current char
;A3>> ptr to char right after last CR or TAB
;D3>> tabWidth
;D4>> counter of all char
;D5>>  textwidth  from last CR to present
;TAB (not inclusive)
;D6>> # of char since last CR or TAB (not inclusive)
;D7>> length of all text
;  all registers preserved except A0,D0


 MOVE   48(SP),D4; byteCount **
 MOVE.L 44(SP),A2; textPtr **
 MOVE.L nowTabs(A5),A0
 MOVE.L (A0),A0
 MOVE   tabWidth(A0),D3
 MOVE.L A2,A3  
 CLR    D5
 CLR    D6

 CLR    -(SP)
 MOVE   D4,-(SP)
 MOVE.L A2,-(SP)
 MOVE.L 48(SP),-(SP) ; numer **
 MOVE.L 48(SP),-(SP) ; denom **
 MOVE.L 48(SP),-(SP) ; fontinfo **
 MOVE   (SP)+,D7 ; length of all text
 SUBQ   #1,D4    ; DBxx counter

 CMP.B  #13,(A2)+; if char = CR, then
 BNE    chkTAB
 CLR    D5; clear LengthSinceLastCR
 BRA    bothCRandTab

 CMP.B  #9,-1(A2); is char = TAB?
 BNE    endLoop
 SUB    D3,D7
; width of tab put D7 in by StdTxMeas
 CLR    -(SP)
 MOVE   D6,-(SP)
 MOVE.L A3,-(SP)
 MOVE.L 48(SP),-(SP) ; numer **
 MOVE.L 48(SP),-(SP) ; denom **
 MOVE.L 48(SP),-(SP) ; fontinfo **
 ADD    (SP)+,D5

 MOVE.L nowTabs(A5),A0
 MOVE.L (A0),A0
 MOVE   lastTab(A0),D0
 BEQ    bothCRandTab
; ignore if no tabs set (this is a pseudoCR!)
 SUBQ   #1,D0    ; convert to DBxx counter
 LEA    TABS(A0),A0

@0 CMP  (A0)+,D5
 DBLT   D0,@0
 BGE    bothCRandTab
; branch if we are past last tab, i.e. ignore it (this is a pseudoCR!)
 ADD    -2(A0),D7
; adding pixel width of line including new tab to D7
 SUB    D5,D7  ; pixel width of line up to new tab
 MOVE   -2(A0),D5; new linewidth including new tab

 MOVEQ  #-1,D6
 ADDQ   #1,D6
 DBF    D4,chkCR

 MOVE   D7,50(SP); answer **

 ADD    #$16,SP
 JMP    (A0)

;----------------- tabTxWrite -----------------
;PROCEDURE tabTxWrite(byteCount: integer; textBuf: Ptr; 
;numer,denom: Point);
;MOVE   byteCount,-(SP) 16+24
;PEA    text12+24
;MOVE.L numer,-(SP) 8+24
;MOVE.L denom,-(SP) 4+24
;JSR    tabTxWrite
;A2>> ptr to current char
;A3>> ptr to char right after last TAB
;D4>> counter of all char
;D5>> length of text up from last CR
;to present TAB (not inclusive)
;D6>> number of char since last TAB (not inclusive)
;D7>> teDestRect.left
; A0,D0 not preserved

 MOVE   40(SP),D4; byteCount **
 SUBQ   #1,D4    ; make DBxx counter
 MOVE.L 36(SP),A2; textPtr **
 MOVE.L A2,A3  
 CLR    D6
 MOVE.L nowTE(A5),A0 ; handle to TErecord
 MOVE.L (A0),A0
 MOVE   2(A0),D7 ; destRect.left

 CMP.B  #9,(A2)+
 BNE    endLoop.

; write all chars since last tab
 MOVE   D6,-(SP) ; bytecount
 MOVE.L A3,-(SP) ; ptr to text
 MOVE.L 38(SP),-(SP) ; numer **
 MOVE.L 38(SP),-(SP) ; denom **

 MOVEQ  #-1,D6   ; reset counter
 MOVE.L A2,A3  ; text ptr points at char after tab

 CLR.L  -(SP)
 MOVE.L (SP)+,D5 ; horiz position in low word
 SUB    D7,D5  ; length of text from zero position

 MOVE.L nowTabs(A5),A0
 MOVE.L (A0),A0
 MOVE   lastTab(A0),D0
 BEQ    endLoop. ; ignore tab if none are set
 SUBQ   #1,D0    ; convert to DBxx counter
 LEA    TABS(A0),A0

@0 CMP  (A0)+,D5
 DBLT   D0,@0
 BGE    endLoop. ; branch if we are past last tab

 MOVE   -2(A0),D0
 SUB    D5,D0
 MOVE   D0,-(SP)
 CLR    -(SP)

 ADDQ   #1,D6
 DBF    D4,chkTab.

 MOVE   D6,-(SP) ; write all chars since last tab
 MOVE.L A3,-(SP)
 MOVE.L 38(SP),-(SP) ; numer **
 MOVE.L 38(SP),-(SP) ; denom **

 ADD    #$12,SP
 JMP    (A0)

;------------- tstTab ----------------------
; on entry:
;A0>> ptr to hText buffer
;D6>> char position of 1st char in line
;D7>> test char position
; sets Zbit of CC if char not pseudoCR (tab)
; preserves all registers except A0,D0,D1

 CMP.B  #$9,(A0,D7); if = TAB
 BNE    @0
 CLR    -(SP)
 MOVE.L A0,-(SP)
 MOVE   D6,-(SP) ; firstChar
 MOVE   D7,-(SP)
 SUB    D6,(SP)  ; byteCount
 MOVE   (SP)+,D0 ; length of text from D6 to D7
 MOVE.L nowTabs(A5),A0
 MOVE.L (A0),A0
 MOVE   lastTab(A0),D1
 LEA    TABS(A0),A0
 ADD    D1,D1
 CMP    -2(A0,D1),D0
 SLT    D0
; if D0 < lastTab position, return NotEqual
 TST.B  D0
@0 RTS


Dr. Nedrud wins $50 and our thanks for this extension to TextEdit, as this month's outstanding article.


Community Search:
MacTech Search:

Software Updates via MacUpdate

Chromium 119.0.6044.0 - Fast and stable...
Chromium is an open-source browser project that aims to build a safer, faster, and more stable way for all Internet users to experience the web. List of changes available here. Version for Apple... Read more
Spotify - Stream music, crea...
Spotify is a streaming music service that gives you on-demand access to millions of songs. Whether you like driving rock, silky R&B, or grandiose classical music, Spotify's massive catalogue puts... Read more
Tor Browser 12.5.5 - Anonymize Web brows...
Using Tor Browser you can protect yourself against tracking, surveillance, and censorship. Tor was originally designed, implemented, and deployed as a third-generation onion-routing project of the U.... Read more
Malwarebytes - Adware remova...
Malwarebytes (was AdwareMedic) helps you get your Mac experience back. Malwarebytes scans for and removes code that degrades system performance or attacks your system. Making your Mac once again your... Read more
TinkerTool 9.5 - Expanded preference set...
TinkerTool is an application that gives you access to additional preference settings Apple has built into Mac OS X. This allows to activate hidden features in the operating system and in some of the... Read more
Paragon NTFS 15.11.839 - Provides full r...
Paragon NTFS breaks down the barriers between Windows and macOS. Paragon NTFS effectively solves the communication problems between the Mac system and NTFS. Write, edit, copy, move, delete files on... Read more
Apple Safari 17 - Apple's Web brows...
Apple Safari is Apple's web browser that comes bundled with the most recent macOS. Safari is faster and more energy efficient than other browsers, so sites are more responsive and your notebook... Read more
Firefox 118.0 - Fast, safe Web browser.
Firefox offers a fast, safe Web browsing experience. Browse quickly, securely, and effortlessly. With its industry-leading features, Firefox is the choice of Web development professionals and casual... Read more
ClamXAV 3.6.1 - Virus checker based on C...
ClamXAV is a popular virus checker for OS X. Time to take control ClamXAV keeps threats at bay and puts you firmly in charge of your Mac’s security. Scan a specific file or your entire hard drive.... Read more
SuperDuper! 3.8 - Advanced disk cloning/...
SuperDuper! is an advanced, yet easy to use disk copying program. It can, of course, make a straight copy, or "clone" - useful when you want to move all your data from one machine to another, or do a... Read more

Latest Forum Discussions

See All

‘Monster Hunter Now’ October Events Incl...
Niantic and Capcom have just announced this month’s plans for the real world hunting action RPG Monster Hunter Now (Free) for iOS and Android. If you’ve not played it yet, read my launch week review of it here. | Read more »
Listener Emails and the iPhone 15! – The...
In this week’s episode of The TouchArcade Show we finally get to a backlog of emails that have been hanging out in our inbox for, oh, about a month or so. We love getting emails as they always lead to interesting discussion about a variety of topics... | Read more »
TouchArcade Game of the Week: ‘Cypher 00...
This doesn’t happen too often, but occasionally there will be an Apple Arcade game that I adore so much I just have to pick it as the Game of the Week. Well, here we are, and Cypher 007 is one of those games. The big key point here is that Cypher... | Read more »
SwitchArcade Round-Up: ‘EA Sports FC 24’...
Hello gentle readers, and welcome to the SwitchArcade Round-Up for September 29th, 2023. In today’s article, we’ve got a ton of news to go over. Just a lot going on today, I suppose. After that, there are quite a few new releases to look at... | Read more »
‘Storyteller’ Mobile Review – Perfect fo...
I first played Daniel Benmergui’s Storyteller (Free) through its Nintendo Switch and Steam releases. Read my original review of it here. Since then, a lot of friends who played the game enjoyed it, but thought it was overpriced given the short... | Read more »
An Interview with the Legendary Yu Suzuk...
One of the cool things about my job is that every once in a while, I get to talk to the people behind the games. It’s always a pleasure. Well, today we have a really special one for you, dear friends. Mr. Yu Suzuki of Ys Net, the force behind such... | Read more »
New ‘Marvel Snap’ Update Has Balance Adj...
As we wait for the information on the new season to drop, we shall have to content ourselves with looking at the latest update to Marvel Snap (Free). It’s just a balance update, but it makes some very big changes that combined with the arrival of... | Read more »
‘Honkai Star Rail’ Version 1.4 Update Re...
At Sony’s recently-aired presentation, HoYoverse announced the Honkai Star Rail (Free) PS5 release date. Most people speculated that the next major update would arrive alongside the PS5 release. | Read more »
‘Omniheroes’ Major Update “Tide’s Cadenc...
What secrets do the depths of the sea hold? Omniheroes is revealing the mysteries of the deep with its latest “Tide’s Cadence" update, where you can look forward to scoring a free Valkyrie and limited skin among other login rewards like the 2nd... | Read more »
Recruit yourself some run-and-gun royalt...
It is always nice to see the return of a series that has lost a bit of its global staying power, and thanks to Lilith Games' latest collaboration, Warpath will be playing host the the run-and-gun legend that is Metal Slug 3. [Read more] | Read more »

Price Scanner via

Clearance M1 Max Mac Studio available today a...
Apple has clearance M1 Max Mac Studios available in their Certified Refurbished store for $270 off original MSRP. Each Mac Studio comes with Apple’s one-year warranty, and shipping is free: – Mac... Read more
Apple continues to offer 24-inch iMacs for up...
Apple has a full range of 24-inch M1 iMacs available today in their Certified Refurbished store. Models are available starting at only $1099 and range up to $260 off original MSRP. Each iMac is in... Read more
Final weekend for Apple’s 2023 Back to School...
This is the final weekend for Apple’s Back to School Promotion 2023. It remains active until Monday, October 2nd. Education customers receive a free $150 Apple Gift Card with the purchase of a new... Read more
Apple drops prices on refurbished 13-inch M2...
Apple has dropped prices on standard-configuration 13″ M2 MacBook Pros, Certified Refurbished, to as low as $1099 and ranging up to $230 off MSRP. These are the cheapest 13″ M2 MacBook Pros for sale... Read more
14-inch M2 Max MacBook Pro on sale for $300 o...
B&H Photo has the Space Gray 14″ 30-Core GPU M2 Max MacBook Pro in stock and on sale today for $2799 including free 1-2 day shipping. Their price is $300 off Apple’s MSRP, and it’s the lowest... Read more
Apple is now selling Certified Refurbished M2...
Apple has added a full line of standard-configuration M2 Max and M2 Ultra Mac Studios available in their Certified Refurbished section starting at only $1699 and ranging up to $600 off MSRP. Each Mac... Read more
New sale: 13-inch M2 MacBook Airs starting at...
B&H Photo has 13″ MacBook Airs with M2 CPUs in stock today and on sale for $200 off Apple’s MSRP with prices available starting at only $899. Free 1-2 day delivery is available to most US... Read more
Apple has all 15-inch M2 MacBook Airs in stoc...
Apple has Certified Refurbished 15″ M2 MacBook Airs in stock today starting at only $1099 and ranging up to $230 off MSRP. These are the cheapest M2-powered 15″ MacBook Airs for sale today at Apple.... Read more
In stock: Clearance M1 Ultra Mac Studios for...
Apple has clearance M1 Ultra Mac Studios available in their Certified Refurbished store for $540 off original MSRP. Each Mac Studio comes with Apple’s one-year warranty, and shipping is free: – Mac... Read more
Back on sale: Apple’s M2 Mac minis for $100 o...
B&H Photo has Apple’s M2-powered Mac minis back in stock and on sale today for $100 off MSRP. Free 1-2 day shipping is available for most US addresses: – Mac mini M2/256GB SSD: $499, save $100 –... Read more

Jobs Board

Licensed Dental Hygienist - *Apple* River -...
Park Dental Apple River in Somerset, WI is seeking a compassionate, professional Dental Hygienist to join our team-oriented practice. COMPETITIVE PAY AND SIGN-ON Read more
Sublease Associate Optometrist- *Apple* Val...
Sublease Associate Optometrist- Apple Valley, CA- Target Optical Date: Sep 30, 2023 Brand: Target Optical Location: Apple Valley, CA, US, 92307 **Requisition Read more
*Apple* / Mac Administrator - JAMF - Amentum...
Amentum is seeking an ** Apple / Mac Administrator - JAMF** to provide support with the Apple Ecosystem to include hardware and software to join our team and Read more
Child Care Teacher - Glenda Drive/ *Apple* V...
Child Care Teacher - Glenda Drive/ Apple ValleyTeacher Share by Email Share on LinkedIn Share on Twitter Read more
Cashier - *Apple* Blossom Mall - JCPenney (...
Cashier - Apple Blossom Mall Location:Winchester, VA, United States ( - Apple Blossom Mall Read more
All contents are Copyright 1984-2011 by Xplain Corporation. All rights reserved. Theme designed by Icreon.