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.
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")))))
:
resynchronize-every-pass in accepting-valuesIt 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:
:resynchronize-every-pass t
. Code in the body may be used to enforce constraints among values.:default
argument to accept is used, then every time that call to accept is run, it will pick up the new value of the default.
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.)
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))
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)) )
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