TweetFollow Us on Twitter

Tabs
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}

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

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

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

VAR
  done: boolean;
  myWindow: WindowPtr;
  nowTE:TEHandle;
  nowTabs:tabHandle;
  textCursor:  cursHandle;
  DragArea: Rect;
  stdTEDoText: LongInt;
  stdLineStart:  LongInt;
  globalA70:LIPtr;
  global7FC:LIPtr;
  myQDProcs:QDProcs;

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

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

PROCEDURE SetUpForTabs(resID:integer; wPtr: windowPtr);
Var
    aHndl,resHndl: handle;
    tabH: tabHandle;
    i,widthZeroChar: integer;
    bTABsize:    longInt;
Begin
  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}
  BlockMove(resHndl^,@tabH^^.lastTab,bTABsize);
  ReleaseResource(resHndl);

  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;
End;

{---------------------------------------------------------}

PROCEDURE SetupMenus;
VarMenuTopic: MenuHandle;
Begin
   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}
   InsertMenu(MenuTopic,0);
 
   MenuTopic := GetMenu(EditMenu);
   InsertMenu(MenuTopic,0);
 
   DrawMenuBar;
End;

{--------------------------------------------------------}

FUNCTION SetUpTextWindow(ID_No: integer): WindowPtr;

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

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

  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);
  SetWRefCon(myW,ord4(Hndl));
{myW refCon has handle to a relocatable block }
{containing only a TEhandle for the moment}

  SetUpForTabs(ID_No,myW);
{should be called once for every new window supporting tabs}
  SetUpTextWindow:= myW;
End;

{-------------------------------------------------------}

PROCEDURE Initialize;

var
  i:  integer;
  r:  Rect;

Begin
  InitGraf(@thePort);     {create a grafport for the screen}
  InitFonts;
  InitWindows;
  InitMenus;
  TEInit;
  InitDialogs(Nil);
  FlushEvents(everyEvent,0);

  r:= ScreenBits.Bounds;
  SetRect(DragArea,r.left+4,r.top+24,r.right-4,r.bottom-4);
  done:= FALSE;  {set by QUIT command to signal end}
  SetupMenus;

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

  textCursor := GetCursor(ibeamCursor);
  HLock(Handle(textCursor));
  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}
  SetStdProcs(myQDProcs);
  myQDProcs.txMeasProc:= @tabTxMeas;
  myQDProcs.textProc:= @tabTxWrite;

End;

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

PROCEDURE ProcessMenu(CodeWord:longint);
Var
  i,Menu_No,{menu number selected}
  Item_No:integer; {item in selected  menu}
  NameHolder:  Str255;    {for desk accessory}
  DNA:  integer; {dummy return}
  ourDlg: dialogPtr;
Begin
  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));
             ModalDialog(NIL,i);
             DisposDialog(ourDlg);
            end
       else begin{Desk Accessories}
            GetItem(GetMHandle(AppleMenu),Item_No,NameHolder);
            DNA := OpenDeskAcc(NameHolder);
            end;
  
    FileMenu: CASE Item_No OF
              1: begin
                  myWindow:= SetUpTextWindow(windID);  {NEW}
                  nowTabs:= tabHandle(GetWRefCon(myWindow));
                  nowTE:= nowTabs^^.tabTE;
                  DisableItem(GetMHandle(FileMenu),1);
                 end;
              2: done:= TRUE; {QUIT}
            end  {CASE};
  
    EditMenu:
      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);
Var
  WindowPointedTo: WindowPtr;
  MouseLoc: Point;
  WindoLoc: integer;
Begin
  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
      inContent:
        BEGIN
           GlobalToLocal(MouseLoc);
 TEClick(MouseLoc,(BitAnd(Event.modifiers,shiftKey)      = shiftKey),nowTE);
          END;
       inGrow: {empty statement};
       inDrag:
 DragWindow(WindowPointedTo,MouseLoc,DragArea);
       inGoAway:
        If TrackGoAway(WindowPointedTo,MouseLoc) then
           begin
             TEDispose(nowTE);
             DisposHandle(handle(nowTabs));
             nowTabs:= NIL;
             DisposeWindow(WindowPointedTo);
            EnableItem(GetMHandle(FileMenu),1);
           end;
      END {CASE};
  End  {CASE};
