TweetFollow Us on Twitter

Elegant Drag N Drop
Volume Number:10
Issue Number:11
Column Tag:Implementor’s Journal

Related Info: TextEdit

Implementing Elegant Drag and Drop
for Styled Text Fields

An implementor’s journal of adding drag and drop to SmalltalkAgents®

By David Simmons, Quasar Knowledge Systems

Note: Source code files accompanying article are located on MacTech CD-ROM or source code disks.

Utilizing the drag manager effectively in your applications can be difficult, but it doesn’t have to be. In this article I will be providing a reconstructed journal of my efforts in putting in drag and drop support into SmalltalkAgents styled text field components (<TEField> class). However, before we jump into the code and annotated explanations, I am going to provide a little background information to set the stage.

The design of SmalltalkAgents direct manipulation interface for the Agents Object System (AOS) Presentation Manager (APM) View System required that we architect specific drag and drop functionality into existing components. One of the more challenging designs was that of the styled text field component. Apple’s sample applications that came with their Drag and Drop Manager toolkit are quite well done and have a number of subtleties. We wanted to be sure that our Smalltalk implementation was up to the Macintosh user-interface standards.

SmalltalkAgents’ Drag and Drop Architecture was derived by examining a large number of applications existing on different operating systems. We wanted to understand how they handled various drag and drop operations including text handling. We also examined the protocols and frameworks for X/Motif, MS-Windows, OpenDoc, Apple Drag and Drop, and Taligent’s architecture to ensure that our framework was generic enough that we could transparently plug into host system frameworks when they were available; and when they were not, we wanted SmalltalkAgents internal framework to have a full-featured architecture of its own.

To accomplish these goals we felt that we had to provide a complete internal Smalltalk architecture that could stand on its own as well as transparently integrating with a given host system. This meant that if a given host system was missing any features our architecture would dynamically supply the missing functionality.

Throughout the remainder of this article I will usually refer to various generic features of SmalltalkAgents framework, the first time I do so, I will try to present the equivalent Apple Drag Manager issues as well as explanations for non-Smalltalk programmers.

Background

As part of preparing the reader, we need to cover some of the basics of what a component performing drag operations is responsible for. Drag operations can be broken down into three distinct areas. First is the initiation of a drag operation, second is the tracking and visual drag acceptance feedback, and third is the drop handling.

Drag initiation for text fields involves modification of the button-press handlers of text components. Specifically it requires that an additional test be made on a button-press to determine if the click occurred inside a selection range of one or more characters. If the test was true then a drag package must be built and the drag manager invoked to handle the dragging and possible dropping.

Drag tracking involves three subsidiary operations for handling a drag action. The first is the drag-enter test where the text-field determines if it has any interest in the drag-package. If it does, then it usually is necessary to allocate scratch data for the tracking within operation. Assuming that it does, the second phase involves tracking the drag operation while the cursor is inside the text field (i.e., drag-within hiliting and auto-scrolling). The third phase is the cleanup operation that the text-field must perform when the cursor exits the text field (i.e., the drag-exit de-hiliting and release of any allocated scratch data).

The drop handling operation is based on a callback to the component (in our case, a text-field) in which the drop occurred. The drop operation involves a number of distinct steps: the first step is to determine if the operation was a self move, or a copy operation. If it was a copy operation then it is simple. If it was a self move operation then we need to handle the animation of the text move within the text field.

And Now The Code!

As a starting point I began the work in the drag tracking methods <TEField> inherited from the <UIComponent> class. As a point of reference, in Smalltalk terminology a “method” is equivalent to the C term “function”. My first task was the filtering of drag requests so that I could determine whether or not to display any form of visible user-feedback to indicate whether or not the dragged package of items contained something of interest for my text field. To do that I needed to specialize the <#dragEnter> method in the <TEField> class.


/* 1 */
TEField[i]dragEnter

   “Validate whether the package is interesting”
    (Mouse dragPackageDetect: 
        [:type | (type isKindOf: String class)]) 
            ifFalse: [^self].

In the above code block we are using <#dragPackageDetect:> method to iterate over the contents of the drag package and inject the “type-of-object” that would be instantiated if we were to ask for the drag package contents (or some subportion of the contents).

If nothing interesting is in the drag package then we simply exit via “^self”.

Note: The drag package is a “promise” of what will be delivered and that in most cases the actual data is not instantiated (reified or made-real ) until you ask for it.

 
/* 2 */
   “Compute the drag area and then display it”
    outer := self contentBounds asRegion copy insetBy: (-1 @ -1).
    inner := outer copy insetBy: 2.
    outer differenceWith: inner.

Here we are computing the content region of our text-field (<TEField> instance) and then creating a 2 pixel wide region that surrounds it.


/* 3 */
   Mouse showDragHiliteRegion: outer.
    

Now tell the Smalltalk drag-manager object (i.e., the <Mouse>; an instance of <CursorDevice>) to display the hilite region. We must ask our drag manager to display the hilite area because it is managing the display of the drag-image and we want to be assured that our hilite region is drawn “visually-behind” the drag-image.


/* 4 */
   “Indicate that we are tracking the drag operation, but that 
    it needs initialization.”
    Mouse dragTargetData: false.

Finally since the package was interesting we set the active-drag-target’s reference object to be non-nil. We can use this for anything we want as long as we are the active drag target. When we lose targeting the reference value will be cleared. Since we own this value (for now) we set it to <false> to indicate that it is interesting but that we are not yet initialized. We will see more about this when we get to the <#dragWithin> and <#dragExit> methods.

