Drag and Drop Explained
Last updated at 6:48 pm UTC on 27 January 2020
Drag and Drop (PBE)
The drop process starts with the HandMorph handling an event
- that leads to #dropMorphs: and #dropMorph:event:
- and then MorphicEventDispatcher>dispatchDropEvent:with:.
This is supposed to find the right recipient morph and get it to deal with the drop. If no morph will handle the event (for example, rejecting it completely) then #rejectDropMorphEvent: is sent to the droppee to clear up.
MorphicEventDispatcher>dispatchDropEvent:with starts by checking if the current morph fullBounds (important distinction) contains the event’s cursor point; if not then there is no drop to consider and we return #rejected.
Next we ask if the recipient wants to reject the drop using #rejectDropEvent:.
Then we recurse down the submorphs, passing the entire dispatch down the tree using 'child processEvent:’ (which usually leads on to MorphicEventDispatcher>dispatchDropEvent:with: for each submorph). The return value that comes back from submorphs is extremely important; if #rejected is returned then the parent morph will keep trying its other submorphs. If any other value is returned then the parents’ looping through submorphs is broken out of; if the event was marked as handled then we return immediately and typically this will return through all the levels of Morph back to the root.
- #rejectDropEvent: is the recipient’s chance to cancel a drop in progress if it is unwanted. It checks to see if the recipient wants to reject the drop with #repelsMorph:event:, checks the drop point is in recipient bounds, and if needed sends the dropped #rejectDropMorphEvent:
- #repelsMorph:event: - tests if the morph wants to avoid the drop event. The default is a simple ^false but PasteUpMorph uses #wantsToBeDroppedInto: + #wantsDroppedMorph:event: + #dropEnabled rather like #handleDropMorph: This is a good place to reject the entire drop transaction and avoid all the following work, or to intercept the entire drop and handle it specifically for the recipient without any submorphs being given a chance. This might be useful to allow dropping into any position within a crowded area; it would avoid any chance of the drop ending up in an inner morph. It is also possible to override #rejectsEvent: (part of the very high level event dispatching) to examine all events incoming and reject them, whether drop event or any other.
- #rejectDropMorphEvent: - handles the consequences of actually repelling the drop. It can send the morph back to its original home, vanish it or send it to trash, depending on whether it remembers the #formerOwner, #formerPosition, or neither. See also #slideBackToFormerSituation: #vanishAfterSlidingTo:event: & #slideToTrash:
If no submorph actually deals with the event then the recipient is given a chance by sending #handleEvent:, which leads eventually leads to Morph>handleDropMorph: evt
- #handleDropMorph: checks both that the recipient wants the dropped morph (#wantsDroppedMorph:event:) and the dropped morph wants to go into the recipient (#wantsToBeDroppedInto:); both must be true for the drop to proceed.
- #wantsDroppedMorph:event: - tests for whether the dropped morph is wanted by recipient; return true or false. It can test the morph, the event, or both. The default Morph implementation is just whether #dropEnabled. Other implementations check class, position, current state of application etc.
- #dropEnabled - a simple test of property by default; it’s used in several situations so be careful about over-riding it. Since this is a per-Morph property you can enable dropping for individual morphs rather than entire classes.
- #wantsToBeDroppedInto: - does the receiver want to be dropped into morph? Default is simple ^true but e.g. MenuMorph checks for the target being the World or only having a single entry (i.e. being a button). A TransferMorph does some tests for transfer convertors etc.
- If both are true, we transform the event coords, mark the event as handled (important later) and do the actual dropping. The recipient is sent #acceptDroppingMorph:event: and the droppee is sent #justDroppedInto:event:. It is quite possible to reject the drop even at this stage - see for example ZoomAndScrollMorph.
- #acceptDroppingMorph:event: - actually handles adding the droppee to the target. The default makes sure the layout policy (if used) gets to work out where it goes. Quite a few implementations also pass this off to the target’s model (i.e. PluggableListMorph) and it is possible to reject the drop by sending #rejectDropMorphEvent: to the droppee (i.e. ZoomAndScrollMorph)
- #justDroppedInto:event: - is used to deal with the aftermath for the droppee; default does quite a lot of stuff & most subclasses send super as well as their own work.
Important drop related messages. (*) marks ones intended for you to over-ride
- * #repelsMorph:event:
- * #wantsToBeDroppedInto:
- * #dropEnabled
- * #acceptDroppingMorph:event:
- * #justDroppedInto:event:
The Dropping process
Consider a fairly simple tree of Morphs; A contains B & C. We drop morph D somewhere over A and get to #dispatchDropEvent: event with: B.
In the case that B’s bounds do not include the event cursor point, we immediately return #rejected. This returns us to the middle of #dispatchDropEvent: event with: A and since the return value is == #rejected we simply move on to looking at C.
In the case of the event being within B’s fullBounds, we try ‘B rejectDropEvent:’.
All this complicated recursion is intended to allow the lowest level morph to get a chance to accept the drop. Note that submorphs are considered first; so a complex morph structure that you do not want to accept drops even if some interior morphs may be capable should implement #repelsMorph:event: to reject the drop immediately.
- If B does reject the event it also marks it as handled and so we return ‘self’ back to the middle of #dispatchDropEvent: event with: A but this time we have a handled event and so exit to whatever parent A might have, job completed.
- If the event is not handled - i.e. typically, not rejected - then we proceed to test each of B’s submorphs, which in our case we have not got and thus we skip to the end where B actually handles the event and gets sent #handleDropMorph:.
- If the drop is accepted then the event is marked as handled before returning and we would exit back to #dispatchDropEvent: event with: A with the job completed.
- If the drop is not accepted we return to A with the event unhandled and but not rejected; this tells us that the drop was within the bounds of B but was not wanted. As a last resort #dispatchDropEvent: event with: A sends the event on to A and it gets a chance to #handleDropMorph:
A very simple example of how this works -
Example for the #dropEnabled property (Morphic)
A little more complicated
Open the halo for the embedded CircleMorph and use its red menu to allow dropping on the CircleMorph. Drag another morph from the Objects viewer and drop it on top of the circle; if you check in the explorer you will see it as a submorph of the circle. So, now we can drop morphs into the RectangleMorph or the CircleMorph but what if we need to only allow dropping into the circle? All we need to do is (using the red menu) turn off ‘accept drops’ for the RectangleMorph - now dropping a morph on the rectangle will fail but on the circle will work.
Building a drop target morph.
A very basic drop target needs only two methods implementing
An example is provided, see below.
- #wantsDroppedMorph:event: to return true for relevant morphs
- #acceptDroppingMorph:event: to do whatever is needed with the dropped morph
When a mouse button is pressed, the HandMorph will create a MouseButtonEvent that gets sent to a Morph with #handleMouseDown:. which then sends itself #mouseDown: There are two distinct paths the code can follow at this point -
When next handling a mouse event, HandMorphhandleEvent: checks for an existing MouseClickState object and sends #handleEvent:from: to it. This is where the actual drag handling work is done.
- The default #mouseDown: checks for an eventHandler (see #on:send:to:) and if there is one, sends it #mouseDown:fromMorph:
- This uses HandMorph>waitForClicksOrDrag:event: to create a MouseClickState and set the default action selectors #( #click: #doubleClick: #doubleClickTimeout: #startDrag:) thus making #startDrag: the typical selector for starting a drag action. The click state is an ephemeral object that carries some input event state around for a while to record the drag values.
- #on:send:to: is used to set up an event handler for the morph. There are many options but only the usage of ‘aMorph on: #startDrag send: #someMessage to: aMorphOrOtherObject’ is relevant to dragging. Although this can be sent at any point, typically it is used during the building of the morphs. You can use #removeLink: #startDrag to remove the event handler at any point.
- It is also possible to over-ride the default #mouseDown: and build a MouseClickState more directly; this is useful for specialised Morph classes. This is marginally faster than using the event handler but it is a class-wide change rather than a per-instance effect. You can either keep to the default selector name(s) and over-ride #startDrag: or use any other message of your choice. Just remember which you choose! PasteUpMorph>mouseDown: uses #dragThroughOnDesktop:, for example. PluggableListMorphmouseDown: makes some run-time choices about the selectors to use.
MouseClickState#handleEvent:from: works out if the action is intended to be a drag. If there is a dragSelector set (see #on:send:to: and some versions of #mouseDown:) it sends #drag: to itself. (NB - this seems pointless, there are no other users, re-does a check for nil selector, should really be folded in).
The ‘clickClient’ set by the prior #on:send:to: or #mouseDown: invocation is sent the dragSelector. Usually that means the clickClient gets sent #startDrag: (simply because most people stick to the default). With either the event handler or the manually created MouseClickState it is possible to have a message sent to any object; there is no requirement that it be the morph involved in the drag operation. We could, for example, have a message logger object that simply reports the action for debugging or tracking purposes.
Finally the morph to drag has to be chosen and attached to the HandMorph. Usually this requires finding a morph at the event’s cursorPosition (#morphAt: helps, but provides all the submorphs intersecting that point which is a bit overwhelming. What is the simpler message?) and then sending the hand #grabMorph: with the chosen morph.
- The default MorphstartDrag: implementation attempts to delegate to the eventHandler (the same one checked in the #mouseDown: & #handleEvent:from: mentioned above) and sends it #startDrag:fromMorph: which then sends its chosen target the chosen message.
- When other morph classes over-ride #startDrag: in various ways don’t forget that use of event handlers could bypass those implementations by choosing a different drag selector to be used. I.E. using ‘on: #startDrag send: #foobley: to: aMorph’ when aMorph also implements #mouseDown: to create a MouseClickState might cause interesting issues.
HandMorphgrabMorph: provides two more opportunities for the dragged morph to deal with the impending drag. The #aboutToBeGrabbedBy: method is used to do any pre-grab cleanup (see SelectionMorph for a simple example) and we should make sure to pass it on to super so that the default actions are followed. After the morph has been attached to the active Hand we send #justGrabbedFrom: to it to allow final clean-up or ex-owner edit saving etc. The only current implementors use it to save eToys scripts for Tile morphs that are being dragged out of an editor.
Important drag related messages. (*) marks ones intended for you to over-ride
- * #mouseDown:
- * #startDrag:
- * #aboutToBeGrabbedBy
A very simple example of how this works -
You still have that rectangle with the embedded circle, right? Using the explorer - it’s still open? - select the rectangle morph and then in the text view at the bottom of the explorer type -
self on: #startDrag send: #halt to: self
Now if you try to drag the rectangle you should get a typical error notifier as the #halt gets sent to the rectangle. A quick look in the debugger will illustrate how the execution got there.
To turn off the action you can use
self removeLink: #startDrag
Now select the circlemorph and DoIt again on the #on:send:to: line; this time when you drag the rectangle it will move as you might expect. Drag the circle instead and you will get the notifier.
Here we are relying upon the event handler mechanism rather than subclassing and over-riding #mouseDown:
Building a drag source morph
To provide basic dragging capability is quite simple
- use ‘on: #startDrag send: #foobley: to: aMorph, or
- implement #handlesMouseDown: to return true (if circumstances warrant - you may need to check some state at run-time) and #mouseDown: to build the MouseClickState with relevant selectors.
An example pair of morphs
Both example classes are subclasses of Morph to keep this as simple as possible; the new morph can be added into some other Morph to provide borders, colours etc.
All submorphs will be draggable, so only add items to be dragged. This example is only a source of morphs, so no handling of drop events is needed. Any morph dragged will provide a copy so that the source does not get emptied.
Since this is a class intended to be for parts bin use, we can over-ride #mouseDown: and build a MouseClickState directly rather than using the event handler per-instance mechanism.
#mouseDown: can check for a submorph that is under the event position and only create the MouseClickState when there is one; this should save having to check later. Since we are only interested in the dragging, the quad of selectors to pass to the #waitForClicksOrDrag:event:selectors:threshold: needs to be three nils and #startDrag:.
The #startDrag: code is very simple - find the target morph and have the hand grab a copy of it.
A side-effect of picking up a morph from our experiment is that you can’t put it back. It is possible to handle this and add some drop capability that checks for a morph recently taken from the source.
The example will be only accept CircleMorphs, so the #wantsDroppedMorph:event: method simply returns whether the dropped morph class matches. In order to do something faintly interesting with the dropped morphs, the #acceptDroppingMorph:event: method
- checks for a layout policy and if one has been set we use it
- otherwise, works out a grid position within the target and puts the dropped there.
Load the Morphic-Demo-Drag package from SqueakSource
Then in a Workspace execute -
Drag the blue square target morph away a little so you can drag white or black circles to the target. Actually implementing rules of the game is left as an exercise for the reader.
Play with Plumbin'!
It's here- it's back! WardCunningham's famous ancient example for Squeak 5.x - Plumbin' - it is in the same repository as the above demo code.
See #send:to:withEvent:fromMorph: for some explanation of details about what parameters can be sent with the message.
The MouseClickState is created by sending #waitForClicksOrDrag:event:selectors:threshold: to a HandMorph (see PluggableListMorph>mouseDown: for an example) with a load of options as to what gets done. The general case is in HandMorphwaitForClicksOrDrag:event: but there are many options.