605 lines
20 KiB
Executable File

:; exec /usr/local/bin/stk -f "$0" "$@"
;;;; a m i b . s t k l o s -- A mini interface builder. I hope it will serve
;;;; as the basis of something more complete...
;;;; Copyright © 1995-1999 Erick Gallesio - I3S-CNRS/ESSI <>
;;;; Permission to use, copy, modify, distribute,and license this
;;;; software and its documentation for any purpose is hereby granted,
;;;; provided that existing copyright notices are retained in all
;;;; copies and that this notice is included verbatim in any
;;;; distributions. No written agreement, license, or royalty fee is
;;;; required for any of the authorized uses.
;;;; This software is provided ``AS IS'' without express or implied
;;;; warranty.
;;;; Author: Erick Gallesio []
;;;; Creation date: 22-May-1995 14:56
;;;; Last file update: 3-Sep-1999 19:13 (eg)
(require "Tk-classes")
;;;; Definitions.
(define *amib-version* 0.5)
(define *pretty-names* (make-hash-table))
(define *current-file* #f)
(define *special-slots* '("id" "eid" "parent" "environment"))
(define *delay* 100)
;;;; All the widgets and their defaults
(define *table-defaults*
`(("Button" ,<Button>
(:text "Button"))
("Canvas" ,<Canvas>
(:width 200 :height 100 :border-width 3 :relief "raised"))
("Check button" ,<Check-button>
(:text "Check" :anchor "w"))
("Frame" ,<Frame>
(:width 50 :height 50 :relief "ridge" :border-width 2))
("Label" ,<Label>
(:text "Label"))
("Labeled entry" ,<Labeled-entry>
(:title "Title"))
("Listbox" ,<Listbox>
(:relief raised))
("Message" ,<Message>
(:text "Message" :relief "raised" :aspect 1000))
("Radio button" ,<Radio-button>
(:text "Radio" :anchor "w"))
("Scale" ,<Scale>
()) ))
;;;; Drag and Drop stuff
(define d-n-d-widget #f) ; The widget whih we can drag and drop
(define d-n-d-defaults #f) ; Its defaults
;; Default bindings
(bind "Dnd" "<ButtonRelease-1>" (lambda (|X| |Y| x y)
(Drag-n-Drop-Finish |X| |Y|)
(define (make-drag-n-drop-widget type initargs)
(let ((m (make <Toplevel> :background "Blue" :override-redirect #t)))
(pack (apply make type :parent m initargs) :padx 2 :pady 2)
(define (Drag-n-Drop-Motion)
(when d-n-d-widget
(slot-set! d-n-d-widget 'geometry
(apply format #f "+~A+~A" (winfo 'pointerxy d-n-d-widget)))
(after *delay* (lambda () (Drag-n-Drop-Motion)))))
(define (Drag-n-Drop-Finish X Y)
(when d-n-d-widget
(let ((dwidth (winfo 'width d-n-d-widget))
(dheight (winfo 'height d-n-d-widget)))
;; iconify the d-n-d-widget and see on which window we depose it
(withdraw d-n-d-widget)
(let* ((p (Id->instance (winfo 'containing X Y)))
(top (Id->instance (winfo 'toplevel p))))
(when (string=? (slot-ref top 'class) "Amib-toplevel")
;; OK. We try to depose the new widget in a valid toplevel
(let* ((w (apply make (car d-n-d-defaults) :parent p
(cadr d-n-d-defaults)))
(pw (max 1 (winfo 'width p)))
(ph (max 1 (winfo 'height p)))
(x (- X (winfo 'x top) (winfo 'x p)))
(y (- Y (winfo 'y top) (winfo 'y p)))
(relw (/ dwidth pw))
(relh (/ dheight ph)))
(place w :relx (/ x pw) :rely (/ y ph) :relwidth relw :relheight relh)
(raise w)
;; Associate bindings for manipulating the new widget
(bind w "<Shift-1>" (lambda (|X| |Y|)
(widget-resize-start w |X| |Y|)
(bind w "<Button-2>" (lambda () (edit-widget w) 'break))
(bind w "<Shift-3>" (lambda () (edit-widget w) 'break)) ; for Win32
(bind w "<Button-3>" (lambda () (destroy w) 'break))))))
;; We can now delete the drag and drop window,which doesn't serve anymore
(destroy d-n-d-widget)
(set! d-n-d-widget #f)))
(define (create-new-widget lb x y Xabs Yabs)
(unless d-n-d-widget
(let* ((index (nearest lb y))
(type (list-ref (value lb) index))
(search (assoc type *table-defaults*)))
(when search
;; Create a drag and drow window and post it under the mouse
(let ((W (apply make-drag-n-drop-widget (cadr search) (cddr search))))
(set! (geometry W) (format #f "+~a+~a" Xabs Yabs))
(bindtags W (cons "Dnd" (bindtags W)))
(set! d-n-d-widget W)
(set! d-n-d-defaults (cdr search))
(after *delay* (lambda () (Drag-n-Drop-Motion))))))))
;;;; Define a Toplevel for working
(define new-amib-toplevel
(let ((count 0))
(lambda ()
(let* ((n (* count 20))
(t (make <Toplevel> :title (format #f "Toplevel # ~A" count)
:class "Amib-toplevel"
:geometry (format #f "450x300+~A+~A" n n))))
(set! count (+ count 1))
(pack (make <Frame> :parent t :border-width 0) :expand #t :fill "both")))))
;;;; build-interface -- construct the button panel
(define (build-interface)
(let* ((mess (make <Label> :relief "ridge" :border-width 3 :foreground "blue"
:text (format #f "A Mini Interface Builder (V~A)"
;; Menus
(menus `((" File "
("Load" ,load-file)
("Save" ,save-file)
("Save as" ,write-file)
("Quit" ,quit))
(" Toplevel "
("Create" ,new-amib-toplevel))
(" Help "
("About" ,(lambda ()
(stk:show-help-file "amib-abt.html")))
("Help" ,(lambda ()
(stk:show-help-file "amib-hlp.html"))))))
;; Widget Panel
(chooser (make <Scroll-Listbox> :value (map car *table-defaults*)))
;; Menu bar
(bar (make-toolbar *top-root* menus :relief "ridge" :border-width 2
(default-release-toolbar chooser)))
(lb (listbox-of chooser)))
;; Associate new bindings to the listbox
(bind lb "<ButtonRelease-1>" (lambda (x y |X| |Y|)
(create-new-widget lb x y |X| |Y|)))
;; Change characteristics of root window
(set! (title *top-root*) (format #f "AMIB ~A" *amib-version*))
(set! (maximum-size *top-root*) '(1000 1000))
(set! (geometry *top-root*) "+10-10")
;; Pack everybody
(pack mess :fill "x" :ipadx 30 :ipady 5 :padx 5 :pady 5)
(pack bar :fill "x" :ipadx 30)
(pack chooser :expand #t :fill 'both :ipadx 5 :ipady 5 :padx 5 :pady 5)))
;;;; Widget resize
(define *cursors* #("top_left_corner" "top_side" "top_right_corner"
"left_side" "crosshair" "right_side"
"bottom_left_corner" "bottom_side" "bottom_right_corner"))
(define *positions* #(NW N NE W center E SW S SE))
(define *grips-on* #f)
(define *resizing* #f)
(define *vector-of-grips* (make-vector 9 #f))
(define (widget-resize-start W X Y)
(let ((parent (parent W))
(width (winfo 'width W))
(height (winfo 'height W))
(bw (if (slot-exists? W 'border-width) (border-width W) 0)))
(if (equal? *grips-on* W)
(set! *grips-on* #f))
(set! *grips-on* W)
(dotimes (i 9)
(let ((butt (make <Frame> :parent parent :width 8 :height 8
:background "blue" :border-width 2 :relief "raised"
:cursor (vector-ref *cursors* i))))
(place butt :in W :bordermode "outside"
:anchor (vector-ref *positions* (- 8 i))
:relx (* 0.5 (modulo i 3))
:rely (* 0.5 (quotient i 3)))
;; Associate bindings to this grip
(bind butt "<ButtonPress-1>"
(lambda ()
(set! *resizing* #t)
(widget-resize-motion W (vector-ref *positions* i))
(bind butt "<ButtonRelease-1>"
(lambda ()
(set! *resizing* #f)
(widget-resize-release W)
;; Keep the grip in the global vector
(vector-set! *vector-of-grips* i butt)))
;; Place the central button on top (its index is 4)
(raise W)
(raise (vector-ref *vector-of-grips* 4))))))
(define (widget-resize-clear)
(for-each (lambda (x) (if (Tk-widget? x) (destroy x)))
(vector->list *vector-of-grips*)))
(define (widget-resize-motion W index)
(when *resizing*
(let* ((parent (parent W))
(pos-x (winfo 'rootx parent))
(pos-y (winfo 'rooty parent))
(width (winfo 'width W))
(height (winfo 'height W))
(x (winfo 'pointerx W))
(y (winfo 'pointery W))
(x1 (- (winfo 'rootx W) pos-x))
(y1 (- (winfo 'rooty W) pos-y))
(x2 (+ x1 width))
(y2 (+ y1 height))
(x (- X pos-x))
(y (- Y pos-y)))
(case index
((NW) (set! x1 x) (set! y1 y))
((N) (set! y1 y))
((NE) (set! x2 x) (set! y1 y))
((W) (set! x1 x))
((E) (set! x2 x))
((SW) (set! x1 x) (set! y2 y))
((S) (set! y2 y))
((SE) (set! x2 x) (set! y2 y))
((center) (set! x1 (- x (quotient width 2)))
(set! y1 (- y (quotient height 2)))
(set! x2 (+ x1 width))
(set! y2 (+ y1 height))))
(place 'forget W)
(place W :in parent :x x1 :y y1 :width (- x2 x1) :height (- y2 y1))
(after 30 (lambda () (widget-resize-motion W index))))))
(define (widget-resize-release W)
;; Calculate the relative width and height of the widget
(let* ((parent (parent W))
(pw (winfo 'width parent))
(ph (winfo 'height parent))
(pos-x (winfo 'rootx parent))
(pos-y (winfo 'rooty parent))
(width (winfo 'width W))
(height (winfo 'height W))
(x (- (winfo 'rootx W) pos-x))
(y (- (winfo 'rooty W) pos-y)))
(place 'forget W)
(place W :in parent
:relx (if (= pw 0) 0 (/ x pw))
:rely (if (= ph 0) 0 (/ y ph))
:relwidth (if (= pw 0) 0 (/ width pw))
:relheight (if (= ph 0) 0 (/ height ph)))))
;;;; Widget Geometry management
(define old-packing-options (if (equal? (winfo 'manager W) "pack")
(pack 'info W)
(define (build-var-name x)
(string->symbol (format #f "amib-~A~A" x (widget-name (Id W)))))
(define (make-var v val)
(let ((var (build-var-name v)))
(eval `(define ,var ',val) (global-environment))
(define (change-pack-opt)
(pack 'forget W)
(pack W :side (eval (build-var-name 'side))
:anchor (eval (build-var-name 'anchor))
:fill (eval (build-var-name 'fill))
:expand (eval (build-var-name 'expand))
:padx (eval (build-var-name 'padx))
:pady (eval (build-var-name 'pady))
:ipadx (eval (build-var-name 'ipadx))
:ipady (eval (build-var-name 'ipady))))
(define (make-side parent)
(let* ((f (make <Frame> :parent parent :relief "groove" :border-width 2))
(val (get-keyword :side old-packing-options "top"))
(v (make-var 'side val)))
(pack (make <Label> :text "Side: " :parent f :font '(Courier -12))
:side "left")
(for-each (lambda (x)
(pack (make <Radio-button> :parent f :text x :variable v
:value x :command change-pack-opt)
:side "left" :expand #t :fill "x"))
'("top" "bottom" "left" "right"))
(define (make-anchor parent)
(let* ((f (make <Frame> :parent parent :relief "groove" :border-width 2))
(val (string->symbol (get-keyword :anchor old-packing-options "center")))
(v (make-var 'anchor val)))
(dotimes (i 3)
(let ((g (make <Frame> :parent f)))
(dotimes (j 3)
(let ((anchor (vector-ref *positions* (+ (* i 3) j))))
(pack (make <Radio-Button> :text anchor :width 10 :parent g
:variable v :value anchor :anchor "w"
:command change-pack-opt)
:side "left" :expand #t :fill "x")))
(pack g :side "top")))
(define (make-fill parent)
(let* ((f (make <Frame> :parent parent :relief "groove" :border-width 2))
(val (get-keyword :fill old-packing-options "none"))
(v (make-var 'fill val)))
(pack (make <Label> :text "Fill: " :parent f :font '(Courier -12))
:side "left")
(for-each (lambda (x)
(pack (make <Radio-button> :parent f :text x :variable v
:value x :command change-pack-opt)
:side "left" :expand #t :fill "x"))
'("none" "x" "y" "both"))
(define (make-expand parent)
(let ((val (get-keyword :expand old-packing-options #f)))
(make <Check-button> :parent parent :relief "groove" :border-width 2
:text "Expand" :variable (make-var 'expand val) :value val
:command change-pack-opt)))
(define (make-padding parent)
(let ((f (make <Frame> :parent parent :relief "groove" :border-width 2)))
(for-each (lambda (x)
(let* ((val (get-keyword (make-keyword x) old-packing-options 10))
(v (make-var x val)))
(pack (make <Scale> :orientation "h" :parent f :text x
:variable v :value val
:command (lambda (_) (change-pack-opt)))
:expand #t :fill "x")))
'(ipadx ipady padx pady))
;; MAKE-PACKING-WINDOW starts here
(let ((top (make <Toplevel> :title "Packer options" :class "Amib"
:geometry "-100+100")))
(pack (make-side top)
(make-anchor top)
(make-fill top)
(make-expand top)
(make-padding top)
:padx 5 :pady 5 :fill "x")
(pack (make <Button> :parent top :text "Dismiss" :command (lambda ()
(destroy top)))
:fill "x")))
(define (use-pack-for-widget W)
(place 'forget W)
(pack W :in (parent W))
(make-packing-window W))
(define (use-place-for-widget W)
(pack 'forget W)
(place W :in (parent W))
;;;; edit-widget -- Interactively change widget options
(define (edit-widget w)
(letrec ((top (make <Toplevel> :class "Amib" :title "Widget Editor"
:geometry "-10+10"))
(slots (map (lambda (x) (symbol->string (if (pair? x) (car x) x)))
(class-slots (class-of w))))
(filter (lambda (slots forget)
(let loop ((l slots) (res '()))
((null? l) res)
((member (car l) forget)
(loop (cdr l) res))
(else (loop (cdr l) (cons (car l) res)))))))
(maxl 0))
;; Display only useful slots
(set! slots (sort (filter slots *special-slots*) string<?))
(set! maxl (apply max (map string-length slots)))
;; Pretty name of this object
(let ((name-editor (make <Labeled-Entry>
:parent top
:title "Widget name"
:value (hash-table-get *pretty-names* w "?none?"))))
(bind (entry-of name-editor) "<Return>"
(lambda ()
(hash-table-put! *pretty-names* w (value name-editor))))
(pack name-editor :expand #t :fill 'x))
;; Display the geometry manager used for this widget
(let* ((f (make <Frame> :border-width 2 :relief "ridge" :parent top))
(v (string->symbol (format #f "cb-var~A" (widget-name (Id w)))))
(c1 (make <Radio-Button> :text "Packed" :variable v :parent f
:value "pack"
:command (lambda () (Use-pack-for-widget w))))
(c2 (make <Radio-Button> :text "Placed" :variable v :parent f
:value "place"
:command (lambda () (Use-place-for-widget w)))))
;; Set the valid check button
(eval `(set! ,v ,(if (null? (place 'info w)) "pack" "place")))
(pack c1 c2 :side "left" :expand #t :fill "x")
(pack f :expand #t :fill "x"))
;; Display the widget editor
(for-each (lambda (s)
(let* ((name (string->symbol s))
(le (make <Labeled-Entry> :parent top :title name
:width 40
:value (slot-ref w (string->symbol s)))))
;; Customize label
(set! (width (label-of le)) maxl)
(set! (anchor (label-of le)) "e")
;; Customize entry
(bind (entry-of le) "<Return>" (lambda ()
(slot-set! w name (value le))))
;; Pack the new entry
(pack le :fill "y" :expand #t)))
;; Dismiss button
(pack (make <Button> :text "Dismiss" :parent top
:command (lambda () (destroy top)))
:expand #t
:fill 'x)))
;;;; Code generation
(define (Pretty-name w)
(let ((name (hash-table-get *pretty-names* w #f)))
(unless name
;; If this object has no name, a name is generated for it
(set! name (if (eqv? w *top-root*) "*top-root*" (gensym "W")))
(hash-table-put! *pretty-names* w name))
;;;; Generate-placement: generate pack or place depending of the geometry manager
;;;; used.
(define-method generate-placement ((w <Tk-widget>))
(let* ((infos (place 'info w))
(use-pack? (null? infos)))
(if use-pack?
(set! infos (pack 'info w)))
(format #t "(~A ~A " (if use-pack? "pack " "place") (pretty-name w))
;; Display informations returned by Tk
(let loop ((i infos))
((null? i) (display ")\n\n"))
((eqv? (car i) ':in) (format #t "\n :in ~A"
(Id->instance (eval (cadr i)))))
(loop (cddr i)))
(ELSE (let ((val (cadr i)))
(format #t "\n ~S " (car i))
(if (number? val)
(display val)
(format #t "\"~A\"" val)))
(loop (cddr i)))))))
(define-method generate-placement ((w <Toplevel>))
;;;; Generate-code-for-widget methods
(define-method generate-code-for-widget ((w <Toplevel>))
(format #t "\n;; Definition of Toplevel ~A\n" (pretty-name w))
(define-method generate-code-for-widget ((w <Tk-widget>))
;; Generate name
(format #t ";-----------\n(define ~A (make ~A\n\t:parent ~A\n"
(pretty-name w) (class-name (class-of w)) (pretty-name (parent w)))
;; Generate non special slots
(for-each (lambda (slot)
(unless (member slot *special-slots*)
(unless (member (symbol->string (car slot)) *special-slots*)
;; Generate code for this slot (which is for sure a list)
(let* ((slot-name (car slot))
(val (slot-ref w slot-name))
(init-key (get-keyword :init-keyword (cdr slot) #f)))
(when (and init-key (not (equal? (slot-ref w slot-name) "")))
(format #t "\t~S ~A~S\n"
init-key (if (list? val) "'" "") val))))))
(class-slots (class-of w)))
;; Close parenthesis
(format #t "))\n\n")
;; Generate code for embedded widgets. Don't do this if w is a composite
(unless (is-a? w <Tk-composite-widget>)
(for-each generate-code-for-widget
(map Id->instance (winfo 'children w))))
;; Generate placement for this widget
(generate-placement w))
;;;; Generate-code (the entry point of code generation)
(define (generate-code file)
(let ((all-tops (map Id->instance (winfo 'children *root*))))
(letrec ((dump(lambda (func)
(for-each (lambda (x)
(when (and (is-a? x <Toplevel>)
(not (equal?(slot-ref x 'class) "Amib")))
(func x)))
(with-output-to-file file
(lambda ()
(format #t ";;\n;; Code generated by Amib (v~A)\n;;\n" *amib-version*)
(format #t "(require \"Tk-classes\")")
(dump generate-code-for-widget)
(dump generate-placement))))))
;;;; File Management
(define (save-file)
(if *current-file*
(generate-code *current-file*)
(define (load-file)
(let ((f (Tk:get-open-file)))
(when f (load f))))
(define (write-file)
(let ((f (Tk:get-save-file)))
(when f
(set! *current-file* f)
(generate-code f))))
;;;; Inits
(bind "all" "<ButtonRelease-1>" (lambda () (set! *resizing* #f)))