So, we have accomplished our first goal. We now can detect a package of interesting information and hilite ourselves as appropriate. What we need to do next is make sure that when we lose drag-targeting we will be un-hilited and that any temporary data structures we created are properly de-referenced (i.e., similar to the notion of destruction in C++).

In Smalltalk the developer does not need to track objects they are using; the virtual-machine tracks and disposes of any objects that are not referenced anywhere in the known object-spaces. To be considered as “referenced”, an object must be referenced through a chain of object references that leads back to a persistent (non-garbage-collected/anchored) object.

So, our only job is to make sure that we do not have any “anchored” objects referencing the temporary objects we created. Again, this is not really necessary but it is more efficient for us to aid the automatic garbage collector by providing it with hints so that it can dispose of these objects more quickly because there may be a large graphic or some other memory intensive object hanging around from our hiliting-feedback operations.

To accomplish our goal we must specialize the <#dragExit> method which <TEField> inherited from the <UIComponent> class.


/* 5 */
TEField[i]dragExit
    (data := Mouse dragTargetData) ifTrue:
    [
       “Hide any visible blinking bar we may have showing”
        (data at: 6) value: false.
    ].
    

If there was any dragging-action that took place within the <TEField> target then we may have created some temporary structures. If so, then make sure that we invoke a good-bye kiss to clean-up. The use of “(data at: 6)” is based on our knowledge of what we created in our TEField[i]dragWithin method.

Note: We can still write this code and test our operations even though we haven’t written the <#dragWithin> method yet.

^super dragExit

The above statement invokes the standard <#dragExit> behavior that was inherited from our superclasses. The default method and/or the <Mouse> will take care of clearing the tracking-feedback hiliting and clearing the drag-target-data.

By the way, I should point out two things now. First, we are guaranteed by the SmalltalkAgents’ Drag and Drop Architecture that a component will always be sent the following sequence of messages during a drag tracking operation:


/* 6 */
    #dragEnter
    #dragWithin   “1 or more times depending on whether the 
                   Mouse moves inside of the field”
    #dragExit

Second, as I mentioned at the beginning, this article is essentially a re-construction of the interactive sequence of coding that I performed while building the text-field drag code. The reason why this is relevant now is because, by this stage, I had live Drag and Drop hiliting feedback in all the <TEField> instances of my Smalltalk modules (a module is an application that shares the Smalltalk environment in a DLL/CFM-like fashion). So far, I was about 5-10 minutes into my development time.

Finally, to complete our tracking we need to provide cursor insertion point feedback as we track the dragging of an a “interesting” package within our TEField instance. So, as you might expect by now, we need to specialize the <#dragWithin> instance method in TEField.

In Smalltalk, instance methods are the behavior that “instances” of a given class will exhibit. Class methods are the behavior that a given class itself will exhibit. Classes are first-class objects which means that they are real and can be sent message just like any other object. In fact, classes are instances of Metaclass or one of its subclasses.

Before proceeding into our design of the <#dragWithin> method, let me point out a bit about my design intentions. I am going to create all the temporary drag operation structures inside the <#dragWithin> method to guarantee locality of reference. By doing so, it will be easier (for a person) to understand what was happening and thus it will easier to maintain the code (i.e., we encapsulate most of the drag-action behavior inside the <#dragWithin> method).

We can accomplish this because SmalltalkAgents, and many other Smalltalk’s (Digitalk Smalltalk/V is one exception), support closures (like in Lisp). In Smalltalk, a closure is constructed using a block declaration, which is part of the basic grammar and semantics of the Smalltalk language.

A block declaration is like a nameless function (or ProcPtr), that when it is first referenced, will result in a <BlockClosure> being instantiated. The resulting <BlockClosure> will also retain the contextual information of the “context” (also known as its “execution-world”) in which it was instantiated. Specifically, it will retain a reference to all the shared method temporary (local) variables that we allocated while the enclosing method’s call frame is active and it will also include any shared block temporary variables defined in any (enclosing) outer-blocks. Blocks are a fundamental part of the Smalltalk language and fortunately it only takes a little bit of time to understand them and learn how to use them effectively.

As contexts are created (reified) for shared use by <BlockClosure> instances, the virtual-machine will relocate any of our temporary stack variables that are now shared, and it will update our call-frame to indicate where to find the variables whose “context” is being shared between the blocks and the compiled-method’s call frame in which the blocks were instantiated. This is necessary because in Smalltalk, blocks are first class objects (as are methods by the way) and can be assigned, passed around, and sent messages to. Specifically, we can create a block within a method and then evaluate that block after the method has returned (i.e., after it has exited). Should that happen, the block(s) need to be ensured that they can still access the same variables (scope/context) that existed when the blocks were created.

Ok, let’s look at what we need to do in our <#dragWithin> method.


/* 7 */
TEField[i]dragWithin

    | bottom offset point line localPosition height top data |

The above construct is the list of method temporaries (i.e., local variables) that need to be allocated when the method is invoked. The actual declaration of the local variables is optional because the compiler automatically detects variable usage as it processes the code. The SmalltalkAgents browser tools use this capability to provide “authors” (developers) with the option to automatically have the declarations pasted into their source code for them.

Note that in SmalltalkAgents the allocation of temporary variables is initially allocated on the active thread’s stack (SmalltalkAgents is pre-emptively multi-threaded). The temporary variables are also initialized by Smalltalk to point to <nil>, which is the sole-instance of the <UndefinedObject> class.


/* 8 */
    data := (Mouse dragTargetData) ? [^self].

