;; custom-menu.gwm --- User-friendlier package customizations
;;
;; Author: Anders Holst  (aho@sans.kth.se)  
;; Copyright (C) 1996  Anders Holst
;; Last change: 23/3 1996
;;
;; This file is copyrighted under the same terms as the rest of GWM
;; (see the X Inc license for details). There is no warranty that it
;; works. 
;;
;; ---------------------------------------------------------------------
;;
;; This file implements a way for other packages to provide easy
;; customization via menus, without spending too much effort on it
;; themselves.
;;
;;
;; The main function is '(custom-menu <MENU-DESCRIPTION>)'.
;;
;; In the simplest case its argument is a list of strings and variables,
;; and it constructs a menu where the user can edit the values of the
;; given variables. Typically the first element in the list should be
;; the name of the menu, but any strings in the list show up as labels.
;;
;; The list can also contain other "menu descriptions" as elements.
;; They will turn up as buttons leading to these "sub-menus". Note that
;; then the first element in each sub-menu description *must* be
;; the name of it (and not a variable or a sub-description).
;;
;; Actually, the first element in each menu-description can also be a
;; list where the first element is the name of it and the rest an
;; association list. Currently the only supported field in this
;; association list is 'hook', which should be some code to run whenever
;; the value of a variable is changed in the menu.
;;
;; To start edit a value in the menu, you click in the corresponding
;; field. If the variable 'custom-immediate-change' is non-nil, the
;; variable is assigned to the new value as soon you finish editing.
;; There is also a special construct, to allow more complex values. If
;; the field starts with a comma (,) the following expression is
;; evaluated before the assignment.
;; 
;; But that's all there is to it really.
;;
;; The rest of the code in this file is to provide a simple way
;; for packages to install their customizable variables in a global
;; custom-menu hierarchy, and to maintain the user's variable settings
;; between GWM sessions.
;;
;;
;; There are two functions intended to be used by the package developer:
;;   (custom-install-symbols <PACKAGE-NAME> <SYMBOL-LIST>)
;;   (custom-install-hook <PACKAGE-NAME> <HOOK-CODE>)
;;
;; The <PACKAGE-NAME> is either a string, or a list of strings serving
;; as a "path" through the menu hierarchy. <SYMBOL-LIST> is the list of
;; all customizable variables in the package. <HOOK-CODE> is a piece of
;; code (quoted) to run whenever any of the customizable variables
;; have changed. It is of course not necessary to provide such a hook,
;; but it is very nice to have one.
;;
;; Another thing to consider for the package developer is to make the
;; variables suitable for customization in this way. Colors and fonts
;; should *not* be stored as numbers but as the strings used when
;; creating them. The contents of variables should preferably not be
;; very long lists, since these are hard to edit. If values are cached
;; there should be some way of updating the cache when the values have
;; changed, and if values are not cached the package should not assume
;; that the contents of variables will remain the same during the whole
;; session.
;;
;;
;; There are three functions intended for the GWM user:
;;   (custom-global-menu)
;;   (custom-load-preferences)
;;   (custom-save-preferences)
;;
;; '(custom-global-menu)' brings you into the top of the custom-menu
;; hierarchy. It is most suitably called from the root menu.
;;
;; The function '(custom-load-preferences)' can be called either when
;; loading the profile, or from the 'screen-opening' hook and 
;; '(custom-save-preferences)' can be called from the 'screen-closing'
;; hook. They try to maintain a file ".customize.gwm" where the
;; variables the user have changed through the custom menus are saved.
;;

(declare-screen-dependent
    custom-font
    custom-symbol-font
    custom-value-font
    custom-value-font-slant
    custom-background
    custom-foreground
    custom-button-background
    custom-value-background
    custom-value-frame
    custom-immediate-feedback
    custom-immediate-change
    custom-keep-comma-string
    custom-preference-file
    custom-global-preferences
    custom-preferences-changed
    custom-global-menu-descr
)