End{MouseDowns};

{-----------------------------------------------}

PROCEDURE KeyDowns(Event:EventRecord);
VarCharCode:char;
Begin
  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);
Var
  TargetWindow:  WindowPtr;
  active: boolean;
  aHndl:handle;

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

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

End{Activates};

{-------------------------------------------------}

PROCEDURE Updates(Event:EventRecord);
Var
  UpDateWindow,
  savePort: WindowPtr;
tempTabs: TabHandle;
Begin
  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));
  BeginUpDate(UpDateWindow);
  EraseRect(UpDateWindow^.VisRgn^^.rgnBBox);
  with nowTabs^^  DO TEUpdate(tabTE^^.viewRect,tabTE);
  EndUpDate(UpDateWindow);
  SetPort(savePort);             {restore to the previous port}
  nowTabs:= tempTabs;     {restore to current tabHandle}
End{Updates};

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

PROCEDURE MainEventLoop;
Var
  Event:EventRecord;
  mousePt: Point;
Begin
  Repeat
    SystemTask;             {run Desk Accessories}
   
    if myWindow = FrontWindow then
      begin
          GetMouse(MousePt);
          if PtinRect(MousePt,nowTE^^.viewRect)
              then SetCursor(textCursor^^)
             else SetCursor(arrow);
          TEIdle(nowTE);
      end;

    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}
End;

{-----------------------------------------------}

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



;
;tabGlue
;
; 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
 ROM128KEQU 0
; 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
 TABS EQU 8
; 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
 ENDIF

;------------- 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)
;
;--------------------------------------
tabTEdoText
 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
 ELSE 
 BPL    @0
 ENDIF
 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
;exit:
;A0 = thePort on exit as mandated by Inside Macintosh
; selRect encloses caret
;------------------------------------------
setCaret
 MOVEM.LD2/D6/D7/A2,-(SP)
 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)
 _TextWidth
 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
 MOVEM.L(SP)+,D2/D6/D7/A2
 MOVE.L (A5),A0
 MOVE.L (A0),A0  ; A0 = thePort (I don't know why)
 RTS

;---------------- 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
;--------------------------------------------------
HitTest
 MOVEM.LA2/D4-D7,-(SP)

 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.L A2,A0
 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
loop
 MOVE   D5,D4  ; save high position
 CLR    -(SP)
 MOVE.L A2,-(SP) ; teTextH pointer
 MOVE   D3,-(SP) ; firstChar
 MOVE   D6,-(SP)
 _TextWidth
 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

StopHT
 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
 MOVEM.L(SP)+,A2/D4-D7
 RTS

 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
;-------------------------------------------

HiLite
 MOVEM.LA2/D3-D5,-(SP)
 CMP    D3,D4
 BEQ    StopHL
 BGT    @0
 EXG    D3,D4

@0 LEA  teLines(A3),A2
 MOVE.L A2,D0
@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)
 _TextWidth
 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

LastRect
 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)
 _TextWidth
 MOVE   (SP)+,D0
 ADD    teDestRect+left(A3),D0
 MOVE   D0,teSelRect+right(A3); RIGHT of last rect
@0 JSR  SubHilite; INVERT last rect

StopHL
 MOVE.L (A5),A0
 MOVE.L (A0),A0  ; thePort
 MOVEM.L(SP)+,A2/D3-D5
 RTS

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

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

 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
;-------------------------------------------------

tablineStart
 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

LSloop
 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
 MOVEM.LD6/D7,-(SP)
 MOVE.L $7F4,A0  ; jumps to routine whose
 JSR    (A0);address is in global 7F4
 MOVEM.L(SP)+,D6/D7
 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
 AND.L  #$FFFF,D7
 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

endLS
 MOVE   D5,D0
 MOVEM.L(SP)+,D1-D7/A1/A2
 RTS
oneWord ; if one word > than the line, break it anyway
 SUBQ   #1,D7
 MOVEM.LD6/D7,-(SP)
 MOVE.L $7F4,A0  ; jumps to routine
 JSR    (A0); whose address is in global 7F4
 MOVEM.L(SP)+,D6/D7
 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