The first action our method takes is to check and see if there was anything interesting in the drag package. Remember that in the <#dragEnter> method, if the drag-package was interesting we set the <dragTargetData> to be <false>. If it was not interesting, then we left the <dragTargetData> as <nil>.

As an aside, I should explain a little about the statement above. The <#?> message takes a single parameter, which in this case happens to be a block. The <#?> method will send the parameter the message <#value> if the message receiver (self) is identical to the <nil> object.

Note that since blocks are first class objects they understand a variety of messages. Sending a block object one of the suite of messages for evalution will cause all the statements the block contains to be invoked. The <#value> message is one of the messages in the “evaluation” protocol’s suite that can be used to evaluate a method or a block.

So, in this case if the <dragTargetData> is <nil> the block “[^self]” will be evaluated. If it is evaluated it will simply return from the <#dragWithin> call-frame with the result value being self; which is the original message receiver (i.e., the TEField instance). Otherwise the <dragTargetData> will be assigned to the local method scoped variable <data>.

    
/* 9 */
   “Compute the offset into the text field”
    offset := self offsetOfPoint: 
        (localPosition := Mouse localPosition).
    

Since the drag-package is interesting we proceed with normal processing. First, we will find the character offset (into our text-field) that is nearest the mouse’s local (current graphic port’s) position. We will also cache the <localPosition> value so that we can use it later.

Note, again, the SmalltalkAgents drag architecture has already guaranteed that the current canvas (graphic-port) is the window containing our TEField component instance.

data ifFalse:

[

The above two lines of code are performing a test to see if the <data> value is equivalent to the <false> object, and if it is then the single block parameter will be instantiated and evaluated. I should point out here that, strictly speaking, the block will not actually be instantiated because modern Smalltalk compiler’s are pretty smart and know how to optimize or inline a great deal of the language and its constructs.

In any case, we will only enter this method once because near the end of this block-scope we will set the drag-target-data to be a list of objects, including an all important instance of block closure. Thus, any time the <#dragWithin> method is subsequently called for the current drag-target, we will be able to make use of the information we set up on this initial call.


/* 10 */
        line := self lineAtOffset: offset.
        point := self pointAtOffset: offset.
        height := self heightFromLine: line to: line.        
        bottom := point - (1@0).
        top := (bottom - (0@height)).

In the above code we perform some basic pre-flighting to convert the mouse location into a given line index. We also compute the spatial characteristics of the line to enable us to accurately draw a text-caret while dragging the package around.

data :=


/* 11 */
        {
            false.              "Not Visible"
            offset.             "old offset"
            top.                "top"
            bottom.             "bottom"
            self hiliteRegion.  "The hilited text region"
            [:doShow |
                ((data at: 1) = doShow) ifFalse:
                [
                   “Draw in XOR mode”
                    activeCanvas
                        pushPen;
                        penMode: #patXor;
                        movePenTo: (data@3);
                        drawLineTo: (data@4);
                        popPen.
                        
                   “Update to reflect current state”
                    data at: 1 put: doShow.
                ].
            ].
        }.

In the above code we created an instance of <List> using the “{“ and “}” dynamic list operators. The dynamic list operator will build the list by executing each statement inside the list and then collating the results from each statement.

Our list will consist of 6 elements consisting of: (1) a flag indicating if the text-drag-caret is currently visible; (2) the offset of the mouse last time the text-drag-caret location was computed; (3) the top coordinate for where to draw the text-drag-caret; (4) the bottom coordinate for where to draw the text-drag-caret; (5) the region encompassing the text-fields (hilited) selection; (6) a block closure that will both draw or erase the text-drag-caret and which will also update the flag indicating whether the text-drag-caret is visible.

      
        Mouse dragTargetData: data.

Here we are storing the list (<data>) into the drag target reference object. By doing so, we are replacing the <false> object that was there. Remember that we can tell which of the three states that the reference object is in because we can easily discriminate between it being <nil>, <false>, or an instance of <List>.

    ].
  

Ok, by here we have done our pre-flighting and we have initialized the data structure we need for handling the steady-state <#dragWithin> operations.

  
   “If the mouse is inside the hilited selection, then hide 
    the caret”
    ((Mouse dragInitiator == self)
        and: [(data at: 5) containsPoint: localPosition]) ifTrue:
    [
       ^(data at: 6) value: false
    ].
    

