Using the Clipboard and Drag and Drop

This document covers the following topics:


Introduction

Both clipboard and drag/drop data transfer make use of a logical clipboard at the Natural language level, allowing a single set of methods to handle both requirements. The PROCESS GUI actions for handling the logical clipboard are as follows: OPEN-CLIPBOARD, SET-CLIPBOARD-DATA, CLOSE-CLIPBOARD, GET-CLIPBOARD-DATA and INQ-FORMAT-AVAILABLE. Each Natural process has exactly one logical clipboard, which is why it is referred to in the product documentation as the "local" clipboard.

OPEN-CLIPBOARD is the first step in building up the logical clipboard data. It takes an optional parameter (owner window), which is typically the handle of the control sourcing the data. If anything was previously on the logical clipboard, this action empties it. Note that you don't need to call this for drag and drop, because Natural does this implicitly before raising the BEGIN-DRAG event (see below).

SET-CLIPBOARD-DATA puts the actual data on the logical clipboard. The first parameter is the clipboard format, specified as a string. There are two pre-defined formats (defined in NGULKEY1 as CF-TEXT and CF-FILELIST), which are used for standard text transfer, and lists of files (suitable for data exchange with the Windows Explorer and many other applications) respectively. In addition, an arbitrary string (which should not begin with a dgit) should be used to indicate a private clipboard format that only Natural applications can understand (they just need to know the format string so they can pass it to GET-CLIPBOARD-DATA to retrieve the data). The second and subsequent arguments are an arbitrary number of data operands. These can be any combination of arrays (incl. index ranges) or scalars (incl. dynamic and large alpha variables). Arrays are internally expanded into their individual elements, which are then handled individually as for scalars.

For example, the following code:

DEFINE DATA LOCAL
1 #ARR(A1/2,3) INIT (1,1)<'A'> (1,2)<'B'> (1,3)<'C'>
                              (2,1)<'X'> (2,2)<'Y'> (2,3)<'Z'>
END-DEFINE
PROCESS GUI ACTION SET-CLIPBOARD-DATA WITH CF-TEXT #ARR(*,*)

is equivalent to:

PROCESS GUI ACTION SET-CLIPBOARD-DATA WITH CF-TEXT
  'A' 'B' 'C' 'X' 'Y' 'Z'

For the pre-defined formats, the operands must be alphanumeric (format A). For private formats, the data arguments can be of almost any type. Exception: handle variables (incl. HANDLE OF OBJECT) are not supported, because they are process-specific. The data for private formats is stored "as-is" (i.e., no conversion).

Note that multiple data formats can be placed on the clipboard by performing a SET-CLIPBOARD-DATA action for each required format. However, any call to SET-CLIPBOARD-DATA for a particular format replaces any data that may already exist in that format.

Note also that the data is not placed on the Windows clipboard. This is done when the logical clipboard is closed (see below).

CLOSE-CLIPBOARD closes the logical clipboard, and places the data on the Windows clipboard, so that it becomes available for pasting into other applications. The data cannot be modified by SET-CLIPBOARD-DATA after this call. Note that this call is not necessary for drag and drop because you usually don't need to also make the dragged data available for pasting. Drag and drop can work directly with the logical clipboard.

GET-CLIPBOARD-DATA is used by the application performing the paste or acting as the drop target to retrieves the data from the drag-drop clipboard (if a drag and drop operation is in progress) or from the Windows clipboard otherwise. The drag-drop clipboard is a synonym for the logical clipboard belonging to the source Natural process. SET-CLIPBOARD-DATA, a clipboard format is specified, followed by an arbitrary list of data operands (with the same format type restrictions). For private formats, the operands need not have the same format type as used in the GET-CLIPBOARD-DATA action. For example, you can place an integer on the clipboard and read it back into a packed numeric (P) variable. Internally, a MOVE conversion is done. Therefore, if different format types are used for setting and getting the data, they must be MOVE-compatible.