;----------------------------------------------------

tabTxMeas
 MOVEM.LA2-A3/D3-D7,-(SP)

 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 **
 _StdTxMeas
 MOVE   (SP)+,D7 ; length of all text
 SUBQ   #1,D4    ; DBxx counter

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

chkTAB
 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 **
 _StdTxMeas
 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

bothCRandTab
 MOVEQ  #-1,D6
 MOVE.L A2,A3
endLoop
 ADDQ   #1,D6
 DBF    D4,chkCR

 MOVE   D7,50(SP); answer **

 MOVEM.L(SP)+,A2-A3/D3-D7
 MOVE.L (SP),A0
 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
;-------------------------------------------------
tabTxWrite

 MOVEM.LA2-A3/D4-D7,-(SP)
 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

chkTab.
 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 **
 _StdText

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

 CLR.L  -(SP)
 MOVE.L SP,-(SP)
 _GetPen
 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)
 _Move

endLoop.
 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 **
 _StdText

StopTO
 MOVEM.L(SP)+,A2-A3/D4-D7
 MOVE.L (SP),A0
 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
;--------------------------------------------
tstTab

 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
 _TextWidth
 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

 END

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

Latest Forum Discussions

See All

Whitethorn Games combines two completely...
If you have ever gone fishing then you know that it is a lesson in patience, sitting around waiting for a bite that may never come. Well, that's because you have been doing it wrong, since as Whitehorn Games now demonstrates in new release Skate... | Read more »
Call of Duty Warzone is a Waiting Simula...
It's always fun when a splashy multiplayer game comes to mobile because they are few and far between, so I was excited to see the notification about Call of Duty: Warzone Mobile (finally) launching last week and wanted to try it out. As someone who... | Read more »
Albion Online introduces some massive ne...
Sandbox Interactive has announced an upcoming update to its flagship MMORPG Albion Online, containing massive updates to its existing guild Vs guild systems. Someone clearly rewatched the Helms Deep battle in Lord of the Rings and spent the next... | Read more »
Chucklefish announces launch date of the...
Chucklefish, the indie London-based team we probably all know from developing Terraria or their stint publishing Stardew Valley, has revealed the mobile release date for roguelike deck-builder Wildfrost. Developed by Gaziter and Deadpan Games, the... | Read more »
Netmarble opens pre-registration for act...
It has been close to three years since Netmarble announced they would be adapting the smash series Solo Leveling into a video game, and at last, they have announced the opening of pre-orders for Solo Leveling: Arise. [Read more] | Read more »
PUBG Mobile celebrates sixth anniversary...
For the past six years, PUBG Mobile has been one of the most popular shooters you can play in the palm of your hand, and Krafton is celebrating this milestone and many years of ups by teaming up with hit music man JVKE to create a special song for... | Read more »
ASTRA: Knights of Veda refuse to pump th...
In perhaps the most recent example of being incredibly eager, ASTRA: Knights of Veda has dropped its second collaboration with South Korean boyband Seventeen, named so as it consists of exactly thirteen members and a video collaboration with Lee... | Read more »
Collect all your cats and caterpillars a...
If you are growing tired of trying to build a town with your phone by using it as a tiny, ineffectual shover then fear no longer, as Independent Arts Software has announced the upcoming release of Construction Simulator 4, from the critically... | Read more »
Backbone complete its lineup of 2nd Gene...
With all the ports of big AAA games that have been coming to mobile, it is becoming more convenient than ever to own a good controller, and to help with this Backbone has announced the completion of their 2nd generation product lineup with their... | Read more »
Zenless Zone Zero opens entries for its...
miHoYo, aka HoYoverse, has become such a big name in mobile gaming that it's hard to believe that arguably their flagship title, Genshin Impact, is only three and a half years old. Now, they continue the road to the next title in their world, with... | Read more »

Price Scanner via MacPrices.net