;;
;;    USER CUSTOMIZABLE VARIABLES
;;    ---------------------------  
;;    Adjust these in your own profile
;;
(with (wob wob)
  (for screen (list-of-screens)
     (defaults-to
       custom-font "6x13bold"
       custom-symbol-font "6x13"
       custom-value-font "6x13"
       custom-value-font-slant 0
       custom-foreground "black"
       custom-background "lightcyan"
       custom-button-background "white"
       custom-value-background "white"
       custom-value-frame t
       custom-immediate-feedback t
       custom-immediate-change t
       custom-keep-comma-string t
)))

(with (wob wob)
  (for screen (list-of-screens)
    (setq custom-global-preferences ())
    (setq custom-preferences-changed ())
    (setq custom-global-menu-descr '("Customizations"))
))

(with (tmp (getenv "GWM_CUSTOM_FILE"))
  (if (= tmp "")
      (setq custom-preference-file "./.customize.gwm")
    (match "/" tmp)
      (setq custom-preference-file tmp)
    (setq custom-preference-file (+ "./" tmp))))

(if (not (boundp 'edit-plug-make))
    (load "edit-plug"))

(defun custom-print-to-string (val)
  (if (member (type val) '(subr fsubr expr fexpr))
      "<PROC>"
    (= (type val) 'string)
      (+ "\"" val "\"")
    (= (type val) 'list)
      (with (parts (mapfor ele val (custom-print-to-string ele))
             len (length parts)
             res (list-make (+ (* len 2) 1))
             i 0 j 1)
        (## 0 res "(")
        (while (< i len)
          (## j res (# i parts))
          (## (+ j 1) res " ")
          (setq i (+ i 1))
          (setq j (+ j 2)))
        (## (max 1 (- j 1)) res ")")
        (apply + res))
      (with-output-to-string (? val))))

(defun custom-read-from-string (_str_)
  (with (_res_ ()
         _bad_ t)
    (error-occurred 
     (if (match "^," _str_)
         (execute-string (+ "(setq _bad_ (error-occurred (setq _res_ "
                            (match "^.\\(.*\\)$" _str_ 1)
                            ")))"))
       (execute-string (+ "(setq _bad_ (error-occurred (setq _res_ (quote "
                          _str_
                          "))))"))))
    (if _bad_
        ()
      (list _res_))))
  
(defun custom-substitute-in-string (from to str)
  (with (res ()
         mlst ()
         head str)
    (while (> (length (setq mlst (match (+ "\\(.*\\)\\(" from "\\)\\(.*\\)")
                                        head 1 2 3)))
              0)
      (setq res (+ (list to (# 2 mlst)) res))
      (setq head (# 0 mlst)))
    (eval (+ '(+) (list head) res))))

;; Specialized normal state, how to start editing.
(: custom-edit-normal-state
      (state-make
	(on (button any any)
            (progn
              (send-user-event 'edit-plug-start wob)
              (edit-plug-xposition (current-event-relative-x))))
	edit-plug-normal-state
    ))

;; Specialized active state, when to stop or abort editing.
(: custom-edit-active-state
      (state-make
	(on focus-out
	    (send-user-event 'edit-plug-done wob))
	edit-plug-active-state
    ))

;; Things to do when starting editing.
(defun custom-start-edit ()
    (set-focus wob)
    (process-events))  ; To let the focus change take effect before the
                       ; fsm enters the active state. Otherwise the fsm
                       ; may immediately leave the active state again,
                       ; due to some spurious focus-out event.

;; Things to do when editing is done.
(defun custom-end-edit ()
  (with (menuwob (custom-top-parent)
         immch (# 'immch menuwob)
         sym (# 'symbol wob)
         str (edit-plug-get)
         comma (and custom-keep-comma-string (match "^," str))
         res (custom-read-from-string str))
    (if res
        (if (not (equal (# 2 sym) (if comma str res)))
            (progn
              (if (not (# 3 sym))
                  (## 3 sym (or (# 2 sym) t)))
              (## 2 sym (if comma str res))
              (if immch
                  (progn
                    (set (# 0 sym) (# 0 res))
                    (## 4 sym t)
                    (eval (# 'hook wob))))
              (if (and (not comma) custom-immediate-feedback)
                  (edit-plug-change (custom-print-to-string (# 0 res))))))
      (if custom-immediate-feedback
          (edit-plug-change (if (# 2 sym)
                                (if (= (type (# 2 sym)) 'string)
                                    (# 2 sym)
                                  (custom-print-to-string (# 0 (# 2 sym))))
                              ""))))))

(defun custom-abort-edit ()
    ())

(defun custom-plug-separator (size)
  (with (fsm ()
         foreground background)
    (plug-make (pixmap-make size 1))))

(defun custom-symbol-plug (label)
  (with (fsm ()
         font (if (= (type custom-symbol-font) 'number)
                  custom-symbol-font
                (font-make custom-symbol-font))
         label-horizontal-margin 2
         label-vertical-margin 2)
    (plug-make (label-make label))))

(defun custom-label-plug (label)
  (with (fsm ()
         font (if (= (type custom-font) 'number)
                  custom-font
                (font-make custom-font))
         label-horizontal-margin 2
         label-vertical-margin 2)
    (plug-make (label-make label))))

(defun custom-value-plug (item)
  (with (font (if (= (type custom-value-font) 'number)
                  custom-value-font
                (font-make custom-value-font))
         font-slant (or custom-value-font-slant 0)
         label-horizontal-margin 4
         label-vertical-margin 1
         edit-plug-start-hook '(custom-start-edit)
         edit-plug-done-hook '(custom-end-edit)
         edit-plug-abort-hook '(custom-abort-edit)
         edit-plug-normal-state custom-edit-normal-state
         edit-plug-active-state custom-edit-active-state
         property (+ (list 'symbol item 'hook hook) property))
    (with (borderwidth (if custom-value-frame 1 0)
           borderpixel foreground)
      (bar-make 
        (with (borderwidth 0)
          (bar-make
            (with (borderwidth 1
                   background (if custom-value-background
                                  (if (= (type custom-value-background)
                                         'number)
                                      custom-value-background
                                    (color-make custom-value-background))
                                background)
                   borderpixel background
                   active-borderpixel foreground)
              (edit-plug-make (if (# 2 item)
                                (if (= (type (# 2 item)) 'string)
                                    (# 2 item)
                                  (custom-print-to-string (# 0 (# 2 item))))
                                "")))))))))

(defun custom-button-plug (label action)
  (with (font (if (= (type custom-font) 'number)
                  custom-font
                (font-make custom-font))
         outer-background background
         background (if custom-button-background
                        (if (= (type custom-button-background) 'number)
                            custom-button-background
                          (color-make custom-button-background))
                      background)
         label-horizontal-margin 5
         label-vertical-margin 3
         pix (label-make label)
         dim (dimensions pix))
    (draw-rectangle pix 2 2 (- (# 2 dim) 4) (- (# 3 dim) 4) 2 1)
    (with (foreground outer-background)
      (draw-line pix 0 0 0 1)
      (draw-line pix 1 0 1 0)
      (draw-line pix 0 (- (# 3 dim) 2) 0 (- (# 3 dim) 1))
      (draw-line pix 1 (- (# 3 dim) 1) 1 (- (# 3 dim) 1))
      (draw-line pix (- (# 2 dim) 1) 0 (- (# 2 dim) 1) 1)
      (draw-line pix (- (# 2 dim) 2) 0 (- (# 2 dim) 2) 0)
      (draw-line pix (- (# 2 dim) 1) (- (# 3 dim) 2)
                     (- (# 2 dim) 1) (- (# 3 dim) 1))
      (draw-line pix (- (# 2 dim) 2) (- (# 3 dim) 1)
                     (- (# 2 dim) 2) (- (# 3 dim) 1)))
    (draw-line pix 2 2 2 2)
    (draw-line pix (- (# 2 dim) 3) 2 (- (# 2 dim) 3) 2)
    (draw-line pix 2 (- (# 3 dim) 3) 2 (- (# 3 dim) 3))
    (draw-line pix (- (# 2 dim) 3) (- (# 3 dim) 3)
                   (- (# 2 dim) 3) (- (# 3 dim) 3))
    (with (fsm (fsm-make (state-make (on-eval '(button any any) action))))
      (plug-make pix))))

(defun custom-top-parent ()
  (with (wob wob
         par wob-parent)
    (while (not (= par window))
      (setq wob wob-parent)
      (setq par wob-parent))
    wob))

(defun custom-done ()
  (with (menuwob (custom-top-parent)
         descr (# 'descr menuwob)
         hook (# 'hook menuwob)
         change ()
         res ())
    (set-focus)
    (process-events)
    (for sym descr
      (if (and (# 0 sym) (# 2 sym) (# 3 sym) (not (# 4 sym)))
          (if (= (type (# 2 sym)) 'string)
              (if (setq res (custom-read-from-string (# 2 sym)))
                  (progn 
                    (set (# 0 sym) (# 0 res))
                    (setq change t)))
            (progn
              (set (# 0 sym) (# 0 (# 2 sym)))
              (setq change t)))))
    (if change
        (with (custom-refresh-menu ())
          (eval hook)))
    (for sym descr
      (if (and (# 0 sym) (# 2 sym) (# 3 sym))
          (custom-put-preference (# 0 sym) (# 2 sym))))
    (if (window-is-valid window)
        (delete-window window))))

(defun custom-cancel ()
  (with (menuwob (custom-top-parent)
         descr (# 'descr menuwob)
         hook (# 'hook menuwob)
         change ()
         res ())
    (set-focus)
    (process-events)
    (for sym descr
      (if (and (# 0 sym) (# 3 sym) (# 4 sym))
          (if (= (# 3 sym) t)
              (unbind (# 0 sym))
            (= (type (# 3 sym)) 'string)
              (if (setq res (custom-read-from-string (# 3 sym)))
                  (progn 
                    (set (# 0 sym) (# 0 res))
                    (setq change t)))
            (progn
              (set (# 0 sym) (# 0 (# 3 sym)))
              (setq change t)))))
    (if change
        (with (custom-refresh-menu ())
          (eval hook)))
    (delete-window window)))

(defun custom-construct-menu (descr)
  (with (fsm ()
         bordertile ()
         bar-max-width 3000
         bar-min-width 0
         background (if (= (type custom-background) 'number)
                        custom-background
                      (color-make custom-background))
         foreground (if (= (type custom-foreground) 'number)
                        custom-foreground
                      (color-make custom-foreground))
         borderpixel background
         borderwidth 1
         direction vertical
         align-column 0
         bar-list ()
         hook (if (not (# 0 (# 0 descr))) (# 'hook (# 2 (# 0 descr)))))
    (with (m 0 sz ())
      (for ele descr
        (if (# 0 ele)
            (with (font (if (= (type custom-symbol-font) 'number)
                            custom-symbol-font
                          (font-make custom-symbol-font)))
              (setq sz (# 2 (dimensions (+ (# 0 ele)))))
              (## 1 ele sz)
              (setq m (max m sz)))))
      (setq align-column (+ m 2)))
    (setq bar-list 
          (mapfor ele descr
            (if (# 0 ele)
                (bar-make 
                  (custom-plug-separator 5)
                  (custom-symbol-plug (# 0 ele))
                  (custom-plug-separator (- align-column (# 1 ele)))
                  (custom-value-plug ele)
                  (custom-plug-separator 10))
              (= (type (# 1 ele)) 'list)
                (bar-make  
                  (custom-plug-separator 10)
                  (custom-button-plug (if (= (type (# 0 (# 1 ele))) 'string)
                                          (# 0 (# 1 ele))
                                        (# 0 (# 0 (# 1 ele))))
                                      (list 'custom-menu
                                            (list 'quote (# 1 ele))
                                            '(+ window-x 15)
                                            '(+ window-y 15))))
              (= (type (# 1 ele)) 'string)
                (bar-make
                  (custom-plug-separator 10)
                  (custom-label-plug (# 1 ele))))))
    (setq bar-list (+ bar-list
                      (list (bar-make 
                             (custom-plug-separator 10)
                             (custom-button-plug "Done" '(custom-done))
                             (custom-plug-separator 15)
                             (custom-button-plug "Cancel" '(custom-cancel))
                             (custom-plug-separator 10)))))
    (with (property (+ (list 'descr descr
                             'hook hook
                             'immch custom-immediate-change) property)
           borderpixel foreground)
      (apply menu-make bar-list))))

(defun custom-menu _args_
  (with (descr (mapfor _cele_ (# 0 _args_)
                 (if (not (member (type _cele_) '(string list)))
                     (list _cele_ ()
                           (or (with (res (custom-get-preference _cele_))
                                 (if (and res (= (type res) 'string)) res ()))
                               (if (boundp _cele_) (list (eval _cele_)) ()))
                           () ())
                   (list () _cele_)))
         xpos (if (= (type (# 1 _args_)) 'number) (# 1 _args_))
         ypos (if (= (type (# 2 _args_)) 'number) (# 2 _args_))
         mn ())
    (if (and (not (# 0 (# 0 descr)))
             (= (type (# 1 (# 0 descr))) 'list)
             (= (type (# 0 (# 1 (# 0 descr)))) 'string))
        (## 0 descr (list ()
                          (# 0 (# 1 (# 0 descr)))
                          (sublist 1 (length (# 1 (# 0 descr)))
                                   (# 1 (# 0 descr))))))
    (setq mn (custom-construct-menu descr))
    (if mn
        (with (reenter-on-opening ()
               dim (dimensions mn)
               x (max 10 (/ (- screen-width (# 2 dim)) 3))
               y (max 10 (/ (- screen-height (# 3 dim)) 3)))
          (if (and xpos ypos)
              (place-menu 'custom mn xpos ypos)
            (place-menu 'custom mn x y))))))

(defun custom-refresh-menu ()
  (with (menuwob (custom-top-parent)
         descr (# 'descr menuwob)
         hook (# 'hook menuwob)
         immch (# 'immch menuwob)
         xpos (+ window-x window-client-x
                 (with (wob window) wob-borderwidth)
                 (with (wob menuwob) (- wob-borderwidth))
                 window-client-borderwidth)
         ypos (+ window-y window-client-y
                 (with (wob window) wob-borderwidth)
                 (with (wob menuwob) (- wob-borderwidth))
                 window-client-borderwidth)
         mn ())
    (if (not (= immch custom-immediate-change))
        (for sym descr
          (if (and (# 0 sym) (# 4 sym))
              (## 4 sym ()))))
    (setq mn (custom-construct-menu descr))
    (if mn
        (with (reenter-on-opening ())
          (delete-window window)
          (place-menu 'custom mn xpos ypos)))))

(defun custom-save-file (file lst)
  (with-output-to-file (+ file ":")
    (? "(setq tmp '(\n")
    (with (i 0)
      (while (< i (length lst))
        (? (custom-print-to-string (# i lst)) " "
           (custom-print-to-string 
            (if (= (type (# (+ i 1) lst)) 'string)
                (custom-substitute-in-string "\"" "\\\""
                                             (# (+ i 1) lst))
              (# (+ i 1) lst)))
           "\n")
        (setq i (+ i 2))))
    (? "))\n"))
  (! "sh" "-c" (+ "mv " file " " file "~ ; mv " file ": " file " ;"))
)

(defun custom-read-file (file)
  (with (tmp 'magicatom)
    (error-occurred
      (load file))
    (if (= tmp 'magicatom)
        ()
      tmp)))

(defun custom-put-preference (var val)
  (if (member var custom-global-preferences)
      (## var custom-global-preferences val)
    (setq custom-global-preferences
          (+ custom-global-preferences
             (list var val))))
  (setq custom-preferences-changed t))

(defun custom-get-preference (var)
  (# var custom-global-preferences))

(defun custom-apply-preferences (prefs)
  (with (len (length prefs)
         i 0
         res ())
    (while (< i len)
      (if (= (type (# (+ i 1) prefs)) 'string)
          (if (setq res (custom-read-from-string (# (+ i 1) prefs)))
              (set (# i prefs) (# 0 res)))
        (set (# i prefs) (# 0 (# (+ i 1) prefs))))
      (setq i (+ i 2)))))

(defun custom-save-preferences ()
  (if custom-preferences-changed
      (custom-save-file custom-preference-file custom-global-preferences))
  (setq custom-preferences-changed ()))

(defun custom-load-preferences ()
  (setq custom-global-preferences (custom-read-file custom-preference-file))
  (custom-apply-preferences custom-global-preferences)
  (setq custom-preferences-changed ()))

(defun custom-insert-symbols (lst syms)
  (with (len (length lst)
         i (- len 1)
         syms (copy syms)
         s1 ()
         s2 ())
    (while (and (> i 0) (= (type (# i lst)) 'list))
      (setq i (- i 1)))
    (setq s1 (sublist 0 (+ i 1) lst))
    (setq s2 (sublist (+ i 1) len lst))
    (setq i (- (length syms) 1))
    (while (> i -1)
      (if (member (# i syms) s1)
          (delete-nth i syms))
      (setq i (- i 1)))
    (+ s1 syms s2)))

(defun custom-insert-hook (head hook)
  (if (not (= (type head) 'list))
      (list head 'hook hook)
    (not (member 'hook head))
      (+ head (list 'hook hook))
      (with (oh (# 'hook head))
        (## 'hook head
            (if (not oh)
                hook
              (and (= (type oh) 'list) (= (# 0 oh) 'progn))
                (+ oh (list hook))
                (list 'progn oh hook))))))

(defun custom-find-name (lst name)
  (with (len (length lst)
         i 1)
    (while (and (< i len)
                (not (and (= (type (# i lst)) 'list)
                          (or (and (= (type (# 0 (# i lst))) 'string)
                                   (= (# 0 (# i lst)) name))
                              (and (= (type (# 0 (# i lst))) 'list)
                                   (= (type (# 0 (# 0 (# i lst)))) 'string)
                                   (= (# 0 (# 0 (# i lst))) name))))))
      (setq i (+ i 1)))
    (if (< i len)
        i
      ())))

(defun custom-install-symbols (name syms)
  (if (not name)
      (setq custom-global-menu-descr
            (custom-insert-symbols custom-global-menu-descr syms))
    (with (name (if (not (= (type name) 'list)) (list name) name)
           len (length name)
           i 0
           lst1 ()
           pos1 ()
           lst custom-global-menu-descr
           pos ())
      (while (< i len)
        (setq pos (custom-find-name lst (# i name)))
        (if (not pos)
            (progn
              (if pos1
                  (## pos1 lst1 (setq lst (+ lst (list (list (# i name))))))
                (setq lst (setq custom-global-menu-descr
                                (+ custom-global-menu-descr
                                   (list (list (# i name)))))))
              (setq pos (- (length lst) 1))))
        (setq pos1 pos)
        (setq lst1 lst)
        (setq lst (# pos1 lst1))
        (setq i (+ i 1)))
      (## pos1 lst1 (custom-insert-symbols lst syms)))))

(defun custom-install-hook (name hook)
  (if (not name)
      (## 0 custom-global-menu-descr
          (custom-insert-hook (# 0 custom-global-menu-descr) hook))
    (with (name (if (not (= (type name) 'list)) (list name) name)
           len (length name)
           i 0
           lst1 ()
           pos1 ()
           lst custom-global-menu-descr
           pos ())
      (while (< i len)
        (setq pos (custom-find-name lst (# i name)))
        (if (not pos)
            (progn
              (if pos1
                  (## pos1 lst1 (setq lst (+ lst (list (list (# i name))))))
                (setq lst (setq custom-global-menu-descr
                                (+ custom-global-menu-descr
                                   (list (list (# i name)))))))
              (setq pos (- (length lst) 1))))
        (setq pos1 pos)
        (setq lst1 lst)
        (setq lst (# pos1 lst1))
        (setq i (+ i 1)))
      (## 0 lst (custom-insert-hook (# 0 lst) hook)))))

(defun custom-global-menu ()
  (custom-menu custom-global-menu-descr))


;; Now, let this package install itself
(custom-install-symbols "custom-menu"
  '(custom-font
    custom-symbol-font
    custom-value-font
    custom-value-font-slant
    custom-foreground
    custom-background
    custom-button-background
    custom-value-background
    custom-value-frame
    custom-immediate-feedback
    custom-immediate-change
    custom-keep-comma-string)
)

(custom-install-hook "custom-menu" '(custom-refresh-menu))

