All Manuals > CLIM 2.0 User Guide > 12 Menus and Dialogs

12.4 Examples of Menus and Dialogs in CLIM

12.4.1 Using accepting-values

This example sets up a dialog in the CLIM window stream that displays the current month, date, hour, and minute (as obtained by a call to get-universal-time) and allows the user to modify those values. The user can select values to change by using the mouse to select values, typing in new values, and pressing RETURN. When done, the user selects <END> to accept the new values, or <ABORT> to terminate without changes.

(defun reset-clock (stream) 
  (multiple-value-bind (second minute hour day month) 
      (decode-universal-time 
       (get-universal-time))
  (declare (ignore second)) 
  (format stream "Enter the time~%") 
    (restart-case 
     (progn 
       (clim:accepting-values (stream) 
        (setq month 
              (clim:accept 'integer :stream stream 
                           :default month :prompt "Month")) 
        (terpri stream) 
        (setq day 
              (clim:accept 'integer :stream stream 
                           :default day :prompt "Day"))
        (terpri stream) 
        (setq hour 
              (clim:accept 'integer :stream stream 
                           :default hour :prompt "Hour")) 
        (terpri stream) 
        (setq minute 
              (clim:accept 'integer :stream stream 
                           :default minute :prompt "Minute")))
       ;; This could be code to reset the time, but instead 
       ;; we're just printing it out
       (format t "~%New values: Month: ~D, Day: ~D, Time: ~D:~2,'0D." 
               month day hour minute)) 
   (abort () (format t "~&Time not set"))))) 

Note that in CLIM, calls to accept do not automatically insert newlines. If you want to put each query on its own line of the dialog, use terpri between the calls to accept.

12.4.2 Using accept-values-command-button

Here is the reset-clock example with the addition of a command button that will set the number of seconds to zero.

(defun reset-clock (stream) 
  (multiple-value-bind (second minute hour day month) 
      (decode-universal-time (get-universal-time))
    (declare (ignore second)) 
    (format stream "Enter the time~%") 
    (restart-case 
 
(progn 
  (clim:accepting-values 
   (stream) 
   (setq month 
         (clim:accept 'integer :stream stream 
                      :default month :prompt "Month")) 
   (terpri stream) 
   (setq day 
         (clim:accept 'integer :stream stream 
                      :default day :prompt "Day"))
   (terpri stream) 
   (setq hour 
         (clim:accept 'integer :stream stream 
                      :default hour :prompt "Hour")) 
   (terpri stream) 
   (setq minute 
         (clim:accept 'integer :stream stream 
                      :default minute :prompt "Minute")))
   (terpri stream)
 
;; Print the current time to the terminal.
(accept-values-command-button
 (stream) "Print-Clock"
 (format t
         "~%Current values: Month: ~D, Day: ~D, Time: ~D:~2,'0D."
         month day hour minute))))
(abort () (format t "~&Time not set")))))

12.4.3 Using :resynchronize-every-pass in accepting-values

It often happens that the programmer wants to present a dialog where the individual fields of the dialog depend on one another. For example, consider a spreadsheet with seven columns representing the days of a week. Each column is headed with that day's date. If the user inputs the date of any single day, the other dates can be computed from that single piece of input.

If you build CLIM dialogs using accepting-values, you can achieve this effect by using the :resynchronize-every-pass argument to accepting-values in conjunction with the :default argument to accept. There are three points to remember:

In this example we show a dialog that accepts two real numbers, delimiting an interval on the real line. The two values are labelled Min and Max, but we wish to allow the user to supply a Min that is greater than the Max, and automatically exchange the values rather than signalling an error.

(defun accepting-interval (&key (min -1.0) (max 1.0) 
                                (stream *query-io*)) 
  (clim:accepting-values (stream :resynchronize-every-pass t) 
                         (fresh-line stream) 
                         (setq min
                               (clim:accept
                                'clim:real :default min
                                :prompt "Min" :stream stream)) 
                         (fresh-line stream)
                         (setq max
                               (clim:accept
                                'clim:real :default max
                                :prompt "Max" :stream stream)) 
                         (when (< max min) 
                           (rotatef min max)))
  (values min max)) 

(You may want to try this example after dropping the :resynchronize-every-pass and see the behavior. Without :resynchronize-every-pass, the constraint is still enforced, but the display lags behind the values and does not reflect the updated values immediately.)

12.4.4 Using the third value from accept in accepting-values

As a second example, consider a dialog that accepts four real numbers that delimit a rectangular region in the plane, but we wish to enforce a constraint that the region be a square. We allow the user to input any of Xmin, Xmax, Ymin, or Ymax, but enforce the constraint that:

 
Xmax - Xmin = Ymax - Ymin 

We want to avoid changing the value that a user inputs, so we decide (in cases where the constraint has to be enforced) to change the X value if the user inputs a Y value, and to change the Y value if the user inputs an X value. When changing values, we preserve the center of the interval. We use the third returned value from accept to control the constraint enforcement.

