Object Editor

CLOS objects make it very easy to implement a simple database in Lisp. This example shows how to use the CAPI interface to write CLOS objects using a simple object editor.

Complete listing

Example

Assuming we have a list of CLOS objects in the variable *cars*. Then we can display a list of the objects with the call:

(make-list-window *cars*)

This displays:

listbefore.gif

Double-clicking an item displays an editor window showing the contents of each slot in the object:

editbefore.gif

If we edit a field, the contents of the corresponding slot are changed:

editafter.gif

If we edited the Name field the change is reflected in the list window:

listafter.gif

The editor automatically reads the slot names from the defclass definition, so adding slots to the defclass definition automatically adds the appropriate fields to the edit window, without any extra programming.

This example takes care of string and integer slots, but other types could easily be added.

The definition

The edit-window class adds a thing slot for the object being edited to the capi:interface class:

(defclass edit-window (capi:interface)
  ((thing :initarg :thing :accessor thing))) 

The fields displayed on the edit-window use the field class, which is based on capi:text-input-pane:

(defclass field (capi:text-input-pane)
  ((thing :initarg :thing :accessor thing)
   (table :initarg :table :accessor table)
   (type :initarg :type :accessor fieldtype))
  (:default-initargs
   :title-args '(:external-min-width 72)
   :editing-callback
   #'(lambda (pane type) (case type (:end (save-field pane))))))

This adds the following extra slots to the text-input-pane class:

  • thing - contains a reference to the parent object being edited.
  • type - the type of slot. Currently only integer is supported.
  • table - refers to the parent table.

Here's the routine to display the edit window:

(defun make-edit-window (thing table)
  (let* ((ok (make-instance 'capi:push-button :text "OK" :default-p t
                            :callback-type :interface :selection-callback #'capi:quit-interface))
         (slots-to-edit (remove-if-not #'(lambda (s) (eq (clos:slot-definition-allocation s) :instance))
                                       (clos:class-slots (class-of thing))))
         (fields (map 'list #'(lambda (slot)
                                (let ((slotname (clos:slot-definition-name slot))
                                      (slottype (clos:slot-definition-type slot)))
                                  (make-instance 'field :name slotname :thing thing 
                                                 :type slottype :table table
                                                 :text (princ-to-string (slot-value thing slotname))
                                                 :title (string-capitalize slotname)))) 
                      slots-to-edit))
         (layout (make-instance 'capi:column-layout :description (append fields (list ok))))
         (window (make-instance 'edit-window :thing thing :title (name thing) :layout layout 
                                :best-width 320 :confirm-destroy-function #'save-field-with-focus)))
    (capi:display window)))

It creates the list slots-to-edit from the class slots of the object definition, and then creates the list fields containing a field for each slot. Each field is named after the slot it relates to; this is used to save edited fields back to the appropriate slot. It then creates a capi:column-layout with the list of fields, and an OK button, as the layout description.

Saving fields

The field class uses the editing-callback to call save-field to when the user clicks out of the field after editing it. This saves the contents of the field to the appropriate slot in the object being edited:

(defmethod save-field ((f field))
  "Called to save data from field."
  (let* ((thing (thing f))
         (table (table f))
         (slot (capi:capi-object-name f))
         (value (case (fieldtype f)
                  (integer (parse-integer (capi:text-input-pane-text f) :junk-allowed t))
                  (t (capi:text-input-pane-text f)))))
    (setf (slot-value thing slot) value)
    (capi:choice-update-item table thing)))

The edit-window also uses the confirm-destroy-function callback to save the field with focus when the edit-window is closed. This is to cover the situation when the user edits a field and then closes the window without moving focus to another field, because CAPI doesn't call the editing-callback for capi:text-input-pane in this situation:

(defun save-field-with-focus (w)
  (let ((focus (capi:pane-descendant-child-with-focus w)))
    (when focus (save-field focus)))
  t)

The list window

Finally, here's the function make-list-window to display a list of the objects to be edited. Double-clicking one calls edit to display the object in an edit-window:

(defun make-list-window (list)
    (let* ((table (make-instance 'capi:list-panel :items list   
                                 :print-function #'name
                                 :callback-type '(:data :collection)
                                 :action-callback #'(lambda (thing table) (edit thing table))))
           (window (make-instance 'capi:interface :best-width 320 :best-height 224 :title "Cars"
                                  :layout (make-instance 'capi:column-layout :description (list table)))))
      (capi:display window)))

The function edit checks whether there is an existing edit-window for the object, and if so, brings that one to the front rather than opening a new one. This is to avoid problems with multiple edit-windows on the same object:

(defun edit (thing table)
  (let ((existing (find thing (capi:collect-interfaces 'edit-window) :key #'thing)))
    (if existing (capi:raise-interface existing)
      (make-edit-window thing table))))

Example data

To test the Object Editor, define an object:

(defclass classic-car ()
  ((name :initarg :name :accessor name)
   (year :initarg :year :type integer)
   (cylinders :initarg :cylinders :type integer)
   (capacity :initarg :capacity :type integer)))

Integer slots should be given :type integer, otherwise slots are assumed to be strings. Then define some data:

(defparameter *cars*
  (list
   (make-instance 'classic-car :name "Saab 96V4" :year 1967 :cylinders 4 :capacity 1498) 
   (make-instance 'classic-car :name "Porsche 911 Carrera" :year 1984 :cylinders 6 :capacity 3200)
   (make-instance 'classic-car :name "MGC" :year 1967 :cylinders 6 :capacity 2912)
   (make-instance 'classic-car :name "Ferrari Daytona" :year 1968 :cylinders 12 :capacity 4390)))

Finally, display the objects in a list window with:

(make-list-window *cars*)

blog comments powered by Disqus