;;; -*- Mode: LISP; Syntax: Common-Lisp; Package: LAPIDARY; Base: 10 -*- ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;; The Garnet User Interface Development Environment. ;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;; This code was written as part of the Garnet project at ;;; ;;; Carnegie Mellon University, and has been placed in the public ;;; ;;; domain. If you are using this code or any part of Garnet, ;;; ;;; please contact garnet@cs.cmu.edu to be put on the mailing list. ;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;; -*- Mode: Lisp; Package: LAPIDARY -*- ;;; ;;; This file provides the functions that handle inferencing and ;;; generalization of constraints for the interactors ;;; #| ============================================================================== Change log: 9/22/93: Fernando -- Renamed "position" variables to "component-position" 7/6/92: Brad Vander Zanden -- changed by-demo dialog box to a query gadget ============================================================================== |# (in-package "LAPIDARY") (defvar *by-demo-obj* nil "object the user modifies when demonstrating how feedback should behave") (defvar *by-demo-copy* nil "object the user actually modifies when demonstrating how feedback should behave") (defvar *by-demo-comparison* nil "object used to contain the ``before'' state of the demonstration") ;;; This is embedded in the by-demo gadget to handle the OK ;;; and cancel functions" (defun by-demo-OKCancel-Function (gadget value) (cond ((string-equal value "OK") (gilt::standard-OK-function gadget)) ((string-equal value "Cancel") ;; restore the original object to the display and reset the ;; by-demo indicator ; (reset-undo) (restore-original-obj) (s-value (g-value gadget :demo-gadget) :demo-p t) ;; restore the original value to the appropriate feedback gadget (s-value (g-value gadget :feedback-gadget) :value (g-value gadget :feedback-gadget :save-value)) (standard-Cancel-function gadget)))) (defun by-demo-ok-function (gadget value) (let ((formula-list (by-demo (g-value gadget :demo-gadget) value)) (feedback-gadget (g-value gadget :feedback-gadget))) (s-value feedback-gadget :field-string nil) (if (eq (g-value feedback-gadget :known-as) :feedback-obj) (dialog-enqueue :feedback-obj nil *choice-inter-queue*) (dialog-enqueue :final-feedback-obj nil *choice-inter-queue*)) (dialog-enqueue :feedback formula-list *CHOICE-INTER-QUEUE*))) ;;; save a formula in an object and all its instances (defun save-formula (obj slot formula &optional (first-p t)) (let ((slot-value (assoc slot (g-value obj :save-values)))) (dovalues (instance obj :is-a-inv :local t) (save-formula instance slot formula nil)) ;; if not the first call to save-formula, create an instance of formula (when (not first-p) (setf formula (kr::make-into-o-formula (formula formula)))) (if slot-value (setf (cdr slot-value) formula) (push (cons slot formula) (g-value obj :save-values))))) ;;; install a formula in an object and all its instances (defun install-formula (obj slot formula &optional (first-p t)) (let ((check-value :lapidary-error) (old-value (get-value obj slot))) ;; if the old-value is a formula, save a copy of the formula. ;; if this is not done, the formula will be destroyed when ;; the new formula is stored in the slot (when (formula-p old-value) (setf old-value (copy-formula old-value))) (if first-p (kr::copy-to-all-instances obj slot (kr::make-into-o-formula formula)) (kr::copy-to-all-instances obj slot (kr::make-into-o-formula (formula formula)))) ;; demand the value of this formula and be prepared to undo this formula ;; if something goes wrong (unwind-protect (setf check-value (g-value obj slot)) (when (eq check-value :lapidary-error) (destroy-constraint obj slot) (s-value obj slot old-value) (format t "~% The bad constraint was destroyed and the old value restored.") (format t "~% To continue, type (inter:main-event-loop)~%") (opal:update-all))))) ;;; restore the original object to the display and select it when a ;;; by-demo operation is completed or canceled (defun restore-original-obj () (declare (special *by-demo-copy* *by-demo-obj* *by-demo-comparison*)) ;; deselect any selected objects (primary-deselect-objects :none) (secondary-deselect-objects :none) ;; destroy the copied objects and place the original by-demo ;; object back into the editor window. This must be done with ;; care by determining the position of the component to be ;; removed, removing the component, and then inserting the ;; component at the appropriate position. If this is not done, ;; an aggrelist can get messed up (let* ((agg (g-value *by-demo-copy* :parent)) (components (g-value agg :components)) (component-position (- (list-length components) (list-length (member *by-demo-copy* components))))) (opal:remove-component agg *by-demo-copy*) (opal:add-component agg *by-demo-obj* :at component-position)) (opal:destroy *by-demo-copy*) (opal:destroy *by-demo-comparison*) ;; select the original by-demo object (primary-select *by-demo-obj*)) ;;; create a copy of the object selected by the user, allow the user to ;;; modify the copy, then find the differences between the original and ;;; the copy and create formulas that reflect the differences (defun by-demo (gadget value) (declare (ignore value)) (let* ((demo-slot (g-value gadget :demo-slot)) (demo-p (g-value gadget :demo-p)) (queue (g-value gadget :queue)) (inter (g-value gadget :inter)) (slot (g-value gadget :slot)) (by-demo-formula-list (assoc slot (symbol-value queue))) (selection (car (g-value *selection-info* :selected))) formula-list start-where start-where-objs control) ;; determine if the demonstration is to begin or is just ending (if demo-p ;; demonstration has just begun. Create two copies ;; of the selected object and replace the selected object with ;; one of the copies in the editor window. (progn ;; first make sure that there is a selection and that it is ;; a member of the objects acted on by this interactor (when (null selection) (lapidary-error "Must select an object to demonstrate with") (return-from by-demo nil)) (setf start-where (get-start-where (symbol-value queue) inter)) (when (eq start-where :not-supplied) (lapidary-error "Must supply a start-where before doing demonstrations") (return-from by-demo nil)) (setf control (if (atom start-where) start-where (car start-where))) (when (member control '(t nil :in-but-not-on)) (lapidary-error (format nil "cannot demonstrate behavior for a start-where that begins with ~S" control)) (return-from by-demo nil)) (setf start-where-objs (find-start-where-objs start-where)) (when (not (member selection start-where-objs)) (lapidary-error (format nil "The selection ~S is not an object that satisfies the interactor's start-where predicate" selection)) (return-from by-demo nil)) ;; save the selected object (setf *by-demo-obj* selection) ;; deselect any selected objects (primary-deselect-objects :none) (secondary-deselect-objects :none) ;; create a copy of the by-demo object and install the current ;; by-demo formulas in it (setf *by-demo-comparison* (opal:copy-gadget selection nil)) ;; create a copy of the by-demo object and place the copy ;; in the editor window (setf *by-demo-copy* (create-instance nil *by-demo-comparison*)) (when by-demo-formula-list (install-by-demo-formulas nil (cdr by-demo-formula-list) *by-demo-comparison*)) ;; add the copied object to the original's aggregate in the ;; same position as the original; then remove the original from ;; the aggregate. Since the remove components method for ;; aggregadgets removes all instances of a prototype from an ;; aggregadget when the prototype is removed from an aggregadget, ;; we must determine the position of the original object, then ;; remove it, then add the copied object (let* ((agg (g-value selection :parent)) (components (g-value agg :components)) (component-position (- (list-length components) (list-length (member selection components))))) (opal:remove-component agg selection) (opal:add-component agg *by-demo-copy* :at component-position)) ;; select the copy (primary-select *by-demo-copy*)) ;; demonstration has just ended--create formulas (progn (setf formula-list (compare-slots *by-demo-comparison* *by-demo-copy* demo-slot)) (when by-demo-formula-list (setf formula-list (merge-by-demo-lists formula-list (cdr by-demo-formula-list)))) (restore-original-obj))) ;; invert the demo-p flag (s-value gadget :demo-p (not demo-p)) ;; return t if the demonstration is being set up, or the formula ;; list if the demonstration is being completed--notice that demo-p ;; will be nil if the demonstration is being completed, so the formula ;; list will be returned (or demo-p formula-list))) ;;;====================================================================== ;;; merge the current list of by-demo formulas with the previous list ;;; of by-demo formulas. Entries on the current list override duplicate ;;; entries on the previous list. Entries are of the form: ;;; (path (slot formula) (slot formula) ...) where path is a sequence ;;; of link names that allows an object to be found in an aggregate ;;; hierarchy ;;; ;;;====================================================================== (defun merge-by-demo-lists (current-list prev-list) (dolist (entry current-list) (let* ((path (car entry)) (slot-formulas (cdr entry)) (prev-entry (assoc path prev-list :test #'equal))) (if prev-entry ;; only retain elements from prev-entry that are not in entry (setf (cdr prev-entry) (append slot-formulas (set-difference (cdr prev-entry) slot-formulas :key #'car))) (push entry prev-list)))) prev-list) ;;;====================================================================== ;;; ;;; compare selected slots from the original and copied objects. When ;;; a difference is found, construct a formula that chooses between ;;; the "before" and "after" states based on the value of the demo slot. ;;; For example, given the following values: ;;; "before" :left (+ (gvl :left-over :left) (gvl :left-offset)) ;;; "after" :left (+ (opal:gv-right (gvl :left-over)) (gvl :left-offset)) ;;; :demo-slot :interim-selected ;;; we might construct the formula: ;;; :left (cond ((gvl :interim-selected) ;;; (+ (opal:gv-right (gvl :left-over)) (gvl :left-offset))) ;;; (t (+ (gvl :left-over :left) (gvl :left-offset)))) ;;; ;;; if the "after" branch already exists, then we simply substitute the ;;; new "after" expression into the expression portion of the branch. For ;;; example, if the user demoed the above object a second time, we might ;;; get the following values: ;;; ;;; "before" :left (cond ((gvl :interim-selected) ;;; (+ (opal:gv-right (gvl :left-over)) ;;; (gvl :left-offset))) ;;; (t (+ (gvl :left-over :left) (gvl :left-offset)))) ;;; ;;; "after" :left (- (opal:gv-right (gvl :left-over)) ;;; (gvl :width) ;;; (gvl :left-offset)) ;;; :demo-slot :interim-selected ;;; we would substitute the "after" expression into the ;;; (gvl :interim-selected) branch of the conditional statement to obtain: ;;; :left (cond ((gvl :interim-selected) ;;; (- (opal:gv-right (gvl :left-over)) ;;; (gvl :width) ;;; (gvl :left-offset))) ;;; (t (+ (gvl :left-over :left) (gvl :left-offset)))) ;;; ;;; The resulting formulas should not be inserted into the objects affected by ;;; this interactor until the OK or NEW buttons are selected. Thus the ;;; formulas will be saved in the following data structure: ;;; ((path (slot formula) (slot formula) ...) ;;; (path (slot formula) (slot formula) ...) ;;; ... ;;; (path (slot formula) (slot formula) ...)) ;;; ;;; where each path is a path that traverses the aggregate hierarchy ;;; to reach the appropriate component of the demoed object ;;; ;;;====================================================================== (defun compare-slots (original copy demo-slot) (let (overall-formula-list formula-list) (when (is-a-p original opal:aggregate) (do ((original-components (g-value original :components) (cdr original-components)) (copy-components (g-value copy :components) (cdr copy-components))) ((null original-components)) (setf overall-formula-list (append overall-formula-list (compare-slots (car original-components) (car copy-components) demo-slot))))) (dolist (primary-slot '(:left :top :width :height :visible :draw-function :font :string :line-style :filling-style :x1 :x2 :y1 :y2)) ;; accumulate all the link and offset slots that should be looked at ;; (e.g., :left-over, :left-offset, :width-scale, etc) (let* ((primary-copy-value (get-value copy primary-slot)) (original-copy-value (get-value original primary-slot)) (slot-list (list primary-slot))) (when (formula-p primary-copy-value) (setf slot-list (append (g-formula-value primary-copy-value :links) slot-list)) (when (g-formula-value primary-copy-value :offset) (push (g-formula-value primary-copy-value :offset) slot-list)) (when (g-formula-value primary-copy-value :scale) (push (g-formula-value primary-copy-value :scale) slot-list))) (when (formula-p original-copy-value) (setf slot-list (union slot-list (g-formula-value original-copy-value :links))) (when (g-formula-value original-copy-value :offset) (pushnew (g-formula-value original-copy-value :offset) slot-list)) (when (g-formula-value original-copy-value :scale) (pushnew (g-formula-value original-copy-value :scale) slot-list))) (dolist (slot slot-list) (let* ((copy-value (get-value copy slot)) (original-value (get-value original slot)) ;; at the end of this loop a new formula is created. The ;; information in the :links, :scale, and :offset slots of ;; the original formulas must be preserved (link-slot (union (if (formula-p copy-value) (g-formula-value copy-value :links) nil) (if (formula-p original-value) (g-formula-value original-value :links) nil))) (offset-slot (cond ((formula-p copy-value) (g-formula-value copy-value :offset)) ((formula-p original-value) (g-formula-value original-value :offset)) (t nil))) (scale-slot (cond ((formula-p copy-value) (g-formula-value copy-value :scale)) ((formula-p original-value) (g-formula-value original-value :scale)) (t nil)))) ;; both of the following tests are required since the copy-value may ;; be a formula and an instance of original-value, in which ;; case the slots are considered similar (when (if (and (formula-p copy-value) (formula-p original-value)) (not (is-a-p copy-value original-value)) (not (equal copy-value original-value))) ;; if either the original or copy slots contains a formula, extract ;; the formula's expression from the formula object (when (formula-p copy-value) (setf copy-value (extract-formula copy-value))) (when (formula-p original-value) (setf original-value (extract-formula original-value))) ;; if the slot's formula contains a conditional, store each ;; of the branches in the expression list; otherwise store the ;; expression with a t affixed to it, since the support slots may ;; have conditional expressions (let* ((expr-list (if (and (listp original-value) (eql 'cond (car original-value))) (copy-tree (cdr original-value)) `((t ,(copy-tree original-value))))) (path `(gvl ,@(gilt:make-path original *by-demo-comparison*) ,demo-slot)) ;; try to match the path against one of the conditions in ;; the branches of the conditional expression (even if there ;; was no previous conditional, there is a branch labeled with ;; t, so the search will be unsuccessful but won't crash) (branch (assoc path expr-list :test #'equal))) ;; if a branch whose condition matches the path is found, ;; substitute the demoed expression into the branch's expression; ;; otherwise create a new branch with the path as a condition ;; and the demoed expression as the branch's expression (if branch (setf (second branch) copy-value) (let ((cond-expr (copy-tree (assoc 't expr-list :test #'equal)))) (setf (car cond-expr) path) (setf (second cond-expr) copy-value) (push cond-expr expr-list))) ;; a formula list has the form: (object (slot formula) ...), so ;; push a (slot formula) cons pair onto the list. (push (cons slot (formula `(cond ,@expr-list) (g-cached-value original slot) (:links link-slot) (:offset offset-slot) (:scale scale-slot))) formula-list))))))) ;; if there is one or more entries on the formula-list, prepend a path ;; to the object onto the front of the list so the list has the form ;; (path (slot formula) (slot formula) ...). Then push this list onto ;; the overall formula list (when formula-list (push (gilt:make-path *by-demo-comparison* original) formula-list) (push formula-list overall-formula-list)) overall-formula-list)) ;;;====================================================================== ;;; ;;; Install the formulas on the formula list in all objects that ;;; satisfy the start-where condition. The formulas are stored in ;;; the following data structure: ;;; ;;; (by-demo-obj (component (slot formula) (slot formula) ...) ;;; (component (slot formula) (slot formula) ...)) ;;; ;;; where by-demo-obj is the object that the demonstration was performed ;;; on and each component is either by-demo-obj or one of the components ;;; of by-demo-obj. To install the formulas in the objects satisfying ;;; the start-where condition, we construct a path for each component ;;; in the formula that will allow us to find the corresponding component ;;; in each of the objects. This path is constructed by working up the ;;; aggregate hierarchy from the component and pushing the values of the ;;; :known-as slots onto the path. ;;; ;;;====================================================================== (defun install-by-demo-formulas (start-where by-demo-list &optional obj) ;; find the list of objects that satisfy the start-where condition (let ((objs (if obj (list obj) (find-start-where-objs start-where)))) ;; if the objects are components of an aggrelist, change the ;; item-prototype rather than the objects themselves--this is something ;; of a hack but it should generally accomplish what the user intends. If ;; only the item-prototype-object is changed, then things do not work ;; properly. For example, if the :left-offset slot is changed, the change ;; is faithfully reflected in the item-prototype-object when the aggrelist ;; is changed. However, the :left-offset slot is filtered out of the ;; item-prototype, which means that the :left formula in the item-prototype ;; does not use :left-offset. Therefore, the inherited :left formula ;; in item-prototype-object does not use :left-offset. Thus the change ;; to :left-offset gets ignored. The alternative approach would be to ;; create a new :left formula for item-prototype-object (copying may not ;; work--we want the actual expression copied, and copy does not guarantee ;; that the expression will be copied--it will create an instance of ;; the grandparent if it exists). However, often the user really wants ;; the item prototype modified as well (e.g., floating buttons), so I ;; chose to modify the item prototype. (when (is-a-p (g-value (car objs) :parent) opal:aggrelist) (setf objs (list (g-value (car objs) :parent :item-prototype)))) (dolist (component-entry by-demo-list) (let* ((path (car component-entry)) (formula-list (cdr component-entry))) (dolist (obj objs) (let ((target-component (eval `(g-value ,obj ,@path)))) (dolist (slot-formula formula-list) ; (undo-save target-component (car slot-formula)) (s-value target-component (car slot-formula) (kr::make-into-o-formula (formula (cdr slot-formula))))))))) ;; return t to indicate a successful installation of the formulas t)) ;;;====================================================================== ;;; ;;; find the list of objects that an interactor can start over ;;; ;;;====================================================================== (defun find-start-where-objs (start-where) (let ((control (if (listp start-where) (car start-where) start-where))) (case control ((:in :in-box) (list (second start-where))) ((:element-of :leaf-element-of :element-of-or-none :leaf-element-of-or-none :check-leaf-but-return-element :check-leaf-but-return-element-or-none) (g-value (second start-where) :components)) ((:list-element-of :list-leaf-element-of :list-element-of-or-none :list-leaf-element-of-or-none :list-check-leaf-but-return-element :list-check-leaf-but-return-element-or-none) (g-value (second start-where) (third start-where)))))) ;;;====================================================================== ;;; ;;; generalize the constraints in the feedback object so that it ;;; can appear over any of the objects in the start-where of the ;;; interactor. also insert a formula that controls the feedback's visibility ;;; into the visibility slot ;;;====================================================================== (defun create-feedback-obj (feedback-obj queue inter) (let* ((start-where (or (cdr (assoc :start-where queue)) (g-value inter :start-where))) (control (if (listp start-where) (car start-where) start-where)) selectable-objs) (when (or (not (listp start-where)) (eq control :in-but-not-on)) (lapidary-error "Cannot generalize constraints if start-where is either t, nil, or :in-but-not-on") (return-from create-feedback-obj)) ;; find the list of objects this feedback object can appear with (setf selectable-objs (find-start-where-objs start-where)) ;; generalize the constraints in the feedback object (generalize-constraints feedback-obj selectable-objs) ;; mark the object as a feedback-object (s-value feedback-obj :feedback-p t))) #| ;; set the visibility slot of the object and any of its instances (save-formula feedback-obj :visible (kr::make-into-o-formula (formula `(gvl :obj-over)))) (install-formula feedback-obj :visible (formula `(if (gv lapidary::editor-menu :build-p) t (gvl :obj-over)))))) |# ;;;====================================================================== ;;; ;;; this function generalizes the constraints in a feedback object so ;;; that it can appear over any element in the start-where of an ;;; interactor. the function is passed the feedback object and the list ;;; of objects that the interactor can select (call these objects the ;;; selectable objects). the function examines ;;; each link in the feedback object that is associated with a position ;;; slot and determines which object this link points to (call this ;;; object the link object). It then determines which of the selectable ;;; objects is an ancestor of the link object, establishes a path ;;; from the selectable object to the link object and stores the path ;;; in the link slot. the path uses :obj-over to refer to the selectable ;;; object, so the link slot will be able to point to any of the ;;; selectable objects. ;;; ;;;====================================================================== (defun generalize-constraints (obj selectable-objs &optional (parent nil)) (let (parent-obj obj-over) ;; first do children (dolist (child (g-value obj :components)) (generalize-constraints child selectable-objs (if parent parent obj))) (dolist (link (if (is-a-line-p obj) (append '(:x1-over :x2-over :y1-over :y2-over) (g-value obj :links)) (append '(:left-over :top-over :width-over :height-over) (g-value obj :links)))) (when (setf obj-over (g-local-value obj link)) (setf parent-obj obj-over) (loop (when (or (member parent-obj selectable-objs) (null parent-obj)) (return)) (setf parent-obj (g-value parent-obj :parent))) ;; the link should be generalized only if the link points to an ;; an object which is a descendent of a selectable object (when parent-obj ;; must set this link to the selectable object ;; because KR will eagerly evaluate the formula ;; and expect to find :obj-over set ; (undo-save obj :obj-over) ; (undo-save obj link) (if parent (s-value parent :obj-over parent-obj) (s-value obj :obj-over parent-obj)) (s-value obj link (kr::make-into-o-formula (formula `(gvl ,@(gilt:make-path obj parent) :obj-over ,@(gilt:make-path parent-obj obj-over))))))))))