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

Latest Forum Discussions

See All

Make the passage of time your plaything...
While some of us are still waiting for a chance to get our hands on Ash Prime - yes, don’t remind me I could currently buy him this month I’m barely hanging on - Digital Extremes has announced its next anticipated Prime Form for Warframe. Starting... | Read more »
If you can find it and fit through the d...
The holy trinity of amazing company names have come together, to release their equally amazing and adorable mobile game, Hamster Inn. Published by HyperBeard Games, and co-developed by Mum Not Proud and Little Sasquatch Studios, it's time to... | Read more »
Amikin Survival opens for pre-orders on...
Join me on the wonderful trip down the inspiration rabbit hole; much as Palworld seemingly “borrowed” many aspects from the hit Pokemon franchise, it is time for the heavily armed animal survival to also spawn some illegitimate children as Helio... | Read more »
PUBG Mobile teams up with global phenome...
Since launching in 2019, SpyxFamily has exploded to damn near catastrophic popularity, so it was only a matter of time before a mobile game snapped up a collaboration. Enter PUBG Mobile. Until May 12th, players will be able to collect a host of... | Read more »
Embark into the frozen tundra of certain...
Chucklefish, developers of hit action-adventure sandbox game Starbound and owner of one of the cutest logos in gaming, has released their roguelike deck-builder Wildfrost. Created alongside developers Gaziter and Deadpan Games, Wildfrost will... | Read more »
MoreFun Studios has announced Season 4,...
Tension has escalated in the ever-volatile world of Arena Breakout, as your old pal Randall Fisher and bosses Fred and Perrero continue to lob insults and explosives at each other, bringing us to a new phase of warfare. Season 4, Into The Fog of... | Read more »
Top Mobile Game Discounts
Every day, we pick out a curated list of the best mobile discounts on the App Store and post them here. This list won't be comprehensive, but it every game on it is recommended. Feel free to check out the coverage we did on them in the links below... | Read more »
Marvel Future Fight celebrates nine year...
Announced alongside an advertising image I can only assume was aimed squarely at myself with the prominent Deadpool and Odin featured on it, Netmarble has revealed their celebrations for the 9th anniversary of Marvel Future Fight. The Countdown... | Read more »
HoYoFair 2024 prepares to showcase over...
To say Genshin Impact took the world by storm when it was released would be an understatement. However, I think the most surprising part of the launch was just how much further it went than gaming. There have been concerts, art shows, massive... | Read more »
Explore some of BBCs' most iconic s...
Despite your personal opinion on the BBC at a managerial level, it is undeniable that it has overseen some fantastic British shows in the past, and now thanks to a partnership with Roblox, players will be able to interact with some of these... | Read more »

Price Scanner via MacPrices.net

You can save $300-$480 on a 14-inch M3 Pro/Ma...
Apple has 14″ M3 Pro and M3 Max MacBook Pros in stock today and available, Certified Refurbished, starting at $1699 and ranging up to $480 off MSRP. Each model features a new outer case, shipping is... Read more
24-inch M1 iMacs available at Apple starting...
Apple has clearance M1 iMacs available in their Certified Refurbished store starting at $1049 and ranging up to $300 off original MSRP. Each iMac is in like-new condition and comes with Apple’s... Read more
Walmart continues to offer $699 13-inch M1 Ma...
Walmart continues to offer new Apple 13″ M1 MacBook Airs (8GB RAM, 256GB SSD) online for $699, $300 off original MSRP, in Space Gray, Silver, and Gold colors. These are new MacBook for sale by... Read more
B&H has 13-inch M2 MacBook Airs with 16GB...
B&H Photo has 13″ MacBook Airs with M2 CPUs, 16GB of memory, and 256GB of storage in stock and on sale for $1099, $100 off Apple’s MSRP for this configuration. Free 1-2 day delivery is available... Read more
14-inch M3 MacBook Pro with 16GB of RAM avail...
Apple has the 14″ M3 MacBook Pro with 16GB of RAM and 1TB of storage, Certified Refurbished, available for $300 off MSRP. Each MacBook Pro features a new outer case, shipping is free, and an Apple 1-... Read more
Apple M2 Mac minis on sale for up to $150 off...
Amazon has Apple’s M2-powered Mac minis in stock and on sale for $100-$150 off MSRP, each including free delivery: – Mac mini M2/256GB SSD: $499, save $100 – Mac mini M2/512GB SSD: $699, save $100 –... Read more
Amazon is offering a $200 discount on 14-inch...
Amazon has 14-inch M3 MacBook Pros in stock and on sale for $200 off MSRP. Shipping is free. Note that Amazon’s stock tends to come and go: – 14″ M3 MacBook Pro (8GB RAM/512GB SSD): $1399.99, $200... Read more
Sunday Sale: 13-inch M3 MacBook Air for $999,...
Several Apple retailers have the new 13″ MacBook Air with an M3 CPU in stock and on sale today for only $999 in Midnight. These are the lowest prices currently available for new 13″ M3 MacBook Airs... Read more
Multiple Apple retailers are offering 13-inch...
Several Apple retailers have 13″ MacBook Airs with M2 CPUs in stock and on sale this weekend starting at only $849 in Space Gray, Silver, Starlight, and Midnight colors. These are the lowest prices... Read more
Roundup of Verizon’s April Apple iPhone Promo...
Verizon is offering a number of iPhone deals for the month of April. Switch, and open a new of service, and you can qualify for a free iPhone 15 or heavy monthly discounts on other models: – 128GB... Read more

Jobs Board

IN6728 Optometrist- *Apple* Valley, CA- Tar...
Date: Apr 9, 2024 Brand: Target Optical Location: Apple Valley, CA, US, 92308 **Requisition ID:** 824398 At Target Optical, we help people see and look great - and Read more
Medical Assistant - Orthopedics *Apple* Hil...
Medical Assistant - Orthopedics Apple Hill York Location: WellSpan Medical Group, York, PA Schedule: Full Time Sign-On Bonus Eligible Remote/Hybrid Regular Apply Now 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
Liquor Stock Clerk - S. *Apple* St. - Idaho...
Liquor Stock Clerk - S. Apple St. Boise Posting Begin Date: 2023/10/10 Posting End Date: 2024/10/14 Category: Retail Sub Category: Customer Service Work Type: Part Read more
Top Secret *Apple* System Admin - Insight G...
Job Description Day to Day: * Configure and maintain the client's Apple Device Management (ADM) solution. The current solution is JAMF supporting 250-500 end points, Read more
All contents are Copyright 1984-2011 by Xplain Corporation. All rights reserved. Theme designed by Icreon.