(defun accepting-square
  (&key (xmin -1.0) (xmax 1.0) 
        (ymin -1.0) (ymax 1.0) 
        (stream *query-io*)) 
 
(let (xmin-changed xmax-changed ymin-changed ymax-changed ptype) 
  (clim:accepting-values
   (stream :resynchronize-every-pass t) 
   (fresh-line stream) 
   (multiple-value-setq
       (xmin ptype xmin-changed) 
       (clim:accept 'clim:real :default xmin
        :prompt "Xmin" :stream stream)) 
   (fresh-line stream) 
   (multiple-value-setq
       (xmax ptype xmax-changed) 
       (clim:accept 'clim:real :default xmax
        :prompt "Xmax" :stream stream)) 
   (fresh-line stream) 
   (multiple-value-setq
       (ymin ptype ymin-changed) 
       (clim:accept 'clim:real :default ymin
        :prompt "Ymin" :stream stream))
   (fresh-line stream) 
   (multiple-value-setq
       (ymax ptype ymax-changed) 
       (clim:accept 'clim:real :default ymax
        :prompt "Ymax"  :stream stream))
   (cond ((or xmin-changed xmax-changed) 
          (let ((y-center (/ (+ ymax ymin) 2.0)) 
                (x-half-width (/ (- xmax xmin) 2.0))) 
            (setq ymin (- y-center x-half-width) 
                  ymax (+ y-center x-half-width))) 
          (setq xmin-changed nil
                xmax-changed nil)) 
         ((or ymin-changed ymax-changed)
          (let ((x-center (/ (+ xmax xmin) 2.0))
                (y-half-width (/ (- ymax ymin) 2.0))) 
            (setq xmin (- x-center y-half-width) 
                  xmax (+ x-center y-half-width)))
          (setq ymin-changed nil
                ymax-changed nil))))) 
(values xmin xmax ymin ymax)) 

12.4.5 Using menu-choose

The simplest use of menu-choose is when each item is not a list. In that case, the entire item will be printed and is also the value to be returned.

 
(clim:menu-choose '("One" "Two" "Seventeen")) 

If you want to return a value that is different from what was printed, the simplest method is as follows. Each item is a list; the first element is what will be printed, the remainder of the list is treated as a plist—the :value property will be returned. (Note nil is returned if you click on Seventeen since it has no :value.)

(clim:menu-choose
 '(("One" :value 1 :documentation "the loneliest number")      
   ("Two" :value 2 :documentation "for tea")   
   ("Seventeen" 
    :documentation "teen magazine"))) 

The list of items you pass to menu-choose can serve other purposes in your application, so you might not want to put the printed appearance in the first element. You can supply a :printer function that will be called on the item to produce its printed appearance.

(clim:menu-choose '(1 2 17)      
                  :printer #'(lambda (item stream)
                               (format stream "~R" item))) 

The items in the menu need not be printed textually:

(clim:menu-choose
  '(circle square triangle) 
  :printer
  #'(lambda (item stream)        
      (case item                 
        (circle (clim:draw-circle* stream 0 0 10)) 
        (square (clim:draw-polygon* stream '(-8 -8 -8 8 8 8 8 -8)))
        (triangle (clim:draw-polygon* stream '(10 8 0 -10 -10 8))))))

The :item-list option of the list form of menu item can be used to describe a set of hierarchical menus.

(clim:menu-choose 
  '(("Class: Osteichthyes" :documentation "Bony fishes" 
     :style (nil :italic nil)) 
    ("Class: Chondrichthyes" 
     :documentation "Cartilaginous fishes" 
     :style (nil :italic nil) 
      :item-list (("Order: Squaliformes" :documentation "Sharks")                  
               ("Order: Rajiformes" :documentation "Rays"))) 
    ("Class: Mammalia" :documentation "Mammals" :style (nil :italic nil) 
     :item-list 
     (("Order Rodentia" :item-list ("Family Sciuridae" 
                                   "Family Muridae"
                                   "Family Cricetidae"
                                   ("..." :value nil))) 
      ("Order Carnivora" :item-list ("Family: Felidae" 
                                    "Family: Canidae" 
                                    "Family: Ursidae" 
                                    ("..." :value nil)))           
      ("..." :value nil)))
 ("..." :value nil)) ) 

12.4.6 Using menu-choose-from-drawer

This example displays in the window *page-stream* the choices One through Ten in boldface type. When the user selects one, the string is returned along with the gesture that selected it.

(clim:menu-choose-from-drawer 
 *page-stream* 'string 
 #'(lambda (stream type) 
     (clim:with-text-face (:bold stream)    
                          (dotimes (count 10)        
                            (clim:present (string-capitalize 
                                           (format nil "~R" (1+ count)))
                                          type :stream stream)        
                            (terpri stream))))) 

This example shows how you can use menu-choose-from-drawer with with-menu to create a temporary menu:

(defun choose-compass-direction (parent-window) 
  (labels
      ((draw-compass-point
        (stream ptype symbol x y) 
        (clim:with-output-as-presentation 
         (:stream stream :object symbol :type ptype) 
         (clim:draw-string* stream
                            (symbol-name symbol) x y 
                            :align-x :center 
                            :align-y :center 
                            :text-style 
                            '(:sans-serif :roman :large)))) 
       (draw-compass
        (stream ptype) 
        (clim:draw-line* stream 0 25 0 -25 :line-thickness 2) 
        (clim:draw-line* stream 25 0 -25 0 :line-thickness 2) 
        (loop for point in '((n 0 -30) (s 0 30) (e 30 0)(w -30 0))
              do (apply #'draw-compass-point 
                        stream ptype point))))
    (clim:with-menu (menu parent-window) 
                    (clim:menu-choose-from-drawer menu 'clim:menu-item
                                                  #'draw-compass))))

CLIM 2.0 User Guide - 01 Dec 2021 19:38:58