|
Volume Number: | 5 | |
Issue Number: | 3 | |
Column Tag: | HyperChat™ |
Related Info: Control Manager Quickdraw
XCMD Corner: Window Scrollbars
By Joe Zuffoletto, Apple Computer, Inc.
Note: Source code files accompanying article are located on MacTech CD-ROM or source code disks.
In Last Month's Episode...
Last month I showed you how to write an event-driven XCMD that displays a normal Macintosh window on top of HyperCard. I discussed some interesting human interface issues that come up when you do this, and I demonstrated how to communicate with HyperCard from an XCMD in order to do window updates and other “human interface maintenance” tasks. I also used the XCMD as a springboard for illustrating more general techniques, including how to be MultiFinder aware, how to do cursor tracking under MultiFinder, and how to cope elegantly with multiple-screen environments. Finally, I gave the window scroll bars that didn’t work, and I promised to “breathe life into them” this month, which I will do by adding code to the XCMD that takes a “snapshot” of the current card and displays it in the window. We can then scroll around and admire the snapshot until we either get bored, learn about scrolling, or both!
Scroll Bars!
The scroll bars are here!
The scroll bars are here!
Why should I care?
People have been scrolling for years....
Yes, but few people have taken the time and effort to explain scrolling to neophyte Mac programmers, who must either figure it out themselves, copy code from someone else, or avoid it completely. Why is this? I have a simple theory: People are ashamed of their scrolling code. In fact, some people might be afraid of their scrolling code. Scrolling code is ugly. It consists of gobs of source riddled with special cases and questionable algorithms. Nobody has found an elegant way to do it, so nobody wants to show their version of it to anybody else.
On top of all this, scrolling is implemented differently depending on the type of document being scrolled, be it bitmapped graphics, object-oriented graphics, text, icon lists, etc. While the basic rules stay the same, the special cases change and often multiply. Therefore, learning everything there is to know about scrolling is a daunting task indeed.
I certainly don’t know everything about scrolling, but I know enough to get you started if you’ve never done it before, and I can touch on a few fundamentals that may help you improve your scrolling code if you’ve written some already. In this article I will show how to scroll bitmapped graphics documents, including how to set up and manage an offscreen bitmap for fast, flicker-free scrolling. I will assume we have a static document that cannot be edited or moved around inside its window. I leave these more challenging cases as proverbial exercises for the reader.
If you haven’t done so already, read the Control Manager chapter of Inside Macintosh, Volume I so I don’t have to explain the Toolbox software behind controls. I will repeat some of the general material in that chapter, but I will not go into the data structures and other nuts-and-bolts information because there just isn’t room for it here.
Those of you who have never written scrolling code stand to benefit the most because, like I said, annotated examples of it don’t exactly abound. Unfortunately, the only thing worse than implementing scroll bars is explaining how the code works, so bear with me because there is a lot to cover and most of it is very dry. My plan is to first discuss the anatomy and arithmetic of scroll bars, then talk about all the special cases and implementation details. Finally, as a special bonus, I will describe how to set up and maintain an offscreen bitmap for a document you are scrolling.
This is a ton of material, but remember: No pain, no gain; no guts, no glory. Onward!
The Anatomy of Scroll Bars
Most of this section is a review of material in the Control Manager chapter of Inside Macintosh, Volume I, and of the Scroll Bars specification in Apple’s Human Interface Guidelines: The Apple Desktop Interface (Addison-Wesley, 1987) I repeat it here in order to fix terms and to lend continuity to the article.
Scroll bars consist of five parts, as illustrated in Figure 1. These parts are the up arrow, page up region, thumb, page down region, and down arrow. The names are the same for a horizontal scroll bar, with the left end being “up.”
Figure 1
The up and down arrows scroll the document arbitrarily small but equal distances in their respective directions. If you are scrolling text, for example, the arrows might scroll it one line at a time. If you are scrolling graphics, you might shift your document 5 to 10 pixels at a time depending on how fast and smooth you want the scrolling to be. In any event, the distance scrolled remains constant, regardless of window size.
Page up and page down scroll the document one “page” or “windowful” at a time. Most applications scroll slightly less than a windowful so users can keep track of where they are as they scroll through long documents. Obviously, the distance scrolled varies with window size.
Finally, the thumb can be dragged anywhere in the scroll bar; when it is released, the application scrolls to the corresponding position in the document. The distance scrolled depends only on the thumb displacement and is independent of window size.
Scroll bars can be either active, unhighlighted, or inactive. A scroll bar is active when the window that owns it is active and the document can be scrolled in the scroll bar’s direction; active scroll bars are illustrated in Figure 2a.
Figure 2a
If the owning window is active but the document cannot be scrolled (e.g., when the entire document is visible in the window), then the scroll bars are unhighlighted, and they appear as in Figure 2b.
Figure 2b
Finally, if the owning window is inactive, the scroll bars must be inactive and look like those in Figure 2c.
Figure 2c
Scroll Bar Arithmetic
You will find it much easier to write scrolling code if you can express scroll bar behavior in simple algebraic form. Good scrolling code, like QuickDraw, stands on a firm mathematical foundation. Building this foundation is best accomplished by drawing some pictures.
Figure 3 shows a large Macintosh window and a long, skinny document we wish to scroll. I have labeled important dimensions on the diagram; some of these will figure prominently in our algebraic expression of scrolling behavior.
Figure 3
I define docWidth and docHeight as the width and height of the document’s rectangle; visWidth and visHeight as the width and height of the portion of the document visible in the window; and dhGraf and dvGraf as the distances the document has been scrolled in the horizontal and vertical directions, respectively. dhGraf = dvGraf = 0 when the top left corners of the document and the window’s portRect coincide, and both of these variables increase as the document moves up and to the left relative to the window; i.e., as we scroll down and to the right. The units for all these variables are pixels.
These are all the variables we need to express the document’s position relative to the window. But we also need to express the thumbs’ positions relative to the top (or bottom) of the scroll bars. The thumbs are moved by changing their control values; if we find the algebraic link between the control values and the variables described above, life will be great.
Inside Macintosh tells us that every control has predefined minimum and maximum values and a current value; this information is stored in the ControlRecord data structure for that control, and is initialized by us when we create the control with NewControl. The current value, of course, is where the thumb happens to be relative to the minimum and maximum values, so our first decision is: what should the minimum and maximum values be? Although you can choose a scheme as convoluted as you desire, I’ll choose zero for the minimums, and docWidth and docHeight as the maximums of the horizontal and vertical scroll bars, respectively. I’ll also initialize both current values (thumbs) to zero, placing us at the top left corner of the document.
The Golden Ratio of Scrolling
From now on, I’ll simplify the discussion by considering only the vertical scroll bar; the exact same principles apply to the horizontal one. Here’s a quick exercise: dvGraf limits how far we can scroll up and down in the document. It’s smallest possible value is zero, when we are at the top of the document. What’s the largest value dvGraf can have? docHeight? No! The correct answer is docHeight - visHeight, because once the bottom of the document is visible above the top of the horizontal scroll bar, we don’t need to scroll down any more.
The largest value the thumb can have, however, is docHeight. At the top and bottom of the document, then, we have the following relationships:
--1 Top: dvGraf = 0 vThumb = 0 Bottom: dvGraf = docHeight - visHeight vThumb = docHeight
This suggests an important proportional relationship we must preserve as we scroll through the document:
--2 vThumb = (dvGraf * docHeight) / (docHeight - visHeight)
As a check, substituting dvGraf = 0 into the above equation gives vThumb = 0, and substituting dvGraf = docHeight - visHeight gives vThumb = docHeight. I call this the Golden Ratio of Scrolling, just because lots of high-falutin’ proportions in mathematics are called the Golden Ratio of something or another. My Golden Ratio will ensure that you scroll your documents the way users expect you to.
We now have enough mathematical background to implement scroll bars. Before diving into the code, however, let’s learn about offscreen bitmaps and blitting, which will make our scrolling smooth and fast.
Bits, Blits, and CopyBits
You may have noticed that some applications have smooth, flicker-free scrolling while others have the choppy, flicker-ful variety. What you are witnessing is the almost total supremacy of CopyBits and blitting over ScrollRect in the scrolling arena. The basic idea is that CopyBits lets you blast an offscreen copy of your document to the screen immediately after you scroll (this is called blitting), while ScrollRect waits around for update events to be handled. On the downside, blitting takes a lot more memory, and you really have to be clever when you try to use it with large documents or large displays.
Scott Knaster gives a great explanation of blitting and offscreen bitmaps in his book Macintosh Programming Secrets (Addison-Wesley, 1988); I’m going to condense a similar discussion into a much smaller space here. If you get confused, check out pages 190-208 of Scott’s book.
Let’s take a concrete example. My XCMD takes a snapshot of HyperCard’s card window, adds 16 pixels of white padding to the right and bottom edges, and stores the picture into an offscreen bitmap, which is just a nonrelocatable buffer in memory with a bitmap’s structure. We will use NewPtr to allocate memory for the bitmap, so we need to determine how many bytes of memory we need. The card window is 342 pixels tall by 512 wide; adding 16 to both dimensions gives a rectangle 358 x 528 pixels. First we calculate rowBytes using Knaster’s incredibly bizarre but effective formula:
rowBytes = (((pict.right - pict.left - 1) div 16) + 1) * 2 = (((528 - 0 - 1) div 16) + 1) * 2 = 66
From here on out things are much more intuitive:
bufferSize = (pict.bottom - pict.top) * rowBytes = 358 * 66 = 23628 bytes
So to create our bitmap we make the following calls:
VAR myBits: BitMap; myBits.rowBytes := 66; myBits.baseAddr := NewPtr(23628); { do error checking, of course } SetRect(myBits.bounds,0,0,528,358);
All that’s left is to copy a picture of the card window into the bitmap; see procedure GetHCBitMap in the source listing for details.
Scrolling and Blitting
Now that our offscreen bitmap is set up, it’s time link it to the scroll bars. This is where that great eclectic concept of blitting comes in.
Quick and efficient blitting is not accomplished by blasting the entire offscreen image to the screen and clipping to the portRect (minus the scroll bars) of the target window. If you did that and Bill Atkinson saw your code, he would say, “No, no, no, that’s dumb.”
Instead, Bill would suggest that you copy a piece of the offscreen image just big enough to fit in your window. Assuming you are scrolling around and want to see different pieces of the image, you need a scheme for deciding which piece to copy. This is where the concept of a blit rectangle comes in.
(By the way, Bill Atkinson probably knows how to compress an arbitrary 512 by 342 pixel bitmap into three bits, so he might suggest something even more efficient. If he does, listen carefully and write it all down, because you’ll never figure it out yourself.)
A blit rectangle is an image of the onscreen window’s portRect (minus the scroll bars) superimposed on the offscreen bitmap; see Figure 4. It is visWidth wide by visHeight tall. If you resize the onscreen window, you resize the blit rectangle accordingly, except that the blit rectangle can never be larger than the offscreen bitmap or you will blast the random garbage that lies beyond into your onscreen window. Similarly, when you scroll the onscreen window, you slide the blit rectangle the appropriate distance and direction, always staying within the bounding rectangle of the offscreen bitmap.
Figure 4
After you have resized or moved the blit rectangle as described above, you copy its contents to the onscreen window with CopyBits. In the case of scrolling, the copy is performed immediately, but in the cases of zooming or resizing, the copy is performed when update events are handled. Summarizing, the sequence of steps is:
• scroll onscreen window and move thumb
• slide blit rectangle proper distance and direction over offscreen bitmap (don’t let it slide off, however!)
• copy contents of blit rectangle to onscreen window immediately
-or-
• zoom or resize onscreen window
• zoom or resize blit rectangle, up to size of offscreen bitmap
• copy contents of blit rectangle to onscreen window at update time
That’s it for the concepts behind scrolling and blitting. Now we can look at the code and see how this stuff is done in practice, complete with refCon data structures, special cases, and other nasty ugliness.
First, a Pair of Data Structures
In this section and the ones that follow, I’m going to walk through the code that was stubbed out of last month’s listing. For your convenience, however, I’m including the entire source of the Window XCMD.
Starting with the TYPE declarations, notice the two data structures I’ve defined, OffScrRecord and ScrollRecord (wonder no more, newcomers to Mac programming, you’re about to find out what on earth refCon fields are used for! ). OffScrRecord contains window-specific information, including a pointer to the grafPort containing the offscreen bitmap for the window, the width and height of the document in the bitmap, the current size of the blit rectangle, and current scroll state of the document. A handle to this information is stored in the window’s refCon field, and I continually update the OffScrRecord via this handle in my scrolling code.
ScrollRecord contains two rather cryptic fields that I use for human interface purposes; I’ll explain them later. In this program, I create only one ScrollRecord on the heap and share it among all my scroll bars. You’ll soon understand why this is possible.
Allocating space on the heap for these data structures is accomplished with a parentheses-rich Pascal statement of the form:
myBlobHandle := BlobHandle(NewHandle(SizeOf(BlobRecord)));
How to Initialize Scroll Bars
Let’s skip down to the main program, and I’ll touch on the procedures used for initializing the scroll bars and offscreen bitmap.
After we’ve put the owning window on the heap, we go ahead and initialize its scroll bars. First we allocate space for the ScrollRecord, because if there’s no room for it we have to quit. Assuming there is space, we grab our handle to it and initialize the ScrollRecord’s fields. Again, I’ll get to what they mean in a minute.
Next we call CreateHScrollBar and CreateVScrollBar, functions of my own design that allocate and return ControlHandles to the respective scroll bars for a given window. I recommend you name your controls as shown because it helps out in debugging.
HiliteScrollBars is called next; it checks the size of the window against the size of the document and unhighlights the appropriate scroll bar if the window is bigger in a given direction (see Figure 2b). Otherwise it checks the scrolled state of the document (given by dvGraf and dhGraf; see Figure 3) and positions the thumbs accordingly. Notice the use of the Golden Ratio. Notice also that HiliteScrollBars is called whenever:
• the window is made active
• the window is resized or zoomed
• the window is updated
After the window and scroll bars are drawn, DrawContents blasts the bits inside the blit rectangle to the screen. The offscreen bitmap has already been initialized by GetHCBitMap, and the blit rectangle has been initialized to the starting window size by the InitBlit procedure.
Scrolling
If we got this far, we should be zipping along in our main event loop, ready to handle events to our scroll bars. Remember from Inside Macintosh that the scroll bars sit on the content region of the window, so when we get a mouseDown there, we first call FindControl, passing it the location of the mouseDown in global coordinates, to find out which control, if any, was hit.
One possibility is that the user grabs the thumb, in which case we call ScrollWithThumb. This routine uses the control titles to determine which of the two thumbs, horizontal or vertical, was grabbed; then it records the start value of the thumb before the thumb is moved. At this point TrackControl is called to handle the dragging of the thumb; when the thumb is released, the ball is in our court again and we immediately record the thumb’s end value. The difference between the start and end values (given by the LONGINT amountToScroll) is munged through a rearranged version of the Golden Ratio, and the result tells us how far to scroll the document. The actual scrolling is accomplished by ScrollContents, which first slides the blit rectangle to the proper location on the offscreen bitmap before calling DrawContents to blast the bits to the screen.
If the user clicks instead on one of the arrows or on a page up/page down region, things get a little more complicated. Let me begin by explaining a feature I call “accelerating thumbs.”
Today’s Macintoshes, of course, are much faster than their early predecessors, which is wonderful. Because of this, however, it is sometimes very difficult to use scroll bars with any precision. Ever try to scroll a list box one line at a time on a Mac II? It requires the reflexes of a fighter pilot to scroll less than a windowful when you hit the up or down arrows. To solve this annoying problem I invented two-speed accelerating thumbs, which scroll slowly at first but switch to top speed if you hold the mouse down long enough. The page up and page down regions work the same way.
Accelerating thumbs are the reason behind the ScrollRecord data structure. The fields of this data structure work in conjunction with procedure MyScroll, which is the actionProc TrackControl calls repeatedly when the user holds the mouse button down in the scroll arrows or page up/page down regions (see pp. I 323-324 of Inside Macintosh). Each time the user clicks in one of these areas, heldDown is initialized to zero and goFast to true before TrackControl is called. TrackControl in turn calls MyScroll, and calls it repeatedly until the mouse button is released. Each pass through MyScroll scrolls the document one notch, the size of the notch depending on whether an arrow or a page region is being pressed. It also increments heldDown by one and compares it to a preset switching value, which in this case is 2. While heldDown is less than 2 we scroll slowly, but when it is greater than 2, goFast is set to true and we switch into high gear. When the user releases the mouse button, TrackControl is finished and the process re-initialized next time a scroll bar is hit.
MyScroll is definitely the workhorse procedure for the “hard part” of scrolling, which is handling the arrows and page regions. I could discuss its workings in great detail here but that would be no more informative than reading the source comments. Therefore, I refer you to the source comments. Just be aware that MyScroll does the math and exception handling for the scroll bars, making extensive use of the Golden Ratio, then calls ScrollContents to adjust the blit rectangle accordingly. Finally, ScrollContents calls DrawContents to blit the stuff to the screen.
Notice that procedure MyScroll and the routines it calls are positioned outside of the Window procedure. This is because MyScroll is an actionProc, and the linker can’t assign a procPtr to it if it is nested. This violates the typical layout of an XCMD but is perfectly legal, as long as you don’t declare global variables.
Wrapping Up
If you study this article and the accompanying code listing carefully, you will gain a clear understanding of the basics of scrolling, offscreen bitmaps, and blitting. If you are a relative newcomer to Mac programming, you should congratulate yourself for learning one of the most challenging aspects of the Apple desktop interface in record time.
Scrolling has some more knots in it that I didn’t even cover. For example, what if the user edits the document being scrolled, possibly changing its length or width? What if you are scrolling a combination of text and graphics? What if your document is too big to fit into an offscreen bitmap? And how about auto-scrolling (when the user drags beyond an edge of the window)? The list goes on and on.
None of these knots are remote possibilities; if your application is at all useful they are inevitable! But untangling them is much easier if you understand the basics I’ve covered in this article. Take a look at what other applications are doing. Then draw some pictures, remember the Golden Ratio, and everything will be all right.
Happy scrolling!
(* © 1988, Apple Computer, Inc. All rights reserved. Window2.p: A HyperCard XCMD in MPW Pascal 2.0.2 by Joe Zuffoletto Version 1.0, 29 June 1988 Form: Window title,top,left,bottom,right Example: window “My Window”,50,100,300,400 Notes: Window puts up a standard document window with scroll bars. The window can be dragged, resized, zoomed, and closed. Command-W is supported for closing the window. Command-spacebar toggles the menubar on and off, as in HyperCard. If you try to draw the window’s title bar off the screen or under the menubar, Window will abort with an error message. Error messages can be examined by looking at HyperCard’s global variable “the result” after calling Window. Window is MultiFinder friendly and works with HyperCard 1.2 or later. It supports multiple displays on the Mac II as well. Window takes a snapshot of the HyperCard card window and displays it like a MacPaint document. You can scroll up and down, etc. This is just for demonstration and amusement. You must supply your own code for displaying whatever you want to display in the window. ------------------------------------------------------------ To compile and link this file using MPW Pascal 2.0.2, select the following lines and press ENTER: Pascal Window2.p link -o “Hard Disk”:HyperCard:”HyperCard Stacks”:Home -rt XCMD=2000 -sn Main=Window Window2.p.o {MPW}Libraries:Interface.o {MPW}PLibraries:PasLib.o -m ENTRYPOINT Use other link files as necessary. The above link directives install the XCMD resource into the Home stack. You can substitute the name of any stack you want; be sure to provide the correct pathname. Also, make sure the target stack already has a resource fork or it won’t work. You can create an empty resource fork in a stack with ResEdit. ------------------------------------------------------------ *) {$R-} {$S Window} UNIT DummyUnit; INTERFACE USES MemTypes,QuickDraw,OSIntf,ToolIntf,PasLibIntf,HyperXCmd; PROCEDURE EntryPoint(paramPtr:XCmdPtr); IMPLEMENTATION TYPE Str31 = String[31]; {Attach to refCon of a window} OffScrHandle = ^OffScrRecPtr; OffScrRecPtr = ^OffScrRecord; OffScrRecord = RECORD gPort: GrafPtr; {offscreen grafPort} docWidth: INTEGER; docHeight: INTEGER; blitRect: Rect; {blit rectangle} blitWidth: INTEGER; blitHeight: INTEGER; dhGraf: LONGINT; dvGraf: LONGINT; END; {Attach to refCon of a scroll bar} ScrollHandle = ^ScrollPtr; ScrollPtr = ^ScrollRecord; ScrollRecord = RECORD heldDown: INTEGER; {see MyScroll} goFast: BOOLEAN; END; PROCEDURE Window(paramPtr:XCmdPtr);FORWARD; PROCEDURE EntryPoint(paramPtr:XCmdPtr); BEGIN Window(paramPtr); END; FUNCTION Min(int1,int2:INTEGER): INTEGER; {Return the smaller of two integers.} BEGIN IF (int1 <= int2) THEN Min := int1 ELSE Min := int2; END; {Min} PROCEDURE InitBlit(theWindow:WindowPtr); {Initialize the blit rectangle so it is in the upper left corner of the offscreen bitmap and so it is the same size as the target window onscreen.} VAR myOffScrHandle: OffScrHandle; BEGIN myOffScrHandle := OffScrHandle(GetWRefCon(theWindow)); MoveHHi(Handle(myOffScrHandle)); HLock(Handle(myOffScrHandle)); WITH myOffScrHandle^^ DO BEGIN dhGraf := 0; dvGraf := 0; {Make sure blit rectangle doesn’t hang off edges of offscreen bitmap, just in case window is larger.} blitWidth := Min((theWindow^.portRect.right - 15 - theWindow^.portRect.left),docWidth); blitHeight := Min((theWindow^.portRect.bottom - 15 - theWindow^.portRect.top),docHeight); SetRect(blitRect,gPort^.portBits.bounds.left, gPort^.portBits.bounds.top, gPort^.portBits.bounds.left + blitWidth, gPort^.portBits.bounds.top + blitHeight); END; HUnlock(Handle(myOffScrHandle)); END; {InitBlit} PROCEDURE InvalContents(theWindow:WindowPtr; theOldSize:Rect); {Perform “intelligent” window updating; i.e., if the window is grown, invalidate for the update event only the part that wasn’t visible before.} VAR myOffScrHandle: OffScrHandle; myWindowRect,windowRect: Rect; tallRect,wideRect: Rect; hWhiteSpace,vWhiteSpace: INTEGER; hBlitOffset,vBlitOffset: INTEGER; myDocWidth,myDocHeight: INTEGER; newDhGraf,newDvGraf: LONGINT; tempDH,tempDV: LONGINT; BEGIN hBlitOffset := 0; vBlitOffset := 0; myWindowRect := theWindow^.portRect; myOffScrHandle := OffScrHandle(GetWRefCon(theWindow)); MoveHHi(Handle(myOffScrHandle)); HLock(Handle(myOffScrHandle)); WITH myOffScrHandle^^ DO BEGIN tempDH := dhGraf; tempDV := dvGraf; myDocWidth := docWidth; myDocHeight := docHeight; END; {If we have scrolled to the bottom and/or right edges of the document and then grow the window, the document will “follow” the window so we don’t just expose a lot of white space. The same thing will happen if we zoom the window to a larger size. In either case, we need to invalidate the entire content region.} {Is any white space being exposed in horizontal direction?} hWhiteSpace := (myWindowRect.right - myWindowRect.left - 15) - (myDocWidth - tempDH); IF (hWhiteSpace > 0) THEN BEGIN {If so, how much?} newDhGraf := tempDH - hWhiteSpace; {Move the document that distance, but not beyond the point where its left edge is flush with the left edge of the window.} IF newDhGraf < 0 THEN newDhGraf := 0; {Adjust blit rectangle accordingly.} hBlitOffset := INTEGER(tempDH - newDhGraf); tempDH := newDhGraf; END; {IF hWhiteSpace} {Repeat for vertical direction.} vWhiteSpace := (myWindowRect.bottom - myWindowRect.top - 15) - (myDocHeight - tempDV); IF (vWhiteSpace > 0) THEN BEGIN newDvGraf := tempDV - vWhiteSpace; IF newDvGraf < 0 THEN newDvGraf := 0; vBlitOffset := INTEGER(tempDV - newDvGraf); tempDV := newDvGraf; END; {IF vWhiteSpace} {Save what we’ve learned to the data structure.} WITH myOffScrHandle^^ DO BEGIN dhGraf := tempDH; dvGraf := tempDV; blitWidth := Min((myWindowRect.right - myWindowRect.left - 15), (myDocWidth - dhGraf)); blitHeight := Min((myWindowRect.bottom - myWindowRect.top - 15), (myDocHeight - dvGraf)); SetRect(blitRect,blitRect.left - hBlitOffset, blitRect.top - vBlitOffset, blitRect.left + blitWidth - hBlitOffset, blitRect.top + blitHeight - vBlitOffset); blitWidth := blitRect.right - blitRect.left; blitHeight := blitRect.bottom - blitRect.top; END; HUnlock(Handle(myOffScrHandle)); {If the document has followed the window, invalidate entire content region.} IF ((hBlitOffset <> 0) OR (vBlitOffset <> 0)) THEN BEGIN EraseRect(theWindow^.portRect); InvalRect(theWindow^.portRect); END ELSE {Otherwise invalidate only the new white space.} BEGIN SetRect(tallRect,theOldSize.right, theWindow^.portRect.top, theWindow^.portRect.right, theWindow^.portRect.bottom); SetRect(wideRect,theWindow^.portRect.left, theOldSize.bottom, theOldSize.right, theWindow^.portRect.bottom); EraseRect(tallRect); EraseRect(wideRect); InvalRect(tallRect); InvalRect(wideRect); END; {IF hBlitOffset} END; {InvalContents} PROCEDURE DrawContents(theWindow:WindowPtr); {Blasts contents of updated blit rectangle to screen.} VAR myWindowRect,windowRect: Rect; myOffScrHandle: OffScrHandle; BEGIN myOffScrHandle := OffScrHandle(GetWRefCon(theWindow)); myWindowRect := theWindow^.portRect; MoveHHi(Handle(myOffScrHandle)); HLock(Handle(myOffScrHandle)); SetRect(windowRect,myWindowRect.left, myWindowRect.top, myWindowRect.left + myOffScrHandle^^.blitWidth, myWindowRect.top + myOffScrHandle^^.blitHeight); ClipRect(windowRect); CopyBits(myOffScrHandle^^.gPort^.portBits,theWindow^.portBits, myOffScrHandle^^.blitRect,windowRect,srcCopy,NIL); HUnlock(Handle(myOffScrHandle)); END; {DrawContents} PROCEDURE ScrollContents(theWindow:WindowPtr; dh,dv:INTEGER); VAR myOffScrHandle: OffScrHandle; leftSpace,topSpace: INTEGER; rightSpace,bottomSpace: INTEGER; BEGIN myOffScrHandle := OffScrHandle(GetWRefCon(theWindow)); MoveHHi(Handle(myOffScrHandle)); HLock(Handle(myOffScrHandle)); {Calculate space between edges of blitRect and edges of the bitmap.} WITH myOffScrHandle^^ DO BEGIN leftSpace := gPort^.portBits.bounds.left - blitRect.left; topSpace := gPort^.portBits.bounds.top - blitRect.top; rightSpace := gPort^.portBits.bounds.right - blitRect.right; bottomSpace := gPort^.portBits.bounds.bottom - blitRect.bottom; END; {Then move blitRect, but not past the edge of the bitmap.} IF (dv = 0) THEN IF (dh > 0) THEN {moving to the right} IF (rightSpace > dh) THEN OffsetRect(myOffScrHandle^^.blitRect,dh,dv) ELSE BEGIN dh := rightSpace; OffsetRect(myOffScrHandle^^.blitRect,dh,dv); END ELSE IF (leftSpace > dh) THEN {moving to the left} BEGIN dh := leftSpace; OffsetRect(myOffScrHandle^^.blitRect,dh,dv); END ELSE OffsetRect(myOffScrHandle^^.blitRect,dh,dv) ELSE IF (dv > 0) THEN {moving down} IF (bottomSpace > dv) THEN OffsetRect(myOffScrHandle^^.blitRect,dh,dv) ELSE BEGIN dv := bottomSpace; OffsetRect(myOffScrHandle^^.blitRect,dh,dv); END ELSE IF (topSpace > dv) THEN {moving up} BEGIN dv := topSpace; OffsetRect(myOffScrHandle^^.blitRect,dh,dv); END ELSE OffsetRect(myOffScrHandle^^.blitRect,dh,dv); {Save actual distances moved to data structure.} WITH myOffScrHandle^^ DO BEGIN dhGraf := dhGraf + dh; dvGraf := dvGraf + dv; END; HUnlock(Handle(myOffScrHandle)); {Shoot new contents of blit rectangle to screen.} DrawContents(theWindow); END; {ScrollContents} PROCEDURE MyScroll(theControl:ControlHandle; partCode:INTEGER); VAR myOffScrHandle: OffScrHandle; myScrollHandle: ScrollHandle; dh,dv: LONGINT; windowFull: LONGINT; myCtlValue: LONGINT; visHeight,visWidth: LONGINT; dontCare: LONGINT; tempDH,tempDV: LONGINT; startValue: INTEGER; myDocWidth,myDocHeight: INTEGER; direction: Str255; theWindow: WindowPtr; BEGIN theWindow := theControl^^.contrlOwner; myOffScrHandle := OffScrHandle(GetWRefCon(theWindow)); MoveHHi(Handle(myOffScrHandle)); HLock(Handle(myOffScrHandle)); {Make local copies of data structure items.} WITH myOffScrHandle^^ DO BEGIN tempDH := dhGraf; tempDV := dvGraf; myDocWidth := docWidth; myDocHeight := docHeight; END; HUnlock(Handle(myOffScrHandle)); {Initialize more variables for our formulas.} visHeight := theWindow^.portRect.bottom - 15 - theWindow^.portRect.top; visWidth := theWindow^.portRect.right - 15 - theWindow^.portRect.left; startValue := GetCtlValue(theControl); GetCTitle(theControl,direction); myScrollHandle := ScrollHandle(GetCRefCon(theControl)); MoveHHi(Handle(myScrollHandle)); HLock(Handle(myScrollHandle)); {Implementation of two-speed, accelerating scroll bars.} WITH myScrollHandle^^ DO BEGIN heldDown := heldDown + 1; IF (heldDown > 2) THEN goFast := TRUE; END; {WITH} CASE partCode OF inUpButton: BEGIN {Don’t scroll up if already at top!} IF (startValue > 0) THEN BEGIN IF (direction = ‘MyVert’) THEN BEGIN IF (visHeight >= myDocHeight) THEN BEGIN setCtlValue(theControl,0); ScrollContents(theWindow,0,-tempDV); END ELSE BEGIN {Scroll up five pixels} dv := tempDV - 5; {Set new thumb value using Golden Ratio.} myCtlValue := LONGINT((dv * myDocHeight) DIV (myDocHeight - visHeight)); ClipRect(theWindow^.portRect); setCtlValue(theControl,INTEGER(myCtlValue)); {Update blit rectangle, blast document to screen.} ScrollContents(theWindow,0,-5); END; {IF visHeight...ELSE} END ELSE BEGIN {Repeat for horizontal direction.} IF (visWidth >= myDocWidth) THEN BEGIN setCtlValue(theControl,0); ScrollContents(theWindow,-tempDH,0); END ELSE BEGIN dh := tempDH - 5; myCtlValue := LONGINT((dh * myDocWidth) DIV (myDocWidth - visWidth)); ClipRect(theWindow^.portRect); setCtlValue(theControl,INTEGER(myCtlValue)); ScrollContents(theWindow,-5,0); END; {IF visWidth...ELSE} END; {IF direction...ELSE} END; {IF startValue} IF (myScrollHandle^^.goFast = FALSE) THEN Delay(10,dontCare); END; {inUpButton} {Pattern is identical for down button, page up, and page down regions.} inDownButton: BEGIN IF (direction = ‘MyVert’) THEN BEGIN IF (startValue < myDocHeight) THEN BEGIN dv := tempDV + 5; myCtlValue := LONGINT((dv * myDocHeight) DIV (myDocHeight - visHeight)); ClipRect(theWindow^.portRect); setCtlValue(theControl,INTEGER(myCtlValue)); ScrollContents(theWindow,0,5); END; {IF startValue} END ELSE BEGIN IF (startValue < myDocWidth) THEN BEGIN dh := tempDH + 5; myCtlValue := LONGINT((dh * myDocWidth) DIV (myDocWidth - visWidth)); ClipRect(theWindow^.portRect); setCtlValue(theControl,INTEGER(myCtlValue)); ScrollContents(theWindow,5,0); END; {IF startValue} END; {IF direction... ELSE} IF (myScrollHandle^^.goFast = FALSE) THEN Delay(10,dontCare); END; {inDownButton} inPageUp: BEGIN IF (startValue > 0) THEN BEGIN IF (direction = ‘MyVert’) THEN BEGIN IF (visHeight >= myDocHeight) THEN BEGIN setCtlValue(theControl,0); ScrollContents(theWindow,0,-tempDV); END ELSE BEGIN windowFull := visHeight - 5; dv := tempDV - windowFull; IF (dv < 0) THEN dv := 0; myCtlValue := LONGINT((dv * myDocHeight) DIV (myDocHeight - visHeight)); ClipRect(theWindow^.portRect); setCtlValue(theControl,INTEGER(myCtlValue)); ScrollContents(theWindow,0,-windowFull); END; {IF visHeight...ELSE} END {IF direction} ELSE BEGIN IF (visWidth >= myDocWidth) THEN BEGIN setCtlValue(theControl,0); ScrollContents(theWindow,-tempDH,0); END ELSE BEGIN windowFull := visWidth - 5; dh := tempDH - windowFull; IF (dh < 0) THEN dh := 0; myCtlValue := LONGINT((dh * myDocWidth) DIV (myDocWidth - visWidth)); ClipRect(theWindow^.portRect); setCtlValue(theControl,INTEGER(myCtlValue)); ScrollContents(theWindow,-windowFull,0); END; {IF visWidth...ELSE} END; {IF direction... ELSE} IF (myScrollHandle^^.goFast = FALSE) THEN Delay(10,dontCare); END; {IF startValue} END; {inPageUp} inPageDown: BEGIN IF (direction = ‘MyVert’) THEN BEGIN IF (startValue < myDocHeight) THEN BEGIN windowFull := visHeight - 5; dv := tempDV + windowFull; myCtlValue := LONGINT((dv * myDocHeight) DIV (myDocHeight - visHeight)); ClipRect(theWindow^.portRect); setCtlValue(theControl,INTEGER(myCtlValue)); ScrollContents(theWindow,0,windowFull); END; {IF startValue} END {IF direction} ELSE BEGIN IF (startValue < myDocWidth) THEN BEGIN windowFull := visWidth - 5; dh := tempDH + windowFull; myCtlValue := LONGINT((dh * myDocWidth) DIV (myDocWidth - visWidth)); ClipRect(theWindow^.portRect); setCtlValue(theControl,INTEGER(myCtlValue)); ScrollContents(theWindow,windowFull,0); END; {IF startValue} END; {IF direction... ELSE} IF (myScrollHandle^^.goFast = FALSE) THEN Delay(10,dontCare); END; {inPageDown} END; {CASE partCode} HUnlock(Handle(myScrollHandle)); ClipRect(theWindow^.portRect); {so TrackControl can unhilite arrows when mouse is released} END; {MyScroll} PROCEDURE Window(paramPtr:XCmdPtr); CONST minParamCount = 5; smallestHeight = 100; smallestWidth = 100; _WaitNextEvent = $A860; _Unimplemented = $A89F; active = 0; inactive = 255; MouseMovedEvt = $FA; SuspendResumeEvt = $01; SuspendEventMask = $1; ConvertScrapMask = $2; browseTool = 6069; HCWidth = 512; HCHeight = 342; padding = 16; VAR toolVis,patVis: BOOLEAN; msgVis,fatVis: BOOLEAN; hasWaitNextEvent: BOOLEAN; inBackGround,smallScreen: BOOLEAN; DoneFlag,HaveEvent: BOOLEAN; wTop,wLeft,wBottom,wRight: INTEGER; partCode,controlCode: INTEGER; largestHeight,largestWidth: INTEGER; dummy,charCode: INTEGER; screenWidth,screenHeight: INTEGER; myDocWidth,myDocHeight: INTEGER; eventPoint: Point; wRect,screenRect,dragRect: Rect; winSizeLimits: Rect; oldSize: Rect; newSize,dontCare: LONGINT; envError: OSErr; cursorRgn: RgnHandle; hScroll,vScroll: ControlHandle; whichControl: ControlHandle; myWindow,whichWindow: WindowPtr; fatBitsWindow: WindowPtr; HCrefresh: Str31; wT,wL,wB,wR,wTitle: Str255; toolStr,patStr,msgStr: Str255; widthStr,heightStr: Str255; myBits: BitMap; theEnv: SysEnvRec; myEvent: EventRecord; wRecord: WindowRecord; HCPort: GrafPtr; theOffScrHandle: OffScrHandle; theScrollHandle: ScrollHandle; myOffScr: OffScrRecord; myScrollRecord: ScrollRecord; FUNCTION TrapAvailable(tNumber: INTEGER; tType: TrapType):BOOLEAN; {Check to see if a given trap is implemented.} BEGIN TrapAvailable := NGetTrapAddress(tNumber, tType) <> GetTrapAddress(_Unimplemented); END; {TrapAvailable} FUNCTION CreateHScrollBar(theWindow:WindowPtr; theValue,theMin,theMax:INTEGER; theRefCon:LONGINT):ControlHandle; {Allocate and draw a horizontal scroll bar in theWindow. Return a controlHandle to the scroll bar.} VAR myWindowRect,hScrRect: Rect; BEGIN SetPort(theWindow); myWindowRect := theWindow^.portRect; SetRect(hScrRect,myWindowRect.left -1, myWindowRect.bottom - 15, myWindowRect.right - 14, myWindowRect.bottom + 1); CreateHScrollBar := NewControl(theWindow,hScrRect, ‘MyHoriz’,TRUE, theValue,theMin,theMax, scrollBarProc,theRefCon); END; {CreateHScrollBar} FUNCTION CreateVScrollBar(theWindow:WindowPtr; theValue,theMin,theMax:INTEGER; theRefCon:LONGINT):ControlHandle; {Allocate and draw a vertical scroll bar in theWindow. Return a controlHandle to the scroll bar.} VAR myWindowRect,vScrRect: Rect; BEGIN SetPort(theWindow); myWindowRect := theWindow^.portRect; SetRect(vScrRect,myWindowRect.right -15, myWindowRect.top - 1, myWindowRect.right + 1, myWindowRect.bottom - 14); CreateVScrollBar := NewControl(theWindow,vScrRect, ‘MyVert’,TRUE, theValue,theMin,theMax, scrollBarProc,theRefCon); END; {CreateVScrollBar} PROCEDURE InvalScroll(theWindow:WindowPtr); {Accumulate the rectangles occupied by theWindow’s horizontal and vertical scroll bars into the update region.} VAR theRect,tallRect,wideRect: Rect; BEGIN SetPort(theWindow); theRect := theWindow^.portRect; ClipRect(theRect); {Accumulate tallRect, which is occupied by the vertical scroll bar } SetRect(tallRect,theRect.right-15, theRect.top, theRect.right, theRect.bottom); EraseRect(tallRect); InvalRect(tallRect); {Accumulate wideRect, which is occupied by the horizontal scroll bar } SetRect(wideRect,theRect.left, theRect.bottom-15, theRect.right, theRect.bottom); EraseRect(wideRect); InvalRect(wideRect); END; {InvalScroll} PROCEDURE Deactivate(theWindow:WindowPtr); {Deactivate the scroll bars in theWindow in accordance with the human interface guidelines. This means we must erase everything enclosed by the control rec- tangles.} VAR theControl: ControlHandle; theControlRect: Rect; BEGIN {I always title my scroll bars ‘MyVert’ and ‘MyHoriz’ so I can easily find them by walking a window’s control list.} theControl := WindowPeek(theWindow)^.controlList; WHILE (theControl <> NIL) DO BEGIN IF (theControl^^.contrlTitle = ‘MyVert’) OR (theControl^^.contrlTitle = ‘MyHoriz’) THEN BEGIN theControlRect := theControl^^.contrlRect; InsetRect(theControlRect,1,1); EraseRect(theControlRect); END; {IF} theControl := theControl^^.nextControl; END; {WHILE} END; {Deactivate} PROCEDURE HiliteScrollBars(theWindow:WindowPtr); VAR myWindowRect: Rect; visWidth,visHeight: INTEGER; myDocWidth,myDocHeight: INTEGER; myCtlValue: INTEGER; tempDH,tempDV: LONGINT; myOffScrHandle: OffScrHandle; BEGIN myOffScrHandle := OffScrHandle(GetWRefCon(theWindow)); MoveHHi(Handle(myOffScrHandle)); HLock(Handle(myOffScrHandle)); {Make local copies of data structure items.} WITH myOffScrHandle^^ DO BEGIN tempDH := dhGraf; tempDV := dvGraf; myDocWidth := docWidth; myDocHeight := docHeight; END; HUnlock(Handle(myOffScrHandle)); myWindowRect := theWindow^.portRect; visHeight := myWindowRect.bottom - 15 - myWindowRect.top; visWidth := myWindowRect.right - 15 - myWindowRect.left; SetPort(theWindow); ClipRect(myWindowRect); {Check to see if window is taller than contents. If so, unhilite vertical scroll bar. Otherwise, use Golden Ratio to draw vertical scroll bar with thumb in right position.} IF ((visHeight >= myDocHeight) AND (tempDV = 0)) THEN HiliteControl(vScroll,INTEGER(inactive)) ELSE BEGIN HiliteControl(vScroll,INTEGER(active)); IF (visHeight >= myDocHeight) THEN SetCtlValue(vScroll,myDocHeight) ELSE BEGIN myCtlValue := LONGINT((tempDV * myDocHeight) DIV (myDocHeight - visHeight)); ClipRect(theWindow^.portRect); setCtlValue(vScroll,INTEGER(myCtlValue)); END; {IF visHeight...ELSE} END; {Repeat for horizontal scroll bar.} IF ((visWidth >= myDocWidth) AND (tempDH = 0)) THEN HiliteControl(hScroll,INTEGER(inactive)) ELSE BEGIN HiliteControl(hScroll,INTEGER(active)); IF (visWidth >= myDocWidth) THEN SetCtlValue(hScroll,myDocWidth) ELSE BEGIN myCtlValue := LONGINT((tempDH * myDocWidth) DIV (myDocWidth - visWidth)); ClipRect(theWindow^.portRect); setCtlValue(hScroll,INTEGER(myCtlValue)); END; {IF visWidth...ELSE} END; {IF visWidth...ELSE} END; {HiliteScrollBars} PROCEDURE MoveScrollBars(theWindow:WindowPtr); {Call this procedure after theWindow has changed size. MoveScrollBars erases theWindow’s scroll bars, resizes them, and redraws them.} VAR myWindowRect: Rect; vScrRect,hScrRect: Rect; BEGIN myWindowRect := theWindow^.portRect; SetRect(hScrRect,myWindowRect.left - 1, myWindowRect.bottom - 15, myWindowRect.right - 14, myWindowRect.bottom + 1); SetRect(vScrRect,myWindowRect.right - 15, myWindowRect.top - 1, myWindowRect.right + 1, myWindowRect.bottom - 14); SetPort(theWindow); ClipRect(myWindowRect); {Hide and resize the scroll bars to fit the new window size.} HideControl(hScroll); HideControl(vScroll); MoveControl(hScroll,hScrRect.left,hScrRect.top); SizeControl(hScroll,(hScrRect.right - hScrRect.left), (hScrRect.bottom - hScrRect.top)); MoveControl(vScroll,vScrRect.left,vScrRect.top); SizeControl(vScroll,(vScrRect.right - vScrRect.left), (vScrRect.bottom - vScrRect.top)); HiliteScrollBars(theWindow); ShowControl(hScroll); ShowControl(vScroll); END; {MoveScrollBars} PROCEDURE ScrollWithThumb(theControl:ControlHandle; theEventPoint:Point); VAR theWindow: WindowPtr; myOffScrHandle: OffScrHandle; visWidth,visHeight: INTEGER; myDocWidth,myDocHeight: INTEGER; startValue,endValue: INTEGER; dummy: INTEGER; amountToScroll: LONGINT; tempDH,tempDV: LONGINT; direction: Str255; BEGIN theWindow := theControl^^.contrlOwner; myOffScrHandle := OffScrHandle(GetWRefCon(theWindow)); MoveHHi(Handle(myOffScrHandle)); HLock(Handle(myOffScrHandle)); {Make local copies of the data structure items.} WITH myOffScrHandle^^ DO BEGIN tempDH := dhGraf; tempDV := dvGraf; myDocWidth := docWidth; myDocHeight := docHeight; END; HUnlock(Handle(myOffScrHandle)); GetCTitle(theControl,direction); visHeight := theWindow^.portRect.bottom - theWindow^.portRect.top - 15; visWidth := theWindow^.portRect.right - theWindow^.portRect.left - 15; {Record where thumb is before it is moved.} startValue := GetCtlValue(theControl); {Move it.} dummy := TrackControl(theControl,theEventPoint,NIL); {Record where thumb is released.} endValue := GetCtlValue(theControl); {Record the distance thumb was moved.} amountToScroll := LONGINT(endValue - startValue); IF (direction = ‘MyVert’) THEN BEGIN {moved vertical scroll bar} IF (endValue = 0) THEN {moved thumb to top} ScrollContents(theWindow,0,-tempDV) ELSE IF (endValue = myDocHeight) THEN {moved thumb to bottom} ScrollContents(theWindow,0,myDocHeight - tempDV) ELSE BEGIN {moved thumb somewhere in middle - use Golden Ratio to translate thumb movement into document movement.} amountToScroll := LONGINT((amountToScroll * (myDocHeight - visHeight)) DIV myDocHeight); IF (ABS(amountToScroll) < 1) THEN {didn’t move thumb far enough to scroll - snap it back to starting position.} SetCtlValue(theControl,startValue) ELSE ScrollContents(theWindow,0, INTEGER(amountToScroll)); END; END ELSE {moved horizontal scroll bar} BEGIN IF (endValue = 0) THEN ScrollContents(theWindow,-tempDH,0) ELSE IF (endValue = myDocWidth) THEN ScrollContents(theWindow,myDocWidth - tempDH,0) ELSE BEGIN amountToScroll := LONGINT((amountToScroll * (myDocWidth - visWidth)) DIV myDocWidth); IF (ABS(amountToScroll) < 1) THEN SetCtlValue(theControl,startValue) ELSE ScrollContents(theWindow, INTEGER(amountToScroll),0); END; END; END; {ScrollWithThumb} FUNCTION WhichDevice(thePoint:Point):GDHandle; {For machines that support color QuickDraw and multiple screens, WhichDevice figures out which screen thePoint is on and returns a GDHandle to that screen. thePoint might be where the mouse was clicked, for example. Thanks to Greg Marriott for this code.} VAR aDevice: GDHandle; foundOne: BOOLEAN; BEGIN aDevice := GetDeviceList; foundOne := FALSE; {Walk the device list until thePoint is contained in some device’s screen rectangle.} WHILE (aDevice <> NIL) AND NOT foundOne DO BEGIN IF PtInRect(thePoint,aDevice^^.gdRect) THEN BEGIN WhichDevice := aDevice; foundOne := TRUE; END; aDevice := aDevice^^.gdNextGD; END; END; {WhichDevice} FUNCTION MenuBarHeight: INTEGER; {Returns the height of the menubar in pixels, as read from the low memory global mBarHeight.} CONST mBarHeight = $BAA; VAR menuBarHeightPtr: ^INTEGER; BEGIN menuBarHeightPtr := Pointer(mBarHeight); MenuBarHeight := menuBarHeightPtr^; END; {MenuBarHeight} FUNCTION OnAScreen(theRect:Rect):BOOLEAN; {OnAScreen returns FALSE if all of a window’s title bar is off the screen or if any part of it is under the menubar. The portRect of the window to be checked should be passed in theRect.} CONST titleBarHeight = 18; VAR deskRgn: RgnHandle; topLeft,topRight: Point; BEGIN deskRgn := GetGrayRgn; topLeft.v := theRect.top - titleBarHeight; topLeft.h := theRect.left + titleBarHeight; topRight.v := topLeft.v; topRight.h := theRect.right - titleBarHeight; IF ((PtInRgn(topLeft,deskRgn)) OR (PtInRgn(topRight,deskRgn))) THEN OnAScreen := TRUE ELSE OnAScreen := FALSE; END; {OnAScreen} PROCEDURE ZoomIt(theWindow:WindowPtr;partCode:INTEGER; clickedWhere:Point); {ZoomIt supports more elegant window zooming on multiple screen systems. theWindow will zoom to fill whatever screen the zoom box was on when it was clicked. The window state toggles between original size and zoomed size, as usual. Thanks to Greg Marriott for this code.} CONST titleBarHeight = 18; TYPE WStatePtr = ^WStateData; WStateHandle = ^WStatePtr; VAR oldRect,newRect: Rect; maxHeight: INTEGER; BEGIN oldRect := theWindow^.portRect; IF theEnv.hasColorQD THEN BEGIN newRect := WhichDevice(clickedWhere)^^.gdRect; IF WhichDevice(clickedWhere) = GetMainDevice THEN newRect.top := newRect.top + MenuBarHeight; END newRect := GetGrayRgn^^.rgnBBox; newRect.left := newRect.left + 2; newRect.top := newRect.top + titleBarHeight + 2; newRect.right := newRect.right - 3; newRect.bottom := newRect.bottom - 3; IF NOT EqualRect(oldRect,newRect) THEN WITH WindowPeek(theWindow)^ DO WStateHandle(dataHandle)^^.stdState := newRect; SetPort(theWindow); EraseRect(whichWindow^.portRect); InvalRect(whichWindow^.portRect); ZoomWindow(theWindow,partcode,FALSE); END; {ZoomIt} {$I XCmdGlue.inc} PROCEDURE Fail(errStr:Str255); {Fail returns errStr to HyperCard and exits the XCMD. errStr can then be checked by inspecting HyperCard’s global variable “the result.” See “XCMD’s for Hyper- Card” by Gary Bond (MIS Press, 1988) for more details. © 1988 by Gary Bond All rights reserved. You may use this code for NON-COMMERCIAL purposes.} BEGIN paramPtr^.returnValue := PasToZero(errStr); SysBeep(1); EXIT(Window); END; {Fail} PROCEDURE CheckParamCount; {CheckParamCount sees if the number of parameters passed to the XCMD matches the number expected. If not, we exit from the XCMD with an error message. See “XCMD’s for HyperCard” by Gary Bond (MIS Press, 1988) for more details. © 1988 by Gary Bond All rights reserved. You may use this code for NON-COMMERCIAL purposes.} VAR numParams: INTEGER; BEGIN numParams := paramPtr^.paramCount; IF(numParams <> minParamCount) THEN Fail(‘Form: HyperWindow “Window Title”,top,left,bottom,right’); END; {CheckParamCount} FUNCTION GetHCVersion: Str255; {Return a string containing the version of HyperCard being used; e.g., ‘1.2’} BEGIN ZeroToPas(EvalExpr(‘the version’)^,GetHCVersion); END; {GetHCVersion} PROCEDURE HideWindoids; {Get and save the visible state of the tool, pattern, message and fatbits windoids; then hide them if they are showing.} VAR toolH,patH,msgH,fatH: Handle; PROCEDURE HideFatBits; {HyperCard does not have a built-in command for hiding and showing the fatbits windoid, so we have to do it ourselves. HideFatBits walks the window list until it finds a window with title “FatBits,” then hides it if the visible field of its WindowRecord is true. HideFatBits also saves the WindowPtr to the fatbits windoid so we can use it later (e.g., to show the windoid again).} CONST windowList = $9D6; {Low memory global location.} VAR theWindow: WindowPeek; theWindowPtr: ^WindowPtr; BEGIN theWindowPtr := Pointer(windowList); theWindow := WindowPeek(theWindowPtr^); fatVis := FALSE; WHILE (theWindow <> NIL) DO BEGIN IF (theWindow^.titleHandle^^ = ‘FatBits’) THEN BEGIN fatBitsWindow := WindowPtr(theWindow); IF (theWindow^.visible = TRUE) THEN BEGIN fatVis := TRUE; HideWindow(fatBitsWindow); theWindow := NIL; END; END; IF (theWindow <> NIL) THEN theWindow := WindowPeek(theWindow)^.nextWindow; END; {WHILE} END; {HideFatBits} BEGIN {HideWindoids} {Get visible state of windoids.} toolH := EvalExpr(‘visible of tool window’); ZeroToPas(toolH^,toolStr); DisposHandle(toolH); toolVis := StrToBool(toolStr); patH := EvalExpr(‘visible of pattern window’); ZeroToPas(patH^,patStr); DisposHandle(patH); patVis := StrToBool(patStr); msgH := EvalExpr(‘visible of message window’); ZeroToPas(msgH^,msgStr); DisposHandle(msgH); msgVis := StrToBool(msgStr); {Hide the ones that are showing.} HideFatBits; IF toolVis THEN SendHCMessage(‘hide tool window’); IF patVis THEN SendHCMessage(‘hide pattern window’); IF msgVis THEN SendHCMessage(‘hide message window’); END; {HideWindoids} PROCEDURE ShowWindoids; {This routine assumes HideWindoids has been called before. ShowWindoids restores the visible state of the windoids to that saved by HideWindoids.} BEGIN IF toolVis THEN SendHCMessage(‘show tool window’); IF patVis THEN SendHCMessage(‘show pattern window’); IF msgVis THEN SendHCMessage(‘show message window’); {As in HideWindoids, we must take care of the FatBits windoid ourselves.} IF fatVis THEN BEGIN ShowWindow(fatBitsWindow); SelectWindow(fatBitsWindow); END; END; {ShowWindoids} PROCEDURE ToggleMenuBar; {Set the visible of the menubar to not the visible of the menubar.} BEGIN IF MenuBarHeight = 0 THEN SendHCMessage(‘show menubar’) ELSE SendHCMessage(‘hide menubar’); END; {ToggleMenuBar} PROCEDURE GetHCBitMap; VAR myDocHeight,myDocWidth: INTEGER; HCRect,myHCRect:Rect; dummyGrafPtr: GrafPtr; HCBits:BitMap; BEGIN MoveHHi(Handle(theOffScrHandle)); HLock(Handle(theOffScrHandle)); WITH theOffScrHandle^^ DO BEGIN myDocWidth := docWidth; myDocHeight := docHeight; END; myBits.rowBytes := (((myDocWidth - 1) DIV 16) + 1) * 2; myBits.baseAddr := NewPtr(myBits.rowBytes * myDocHeight); IF (myBits.baseAddr = NIL) THEN Fail(‘Could not allocate bitmap.’); HCRect := HCPort^.portRect; HCBits := HCPort^.portBits; SetRect(myBits.bounds,0, 0, 528, 358); dummyGrafPtr := GrafPtr(NewPtr(sizeOf(GrafPort))); IF (dummyGrafPtr = NIL) THEN BEGIN DisposPtr(myBits.baseAddr); Fail(‘Could not allocate offscreen grafPort.’); END; theOffScrHandle^^.gPort := dummyGrafPtr; OpenPort(theOffScrHandle^^.gPort); SetOrigin(0,0); SetPortBits(myBits); MovePortTo(0,0); PortSize(myDocWidth,myDocHeight); RectRgn(theOffScrHandle^^.gPort^.visRgn, theOffScrHandle^^.gPort^.portRect); RectRgn(theOffScrHandle^^.gPort^.clipRgn, theOffScrHandle^^.gPort^.portRect); EraseRect(theOffScrHandle^^.gPort^.portRect); SetRect(myHCRect,0, 0, 512, 342); CopyBits(HCBits,theOffScrHandle^^.gPort^.portBits, HCRect,myHCRect,srcCopy,NIL); HUnlock(Handle(theOffScrHandle)); END; {GetHCBitMap} PROCEDURE AdjustCursor; {AdjustCursor changes cursorRgn to the region that contains the cursor. As soon as the cursor moves out of cursorRgn, we get an event and can change the cursor and cursorRgn again. cursorRgn is either the content region of our window or the region containing everything BUT the content region of our window.} VAR mousePt: Point; myWinContRect: Rect; myWinContRgn: RgnHandle; deskRgn: RgnHandle; handHdl: CursHandle; BEGIN SetPort(myWindow); GetMouse(mousePt); LocalToGlobal(mousePt); myWinContRgn := NewRgn; {Calculate the “work region” of our window, which is its content region minus the scroll bars and grow icon. This is the region within which we want the cursor to change to HyperCard’s browse tool, and outside of which we want it to be an arrow.} WITH WindowPeek(myWindow)^.contRgn^^.rgnBBox DO SetRect(myWinContRect,left, top, right - 15, bottom - 15); RectRgn(myWinContRgn,myWinContRect); IF PtInRect(mousePt,myWinContRect) THEN BEGIN {The cursor is in the work region of our window. Set to browse tool.} handHdl := GetCursor(browseTool); IF (handHdl <> NIL) THEN SetCursor(handHdl^^) ELSE {Set to arrow if can’t find browse tool resource.} InitCursor; {Set the cursor region equal to our window’s work region.} SetEmptyRgn(cursorRgn); CopyRgn(myWinContRgn,cursorRgn); END ELSE BEGIN {The cursor is outside our window. Set to arrow.} InitCursor; {Get the current desktop region.} deskRgn := GetGrayRgn; {Set cursorRgn to the desktop region’s bounding box. It is important to add the menu bar area to cursorRgn too.} SetRectRgn(cursorRgn, deskRgn^^.rgnBBox.left, deskRgn^^.rgnBBox.top, deskRgn^^.rgnBBox.right, deskRgn^^.rgnBBox.bottom); {Punch out our window’s content region from the big region.} DiffRgn(cursorRgn,myWinContRgn,cursorRgn); END; DisposeRgn(myWinContRgn); END; {AdjustCursor} BEGIN {Main Program} {Check the HyperCard version. Must be 1.2 or greater.} IF GetHCVersion < ‘1.2’ THEN Fail(‘Sorry, must have HyperCard 1.2 or greater.’); {Save thy grafPort upon entering!} GetPort(HCPort); {Check and reset our environment.} CheckParamCount; FlushEvents(everyEvent,0); InitCursor; {Find out what kind of machine we’re running on.} envError := SysEnvirons(1,theEnv); IF (envError <> noErr) THEN Fail(‘SysEnvirons call failed.’); {Convert HyperTalk input parameters for use here.} ZeroToPas(paramPtr^.params[1]^,wTitle); ZeroToPas(paramPtr^.params[2]^,wT); ZeroToPas(paramPtr^.params[3]^,wL); ZeroToPas(paramPtr^.params[4]^,wB); ZeroToPas(paramPtr^.params[5]^,wR); wTop := INTEGER(StrToNum(wT)); wLeft := INTEGER(StrToNum(wL)); wBottom := INTEGER(StrToNum(wB)); wRight := INTEGER(StrToNum(wR)); {If window size parameters are too small or illegal, set the window to a predefined minimum size.} IF ((wRight - wLeft) < smallestWidth) THEN wRight := wLeft + smallestWidth; IF ((wBottom - wTop) < smallestHeight) THEN wBottom := wTop + smallestHeight; {Make sure the user is not trying to draw the window off the screen or under the menubar.} SetRect(wRect,wLeft,wTop,wRight,wBottom); IF NOT OnAScreen(wRect) THEN Fail(‘You are trying to draw your window off the screen!’); {Get the bounds of the desktop.} screenRect := GetGrayRgn^^.rgnBBox; {If we have a small screen, make a note of it so we can hide the card window during context switches under MultiFinder.} ZeroToPas(EvalExpr(‘item 3 of the screenRect’)^,widthStr); screenWidth := INTEGER(StrToNum(widthStr)); ZeroToPas(EvalExpr(‘item 4 of the screenRect’)^, heightStr); screenHeight := INTEGER(StrToNum(heightStr)); IF (screenWidth = 512) AND (screenHeight = 342) THEN smallScreen := TRUE ELSE smallScreen := FALSE; HideWindoids; IF MenuBarHeight > 0 THEN menuWasHidden := FALSE ELSE menuWasHidden := TRUE; theOffScrHandle := OffScrHandle(NewHandle (SizeOf(OffScrRecord))); IF MemError <> noErr THEN Fail(‘Out of memory. Buy more.’); myDocWidth := HCWidth + padding; myDocHeight := HCHeight + padding; MoveHHi(Handle(theOffScrHandle)); HLock(Handle(theOffScrHandle)); WITH theOffScrHandle^^ DO BEGIN docWidth := myDocWidth; docHeight := myDocHeight; END; GetHCBitMap; {Draw the window!} myWindow := NewWindow(@wRecord,wRect, wTitle,TRUE,zoomDocProc, WindowPtr(-1),TRUE,1); IF (myWindow = NIL) THEN BEGIN ShowWindoids; Fail(‘Not enough memory to draw window.’); END ELSE BEGIN {This is for scrolling. Stay tuned....} SetWRefCon(myWindow,LONGINT(theOffScrHandle)); InitBlit(myWindow); DrawGrowIcon(myWindow); theScrollHandle := ScrollHandle(NewHandle (SizeOf(ScrollRecord))); IF MemError <> noErr THEN Fail(‘Out of memory. Buy more.’); {Draw horizontal and vertical scroll bars in our window.} hScroll := CreateHScrollBar(myWindow,0,0, myDocWidth, LONGINT(theScrollHandle)); vScroll := CreateVScrollBar(myWindow,0,0, myDocHeight, LONGINT(theScrollHandle)); HiliteScrollBars(myWindow); DrawContents(myWindow); SetRect(dragRect,screenRect.left + 4, screenRect.top, screenRect.right - 4, screenRect.bottom - 4); largestHeight := screenRect.bottom - screenRect.top; largestWidth := screenRect.right - screenRect.left; SetRect(winSizeLimits,smallestWidth, smallestHeight, largestWidth, largestHeight); HCRefresh := ‘Go to this card’; cursorRgn := NewRgn; inBackGround := FALSE; DoneFlag := FALSE; REPEAT {Call WaitNextEvent, if available. Otherwise call GetNextEvent.} IF hasWaitNextEvent THEN HaveEvent := WaitNextEvent(everyEvent, myEvent,30,cursorRgn) ELSE BEGIN HaveEvent := GetNextEvent(everyEvent,myEvent); AdjustCursor; END; IF HaveEvent THEN BEGIN IF (myEvent.what = app4Evt) THEN {Pre-process app4Evt’s fed to us by MultiFinder.} CASE BSR(myEvent.message,24) OF MouseMovedEvt: AdjustCursor; SuspendResumeEvt: BEGIN myEvent.what := activateEvt; {Resume event.} IF (BAND(myEvent.message,SuspendEventMask) <> 0) THEN inBackground := FALSE ELSE {Suspend event.} inBackground := TRUE; myEvent.message := LONGINT(myWindow); END; {SuspendResumeEvt} END; {CASE BSR} END; {IF HaveEvent} CASE myEvent.what OF mouseDown: BEGIN partCode := FindWindow(myEvent.where,whichWindow); IF (whichWindow = myWindow) THEN BEGIN {Deal with mouse hits to our window.} CASE partCode OF inDrag: BEGIN SelectWindow(whichWindow); {DragWindow bug} DragWindow(whichWindow,myEvent.where, dragRect); SendCardMessage(HCrefresh); AdjustCursor; END; inGrow: IF StillDown THEN {GrowWindow bug} BEGIN oldSize := whichWindow^.portRect; newSize := GrowWindow(whichWindow, myEvent.where, winSizeLimits); IF (newSize <> 0) THEN BEGIN InvalScroll(whichWindow); SizeWindow(whichWindow, LOWORD(newSize), HIWORD(newSize),FALSE); InvalContents(whichWindow,oldSize); DrawGrowIcon(whichWindow); MoveScrollBars(whichWindow); END; {IF newSize} END; {IF StillDown} END; {inGrow} inZoomIn,inZoomOut: BEGIN IF (TrackBox(whichWindow, myEvent.where, partCode)) THEN BEGIN InvalScroll(whichWindow); ZoomIt(whichWindow,partCode, myEvent.where); InvalContents(whichWindow,oldSize); DrawGrowIcon(whichWindow); MoveScrollBars(whichWindow); END; {IF TrackBox} END; {inZoomIn,inZoomOut} inContent: BEGIN SetPort(whichWindow); eventPoint := myEvent.where; GlobalToLocal(eventPoint); controlCode := FindControl(eventPoint, whichWindow, whichControl); ClipRect(whichWindow^.portRect); IF (controlCode = inThumb) THEN ScrollWithThumb(whichControl,eventPoint) ELSE IF (controlCode <> 0) THEN dummy := TrackControl(whichControl, eventPoint, @MyScroll); END; {inContent} inGoAway: BEGIN IF (TrackGoAway(whichWindow,myEvent.where)) THEN DoneFlag := TRUE; END; {inGoAway} END; {CASE partCode} END; {IF whichWindow = myWindow} END; {mouseDown} activateEvt: BEGIN IF (WindowPtr(myEvent.message) = myWindow) THEN BEGIN SetPort(myWindow); ClipRect(myWindow^.portRect); DrawGrowIcon(myWindow); IF inBackground THEN BEGIN {We’ve been sent behind another application under MultiFinder, so deactivate the scroll bars and show the menubar if it was hidden.} Deactivate(myWindow); IF smallScreen THEN ShowHide(WindowPtr(HCPort),FALSE); IF MenuBarHeight = 0 THEN BEGIN SendHCMessage(‘show menubar’); menuWasHidden := TRUE; END ELSE menuWasHidden := FALSE; END ELSE BEGIN {We’ve been brought to the front under Multi- Finder, so reactivate the scroll bars and hide the menubar if it was hidden before.} ShowHide(WindowPtr(HCPort),TRUE); IF menuWasHidden THEN SendHCMessage(‘hide menubar’); DrawControls(myWindow); HiliteScrollBars(myWindow); FlushEvents(everyEvent,0); END; {IF inBackground} END; {IF WindowPtr} END; {activateEvt} updateEvt: BEGIN {Handle updates to our window.} IF (WindowPtr(myEvent.message) = myWindow) THEN BEGIN {Always do this stuff} SetPort(myWindow); BeginUpdate(myWindow); ClipRect(myWindow^.portRect); DrawGrowIcon(myWindow); {Always do this stuff under single Finder but only if in foreground under MultiFinder} IF NOT inBackground THEN BEGIN HiliteScrollBars(myWindow); DrawControls(myWindow); AdjustCursor; END; {Always do this stuff} DrawContents(myWindow); EndUpdate(myWindow); END {IF myEvent.message} {Handle updates to HyperCard’s card window.} ELSE IF (WindowPtr(myEvent.message) = WindowPtr(HCPort)) THEN BEGIN SendHCMessage(HCRefresh); {Must zero out card window’s update region ourselves because the SendHCMessage call doesn’t do it, and we’ll wind up in an infinite loop if we don’t.} BeginUpdate(WindowPtr(HCPort)); EndUpdate(WindowPtr(HCPort)); END; {IF... ELSE} END; {updateEvt} keyDown: BEGIN IF (BitAnd(myEvent.modifiers,cmdKey) <> 0) THEN BEGIN charCode := BitAnd(myEvent.message, charCodeMask); {Pressing command-spacebar toggles the menubar.} IF (CHR(charCode) = ‘ ‘) THEN ToggleMenuBar; {Pressing command-W closes the window and exits the XCMD.} IF (CHR(charCode) = ‘w’) THEN DoneFlag := True; END; END; END; {CASE myEvent.what} UNTIL (DoneFlag = True); {Clean up and get outta here!} DisposeRgn(cursorRgn); DisposHandle(Handle(theOffScrHandle)); DisposHandle(Handle(theScrollHandle)); CloseWindow(myWindow); SendCardMessage(HCrefresh); ShowWindoids; InitCursor; FlushEvents(everyEvent,0); {Restore thy grafPort} SetPort(HCPort); END; {IF myWindow...ELSE} END; {Main} END. {Window}
- SPREAD THE WORD:
- Slashdot
- Digg
- Del.icio.us
- Newsvine