CLIM mail archive
[Prev][Next][Index][Thread]
Re: Wanted - example of writing a presentation-type using gadgets
Date: Wed, 29 Sep 93 11:39:36 EDT
From: mthome@BBN.COM
I would like to write some presentation types which have
accept methods using +gadget-dialog-view+ which use gadgets -
are there any examples of doing this in clim2.0?
thanks,
-Michael Thome (mthome@bbn.com)
Mike,
Try these out.
jeff
;;; Syntax: Common-lisp; Package: clim-user
(in-package :clim-user)
;;; This file defines two custom gadgets for clim:
;;; 1) Ellipse-push-button. Shows how an action-gadget can be implemented.
;;; 2) Simple-slider. Shows how a value-gadget can be implemented.
;;; (There is also a corresponding simple-slider-view for use with ACCEPT.)
;;;
;;; Invoke (clim-user::custom-gadget-demo) to see a demo.
;;;
;;; The primary purpose of this file is to provide some instructive
;;; examples that are relatively easy to understand. These gadgets
;;; are not particularly good, but they may illustrate enough of the protocol
;;; that you could now go off and make some truly useful gadgets.
;;;
;;; Note that there is currently no plan to support the introduction of
;;; 3rd party gadgets into clim. In my opinion, this is a sorry state
;;; of affairs, but it may change if enough people complain to the clim vendors.
;;; For now, the whole gadget has to be written in lisp.
;;;
;;; Jeff Morrill (jmorrill@bbn.com)
;;; 26 July 1993
;;;**************************************************
;;;
;;; ELLIPSE-PUSH-BUTTON
;;;
;;; The following shows how a push-button gadget might be implemented.
;;; It is derived from the example in the clim 2 specification.
(defclass ellipse-push-button
(action-gadget ; provides activate-callback
labelled-gadget-mixin ; provides label
silica::leaf-pane
silica:space-requirement-mixin)
;; ARMED has three states:
;; NIL ==> the button is not armed
;; T ==> the button is armed, waiting for a pointer button press
;; :ACTIVE ==> the button is armed, waiting for a pointer button release
((armed :initform nil)))
;; General highlight-by-inverting method.
(defmethod highlight-button ((pane ellipse-push-button) medium)
(with-bounding-rectangle* (left top right bottom)
(sheet-region pane)
(decf right) ; can't draw last pixel
(decf bottom) ; can't draw last pixel
(draw-ellipse* medium
(truncate (+ left right) 2)
(truncate (+ bottom top) 2)
(truncate (- right left) 2)
0 0
(truncate (- bottom top) 2)
:ink +flipping-ink+
:filled t))
(medium-force-output medium))
;; Compute the amount of space required by the pane.
(defmethod compose-space ((pane ellipse-push-button) &key width height)
(with-sheet-medium (medium pane)
(multiple-value-bind (w h)
(text-size medium (gadget-label pane)
:text-style (slot-value pane 'text-style))
(incf h (+ 2 (stream-vertical-spacing pane)))
(incf w 2)
(make-space-requirement :width (if width (max width w) w)
:height (if height (max height h) h)))))
;; This gets invoked to draw the push button.
(defmethod handle-repaint ((pane ellipse-push-button) region)
(declare (ignore region))
(with-sheet-medium (medium pane)
(let ((text (gadget-label pane))
(text-style (slot-value pane 'text-style))
(armed (slot-value pane 'armed))
(region (sheet-region pane)))
(with-bounding-rectangle* (left top right bottom)
region
(decf right) ; can't draw last pixel
(decf bottom) ; can't draw last pixel
(draw-ellipse* medium
(truncate (+ left right) 2)
(truncate (+ bottom top) 2)
(truncate (- right left) 2)
0 0
(truncate (- bottom top) 2)
:filled nil))
(draw-text medium text (bounding-rectangle-center region)
:text-style text-style
:align-x :center :align-y :center)
(when (eql armed :active)
(highlight-button pane medium)))))
;; When we enter the push button's region, arm it.
(defmethod handle-event ((pane ellipse-push-button)
(event pointer-enter-event))
(with-slots (armed) pane
(unless armed
(cond ((let ((pointer (pointer-event-pointer event)))
(and (pointer-button-state pointer)
(not (zerop (pointer-button-state pointer)))))
(setf armed :active)
(with-sheet-medium (medium pane)
(highlight-button pane medium)))
(t (setf armed t)))
(armed-callback pane (gadget-client pane) (gadget-id pane)))))
;; When we leave the push button's region, disarm it.
(defmethod handle-event ((pane ellipse-push-button)
(event pointer-exit-event))
(with-slots (armed) pane
(when armed
(when (prog1 (eq armed :active) (setf armed nil))
(with-sheet-medium (medium pane)
(highlight-button pane medium)))
(disarmed-callback pane (gadget-client pane) (gadget-id pane)))))
;; When the user presses a pointer button, ensure that the button
;; is armed, and highlight it.
(defmethod handle-event ((pane ellipse-push-button)
(event pointer-button-press-event))
(with-slots (armed) pane
(when armed
(setf armed :active)
(with-sheet-medium (medium pane)
(highlight-button pane medium)))))
;; When the user releases the button and the button is still armed,
;; call the activate callback.
(defmethod handle-event ((pane ellipse-push-button)
(event pointer-button-release-event))
(with-slots (armed) pane
(when (eq armed :active)
(setf armed t)
(with-sheet-medium (medium pane)
(highlight-button pane medium))
(activate-callback pane (gadget-client pane) (gadget-id pane)))))
;; Make and display a button.
(defun draw-command-button (stream label command x y active)
(stream-set-cursor-position stream x y)
(updating-output
(stream :unique-id command :cache-value command)
(with-output-as-gadget (stream)
(make-pane 'ellipse-push-button
:label label
:active active
:activate-callback
#'(lambda (button)
;; This funny thing makes sure we go through
;; the usual command loop.
(throw-highlighted-presentation
(make-instance 'standard-presentation
:object command
:type 'command)
*input-context*
(make-instance 'pointer-button-press-event
:sheet (sheet-parent button)
:x 0 :y 0
:button +pointer-left-button+)))))))
;; In user code, the first argument to MAKE-PANE is typically an abstract
;; class that is not actually instantiated. The abstract class is
;; mapped to a concrete class using the generic function MAKE-PANE-CLASS.
;; This enables the user to realize a gadget that reflects the current
;; look and feel without changing any source code.
;;
;; MAKE-PANE-CLASS returns NIL if there is no special mapping for the
;; class. This happens above with ELLIPSE-PUSH-BUTTON. Since the name
;; does not get mapped, it is used directly.
;;
;; But if we wanted Motif to use ELLIPSE-PUSH-BUTTON whenever MAKE-PANE
;; needed a PUSH-BUTTON, the following hack would be required.
#+example
(defmethod silica:make-pane-class :around
((framem xm-silica::motif-frame-manager) symbol &rest options)
;; OPTIONS = the other args to make-pane
(if (eq symbol 'push-button) 'ellipse-push-button (call-next-method)))
;;;**************************************************
;;;
;;; SIMPLE-SLIDER
;;;
;;; The following is how a value-gadget like a slider might
;;; be implemented.
(defclass simple-slider
(silica::range-gadget-mixin ; provides min & max
value-gadget ; provides gadget-value
silica::leaf-pane ; provides drawing surface
silica:space-requirement-mixin)
((tick-length :initform 5 :initarg :tick-length)
(tick-number :initform 5 :initarg :tick-number)))
(defmethod draw-slider ((pane simple-slider))
"Invoked by handle-repaint to draw the whole slider"
;; Draws directly on the medium without recording any output history.
(with-sheet-medium (medium pane)
(with-slots (tick-length tick-number) pane
(multiple-value-bind (width height) (bounding-rectangle-size pane)
(decf width) ; can't draw last pixel
(decf height) ; can't draw last pixel
(let* ((min-value (gadget-min-value pane))
(max-value (gadget-max-value pane))
(x 0)
(y (truncate height 2))
(dx (truncate width (1- tick-number))))
(setq width (* dx (1- tick-number))) ; correct for roundoff
(flet ((draw-tick (a b)
(medium-draw-line* medium a (- b tick-length) a (+ b tick-length)))
(draw-label (a label alignment)
(let ((text (format nil "~A" label)))
(medium-draw-text* medium text a height
0 nil
alignment :bottom
nil nil nil))))
;; Draw all the lines
(medium-draw-line* medium x y (+ x width) y)
(dotimes (i tick-number) (draw-tick (+ x (* i dx)) y))
;; Label min & max
(draw-label x min-value :left)
(draw-label width max-value :right)
;; Draw the value
(draw-value pane (gadget-value pane))))))))
(defmethod draw-value ((pane simple-slider) value)
"Draws just the value part of the slider."
(assert (numberp value))
(with-sheet-medium (medium pane)
(with-slots (tick-length tick-number) pane
(multiple-value-bind (width height) (bounding-rectangle-size pane)
(decf width)
(decf height)
(let* ((min-value (gadget-min-value pane))
(max-value (gadget-max-value pane))
(x (truncate (* width (/ (- value min-value) (- max-value min-value))))))
(when (<= 0 x width)
(medium-draw-rectangle* medium
(- x 2) 0 (+ x 3) height
t)))))))
(defmethod update-slider-value ((pane simple-slider) from to)
"Erase the old value and display the new value."
(with-drawing-options (pane :ink +flipping-ink+)
(when from (draw-value pane from))
(when to (draw-value pane to))))
(defmethod compose-space ((pane simple-slider) &key width height)
(make-space-requirement :width (or width 100)
:height (or height 40)
:max-width +fill+))
(defmethod handle-repaint ((pane simple-slider) region)
(declare (ignore region))
(draw-slider pane))
(defmethod (setf gadget-value) :around (value (pane simple-slider)
&key (invoke-callback t))
(let ((old (gadget-value pane)))
(call-next-method value pane :invoke-callback invoke-callback)
(update-slider-value pane old value)))
(defmethod handle-event ((pane simple-slider)
(event pointer-button-press-event))
;; When you click on the slider, change the value.
(let* ((x (pointer-event-x event))
(width (bounding-rectangle-width pane))
(min-value (gadget-min-value pane))
(max-value (gadget-max-value pane))
(value (+ min-value (* (/ (float x) width) (- max-value min-value)))))
(setf (gadget-value pane) value :invoke-callback t)))
(defmethod handle-event ((pane simple-slider)
(event pointer-motion-event))
(when (logtest (pointer-button-state (pointer-event-pointer event))
+pointer-left-button+)
;; User is trying to drag. Change the value.
(let* ((x (pointer-event-x event))
(width (bounding-rectangle-width pane))
(min-value (gadget-min-value pane))
(max-value (gadget-max-value pane))
(value (+ min-value (* (/ (float x) width) (- max-value min-value)))))
(setf (gadget-value pane) value :invoke-callback t))))
;;; Now define a corresponding view.
(defclass simple-slider-view (gadget-view)
((initargs :initarg nil)))
(defmethod initialize-instance :after ((view simple-slider-view)
&rest keys &key &allow-other-keys)
;; Just stuff all the initargs onto a slot
(setf (slot-value view 'initargs) keys))
(define-presentation-method
accept-present-default
((type t)
stream
(view simple-slider-view)
default default-supplied-p present-p query-identifier &key)
"Generates a simple-slider gadget for this query."
(declare (ignore present-p))
(unless default-supplied-p
(error "A default must be supplied to a simple-slider-view."))
(updating-output
(stream :unique-id query-identifier :cache-value query-identifier)
(with-output-as-gadget (stream)
(apply #'make-pane
'simple-slider
:client stream
:id query-identifier
:presentation-type type
:value default
:value-changed-callback
#'(lambda (gadget value)
;; This callback seems absurdly complicated
(throw-highlighted-presentation
(make-instance 'standard-presentation
:object `(clim-internals::com-change-query
,(gadget-id gadget)
,value)
:type 'command)
*input-context*
(make-instance 'pointer-button-press-event
:sheet (sheet-parent gadget)
:x 0 :y 0
:button +pointer-left-button+)))
;; Just pass the initargs along unadulterated
(slot-value view 'initargs)))))
;;;**************************************************
;;;
;;; Test Frame
;;;
(define-application-frame custom-gadget-demo ()
((volume :initform 1 :accessor volume)
(how-many :initform 1 :accessor how-many))
(:panes
(display :accept-values
:borders nil
:scroll-bars nil
:initial-cursor-visibility :off
:display-function
'(accept-values-pane-displayer
:displayer display-button-dialog
:resynchronize-every-pass t)))
(:layouts (main (vertically () display)))
(:command-table (custom-gadget-demo :inherit-from (accept-values-pane))))
(defun display-button-dialog (frame stream)
(draw-command-button stream "Make Sound" '(com-beep) 50 100 nil)
(terpri stream)
(terpri stream)
(terpri stream)
(setf (volume frame)
(accept 'number
:view '(simple-slider-view
:width 300
:tick-number 10
:min-value 0.0
:max-value 5.0)
:stream stream
:prompt "Volume"
:default (volume frame)))
(terpri stream)
(terpri stream)
(terpri stream)
(draw-command-button stream " OK " '(com-exit) 400 400 t))
(define-command (com-beep :command-table custom-gadget-demo) ()
(menu-choose '(beep honk boom)
:gesture :select
:label "Choose a sound"))
(define-command (com-exit :command-table custom-gadget-demo) ()
(frame-exit *application-frame*))
(defun custom-gadget-demo ()
(let ((frame (make-application-frame 'custom-gadget-demo
:width 500
:height 500)))
(run-frame-top-level frame)
(volume frame)))
References:
Main Index |
Thread Index