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 TE record). 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 TE record
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