List Panels with Drag

CAPI provides a double-list-panel for applications in which you need to select a subset of a list. For example, selecting the packages you want to search for a symbol, or selecting the users to be added to a group.

LispWorks Version 6 added drag-and-drop capabilities to list-panels. This application shows how to create a double-list-panel similar in functionality to the built-in one, but with drag and drop capabilities.

Complete listing

Example

For example, if you create it with:

(make-chooser '("Alligator" "Buzzard" "Cheetah" "Dolphin" "Elephant"))

you can drag items from either list to the other list, as an alternative to using the ">>>" or "<<<" buttons:

drag.gif

The definition

First we create a class from list-panel, with :drag-callback and :drop-callback callbacks to detect a drag from the panel, or a drop onto the panel:

(defclass drag-list (capi:list-panel) ()
  (:default-initargs
   :drag-callback #'drag-callback
   :drop-callback #'drop-callback
   :interaction :extended-selection))

Here's the definition of the routine to create the interface:

(defun make-chooser (items)
  (let* ((deselected-items-pane (make-instance 'drag-list :title "Unselected Items:" :items items))
         (selected-items-pane (make-instance 'drag-list :title "Selected Items:"))
         (select-button
          (make-instance 'capi:push-button :text ">>>" :callback-type :none
                         :callback #'(lambda ()
                                       (move-items (capi:choice-selected-items deselected-items-pane)
                                             deselected-items-pane selected-items-pane))))
         (deselect-button
          (make-instance 'capi:push-button :text "<<<" :callback-type :none
                         :callback #'(lambda () 
                                       (move-items (capi:choice-selected-items selected-items-pane)
                                             selected-items-pane deselected-items-pane))))
         (button-layout 
          (make-instance 'capi:column-layout :description (list select-button deselect-button)))
         (layout 
          (make-instance 'capi:row-layout :adjust :center 
                         :description (list deselected-items-pane button-layout selected-items-pane) ))
         (window
          (make-instance 'capi:interface :title "Double-List Panel Test" 
                         :best-width 500 :best-height 300 :layout layout)))
    (capi:display window)))

The list panels have :drag-callback and :drop-callback callbacks to detect a drag from the panel, or a drop onto the panel.

Drag callback

The drag callback creates a list containing the pane followed by the collection items, and gives it type :lisp:

(defun drag-callback (pane indices)
  (list :lisp (cons pane (map 'list #'(lambda (i) (elt (capi:collection-items pane) i)) indices))))

Drop callback

The drop callback has three stages:

The :formats stage specifies what formats are accepted by the drop target. In this case it's just our :lisp format.

The :drag stage highlights the drop target when there's a drag over it from a different pane. The LispWorks drag-and-drop support allow you to highlight:

  • Individual items (eg for applications in which the drop will replace an item).
  • The gaps between items (eg for applications in which the drop will insert an item at that position).
  • The whole panel, for cases where the position of the drop is irrelevant.

In this application we're not interested in the position of the drop, so we set the capi:drop-object-collection-index of the object to the values -1 :item.

The :drop stage receives the items when there's a drop.

(defmethod drop-callback (pane drop-object stage)
  (case stage
    (:formats
     (capi:set-drop-object-supported-formats drop-object (list :lisp)))
    (:drag
       (when (and 
              (capi:drop-object-provides-format drop-object :lisp)
              (capi:drop-object-allows-drop-effect-p drop-object :move)
              ;; Don't allow drop on self
              (not (eq pane (car (capi:drop-object-get-object drop-object pane :lisp)))))
         ;; Ignore drop position
         (setf (capi:drop-object-collection-index drop-object)
               (values -1 :item))
         (setf (capi:drop-object-drop-effect drop-object) :move)))
    (:drop
       (when (and 
              (capi:drop-object-provides-format drop-object :lisp)
              (capi:drop-object-allows-drop-effect-p drop-object :move))
         (let ((drag (capi:drop-object-get-object drop-object pane :lisp)))
           (move-items (cdr drag) (car drag) pane)
           (setf (capi:drop-object-drop-effect drop-object) :copy))))))
Both the buttons and the drop action use move-items to actually move items between the two lists:
(defun move-items (items from to)
  (capi:remove-items from items)
  (capi:append-items to items))

Further applications

This example can be generalised to a window with three or more list panels, with the ability to drag items between any of them, simply by adding more instances of drag-list:

drag3.gif

For example, an application might need to allow you to allocate a set of users between several groups. This would be quite difficult to implement with buttons alone.

Restrictions

Note that this example doesn't implement some refinements provided by the built-in CAPI double-list-panel:

  • Disabling of the buttons when no items are selected in the appropriate panel.
  • Use of Return key.
  • Sorting of items.

blog comments powered by Disqus