In the code above we are checking to see if the receiver (our TEField instance) is identical (#== test for the same object as) to the object which initiated the drag operation. If so, then we check to see if the character position that the cursor is over is inside the selection region. The purpose for this check is to catch the case where the drag was initiated from the receiver and the cursor (mouse) is currently over the text-selection which was originally dragged by the user. In this special case we want to hide the text-drag-caret and exit. We accomplish this hide and exit by evaluating the “cached” block-closure with the parameter <false>. The cached block will see the parameter as its block (scoped) temporary variable <doShow>, and will hide the caret accordingly.


/* 12 */
   “If it hasn't changed then just toggle/blink the vertical bar”
    (offset = (data at: 2)) ifTrue:
    [
       “Re-draw, toggling the visibility”
        (data at: 6) value: 
            ((Clock ticks // Gestalt DoubleTime) isEven).
    ] 

At this point we are in the middle of a keyword message for <#ifTrue:ifFalse:> where each parameter to the message is a block closure. The receiver statement will be tested for equivalence to <true> and if it matches then the first block will be evaluated, if it doesn’t match then the second block will be evaluated.

The purpose of this test sequence is to enable us to discriminate the cases where the cursor has moved, from the cases where the cursor is in the same position but should be blinking on/off.

We detect the case where the cursor hasn’t moved by comparing the current <offset> against the offset the last time the text-drag-caret location was computed. If the offset hasn’t changed then the mouse hasn’t moved; so we evaluate a drawing block and tell it to draw the text-drag-caret based on whether the current clock ticks scaled by the Mac OS DoubleTime value is an even or an odd value. This latter test is a trick that enables us to avoid having to access extra state information. It also means that in a threaded situation it is easy to guarantee that the time period during which the caret is displayed will be essentially the same as the period during which it is not displayed.

    
/* 13 */
   “Otherwise, erase the old position and recalculate”
    ifFalse:
    [
       “Erase the old position”
        (data at: 6) value: false.
        
        line := self lineAtOffset: offset.
        point := self pointAtOffset: offset.
        height := self heightFromLine: line to: line.
        bottom := point - (1@0).
        top := (bottom - (0@height)).
        
        data
            at: 2 put: offset;
            at: 3 put: top;
            at: 4 put: bottom.

“Re-draw, toggling the visibility”


/* 14 */
        (data at: 6) value: true.
    ].

In this second (ifFalse:) block we are handling the case where the cursor (mouse) has moved and we need to recalculate where the caret should be displayed. The first step is to erase the old caret. Then we calculate its new location and then re-draw the caret.

Well, we are really cooking now. At this point I was about an hour into my design and was able to see all the drag tracking operations working smoothly in my text fields. I should point out that during all this activity I was inside my Smalltalk environment simply adding these operations as extensions. I never had to quit or exit and I was able to instantly see if my code failed or not. During the design of the <#dragWithin> method I had a few code errors that caused exceptions, but the dynamic environment trapped the errors and halted the erroneous threads. I was then able to debug the code and make corrections while all my applications continued to run.

I should point out that more code is actually involved in the <#dragWithin> method if we want to give text fields the ability to perform auto-scrolling during a drag. The code for that has not been presented because it would have made this article longer than necessary to illustrate the salient points of drag and drop.

So, we have two more methods that we need to design. We need a method to handle the case where an item is dropped and we need to be able to initiate a drag operation from a styled text-edit field.

We will now go over the code for handling a drag item being dropped in a styled text field. Before we do that, however, I need to mention that this method is where I spent that largest portion of my efforts because the animation and graphic rendering synchronization of the drag manager and my drag-drop animation was more tricky than I originally thought it would be.


/* 15 */
TEField[i]dragItemDropped

    | selEnd offset dropPoint dragData selStart result |

Again we declare the method temporaries (local variables) that we will need during execution scope this method.


/* 16 */
   “Grab the target data, if any”
    (dragData := Mouse dragTargetData) ? [^self dragExit].

Again, we check to see if we have any interest in the drag package. We always are notified of a drop, even if we have no interest in the package itself. The drag manager can only determine if we had an interest in the package itself when we request data from the package during the processing of the <#dragItemDropped> method.

Knowing this information is useful because it allows the drag manager to automatically inform the drag-initiator what the disposition of the drag package was. I should also point out that this functionality is outside the scope of the Apple Drag and Drop Manager’s architecture.


/* 17 */
    dropPoint := Mouse localPosition.
    offset := dragData at: 2.

In the code above we obtain the actual drop-point for the package and then we recover the last known mouse location from our drag-within <data> list. For a variety of subtle reasons these may not be the same value and we will need to have both to properly clean up the visual state of the desktop.


/* 18 */
    self dragExit.

We now issue a <#dragExit> message which will put us in a clean state for the <#dragItemDropped> operation. The <#dragExit> operation will be called twice because of this usage. The first time is our call above, the second time will be when we return to the drag manager after this <#dragItemDropped> method completes. We know this is safe because both the <Mouse> drag methods and the <TEField> methods that we wrote don’t perform unnecessary actions (like unhiliting the drag-region if it was previously hilited). This kind of added flexibility provided by using safety checks was part of the basic design architecture of the SmalltalkAgents drag mechanism.

    
/* 19 */
   “If we were the drag initiator, and the drop was inside our
    hilited text, then do nothing.”
    (Mouse dragInitiator == self
        and: [((Mouse dragInitiatorData@1) & 0x44) not
            and: [Keyboard AltKey not]]) ifTrue:
    [

The code is testing that three conditions are true: (1) That the drag-initator is the same as the drop-target; (2) that the drag was not initiated with using the ALT/OPTION metakey [which indicates a copy]; (3) that the ALT/OPTION metakey is currently not pressed. Clearly we are looking at code that was modified based on knowledge about the way the drag-initiation was going to function. There is also a certain redundancy here with regard to the implementation of option-copy semantics which may be further refined in future versions of this architecture.

The 0x44 is a portable mask we can use for testing whether the left or right option keys are pressed on the keyboard. The SmalltalkAgents virtual machine defines a universal keyboard (i.e., a portable key-code system) to ensure that key-code operations can be written independently from the type of host-operating system or host-hardware.

If the above three conditions are true then we are processing the complicated case of dragging within the same component (i.e., container) and the operation is therefore a move not a copy. The move operation is tricky because we need to preflight some calculations so that our drag animation from the selection’s current location to its new location will be uniformly smooth.


/* 20 */
        ((dragData@5) containsPoint: dropPoint) ifTrue:
        [
            ^self
        ].

In the above code we are testing to see if the drop point is located over the source-selection. If it is then no action is required because we don’t move text on top of itself. Therefore in this situation we simply exit because we are done.

        
/* 21 */
        selEnd := self selectionEnd.
        selStart := self selectionStart.
  
        (selEnd   selStart) ifTrue:
        [
            | line height point |

Now we begin the calculation to see which of two possible drag animation situations exists. The above code is doing a pre-flight to compute the selection ranges. Strictly speaking the selEnd selStart test is not needed because there must be a selection or we (presumably) would never have initiated the drag (however, it is here for safety).

The “| line height point |” block temporary variables are declared here and are locally scoped to only be defined within the block itself.

            
/* 22 */
            line := self lineAtOffset: offset.
            height := self heightFromLine: line to: line.        
            point := self pointAtOffset: offset.
            
           “If on the same line then remove the width from
            the offset because it will be cut anyway.”
            ((offset   selStart) 
                and: [(self lineAtOffset: selStart) = line])
                    ifTrue:
            [
                point := point - (((dragData@5) bounds 
                             width + 1)@height).
            ] ifFalse:
            [
                point := point - (1@height).
            ].

In the above code we have preflighted the calculation of the line, its height, and the baseline of the area where the text-drag-caret was displayed. We use the caret location because we can be sure that it is located precisely over a character even if the drop-point was not. This situation can occur when the drop point is below the last line or to the right of the last character in a given line.

If the character drop location is to the right of the selection and it is on the same line then we need to adjust the animation destination because the target insertion point will move by the number of characters that we are “cutting” from the display.

What we are trying to do here is to define a starting and ending path for our drag animation. The starting point is the current selection region (in data@5). The ending point has to be calculated based on whether the insertion point will shift after the cut operation.

We subtract the height of the destination line so that the drag path will be based on a top-left base difference between the current selection origin and its final after the clear and insert has been performed.


/* 23 */
           “Animate the move operation”
            (dragData@5)
                zoomBy: (point - ((dragData@5) bounds origin))
                mode: 1
                steps: 12
                rate: 12.

Now we use the Smalltalk drag animation method that will animate an arbitrary region along a linear path. The path begins at the receiver’s origin and moves for some delta distance based on the “zoomBy:” parameter. The receiver to the above animation message can be any kind of <GraphicPrimitive> object, so our selection-region is appropriate here.

Apple’s drag manager would be substituted here for drag animation but since we had our own with a bit more tuning flexibility, I used the SmalltalkAgent’s one instead. The Smalltalk animation method has finer control of steps, and of the acceleration in terms of both the step scaling and the step rate.

              
/* 24 */

           “Clear the existing selection”
            self doClear.
            
           “Adjust for the portion we remove”
            (selEnd ¾ offset) ifTrue:
            [
                offset := offset - (selEnd - selStart).
            ].
        ].
        
       “Select the insertion point”
        self selectFrom: offset+1 to: offset. 
/* 1 */

The code above has cleared the current selection thus removing it from the text field. 
Then we adjust the offset for the insertion point to account for the cleared text.  Finally 
we set the new insertion point in preparation for our insert (i.e., prepatory work before 
we can issue a <#nextPutAll:> message) operation.
       
/* 25 */
    ] ifFalse:
    [
       “Revise the insertion point”
        self selectFrom: offset+1 to: offset.
    ].

The #ifFalse: block parameter is the case where it was not a self move, but rather a copy operation. In this case we want to leave any current selection intact and thus we just set the insertion point to where the user indicated the character drop point should be.

   
/* 26 */
   “Insert the data”
    Mouse dragItemsDo:
    [:index :typeList | mapType |

The block above takes up to two parameters and declares one block temporary variable to hold the map type. What this method is going to do is look at each of the objects in the drag-package and extract out all the objects that we can coerce to some form of <String>. This block is evaluated, by the <Mouse> object, once for each of the different clipping groups in the drag package.

A clipping group is a list of “types” (i.e., formats) that are available for a given promised object. This mechanism is the same technique that is used for Macintosh clipboard entries where there might be a pixel-map, a picture, and an icon which form a clipping group that represents a Finder copy of a desktop icon.


/* 27 */
        mapType := nil.
        (typeList includes: Text) ifTrue: 
        [
            mapType := Text
        ]
        ifFalse:
        [
            (typeList includes: String) ifTrue: 
                [mapType := String].
        ].

In the above code we examine the <typeList> for the clipping group and determine if there is an object of interest in the group and if so we record its type so that we can extract it. The SmalltalkAgents environment handles data type coercions so if we asked for a <String> when <Text> was available the framework perform the appropriate coercion (this is similar to the AppleEvent type-coercion system).

      
/* 28 */

        mapType ifTrue:
        [
            result ifNil:
            [
                result := Mouse 
                    extractDragItem: index 
                    asInstanceOf: mapType.
            ] ifNotNil:
            [
                result := result,(Mouse 
                    extractDragItem: index 
                    asInstanceOf: mapType).
            ].
        ].

In the above code block we append the extracted text or string data from the current clipping group onto the result. The Smalltalk class for <Text> preserves style information when concatenating (via the <#,> message) so we don’t have to pay attention to the complexities of Macintosh styl/text resource type concatenation.


/* 29 */
    ].
    
    result ? [^self].

If there was no data extracted then <result> will be <nil> and which means that we have no more work to do so we exit returning the receiver (self) as the method’s result. In Smalltalk a method always returns an object which enables all message operations to be treated uniformly. The default return value, if there is no explicit return value, is the message’s receiver <self>.

  
/* 30 */
    self 
        nextPutAll: result;
        selectFrom: offset to: offset+ result size.

Now we insert the drag-package data we extracted and then we select the inserted data and we are done!

Well, we are really on the way now. At this point I had spent approximately 3 hours implementing drag and drop and was ready for the final operation which was initiating a drag operation. Prior to writing my own initiator I had been dragging from the sample drag tools that Apple provided with the Drag and Drop developer kit from APDA.

In fact, we are almost done, althought I didn’t realize it at the time because I thought it would be a lot harder to build the drag initiation method than it turned out to be. The <#handleDragEvent:> method involved a minor modification to my existing <TEField> button-press method for so that I could detect when a single-click had occurred over a selected range of text. This test is the basis I used for determining whether the user intended to drag a text chunk around.

I have deleted portions of the source code from the method that follows to help you (the reader) maintain your focus on the portions of the code that were relevant to the styled text field’s drag and drop implementation.


/* 31 */
TEField[i]buttonPress: event
... Portions Deleted ...

    (Switch new)
        case: 1 do: 
        [
            (self handleDragEvent: event) ifFalse:
            [
                <<TEClick(localWhere:long; 
                    (event ShiftKeyOnly):Boolean; self:Handle)>>.
            ].

What we have done here is to test for the case where a single-click occurred. Then we pass the event to our #handleDragEvent: method and see if it handled the button-press operation. If it did not then we process the button-press as if drag and drop did not exist.

            
/* 32 */
        ];

    ... Portions Deleted ...

        on: event clickCount.
    
    self 
        updateScrollers;
       "scrollSelectionIntoView;"
        privateSetCursor.
        
    canvas popPen. 
    thread popCanvas.


TEField[i]handleDragEvent: event

   “Implement the drag drop defaults”
    ((self->selStart)   (self->selEnd)) ifTrue:
    [   
        | dragRegion |

In the code above we are accessing the structured storage of the TERecord and testing to see whether the selection range includes any characters. If it does then we enter this block and begin testing to see if the button press event location occurred over top of the current text selection.


/* 33 */
        ((dragRegion := self hiliteRegion) 
            containsPoint: (event localWhere)) ifTrue:
        [

If the hilited text selection contains the event’s local-where point and mouse button-1 is pressed then we fix up the starting location for the drag operation to ensure that any event processing latency will be corrected as we build our drag image.


/* 34 */
            [(Mouse isButton1Down) and: 
                [(Mouse localWhere maxDelta: event localWhere) 
                    ¾ 2]] whileTrue.
                

Now if <Mouse> button 1 is still down, then we construct: a drag package; an image to drag around the screen; configure drag initiation data. We also configure the drag flags such that the drag will block our initial hiliting and disable auto-centering of our drag image.

The <drag:> parameter is a clipping group that we are offering for dragging. There are other variations of the drag initiation command that the <Mouse> supports that allow multiple items to be dragged.

The <withImage:> parameter can be any arbitrary graphic object that conforms to some basic rendering protocols (i.e., responds to a predefined set of messages). In this case we use the current selection region and create an outline for the user to drag around.

The initiator data is private for the use of the drag initator and can be anything. We currently use it to hold the meta-key state at the time of drag initiation and also to hold the original drag region for text movement (as opposed to copy) animation.


/* 35 */
            Mouse isButton1Down ifTrue:
            [
                Mouse 
                    drag: {self selectedContents}
                    withImage: (dragRegion copy differenceWith:
                        (dragRegion copy insetBy: 1))
                    startingFrom: event localWhere
                    initiator: self
                    initiatorData: 
                        {Keyboard metakeys. dragRegion}

                   “Don't block initial hilite and don't 
                    auto-center”
                    flags: 0x03.   
               ^true  

Since we initiated a drag operation we signal that we have handled the button press event by returning <true> as our method result.

 
/* 36 */
            ].
        ].
    ].
   ^false

The SmalltalkAgents flags parameter allows us a finer grain of control than that which is provided by Apple’s Drag and Drop Manager. Specifically the flags enable us to control the drag tracking pre-flight operations, and also let us control where the dragging can occur. Currently the flags allow us to restrict dragging to within the initiators window, to the module that the window belongs to, to the Smalltalk environment that the window belongs to, or to the entire Macintosh Application space of the desktop.

If the dragging is not to the entire Macintosh Application space, or Apple’s Drag and Drop Manager is not available, then dragging will be completely handled in Smalltalk. The SmalltalkAgents Drag and Drop Architecture functions in a portable fashion that is independent of the presence of the Apple Drag and Drop Manager.


/* 37 */
Mouse Drag Flag Definitions

        0x0001 .. Do not initially hilite the drag-source
                  component
        0x0002 .. Do not auto-center the drag-image
        0x0010 .. Restrict dragging to the source Environment
        0x0020 .. Restrict dragging to the source Module
        0x0040 .. Restrict dragging to the source Window”

Well, that’s it. The code is all that was needed to add drag and drop with animation of dragging and the drop-moving of text selections. I hope this article helped to shed some light on the subject of how such things were done in a language like Smalltalk. The best part for me in doing all this was that it was really fun watching it come alive as I worked on a running application environment. Even when I had bugs, they were caught and I could continue my work in an uninterrupted flow. The dynamic, interactive nature of the development process really allowed me to fine tune the user interface behavior to get exactly the effects I wanted.

Closing

Overall, the adding of drag and drop to the <TEField> class took me about 4 hours from design concept to end result. Prior to doing the design and implementation, I spent about 2 days looking at other drag and drop behavior and implementations in various applications on different operating systems. After I was done with the implementation I let it sit and “breathe” for a few days and then I came back and spent some 2-3 hours interactively experimenting with variations on the animation and cursor hilite-tracking aesthetics.

Once we had drag and drop available in-house, we found ourselves using it all the time to create clippings. For example, we use it as an extended clipboard for code editing and saving operations - specifically we create useful “snippets” of code or tools that we can then compile & execute (evaluate) any time during the development process.

This has also led to a whole avenue of other unforeseen possibilities such as clipping scrapbooks, and the use of clippings as a documentation tool for our Platform Independent Portable Object (PIPO’s) packages. A PIPO is an arbitrary collection of objects that can include executable code. SmalltalkAgents PIPO mechanisms provide a real-time persistent storage, streaming, and object linking mechanism.

In the latter case, we might have a resource of type PIPO in the clipping file, and we might also have a styled text resource. The Finder would ignore the PIPO resource and would just display the styled text thus enabling the text to be used as a means of displaying a description of the other clipping contents (i.e., the PIPO).

The Finder is quite flexible in the way it handles clippings because it allows the clipping file-creator to be any type; it identifies a clipping by the file-type of clip. With a little ingenuity, an application can use the custom-icon feature of System 7 to set the clipping file’s icon to use their own stylized version to enable differentiation.

Adding a version 2 resource in the clipping file might also be a desireable behavior so that the clipping creator can be properly identified from a Finder “Get Info” window.

Feedback

The author can be reached at “David_Simmons@qks.com”. The electronic mail address “info@qks.com” is available for any product questions you might have. For more general information on SmalltalkAgents you can access:

WorldWideWeb:
The QKS World Wide Web site “http://www.qks.com/”.

INTERNET:
Anonymous ftp via “ftp.qks.com”.

Compuserve:
“Go Smalltalk”
or “Go MacDev [Dynamic Languages Section]”
or “Go MacDev [Object Oriented Section]”.

There are also a number of electronic discussion groups that talk about Smalltalk, and some that are dedicated to SmalltalkAgents. QKS provides its own “STA-Forum@QKS.COM” e-mail forum that anyone is welcome to join (for details send mail to either “listserv@qks.com” or “postmaster@qks.com”).

Standard Disclaimers

SmalltalkAgents is a registered trademark of Quasar Knowledge Systems, Inc. All other brand or product names mentioned are trademarks or registered trademarks of their respective holders.

 

Community Search:
MacTech Search:

Software Updates via MacUpdate

coconutBattery 3.9.14 - Displays info ab...
With coconutBattery you're always aware of your current battery health. It shows you live information about your battery such as how often it was charged and how is the current maximum capacity in... Read more
Keynote 13.2 - Apple's presentation...
Easily create gorgeous presentations with the all-new Keynote, featuring powerful yet easy-to-use tools and dazzling effects that will make you a very hard act to follow. The Theme Chooser lets you... Read more
Apple Pages 13.2 - Apple's word pro...
Apple Pages is a powerful word processor that gives you everything you need to create documents that look beautiful. And read beautifully. It lets you work seamlessly between Mac and iOS devices, and... Read more
Numbers 13.2 - Apple's spreadsheet...
With Apple Numbers, sophisticated spreadsheets are just the start. The whole sheet is your canvas. Just add dramatic interactive charts, tables, and images that paint a revealing picture of your data... Read more
Ableton Live 11.3.11 - Record music usin...
Ableton Live lets you create and record music on your Mac. Use digital instruments, pre-recorded sounds, and sampled loops to arrange, produce, and perform your music like never before. Ableton Live... Read more
Affinity Photo 2.2.0 - Digital editing f...
Affinity Photo - redefines the boundaries for professional photo editing software for the Mac. With a meticulous focus on workflow it offers sophisticated tools for enhancing, editing and retouching... Read more
SpamSieve 3.0 - Robust spam filter for m...
SpamSieve is a robust spam filter for major email clients that uses powerful Bayesian spam filtering. SpamSieve understands what your spam looks like in order to block it all, but also learns what... Read more
WhatsApp 2.2338.12 - Desktop client for...
WhatsApp is the desktop client for WhatsApp Messenger, a cross-platform mobile messaging app which allows you to exchange messages without having to pay for SMS. WhatsApp Messenger is available for... Read more
Fantastical 3.8.2 - Create calendar even...
Fantastical is the Mac calendar you'll actually enjoy using. Creating an event with Fantastical is quick, easy, and fun: Open Fantastical with a single click or keystroke Type in your event details... Read more
iShowU Instant 1.4.14 - Full-featured sc...
iShowU Instant gives you real-time screen recording like you've never seen before! It is the fastest, most feature-filled real-time screen capture tool from shinywhitebox yet. All of the features you... Read more

Latest Forum Discussions

See All

The iPhone 15 Episode – The TouchArcade...
After a 3 week hiatus The TouchArcade Show returns with another action-packed episode! Well, maybe not so much “action-packed" as it is “packed with talk about the iPhone 15 Pro". Eli, being in a time zone 3 hours ahead of me, as well as being smart... | Read more »
TouchArcade Game of the Week: ‘DERE Veng...
Developer Appsir Games have been putting out genre-defying titles on mobile (and other platforms) for a number of years now, and this week marks the release of their magnum opus DERE Vengeance which has been many years in the making. In fact, if the... | Read more »
SwitchArcade Round-Up: Reviews Featuring...
Hello gentle readers, and welcome to the SwitchArcade Round-Up for September 22nd, 2023. I’ve had a good night’s sleep, and though my body aches down to the last bit of sinew and meat, I’m at least thinking straight again. We’ve got a lot to look at... | Read more »
TGS 2023: Level-5 Celebrates 25 Years Wi...
Back when I first started covering the Tokyo Game Show for TouchArcade, prolific RPG producer Level-5 could always be counted on for a fairly big booth with a blend of mobile and console games on offer. At recent shows, the company’s presence has... | Read more »
TGS 2023: ‘Final Fantasy’ & ‘Dragon...
Square Enix usually has one of the bigger, more attention-grabbing booths at the Tokyo Game Show, and this year was no different in that sense. The line-ups to play pretty much anything there were among the lengthiest of the show, and there were... | Read more »
Valve Says To Not Expect a Faster Steam...
With the big 20% off discount for the Steam Deck available to celebrate Steam’s 20th anniversary, Valve had a good presence at TGS 2023 with interviews and more. | Read more »
‘Honkai Impact 3rd Part 2’ Revealed at T...
At TGS 2023, HoYoverse had a big presence with new trailers for the usual suspects, but I didn’t expect a big announcement for Honkai Impact 3rd (Free). | Read more »
‘Junkworld’ Is Out Now As This Week’s Ne...
Epic post-apocalyptic tower-defense experience Junkworld () from Ironhide Games is out now on Apple Arcade worldwide. We’ve been covering it for a while now, and even through its soft launches before, but it has returned as an Apple Arcade... | Read more »
Motorsport legends NASCAR announce an up...
NASCAR often gets a bad reputation outside of America, but there is a certain charm to it with its close side-by-side action and its focus on pure speed, but it never managed to really massively break out internationally. Now, there's a chance... | Read more »
Skullgirls Mobile Version 6.0 Update Rel...
I’ve been covering Marie’s upcoming release from Hidden Variable in Skullgirls Mobile (Free) for a while now across the announcement, gameplay | Read more »

Price Scanner via MacPrices.net

New low price: 13″ M2 MacBook Pro for $1049,...
Amazon has the Space Gray 13″ MacBook Pro with an Apple M2 CPU and 256GB of storage in stock and on sale today for $250 off MSRP. Their price is the lowest we’ve seen for this configuration from any... Read more
Apple AirPods 2 with USB-C now in stock and o...
Amazon has Apple’s 2023 AirPods Pro with USB-C now in stock and on sale for $199.99 including free shipping. Their price is $50 off MSRP, and it’s currently the lowest price available for new AirPods... Read more
New low prices: Apple’s 15″ M2 MacBook Airs w...
Amazon has 15″ MacBook Airs with M2 CPUs and 512GB of storage in stock and on sale for $1249 shipped. That’s $250 off Apple’s MSRP, and it’s the lowest price available for these M2-powered MacBook... Read more
New low price: Clearance 16″ Apple MacBook Pr...
B&H Photo has clearance 16″ M1 Max MacBook Pros, 10-core CPU/32-core GPU/1TB SSD/Space Gray or Silver, in stock today for $2399 including free 1-2 day delivery to most US addresses. Their price... Read more
Switch to Red Pocket Mobile and get a new iPh...
Red Pocket Mobile has new Apple iPhone 15 and 15 Pro models on sale for $300 off MSRP when you switch and open up a new line of service. Red Pocket Mobile is a nationwide service using all the major... Read more
Apple continues to offer a $350 discount on 2...
Apple has Studio Display models available in their Certified Refurbished store for up to $350 off MSRP. Each display comes with Apple’s one-year warranty, with new glass and a case, and ships free.... Read more
Apple’s 16-inch MacBook Pros with M2 Pro CPUs...
Amazon is offering a $250 discount on new Apple 16-inch M2 Pro MacBook Pros for a limited time. Their prices are currently the lowest available for these models from any Apple retailer: – 16″ MacBook... Read more
Closeout Sale: Apple Watch Ultra with Green A...
Adorama haș the Apple Watch Ultra with a Green Alpine Loop on clearance sale for $699 including free shipping. Their price is $100 off original MSRP, and it’s the lowest price we’ve seen for an Apple... Read more
Use this promo code at Verizon to take $150 o...
Verizon is offering a $150 discount on cellular-capable Apple Watch Series 9 and Ultra 2 models for a limited time. Use code WATCH150 at checkout to take advantage of this offer. The fine print: “Up... Read more
New low price: Apple’s 10th generation iPads...
B&H Photo has the 10th generation 64GB WiFi iPad (Blue and Silver colors) in stock and on sale for $379 for a limited time. B&H’s price is $70 off Apple’s MSRP, and it’s the lowest price... Read more

Jobs Board

Optometrist- *Apple* Valley, CA- Target Opt...
Optometrist- Apple Valley, CA- Target Optical Date: Sep 23, 2023 Brand: Target Optical Location: Apple Valley, CA, US, 92308 **Requisition ID:** 796045 At Target Read more
Senior *Apple* iOS CNO Developer (Onsite) -...
…Offense and Defense Experts (CODEX) is in need of smart, motivated and self-driven Apple iOS CNO Developers to join our team to solve real-time cyber challenges. Read more
*Apple* Systems Administrator - JAMF - Activ...
…**Public Trust/Other Required:** None **Job Family:** Systems Administration **Skills:** Apple Platforms,Computer Servers,Jamf Pro **Experience:** 3 + years of Read more
Child Care Teacher - Glenda Drive/ *Apple* V...
Child Care Teacher - Glenda Drive/ Apple ValleyTeacher Share by Email Share on LinkedIn Share on Twitter Share on Facebook Apply Read more
Machine Operator 4 - *Apple* 2nd Shift - Bon...
Machine Operator 4 - Apple 2nd ShiftApply now " Apply now + Start apply with LinkedIn + Apply Now Start + Please wait Date:Sep 22, 2023 Location: Swedesboro, NJ, US, Read more
All contents are Copyright 1984-2011 by Xplain Corporation. All rights reserved. Theme designed by Icreon.