Elegant Drag N Drop
Volume Number: | | 10
|
Issue Number: | | 11
|
Column Tag: | | Implementors Journal
|
Related Info: TextEdit
Implementing Elegant Drag and Drop
for Styled Text Fields
An implementors 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 doesnt 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. Apples 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 Taligents 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-targets 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 havent 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 Smalltalks (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 methods 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-methods 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, lets 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 threads 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 protocols 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 mouses local (current graphic ports) 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 compilers 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 doesnt 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 hasnt moved by comparing the current <offset> against the offset the last time the text-drag-caret location was computed. If the offset hasnt changed then the mouse hasnt 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 Managers 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 dont 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 selections 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 dont 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 receivers 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.
Apples drag manager would be substituted here for drag animation but since we had our own with a bit more tuning flexibility, I used the SmalltalkAgents 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 dont 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 methods 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 messages 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 didnt 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 fields 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 events 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 Apples 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 Apples 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, thats 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 (PIPOs) 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 files 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.