[futurebasic] Welcome to the pleasure dome - part 2

Message: < previous - next > : Reply : Subscribe : Cleanse
Home   : October 2001 : Group Archive : Group : All Groups

From: jonathan <jonnnathan@...>
Date: Tue, 02 Oct 2001 23:00:26 +0200
As usual, beware of line wraps and mailservers eating underscore chars.
:-j

intro
-----

alain has sent me some minor remarks and corrections to Part One. As these
are minor I won't bother posting them here but will keep them for the
revised edition. This will thus be the rule; major bobos will get posted as
soon as they are pointed out, minor stuff will be held over. [it has also
been pointed out to me that i should use sexier, more punning titles... here
goes].

"It's a hilite if you can get it"
---------------------------------

We halted at the end of the last episode with just the means to find out if
the DragManager was present, and were it so, what features had it brought to
the party. I hope that if you are interested in this technology, you took
the time to download and then at least browse the files that I listed at the
end. If not, go ahead and do it now.

"grokking procs"
----------------
You will need to have a good idea of the vocabularly that Apple uses; about
the three stages - initiating, tracking and receiving a drag -, about how
and why hiliting functions, about flavors. We will cover all of these topics
from the FB point of view, but it is easier for us all if you have an idea
of what a drag is. That's done? Let's go.

You will have read then that the DragManager uses handlers - PROCs - to do
its stuff, or rather, it expects you to do that. If you have played around
with FB, even if you haven't programmed a ENTERPROC yourself, you will have
probably seen them in code here and there.

Here is a simple one that I use to simulate button presses in a DLOG window:
'---------------- SAMPLE CODE ------------------
troff
goto "PROC:END"
'-----------------------------------------------
"USERDIALOGPROC"
enterproc( theDialogPtr as ptr,[+]
           theEvtPtr    as ptr to EventRecord,[+]
           theItemHit   as ptr to int )
dim myKey  as Byte
dim myBool as boolean

 myBool  = 0
 myKey   = |theEvtPtr +_evtMessage +3|

 select( myKey)
   case 13,3:             ' Simulate an _okBtn Event
     myBool =-1 : % theItemHit,_okBtn
   case 27                ' Simulate a _cancelBtn Event
     myBool =-1 : % theItemHit,_cancelBtn
 end select
exitproc = myBool
'-----------------------------------------------
"PROC:END"
'-----------------------------------------------

You can see that it intercepts enter, return and esc keys and maps then to
button presses. However what is interesting here is the other stuff. First
of all you can't use the debugger in a proc, so we close off access with the
'troff' statement. Then we jump over the the routine with the 'goto'
statement; this is because the runtime treats all code that it finds outside
of a LOCAL FN as code that it should execute. If we didn't protect the
routine with this jump around it, the runtime would try to execute the code
and become just as confused as we are likely to be when it starts happening.

The label that we jump to, "PROC:END", is inside quote marks, this is how it
is recognised as a label. You will also have noticed that the procedure
itself also starts with a label; as it isn't a function we can't call it in
the manner of a LOCAL FN, the label is thus the mechanism for calling and
you will see this used later.

The code inside is nothing special, ordinary FB code. So you can see, with a
few precautions, putting together a PROC isn't really difficult.

For the DragManager we will need at least two sets of PROCs; the tracking
handler, and the receive handler. If you have read the docs you will
remember that the first provides the feedback - usually hilighting - inside
your app so that your user knows that a drag is in progress, and more
importantly, can see where she can drop a drag item or a clipping; the
second will treat the data that is dragged in.

"proc there and gamble"
-----------------------

I will reuse the code that I presented in the previous episode, and augment
it to install the handlers and then we'll walk through the code. Ready?

INCLUDE "Tlbx DragMgr.Incl"
'--- myGlobals.GLBL ---------------------------
begin record dmRecord
 dim dmPresent      as boolean
 dim dmFloatingWnd  as boolean
 dim dmPPCDragLib   as boolean
 dim dmImageSupport as boolean
 dim dmStartInFloat as boolean
 dim dmSetDragImage as boolean
