;;; -*- Mode:Lisp; Package:CLIO-OPEN; Base:10; Lowercase:T; Syntax:Common-Lisp -*- ;;;----------------------------------------------------------------------------------+ ;;; | ;;; TEXAS INSTRUMENTS INCORPORATED | ;;; P.O. BOX 149149 | ;;; AUSTIN, TEXAS 78714 | ;;; | ;;; Copyright (C) 1989, 1990 Texas Instruments Incorporated. | ;;; | ;;; Permission is granted to any individual or institution to use, copy, modify, and | ;;; distribute this software, provided that this complete copyright and permission | ;;; notice is maintained, intact, in all copies and supporting documentation. | ;;; | ;;; Texas Instruments Incorporated provides this software "as is" without express or | ;;; implied warranty. | ;;; | ;;;----------------------------------------------------------------------------------+ (in-package "CLIO-OPEN") (export '(scroll-frame make-scroll-frame scroll-frame-area scroll-frame-content scroll-frame-horizontal scroll-frame-left scroll-frame-position scroll-frame-reposition scroll-frame-top scroll-frame-vertical )) (defcontact scroll-frame (core composite) ((horizontal :type switch :initform :on :initarg :horizontal :reader scroll-frame-horizontal) ; setf defined below (left :type integer :initform 0 :initarg :left :accessor scroll-frame-left) (top :type integer :initform 0 :initarg :top :accessor scroll-frame-top) (vertical :type switch :initform :on :initarg :vertical :reader scroll-frame-vertical)) ; setf defined below (:resources (border-width :initform 0) (content :type (or function list) :initform nil) horizontal left top vertical) (:documentation "Provide horizontal and/or vertical scrolling controls for an arbitrary content contact")) ;;;----------------------------------------------------------------------------+ ;;; | ;;; Accessors | ;;; | ;;;----------------------------------------------------------------------------+ (defmethod scroll-frame-content ((scroll-frame scroll-frame)) (first (composite-children (scroll-frame-area scroll-frame)))) (proclaim '(inline scroll-frame-hscroller)) (defun scroll-frame-hscroller (scroll-frame) (with-slots (children) scroll-frame (find :hscroller children :key #'contact-name :test #'eq))) (proclaim '(inline scroll-frame-vscroller)) (defun scroll-frame-vscroller (scroll-frame) (with-slots (children) scroll-frame (find :vscroller children :key #'contact-name :test #'eq))) (defmethod scroll-frame-area ((scroll-frame scroll-frame)) (with-slots (children) scroll-frame (find :scroll-area children :key #'contact-name :test #'eq))) (defmethod (setf contact-foreground) :after (value (self scroll-frame)) (declare (ignore value)) (with-slots (foreground) self (let ((hscroller (scroll-frame-hscroller self)) (vscroller (scroll-frame-vscroller self))) (when hscroller (setf (contact-foreground hscroller) foreground)) (when vscroller (setf (contact-foreground vscroller) foreground))) (setf (window-border (scroll-frame-area self)) foreground))) (defmethod (setf scroll-frame-vertical) (value (self scroll-frame)) (with-slots (foreground top vertical) self (setf vertical value) (let ((vscroller (scroll-frame-vscroller self)) (content (scroll-frame-content self))) (ecase value (:on (if vscroller ;; Map existing scroller (setf (contact-state vscroller) :mapped) (progn ;; Create a new scroller (setf vscroller (make-scroller :parent self :name :vscroller :foreground foreground :border-width 0 :orientation :vertical)) ;; Program scroller to scroll content (add-callback vscroller :new-value #'(lambda (new-top scroll-frame) (with-slots (left top) scroll-frame (unless (= new-top top) (sf-scroll-to scroll-frame left (setf top new-top))))) self))) ;; Calibrate scroller with current content (when content (sf-vertical-calibrate content vscroller top (contact-height (scroll-frame-area self))))) (:off (when vscroller (setf (contact-state vscroller) :withdrawn))))) value)) (defmethod (setf scroll-frame-horizontal) (value (self scroll-frame)) (with-slots (foreground left horizontal) self (setf horizontal value) (let ((hscroller (scroll-frame-hscroller self)) (content (scroll-frame-content self))) (ecase value (:on (if hscroller ;; Map existing scroller (setf (contact-state hscroller) :mapped) (progn ;; Create a new scroller (setf hscroller (make-scroller :parent self :name :hscroller :foreground foreground :border-width 0 :orientation :horizontal)) ;; Program scroller to scroll content (add-callback hscroller :new-value #'(lambda (new-left scroll-frame) (with-slots (left top) scroll-frame (unless (= new-left left) (sf-scroll-to scroll-frame (setf left new-left) top)))) self))) ;; Calibrate scroller with current content (when content (sf-horizontal-calibrate content hscroller left (contact-width (scroll-frame-area self))))) (:off (when hscroller (setf (contact-state hscroller) :withdrawn))))) value)) (defmethod scroll-frame-position ((self scroll-frame)) (with-slots (left top) self (values left top))) (defmethod scroll-frame-reposition ((self scroll-frame) &key left top) "Changes the horizontal/vertical position of the content (in content units) which appears at the left/top edge of the scroll-frame. The final content position (possibly adjusted via :horizontal-adjust and :vertical-adjust callbacks) is returned." (with-slots ((current-left left) (current-top top) vertical horizontal) self (let* ((content (scroll-frame-content self)) (left-changed-p (and left (/= (setf left (apply-callback-else (content :horizontal-adjust left) left)) current-left))) (top-changed-p (and top (/= (setf top (apply-callback-else (content :vertical-adjust top) top)) current-top)))) (when left-changed-p (setf current-left left) (when (eq :on horizontal) (setf (scale-value (scroll-frame-hscroller self)) current-left))) (when top-changed-p (setf current-top top) (when (eq :on vertical) (setf (scale-value (scroll-frame-vscroller self)) current-top))) (when (or left-changed-p top-changed-p) ;; Redisplay content at new position (sf-scroll-to self current-left current-top)) (values current-left current-top)))) (defun sf-scroll-to (scroll-frame left top) (let ((content (scroll-frame-content scroll-frame))) (when content (apply-callback-else (content :scroll-to left top) ;; Default scrolling by moving content window w.r.t area. ;; Content units are n pixels, where n is determined from ;; pixels-per-unit used to calibrate scroller indicator size. (let ((hscroller (scroll-frame-hscroller scroll-frame)) (vscroller (scroll-frame-vscroller scroll-frame)) (area (scroll-frame-area scroll-frame))) (with-state (content) (move content (- (pixel-round (if hscroller (/ (* left (contact-width area)) (scale-indicator-size hscroller)) left))) (- (pixel-round (if vscroller (/ (* top (contact-height area)) (scale-indicator-size vscroller)) top)))))))))) (defun sf-horizontal-calibrate (content hscroller left width) ;; Program scroller to adjust value (add-callback hscroller :adjust-value #'(lambda (value content) (or (when content (apply-callback content :horizontal-adjust value)) value)) content) ;; Update scroller values (multiple-value-bind (min max ppu) (apply-callback-else (content :horizontal-calibrate) (values 0 (max 0 (- (contact-width content) width)) 1)) ;; Clamp current left to new range (let ((value (min max left))) (scale-update hscroller :value value :minimum min :maximum max :indicator-size (/ width ppu) :increment 1) ;; Returned clamped value value))) (defun sf-vertical-calibrate (content vscroller top height) ;; Program scroller to adjust value (add-callback vscroller :adjust-value #'(lambda (value content) (or (when content (apply-callback content :vertical-adjust value)) value)) content) ;; Update scroller values (multiple-value-bind (min max ppu) (apply-callback-else (content :vertical-calibrate) (values 0 (max 0 (- (contact-height content) height)) 1)) (let ((value (min max top))) (scale-update vscroller :value value :minimum min :maximum max :indicator-size (/ height ppu) :increment 1) ;; Return clamped value value))) ;;;----------------------------------------------------------------------------+ ;;; | ;;; Geometry Management | ;;; | ;;;----------------------------------------------------------------------------+ (defmethod change-layout ((self scroll-frame) &optional newly-managed) (declare (ignore newly-managed)) (with-slots (width height horizontal vertical) self ;; Is initial scroll-frame size still undefined? (if (unless (realized-p self) (or (zerop width) (zerop height))) ;; Yes, change to valid initial size (this invokes change-layout again) (multiple-value-bind (preferred-width preferred-height) (preferred-size self) (change-geometry self :width preferred-width :height preferred-height :accept-p t)) ;; No, update layout for valid size. (let* ((hscroller (when (eq :on horizontal) (scroll-frame-hscroller self))) (vscroller (when (eq :on vertical) (scroll-frame-vscroller self))) (area (scroll-frame-area self)) (hheight (if hscroller (contact-height hscroller) 0)) (vwidth (if vscroller (contact-width vscroller) 0)) (hwidth (max 0 (- width vwidth))) (vheight (max 0 (- height hheight))) (abw (* 2 (contact-border-width area)))) ;; Lay out scrollers (when hscroller (with-state (hscroller) (resize hscroller hwidth hheight 0) (move hscroller 0 (- height hheight)))) (when vscroller (with-state (vscroller) (resize vscroller vwidth vheight 0) (move vscroller (- width vwidth) 0))) ;; Layout scroll area (with-state (area) (resize area (max 0 (- width vwidth abw)) (max 0 (- height hheight abw)) (contact-border-width area)) (move area 0 0)) )))) (defmethod manage-geometry ((self scroll-frame) child x y width height border-width &key) (case (contact-name child) (:scroll-area ;; Approve if total outside size/position remains unchanged. (let* ((approved-bw (or border-width (contact-border-width child))) (delta-bw (* 2 (- (contact-border-width child) approved-bw))) (approved-x 0) (approved-y 0) (approved-width (+ (contact-width child) delta-bw)) (approved-height (+ (contact-height child) delta-bw))) (values (when ;; Change approved? (and (or (null x) (= x approved-x)) (or (null y) (= y approved-y)) (or (null width) (= width approved-width)) (or (null height) (= height approved-height)) (= border-width approved-bw)) ;; Yes, update layout if change is performed 'change-layout) approved-x approved-y approved-width approved-height approved-bw))) (otherwise ;; Approve any scroller size change. This should happen only during rescale. (values (when (and (or (null border-width) (= border-width (contact-border-width child))) (or (null x) (= x (contact-x child))) (or (null y) (= y (contact-y child)))) 'change-layout) (contact-x child) (contact-y child) (or width (contact-width child)) (or height (contact-height child)) (contact-border-width child))))) (defmethod preferred-size ((self scroll-frame) &key width height border-width) (with-slots ((self-width width) (self-height height) (self-border-width border-width)) self (let ((suggested-width (or width self-width)) (suggested-height (or height self-height)) (suggested-border-width (or border-width self-border-width))) (values (if (plusp suggested-width) suggested-width (let ((content (scroll-frame-content self)) (hscroller (scroll-frame-hscroller self)) (vscroller (scroll-frame-vscroller self))) (+ (max (if content (contact-width content) 0) (if hscroller (contact-width hscroller) 0)) (if vscroller (contact-width vscroller) 0)))) (if (plusp suggested-height) suggested-height (let ((content (scroll-frame-content self)) (hscroller (scroll-frame-hscroller self)) (vscroller (scroll-frame-vscroller self))) (+ (max (if content (contact-height content) 0) (if vscroller (contact-height vscroller) 0)) (if hscroller (contact-height hscroller) 0)))) suggested-border-width )))) (defmethod resize :after ((self scroll-frame) new-width new-height new-border-width) (declare (ignore new-width new-height new-border-width)) (change-layout self)) (defmethod add-child :before ((self scroll-frame) child &key) (assert (member (contact-name child) '(:hscroller :vscroller :scroll-area) :test #'eq) () "A scroll-frame does not allow you to define new children.")) ;;;----------------------------------------------------------------------------+ ;;; | ;;; Initialization | ;;; | ;;;----------------------------------------------------------------------------+ (defun make-scroll-frame (&rest initargs) (apply #'make-contact 'scroll-frame initargs)) (defmethod initialize-instance :after ((self scroll-frame) &key content &allow-other-keys) (with-slots (foreground vertical horizontal) self (let (;; Create scroll area (area (make-contact 'scroll-area :parent self :name :scroll-area :border-width 1 :border foreground))) ;; Create content, if given. (when content (multiple-value-bind (content-constructor content-initargs) (etypecase content (function content) (list (values (first content) (rest content)))) (apply content-constructor :name (or (getf content-initargs :name) :content) :parent area content-initargs)))) ;; Initialize scroll bars (setf (scroll-frame-horizontal self) horizontal) (setf (scroll-frame-vertical self) vertical))) ;;;----------------------------------------------------------------------------+ ;;; | ;;; Scroll Area | ;;; | ;;;----------------------------------------------------------------------------+ (defcontact scroll-area (composite) () (:documentation "Geometry manager for the scroll area of a scroll frame.")) ;;; Geometry management policy: ;;; 1. Content border width forced to 0. This prevents the bottom/right edges ;;; of a small content from intruding. ;;; 2. Content size and position is unrestricted. ;;; 3. Only one content child allowed. (defmethod add-child :before ((self scroll-area) child &key) (declare (ignore child)) (assert (not (composite-children self)) () "A scroll area can have only one child.")) (defmethod change-layout ((self scroll-area) &optional newly-managed) (declare (ignore newly-managed)) (with-slots (children (scroll-frame parent) width height) self (let ((content (first children))) (when content ;; If realized, then recalibrate scrollers for new content ;; (otherwise, not necessary since initial calibration will be done ;; after initial scroll-area size is set). (when (realized-p self) (sf-recalibrate scroll-frame)) ;; Define content callbacks used by application to report new calibration data (flet ((horizontal-update (&key position minimum maximum pixels-per-unit scroll-frame) ;; Recalibrate scroller, if necessary (when (eq :on (scroll-frame-horizontal scroll-frame)) (scale-update (scroll-frame-hscroller scroll-frame) :value position :minimum minimum :maximum maximum :indicator-size (when pixels-per-unit (/ (contact-width (scroll-frame-area scroll-frame)) pixels-per-unit)))) ;; Update current scroll-frame position (when position (with-slots (left top) scroll-frame (sf-scroll-to scroll-frame (setf left position) top)))) (vertical-update (&key position minimum maximum pixels-per-unit scroll-frame) ;; Recalibrate scroller, if necessary (when (eq :on (scroll-frame-vertical scroll-frame)) (scale-update (scroll-frame-vscroller scroll-frame) :value position :minimum minimum :maximum maximum :indicator-size (when pixels-per-unit (/ (contact-height (scroll-frame-area scroll-frame)) pixels-per-unit)))) ;; Update current scroll-frame position (when position (with-slots (left top) scroll-frame (sf-scroll-to scroll-frame left (setf top position)))))) (add-callback content :horizontal-update #'horizontal-update :scroll-frame scroll-frame) (add-callback content :vertical-update #'vertical-update :scroll-frame scroll-frame)) ;; Initialize content (with-state (content) ;; Initialize content position (this may be changed later if ;; default pixel scrolling is used) (move content 0 0) ;; Force content border width to 0 (with-slots ((content-width width) (content-height height)) content (resize content content-width content-height 0))))))) (defmethod manage-geometry ((self scroll-area) content x y width height border-width &key) (flet ((update-scroller-maximum (scroll-area) ;; Called when an approved content geometry change is performed. When default ;; scrolling is used, then scrollers must be updated to reflect new ;; pixel size of content w.r.t scroll-area. (let ((content (first (composite-children scroll-area)))) ;; Default scrolling? (unless (callback-p content :scroll-to) (let ((frame (contact-parent scroll-area)) (max-h (max 0 (- (contact-width content) (contact-width scroll-area)))) (max-v (max 0 (- (contact-height content) (contact-height scroll-area))))) (apply-callback content :horizontal-update :maximum max-h :position (min (scroll-frame-left frame) max-h)) (apply-callback content :vertical-update :maximum max-v :position (min (scroll-frame-top frame) max-v))))))) (values (when (or (null border-width) (= border-width 0)) #'update-scroller-maximum) (or x (contact-x content)) (or y (contact-y content)) (or width (contact-width content)) (or height (contact-height content)) 0))) (defmethod resize :after ((self scroll-area) new-width new-height new-bw) (declare (ignore new-width new-height new-bw)) (with-slots (parent) self (let ((scroll-frame parent)) (sf-recalibrate scroll-frame) (unless (realized-p self) ;; Move content into initial position, now that content units have been ;; defined. (sf-scroll-to scroll-frame (scroll-frame-left scroll-frame) (scroll-frame-top scroll-frame)))))) (defun sf-recalibrate (scroll-frame) (let ((content (scroll-frame-content scroll-frame))) (when content (with-slots (left top horizontal vertical) scroll-frame (with-slots (width height) (scroll-frame-area scroll-frame) (let ((new-left left) (new-top top)) (let ((hscroller (when (eq :on horizontal) (scroll-frame-hscroller scroll-frame)))) (when hscroller (setf new-left (sf-horizontal-calibrate content hscroller left width)))) (let ((vscroller (when (eq :on vertical) (scroll-frame-vscroller scroll-frame)))) (when vscroller (setf new-top (sf-vertical-calibrate content vscroller top height)))) (scroll-frame-reposition scroll-frame :left new-left :top new-top)))))))