For pre-defined formats, where the individual data items are either delimited by CR/LF (for CF-TEXT format) or by null-terminators (for CF-FILELIST format), only one item is usually read into each receiving field. Exception: If the last receiving field is a dynamic alpha variable, it receives all remaining data items, including the delimiters. This exception allows the application to use (for example) a single dynamic alpha variable to set and get multiple lines of data or multiple file/directory names. Regardless of the format used, if too many receiving operands are specified, the excess fields are reset (see the RESET statement). Note that individual data fields may be skipped by using the nX notation. For example, 5x skips 5 data items (where a "data item" is a single line for CF-TEXT format).

INQ-FORMAT-AVAILABLE is used for querying whether data is available in a given format (see specification for syntax). It is typically used to determine whether to enable or disable the Paste command, or whether to display the "no drop" cursor for drag/drop operations.

Clipboard Specifics

The actual clipboard data transfer has been covered above. However, Natural allows you to define signals, menu items and toolbar items of the special types Cut, Copy, Paste, Delete and Undo, which (unlike normal commands) do not raise CLICK events. For input fields, edit areas, selection boxes and table controls, it's obvious what Natural should do, and Natural does this implicitly. For Natural, these commands now support list boxes and ActiveX controls. However, the mechanism is different in this case, because it is ambiguous as to how Natural should respond to these commands. Therefore, Natural needs some assistance from the application. This assistance comes in the form of six new events: CUT, COPY, PASTE, DELETE, UNDO and CLIPBOARD-STATUS, all of which are suppressible (via the new SUPPRESS-CUT-EVENT, SUPPRESS-COPY-EVENT, SUPPRESS-PASTE-EVENT, SUPPRESS-DELETE-EVENT, SUPPRESS-UNDO-EVENT and SUPPRESS-CLIPBOARD-STATUS-EVENT attributes). All six events are suppressed by default. The CUT, COPY, PASTE, DELETE and UNDO events are raised whenever the respective command is triggered. The corresponding event suppression flags are used by Natural to decide whether to enable or disable the corresponding command(s) in the user interface.

The CLIPBOARD-STATUS event is sent to the focus control during idle processing to give the application a chance to set these event suppression flags dynamically according to the context (e.g., whether or not there is an active selection). Natural raises this event before it queries the event suppression flags for the purpose of clipboard command status updating). Note that these new events are (currently) only sent to list boxes and ActiveX controls (and, of course, only if they currently have the focus). Input fields, selection boxes, etc., are still handled implicitly.

The CLIPBOARD-STATUS event is only raised if there is at least one clipboard command in the user interface that needs to be updated.

The following example shows a typical CLIPBOARD-STATUS event for a list box control:

DEFINE DATA LOCAL
1 #CONTROL HANDLE OF GUI
1 #FMT (A10) CONST<'MYDATAFMT'>
1 #AVAIL (L)
END-DEFINE
...
#CONTROL := *CONTROL
/*
/*
Cut, Copy & Delete are enabled if an item is selected,
/*or disabled otherwise
/*
IF #CONTROL.SELECTED-SUCCESSOR <> NULL-HANDLE
  #CONTROL.SUPPRESS-CUT-EVENT := NOT-SUPPRESSED
  #CONTROL.SUPPRESS-COPY-EVENT := NOT-SUPPRESSED 
  #CONTROL.SUPPRESS-DELETE-EVENT := NOT-SUPPRESSEDELSE 
  #CONTROL.SUPPRESS-CUT-EVENT := SUPPRESSED 
  #CONTROL.SUPPRESS-COPY-EVENT := SUPPRESSED
  #CONTROL.SUPPRESS-DELETE-EVENT := SUPPRESSED
END-IF
/*
/* Paste command is enabled if data is available in a 
/* recognized format, or disabled otherwise
/*
PROCESS GUI ACTION INQ-FORMAT-AVAILABLE 
 WITH #FMT #AVAIL GIVING *ERROR
/*
IF #BOOL 
  #CONTROL.SUPPRESS-PASTE-EVENT := NOT-SUPPRESSED
ELSE 
  #CONTROL.SUPPRESS-PASTE-EVENT := SUPPRESSED
END-IF

Drag and Drop Specifics

Drag and drop operations can be triggered automatically (for list boxes and bitmap controls) or manually, via the new PERFORM-DRAG-DROP action (typically for ActiveX controls in response to control-specific mouse click or drag start events). For automatic drag/drop, the mouse cursor must be over the active selection (if any). For manual drag/drop, the parameters for PERFORM-DRAG-DROP include the handle of the control that should receive the drag/drop events (the drag source), and an optional flag indicating whether drag and drop should begin immediately, or only after the user moves the mouse a system-defined minimum number of pixels. Both automatic and manual drag/drop use the same code internally, so the same events are received in both cases.

Drag/drop is controlled by two new I4 attributes, DRAG-MODE (for drag sources) and DROP-MODE (for drop targets). These attributes can be set to one of 8 values (defined in NGULKEY1): DM-NONE (no drag/drop allowed), DM-COPY (copy allowed), DM-MOVE (move allowed), DM-COPYMOVE (copy and move allowed), DM-LINK (link allowed), DM-COPYLINK (copy and link allowed), DM-MOVELINK (move and link allowed), DM-COPYMOVELINK (copy, move and link allowed). Link operations imply that the drop target should create a link to the source data, rather than creating a copy of it. For file operations, desktop shortcuts are typically used (not currently explicitly supported by Natural). Drag operations are only initiated if the source's DRAG-MODE attribute is set to something other that the default DM-NONE value. In addition, the application must respond to the BEGIN-DRAG event (see below).

Control types capable of acting as drop targets are: ActiveX controls, bitmap controls, list boxes, control boxes, edit areas, and dialogs (tab controls and table controls are planned for the future but are not currently supported). These windows are, however, only registered with OLE as drop targets if their DROP-MODE attribute is set to something other that the default DM-NONE value. During a drag/drop operation, OLE automatically searches up through the window hierarchy, starting with the window immediately under the cursor, until it finds a window that has been registered as a drop target. This is the window that gets the OLE drop notifications and therefore is the window that receives the Natural drag/drop events (see below).

The new drag/drop related events are: BEGIN-DRAG, END-DRAG, DRAG-ENTER, DRAG-OVER and DRAG-LEAVE. In addition, the existing DRAG-DROP event (for the Mickey Mouse non-OLE drag/drop support for bitmap controls) is also used. All events are suppressible via the appropriate event suppression attributes (SUPPRESS-BEGIN-DRAG-EVENT etc.), all of which are SUPPRESSED by default.

The BEGIN-DRAG event (if not suppressed) is sent to the drag source on initiation of a drag operation. The application must use the SET-CLIPBOARD-DATA action to place some data on the drag/drop clipboard before returning from this event, otherwise the drag/drop operation is implicitly cancelled without the mouse cursor having changed. Note that there is no need to call either of the OPEN-CLIPBOARD or CLOSE-CLIPBOARD actions.

The END-DRAG event (if not suppressed) is sent to the drag source after a drag/drop operation has completed (even if the drag operation was cancelled). The main use of this event is to delete the source data if a Move operation occurred. The application can find out whether a Move operation has occurred by calling the existing INQ-DRAG-DROP action, which has been extended with two new optional integer output parameters. The first of these new parameters indicates which mouse buttons are currently pressed (1 = Left button, 2 = Right button, 4 = Middle button, or a combination thereof). The second new parameter is the one we need here, and contains the drop effect resulting from a drag/drop operation (DM-NONE if no drop or if the operation was cancelled, DM-COPY if a Copy operation was performed, DM-MOVE if a Move operation was performed, and DM-LINK if a link operation was performed.

The DRAG-ENTER event (if not suppressed) is sent to the drop target when the drag cursor (re-)enters the region occupied by the drop target. The application typically responds to this event by calling the INQ-FORMAT-AVAILABLE action to find out if a compatible data format is available on the clipboard, and then setting the SUPPRESS-DRAG-DROP-EVENT attribute accordingly. The SUPPRESS-DRAG-DROP-EVENT is important because it not only determines whether the DRAG-DROP event should be raised, but also informs Natural as to whether a drop should be allowed. After raising the DRAG-ENTER and DRAG-OVER events, Natural inspects the SUPPRESS-DRAG-DROP-EVENT attribute and displays a "no drop" symbol. Otherwise, the drop effect is determined by the combination of the drag source's DRAG-MODE value, the drop target's DROP-MODE value, and the augmentation keys (SHIFT and CTRL) that are currently being pressed.

The DRAG-OVER event (if not suppressed) is frequently sent to the drop target as the drag cursor moves over the drop target. It can be used, for example, to update the drop emphasis (if any) as the user traverses the items within the control and/or to update the SUPPRESS-DRAG-DROP-EVENT attribute if the feasibility of a drop operation depends on the position within the drop target.

The DRAG-LEAVE event (if not suppressed) is sent to the drop target when the drag cursor leaves the region occupied by the drop target without a drop having occurred. This is mainly used (if at all) to remove the drop emphasis (if any) applied in the DRAG-OVER event.

The DRAG-DROP event (if not suppressed) is sent to the drop target when the user performs a drop. drag cursor leaves the region occupied by the drop target without a drop having occurred. The application should respond to this by effectively performing a Paste operation, using the current relative position within the control, if necessary. Both the relative position and the type of operation can be retrieved via the INQ-DRAG-DROP action. The latter is returned in the new (optional) "drop effect" parameter (see the description of the END-DRAG event above for more information).

Drag and Drop Insertion Marks

For list boxes, a new "insertion mark (i)" style can be used to indicate that a dashed horizontal line be used to indicate the current insert position when the drag cursor is moved over the control (assuming it is a drop target). The application cannot query the insertion mark position directly, but can find out where to insert the data by querying the relative position within the control via the INQ-DRAG-DROP action, then passing these coordinates to the INQ-ITEM-BY-POSITION action, as in the following example:

DEFINE DATA LOCAL
1 #Y (I4)
1 #CONTROL HANDLE OF GUI
1 #ITEM    HANDLE OF GUIEND-DEFINE
...
/* DRAG-DROP event:
PROCESS GUI ACTION INQ-DRAG-DROP WITH 4X #Y GIVING *ERROR
*
IF #Y < 0
   #Y := 0
END-IF
#CONTROL := *CONTROL
PROCESS GUI ACTION INQ-ITEM-BY-POSITION WITH
 #CONTROL 0 #Y #ITEM GIVING *ERROR

After the above code has executed, the variable #ITEM contains the handle of the item immediately following the insertion point. You can then dynamically insert one or more list box items at this position by calling the ADD action with the WITH PARAMETERS clause, setting the SUCCESSOR attribute to #ITEM.

Note that the correction for negative y-coordinate in the above example is necessary to cover the situation where the drop position is on the list boxes top border. If no correction would be made here, #ITEM would be set to NULL-HANDLE and the new list box item(s) would be added undesirably at the end of the list instead of at the beginning if we were to directly use #ITEM as the SUCCESSOR attribute, as described above.

Drag-Drop Checklist

For convenience, here is a brief overview of the steps involved in implementing drag-drop in Natural applications:

  1. Set the DRAG-MODE for each drag source. If the drag source is a bitmap control, its DRAGGABLE attribute must also be set to TRUE.

  2. Set SET-CLIPBOARD-DATA in the BEGIN-DRAG event for each drag source to provide the transfer data.

  3. Set the DROP-MODE for each drop target.

  4. In the DRAG-ENTER event, use the INQ-FORMAT-AVAILABLE action to set the SUPPRESS-DRAG-DROP-EVENT attribute to NOT-SUPPRESSED (0) if a supported clipboard format is available, or SUPPRESSED (1) otherwise. If the control can also act as a drag source and you need to prohibit drag-drop operations within the control, call INQ-DRAG-DROP to get the source control handle and compare it to the current control (*CONTROL), suppressing the drag-drop event if both are identical.

  5. If the effect of the drop is position-sensitive within the target control, use the INQ-DRAG-DROP action within the DRAG-OVER event to get the current position, determine the item under the drag cursor (e.g. via the INQ-ITEM-BY-POSITION action) and set the SUPPRESS-DRAG-DROP-EVENT attribute appropriately. Highlight the current item if desired.

  6. If the current item was highlighted in step 5 above, unhighlight it (if necessary) in the DRAG-LEAVE and (potentially) DRAG-DROP events.

  7. Use GET-CLIPBOARD-DATA in the DRAG-DROP event to retrieve the transfer data and process it accordingly.

  8. In the END-DRAG event for the drag source, delete the source data if the drop effect returned by INQ-DRAG-DROP is set to DM-MOVE.

  9. If the drag source is an ActiveX control, call the PERFORM-DRAG-DROP action to initiate the drag-drop operation in response to a "MouseDown" event (for example) if a location within the current selection is clicked.

Example - Use of X-Arrays for Transferring Data

One of the problems in setting or retrieving data that may need to be placed or already have been placed on the Windows or drag-drop clipboard in response to a user interaction is being able to cope with an arbitrary amount of data at run-time. For example, the user may select a single, a few, or possibly even hundreds or thousands of list box items before performing a clipboard or drag-drop operation on them. With fixed-size arrays, one would have to define huge arrays to cope with the worst-case scenario, even though typically only a small percentage would be used most of the time.

There are two possible solutions to this problem available in Natural. The first way is to use a single dynamic alpha variable to contain all items to be set or retrieved. The application is then responsible for building up the items (including delimiters) in the dynamic variable before calling SET-CLIPBOARD-DATA, and for extracting the items from the dynamic variable after calling GET-CLIPBOARD-DATA. This approach is not possible for private formats, because these are not delimited.

The second approach is to make use of X-Arrays. For setting clipboard data, these behave similarly to fixed-size arrays, except that their size can be modified to contain exactly the number of elements needed in a specific situation. For example, if there are 17 items that need to be written to the clipboard, then you can use:

DEFINE DATA LOCAL
1 #X-ARR(A80/1:*)
1 #UPB (I4) INIT <17>
END-DEFINE
RESIZE ARRAY #X-ARR TO (1:#UPB)
PROCESS GUI ACTION SET-CLIPBOARD-DATA WITH CF-TEXT #X-ARR(*)

instead of having to use a wastefully large fixed array, of which only a small range is used:

DEFINE DATA LOCAL
1 #ARR(A80/10000)
1 #UPB (I4) INIT <17>
END-DEFINE
PROCESS GUI ACTION SET-CLIPBOARD-DATA WITH CF-TEXT ARR(1:#UPB)

When retrieving clipboard data, X-Arrays are even more useful, because the application does not know in advance how many items are on the clipboard. Passing all array elements (10,000 in the above example) would be relatively slow, because all unused elements need to be reset.

However, if an X-Array is used instead, Natural automatically resizes the array to (1:N), where N is the minimum of the number of items (remaining) on the clipboard and the array's maximum upper bound (as defined in DEFINE-DATA, where * indicates the maximum possible value). Note that there are three restrictions on the use of X-Arrays in conjunction with GET-CLIPBOARD-DATA:

  • The X-Array must be the last (or only) parameter.

  • Only 1-dimensional X-Arrays are supported.

  • The X-Arrays defined range must include the element 1.

Here is an example program illustrating the use of a dynamic X-Array for retrieving clipboard data, including the use of a second X-Array to store and display the data lengths:

DEFINE DATA LOCAL
1 #FMT (A10) CONST<'MYDATAFMT'>
1 #X-ARR (A/1:*) DYNAMIC
1 #X-LEN (I4/1:*)
1 #UPB (I4)
1 #I (I4)
END-DEFINE
PROCESS GUI ACTION OPEN-CLIPBOARD GIVING *ERROR
PROCESS GUI ACTION SET-CLIPBOARD-DATA WITH #FMT
  'MIKE' 'FRED' 'JIM' 'LULU' 'FRANK' 'JANA' 'ELIZABETH'
'TONY'  
  GIVING *ERROR
PROCESS GUI ACTION CLOSE-CLIPBOARD GIVING *ERROR
PROCESS GUI ACTION GET-CLIPBOARD-DATA WITH #FMT #X-ARR(*)
  GIVING *ERROR
#UPB := *UBOUND(#X-ARR)
RESIZE ARRAY #X-LEN TO (1:#UPB)
FOR #I 1 #UPB
  #X-LEN(#I) := *LENGTH(#X-ARR(#I))
END-FOR
DISPLAY #X-ARR(*) (AL=10) #X-LEN(*) / '*** END OF DATA ***'
END