B&H has Apple’s 13-inch M2 MacBook Airs o...
B&H Photo has 13″ MacBook Airs with M2 CPUs and 256GB of storage in stock and on sale for up to $150 off Apple’s new MSRP, starting at only $849. Free 1-2 day delivery is available to most US... Read more
M2 Mac minis on sale for $100-$200 off MSRP,...
B&H Photo has Apple’s M2-powered Mac minis back in stock and on sale today for $100-$200 off MSRP. Free 1-2 day shipping is available for most US addresses: – Mac mini M2/256GB SSD: $499, save $... Read more
Mac Studios with M2 Max and M2 Ultra CPUs on...
B&H Photo has standard-configuration Mac Studios with Apple’s M2 Max & Ultra CPUs in stock today and on Easter sale for $200 off MSRP. Their prices are the lowest available for these models... Read more
Deal Alert! B&H Photo has Apple’s 14-inch...
B&H Photo has new Gray and Black 14″ M3, M3 Pro, and M3 Max MacBook Pros on sale for $200-$300 off MSRP, starting at only $1399. B&H offers free 1-2 day delivery to most US addresses: – 14″ 8... Read more
Department Of Justice Sets Sights On Apple In...
NEWS – The ball has finally dropped on the big Apple. The ball (metaphorically speaking) — an antitrust lawsuit filed in the U.S. on March 21 by the Department of Justice (DOJ) — came down following... Read more
New 13-inch M3 MacBook Air on sale for $999,...
Amazon has Apple’s new 13″ M3 MacBook Air on sale for $100 off MSRP for the first time, now just $999 shipped. Shipping is free: – 13″ MacBook Air (8GB RAM/256GB SSD/Space Gray): $999 $100 off MSRP... Read more
Amazon has Apple’s 9th-generation WiFi iPads...
Amazon has Apple’s 9th generation 10.2″ WiFi iPads on sale for $80-$100 off MSRP, starting only $249. Their prices are the lowest available for new iPads anywhere: – 10″ 64GB WiFi iPad (Space Gray or... Read more
Discounted 14-inch M3 MacBook Pros with 16GB...
Apple retailer Expercom has 14″ MacBook Pros with M3 CPUs and 16GB of standard memory discounted by up to $120 off Apple’s MSRP: – 14″ M3 MacBook Pro (16GB RAM/256GB SSD): $1691.06 $108 off MSRP – 14... Read more
Clearance 15-inch M2 MacBook Airs on sale for...
B&H Photo has Apple’s 15″ MacBook Airs with M2 CPUs (8GB RAM/256GB SSD) in stock today and on clearance sale for $999 in all four colors. Free 1-2 delivery is available to most US addresses.... Read more
Clearance 13-inch M1 MacBook Airs drop to onl...
B&H has Apple’s base 13″ M1 MacBook Air (Space Gray, Silver, & Gold) in stock and on clearance sale today for $300 off MSRP, only $699. Free 1-2 day shipping is available to most addresses in... Read more

Jobs Board

Medical Assistant - Surgical Oncology- *Apple...
Medical Assistant - Surgical Oncology- Apple Hill Location: WellSpan Medical Group, York, PA Schedule: Full Time Sign-On Bonus Eligible Remote/Hybrid Regular Apply Read more
Omnichannel Associate - *Apple* Blossom Mal...
Omnichannel Associate - Apple Blossom Mall Location:Winchester, VA, United States (https://jobs.jcp.com/jobs/location/191170/winchester-va-united-states) - Apple Read more
Cashier - *Apple* Blossom Mall - JCPenney (...
Cashier - Apple Blossom Mall Location:Winchester, VA, United States (https://jobs.jcp.com/jobs/location/191170/winchester-va-united-states) - Apple Blossom Mall Read more
Operations Associate - *Apple* Blossom Mall...
Operations Associate - Apple Blossom Mall Location:Winchester, VA, United States (https://jobs.jcp.com/jobs/location/191170/winchester-va-united-states) - Apple Read more
Business Analyst | *Apple* Pay - Banco Popu...
Business Analyst | Apple PayApply now " Apply now + Apply Now + Start applying with LinkedIn Start + Please wait Date:Mar 19, 2024 Location: San Juan-Cupey, PR Read more
All contents are Copyright 1984-2011 by Xplain Corporation. All rights reserved. Theme designed by Icreon.