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.