

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

- SPREAD THE WORD:
- Slashdot
- Digg
- Del.icio.us
- Newsvine