end record
'
dim gDMrecord        as handle to dmRecord
dim gTheCowsComeHome as int
'
end globals
'--- myDragManager.INCL -----------------------

'----------------------------------------------
'    install the handlers
'----------------------------------------------
clear local
local fn doInstallHandler( theWndPtr        as ptr,[+]
                           theHandlerRefCon as long)
 dim osErr as OSErr
 '
 osErr = fn INSTALLTRACKINGHANDLER( [+]
    proc "DragTrackProc",theWndPtr,#theHandlerRefCon)
 long if( osErr =_noErr)
  // all went ok - continue
  osErr = fn INSTALLRECEIVEHANDLER( [+]
    proc "DragRecProc",theWndPtr,#theHandlerRefCon)
  long if( osErr !=_noErr)
   // oops! error - uninstall
   osErr = fn REMOVETRACKINGHANDLER( [+]
    proc "DragTrackProc",theWndPtr)
  end if
 end if
 '
end fn = osErr
'----------------------------------------------
'    remove the handlers
'----------------------------------------------
clear local
local fn doRemoveHandler( theWndPtr as ptr)
 dim osErr  as OSErr
 '
 osErr = FN REMOVETRACKINGHANDLER( proc "DragTrackProc",theWndPtr)
 osErr = FN REMOVERECEIVEHANDLER(  proc "DragRecProc"  ,theWndPtr)

end fn = osErr
'----------------------------------------------
'    is DM available - return 'true' or 'false'
'----------------------------------------------
clear local
local fn isDragManagerAvailable( theDMrecord as handle to dmRecord)
 dim @ myReturn as ptr
 '
 long if fn GESTALT( _gestaltDragMgrAttr,myReturn) =_noErr
  theDMrecord..dmPresent      =_zTrue
  theDMrecord..dmFloatingWnd  = myReturn [+]
          and _gestaltDragMgrFloatingWind%
  theDMrecord..dmPPCDragLib   = myReturn [+]
          and _gestaltPPCDragLibPresent%
  theDMrecord..dmImageSupport = myReturn [+]
          and _gestaltDragMgrHasImageSupport%
  theDMrecord..dmStartInFloat = myReturn [+]
          and _gestaltCanStartDragInFloatWindow%
  theDMrecord..dmSetDragImage = myReturn [+]
          and _gestaltSetDragImageUpdates%
 xelse
  theDMrecord..dmPresent =_false
 end if
'
end fn
'----------------------------------------------
'    the track proc
'----------------------------------------------
clear local
local fn DragTrackProc( theDragMsg    as int,[+]
                        theDragWPtr   as pointer,[+]
                        theDragRefCon as long,[+]
                        theDrag       as DragRef)
    dim myReturn as word
    '
    myReturn  =_noErr
    select( theDragMsg)
        '----------------------------------------------
        case _dragTrackingEnterHandler
        // set up any needed memory here
        DEBUGSTR( "entering tracking handler")
        '----------------------------------------------
        case _dragTrackingEnterWindow
        // call isMyTypeAvailable function
        DEBUGSTR( "entering window tracking handler")
        '----------------------------------------------
        case _dragTrackingInWindow
        // can we do something with the drag,
        // call getdrag mouse for location
        // check drag has left source window
        // check for hilight region and call show/hidedraghilite
        // call mytrackitemundermouse
        DEBUGSTR( "in window handler")
        '----------------------------------------------
        case _dragTrackingLeaveWindow
        // call hidedraghilite to erase hilighting
        DEBUGSTR( "leaving window tracking handler")
        '----------------------------------------------
        case _dragTrackingLeaveHandler
        // free up any memory here
        DEBUGSTR( "leaving tracking handler")
        '----------------------------------------------
    end select
 '
end fn = myReturn
'----------------------------------------------
'    the receive proc
'----------------------------------------------
clear local
local FN DragRecvProc( theDragWPtr   as pointer,[+]
                      theDragRefCon as long,[+]
                      theDrag       as DragRef)
dim myReturn as word
'
myReturn  =_noErr
'
end fn = myReturn
'----------------------------------------------
'     PROCS
'----------------------------------------------
goto "DRAGPROCS:END"
'----------------------------------------------
troff
'----------------------------------------------
"DragRecProc"
'----------------------------------------------
enterproc  fn Drag_Rec_Proc( [+]
           MyDragwPtr&,MyDragRefCon&,MyTheDrag&) = word
exitproc = fn DragRecvProc( [+]
           MyDragwPtr&,MyDragRefCon&,MyTheDrag&)
'----------------------------------------------
"DragTrackProc"
'----------------------------------------------
enterproc  fn Drag_Track_Proc( [+]
 MyDragMsg%,MyDragwPtr&,MyDragRefCon&,MyTheDrag&) = word
exitproc = fn DragTrackProc( [+]
 MyDragMsg%,MyDragwPtr&,MyDragRefCon&,MyTheDrag&)
'----------------------------------------------
tron
'----------------------------------------------
"DRAGPROCS:END"
'----------------------------------------------
'--- myMain.MAIN ------------------------------
clear local
local fn inits
 '
 gDMrecord = fn NEWHANDLECLEAR( sizeof( dmRecord))
 '
 long if( gDMrecord)
  fn isDragManagerAvailable( gDMrecord)
  long if( gDMrecord..dmPresent)
      print "drag manager present "
      if( gDMrecord..dmFloatingWnd)  then [+]
        print "and supports floating windows "
      if( gDMrecord..dmPPCDragLib)   then [+]
        print "and PPC DragLib is present "
      if( gDMrecord..dmImageSupport) then [+]
        print "and allows SetDragImage call"
      if( gDMrecord..dmStartInFloat) then [+]
        print "and supports starting a drag in a floating window"
      if( gDMrecord..dmSetDragImage) then [+]
        print "and supports drag image updating in SetDragImage"
  xelse
      print "no drag manager"
  end if
 end if
 '
 if( gDMrecord..dmPresent) then fn doInstallHandler( 0,0)
 '
end fn
'----------------------------------------------
window 1
fn inits

do
 handleevents
until gTheCowsComeHome

if( gDMrecord..dmPresent) then fn doRemoveHandler( 0)
end
'----------------------------------------------

You may have noticed that everything is here to track and receive a drag,
but not to initiate it. And you may be inclined to ask, Isn't this a rather
backward way of going about things? Not necessarily. If we start by
initiating a drag you will need somewhere to test that it is indeed working,
so I feel that it is better to have your tracking and receiving routines
working correctly beforehand, and then worry about how to create your own
drags.

I have also made a minor change in the code. In order to render it more
legible, toolbox calls, like NEWHANDLECLEAR, now appear in uppercase, while
FB code remains in mixed, but mainly lower-, case.

Now I will walk backwards through the code.

"if( gDMrecord..dmPresent) then fn doRemoveHandler( 0)"
I have included an end condition this time - even if we can never get there
yet - this is in order to demonstrate handler removal. It is the corollary
of the install handler. For the moment, take it on trust as I will explain
more later. You may notice that I pass a parameter, even if here it is zero,
and in the Apple example files you don't have this mechanism. This also will
be explained later.

"if( gDMrecord..dmPresent) then fn doInstallHandler( 0,0)"
In the initialisation function I have now included the call to the handler
Install routine. As with the remove handler, we test to see if the
DragManager is present before calling. This is just building logically on
the previous episode.

PROCS
Still working backwards, at the end of the "myDragManager.INCL" file we have
the PROC routines. You can see how they are 'protected' and labelled like
the example earlier in this episode. They are quite clear, simply wrapping
the call to the FB functions inside the enterproc structure that the
DragManager needs. You can see that we pass the same parameters, and return
the value. And that there are two: one for the receive handler and one for
the tracking handler.

The Install

Let's jump to the beginning now and look at the actual installing.

local fn doInstallHandler( theWndPtr        as ptr,[+]
                           theHandlerRefCon as long)

Here we receive 2 parameters, but looking at the calling code, they are both
zero. Isn't this a waste of time and space? Well, yes and no.

If you just need to add one simple set of dragging rules all through your
application than those values are going to be, and stay, zeros, and you can
scrub them out. But what are they, and why are they there.

Let's start with the more simple of the two, theHandlerRefCon. You may have
seen refCons before, they are available in most Apple Wodgets - controls,
windows, fields... They are essentially a 4-byte long hole that you can fill
as you wish. The RefCon in a window record is frequently used to store a
handle [4 bytes long] that then contains a linked list of all the wodjets in
that window - that's how FB stores that info, for example. Or you can just
create a reference number, like a window, button or field 'class', and store
that in there. Apple is essentially saying; Look there's this space, you use
it as you see fit. They also supply toolbox calls for all of these
constructs that will return that refCon value so that you can get to it
again later. We won't be using it and that is why we pass zero, but now you
know that it is there if you need it.

"making a hash of things"
-------------------------

If you look at the code you will see that when we pass this value to the
toolbox we prefix it with a '#'. You have certainly seen this before, even
wondered about it. This is a simple, but useful, shortcut in FB and you will
often see it when passing refCon values, in other places too. As we can pass
more or less what we want here we don't want the FB compiler to assume that
the value means something else, perhaps treating it as a pointer to some
other value. We need a means to say, just take this and don't worry your
sweet head with it. That is the purpose of the '#'. When you prefix a passed
variable with it, the compiler should not check it for correctness, you are
effectively taking charge. Of course, you can get into deep water like this,
but as a general rule, refCons can be safely prefixed.

The second parameter that we pass is the window pointer. Or it is supposed
to be. We've got a window open, so we could get a window pointer, but we
passed zero instead. The obvious question is, Why? The obvious answer is,
because it works! But here's how and why.

The DragManager is extremely flexible. Suppose that you have two main
windows in your app; one that deals with text, and one that deals with
images. You will need to drag text to one, and images to the other. However
you will need also need to detect if the user if dragging text to the image
window and vice-versa, and in that case you should not hilite the window for
an acceptable drag. You can have one mega handler that decides if it is
dealing with text or images and reacts accordingly, but the easier way is to
have two; one for each specific case, and have the right one installed in
the right window.

In this case you would create two handlers - myTextHandler and
myImageHandle, for example - and, you can see this coming... use the
corresponding window pointer to identify where they should go.

By passing a value of zero you are saying to the DragManager, I have no
special handlers, please install this one in _all_ of the windows in this
application

It is also possible to install more than one handler in more than one
window. For example, a text handler can be installed in all windows so that
you can drag text to any edit field, be it in your main window, in a prefs
window, in a 'find' dialog window... but you wish that your work windows can
also handle 'pict' drags. So you will install the default 'text' handler by
passing a window pointer of zero, and install your special 'pict' handler
only when the user opens a work window and then you pass that pointer during
the initialisation routine for the work window. Symmetrically, you should
also remove these handlers when the user seeks to close a window.

When removing a handler it should be passed the same window pointer as when
you installed it. So if you used the zero default, that is what you use when
removing it; if you used the window pointer, you use that.

"How much is that flavor in the window?"
----------------------------------------

If you do seek to install more than one handler then you have to be careful
about the order in which you install them. They will be polled in the very
same order that you installed them. To understand this, and the dangers
involved, we had better have a clear idea about 'flavors' - [yes, american
spelling].

I have already talked about a window accepting dragged text or a dragged
pict. These are the premises of 'flavors'; a flavor is the type of data that
you are transporting but this can be deceptive. A 'text' drag can in fact
have more flavors also... 'text' of course, 'styl' for styled text, and even
some other forms. If you drop it on an unstyled text field, only the basic
'text' handler will be involved; if you drop it on a styled edit field the
handler will probably seek both the text and the style information.

The DragManager provides a mechanism for querying a drag and finding out
what 'flavors' are available, so that you can see if the drag interests you,
or not. This is done prior to hiliting the window or drag area [or not, as
the case may be]. There is also a mechanism to 'promise' a flavor. This
promise would say, This drag contains 'text' and 'styl' data, but I can also
give you 'rtf ' if you want; that is a promise. It is a mechanism for
providing information on the data but not actually giving it. This is useful
if the data format, rtf for example, is not very often demanded. You prepare
and package the more usual types, and only promise the more rare types.
Apple also mentions that you can use a promised type if the data can be long
or processor heavy to prepare; you only do it when it is really needed.

When receiving a drag you must then poll for your preferred type first. If
you want 'rtf' data you must poll for it, and ask for it, before doing the
same for 'text' data, otherwise you will get the default text. So, you will
understand that your rtf handler must be installed before your text handler.

Another case. Let's come back to our 'text' window and our 'pict' window in
our imagionary application. The text window just wants 'text' data. No
styles. No other data. The 'pict' window however wants, and gives two sorts
of data;  a quickdraw 'pict', that is one made up of quickdraw commands, and
'text' data, as text can often be encapsulated in a quickdraw 'pict'.
However, if the 'text' handler was installed _before_ the 'pict' handler, it
would never get the preferred pict, just the text.

Similarly, you may use a proprietary internal data format ['toto'] in your
application, but also provide, or promise, a more flexible open format
['text'] for dragging data to windows outside of your application. However,
if you install the non-proprietary format first, you will never be able to
drag information in your internal format between your own windows, not even
within the same window! The 'text' handler will receive and accept the data
before the 'toto' handler gets called.

So the rule when installing drag handlers is 'install the most specific
handlers first and the most general ones last', or 'install from the
specific to the general'. OK?

The rest of this function is pretty clear. The 'proc "DragTrackProc"' is
referencing the enterprocs that we have already seen. You can see how each
part builds on the previous being correct, and how the routine bails out if
things go wrong. 

local fn doInstallHandler( theWndPtr        as ptr,[+]
                           theHandlerRefCon as long)
 dim osErr as OSErr
 '
 osErr = fn INSTALLTRACKINGHANDLER( proc
"DragTrackProc",theWndPtr,#theHandlerRefCon)
 long if( osErr =_noErr)
  // all went ok - continue
  osErr = fn INSTALLRECEIVEHANDLER( [+]
        proc "DragRecProc",theWndPtr,#theHandlerRefCon)
  long if( osErr !=_noErr)
   // oops! error - uninstall
   osErr = fn REMOVETRACKINGHANDLER( [+]
         proc "DragTrackProc",theWndPtr)
  end if
 end if
 '
end fn = osErr

A quick look at the symmetrical "local fn doRemoveHandler( theWndPtr as
ptr)" shows that there's no much to understand, nor go wrong, as long as you
remember to pass the same value in the wndPtr as when you called the install
handler.

"OK, I've got a drag, what do I do with it?"
--------------------------------------------

You've shaken hands with the DragManager and safely installed your handlers,
you've launched your app. You grab a clipping in the Finder and pull it to
your window... [By the way, you can do that with the code that I posted
earlier] ...and what happens? If you try the code here you will drop into
macsbug, that is the first thing, but that is just a demonstration of the
more important thing: you application has recieved a drag and the correct
handler has been called. That is the tracking handler "local fn
DragTrackProc".

For the moment that is all that this code does. We'll make it a bit more
exiting in the next episode by actually putting some code in there, hiliting
the window, and receiving a drag! Be warned, this might take two posts as
there is a lot of code in there to walk through.

If you look at the "local fn DragTrackProc" you should notice two things for
the moment. One, I have included, in comments, the actions that we will need
to do in each case; Two, I have put in a Toolbox call to MacsBug.

The actions you can ponder over for next time.

For MacsBug, I will explain now. It is very difficult to use the FB Debugger
during a drag. When asked recently how the Finder performed certain actions
during a drag, our FB guru Andy Gariepy replied succinctly, "black magic".
And part of that black magic involves taking low-level liberties with the
environment. Technically Apple says that it cannot be guaranteed that your
A5 world [or whatever passes for that in PPC land] will be in place.
Practically, this means that your lowlevel tools [like the Debugger] can
call functions, in good faith, and end up somewhere else. So the only 'sure'
way of finding your way around is to write rock-steady code, and to count on
MacsBug to help you check everything three times. These calls are just
giving you an advance taste of things.

If you have been, thank you for listening.

As usual comments etc. to the gnome.

[END OF EPISODE TWO]