Search code examples
macrosclojurequoting

Forcing an argument in a Clojure macro to get namespace-captured


I am working on a Clojure macro to help build GridBagLayout-based JPanels. I can get Java classes in a defaults map inside the macro to namespace-qualify, but not those passed in as arguments. What magic combination of backquotes, quotes, tildas, or something else do I need?

(import [java.awt GridBagConstraints GridBagLayout Insets]
        [javax.swing JButton JPanel])

(defmacro make-constraints [gridx gridy & constraints]
  (let [defaults
        {:gridwidth 1 :gridheight 1 :weightx 0 :weighty 0
         :anchor 'GridBagConstraints/WEST :fill 'GridBagConstraints/NONE
         :insets `(Insets. 5 5 5 5) :ipadx 0 :ipady 0}

        values
        (assoc (merge defaults (apply hash-map constraints))
          :gridx gridx :gridy gridy)]
    `(GridBagConstraints. ~@(map (fn [value]
                                   (if
                                    (or
                                     (number? value)
                                     (string? value)
                                     (char? value)
                                     (true? value)
                                     (false? value)
                                     (nil? value))
                                    value
                                    `~value))
                                 (map values
                                      [:gridx :gridy :gridwidth :gridheight
                                       :weightx :weighty :anchor :fill
                                       :insets :ipadx :ipady])))))

When I use the Insets defined in the defaults map, it gets qualified (not "symbol-captured") as (java.awt.Insets ...):

user=> (macroexpand-1 '(make-constraints 0 0 :weightx 1))
(java.awt.GridBagConstraints.
 0 0 1 1 1 0
 GridBagConstraints/WEST GridBagConstraints/NONE
 (java.awt.Insets. 5 5 5 5) 0 0)

but when I pass it as an argument, it does not:

user=> (macroexpand-1 '(make-constraints 1 1 :insets (Insets. 2 2 2 2)))
(java.awt.GridBagConstraints.
 1 1 1 1 0 0
 GridBagConstraints/WEST GridBagConstraints/NONE
 (Insets. 2 2 2 2) 0 0)

I'm not just trying to be a stickler. I am getting compiler errors that it cannot find a proper GridBagConstraints constructor.


Solution

  • Here is my solution. I am using it in a Swing application that I am writing. It has already saved me many lines of code writing (for two different panels) and will be as fast as hand-written code.

    (defmacro grid-bag-container [container & args]
      "Fill and return a java.awt.Container that uses the GridBagLayout.
      The macro defines a set of default constraints for the GridBagConstraints:
        :gridwidth 1
        :gridheight 1
        :weightx 0
        :weighty 0
        :anchor :WEST
        :fill :NONE
        :insets (Insets. 5 5 5 5)
        :ipadx 0
        :ipady 0
      These defaults can be overridden in the call to the macro in two way:
        - If the first argument is a hash-map of constraint names and values
          (e.g.: {:weightx 1}), these will override the defaults for the
          entire container.
        - Each individual item (see below) can override the global defaults
          and container defaults for itself.
      The constraints consist of constraint name (as a keyword with the same
      name as the GridBagConstraints field), and a value, which can also be
      a keyword, in which case the appropriate constant from GridBagConstraints
      will be substituted (e.g.: :NONE == GridBagConstraints.NONE), or the value
      can be an expression (e.g.: 0 or (Insets. 2 2 2 2)).
      Following the optional container default overrides hash-map are one or
      more row specification vectors. Each vector represents one row and
      increments gridy (starting from 0). Each vector contains one or more
      item vectors representing the individual components to be added to the
      container. Each item vector has the component as its first value,
      followed by zero or more constraint overrides as keyword-value pairs.
      (e.g.: [myButton :gridwidth 2 :weightx 1]). The values may be keywords
      and are expanded to GridBagConstraints constants as described above.
      Each item vector gets the next value of gridx (starting with 0) in that
      row.
      For example:
        (grid-bag-container panel
          {:insets (Insets. 1 1 1 1)}
          [[button :gridwidth 2 :weightx 1.0 :fill :HORIZONTAL]]
          [[check-box :gridwidth 2 :weightx 1.0 :anchor :CENTER]]
          [[arrive-label] [arrive-text-field :fill :HORIZONTAL]]
          [[depart-label] [depart-text-field :fill :HORIZONTAL]])
      will expand to the hand-written equivalent:
        (doto panel
          (.add button
            (GridBagConstraints. 0 0 2 1 1.0 0  ; gridx: 0 gridy: 1
                                 GridBagConstraints/WEST
                                 GridBagConstraints/HORIZONTAL
                                 (Insets. 1 1 1 1) 0 0))
          (.add check-box
            (GridBagConstraints. 0 1 2 1 1.0 0  ; gridx: 0 gridy: 1
                                 GridBagConstraints/CENTER
                                 GridBagConstraints/NONE
                                 (Insets. 1 1 1 1) 0 0))
          (.add arrive-label
            (GridBagConstraints. 0 2 1 1 0 0    ; gridx: 0 gridy: 2
                                 GridBagConstraints/WEST
                                 GridBagConstraints/NONE
                                 (Insets. 1 1 1 1) 0 0))
          (.add arrive-text-field
            (GridBagConstraints. 1 2 1 1 0 0    ; gridx: 1 gridy: 2
                                 GridBagConstraints/WEST
                                 GridBagConstraints/HORIZONTAL
                                 (Insets. 1 1 1 1) 0 0))
          (.add depart-label
            (GridBagConstraints. 0 3 1 1 0 0    ; gridx: 0 gridy: 3
                                 GridBagConstraints/WEST
                                 GridBagConstraints/NONE
                                 (Insets. 1 1 1 1) 0 0))
          (.add depart-text-field
            (GridBagConstraints. 1 3 1 1 0 0    ; gridx: 1 gridy: 3
                                 GridBagConstraints/WEST
                                 GridBagConstraints/HORIZONTAL
                                 (Insets. 1 1 1 1) 0 0))
      @param container the java.awt.Container to fill
      @param args the components and GridBagContraints speicifcations
      @returns the filled Container"
      (let [global-defaults
            {:gridwidth 1
             :gridheight 1
             :weightx 0
             :weighty 0
             :anchor :WEST
             :fill :NONE
             :insets `(Insets. 5 5 5 5)
             :ipadx 0
             :ipady 0}
    
            [defaults rows]
            (if (map? (first args))
              [(into global-defaults (first args)) (rest args)]
              [global-defaults args])]
        `(doto ~container
          ~@(loop [gridy 0 rows rows ret []]
            (if (seq rows)
              (recur (inc gridy) (rest rows)
                (into ret
                  (let [row (first rows)]
                    (loop [gridx 0 row row ret []]
                      (if (seq row)
                        (recur (inc gridx) (rest row)
                          (conj ret
                            (let [item
                                  (first row)
    
                                  component
                                  (first item)
    
                                  constraints
                                  (assoc (merge defaults
                                                (apply hash-map (rest item)))
                                    :gridx gridx :gridy gridy)
    
                                  constraint-values
                                  (map (fn [value]
                                    (if (keyword? value)
                                      `(. GridBagConstraints
                                          ~(symbol (name value)))
                                      `~value))
                                    (map constraints
                                      [:gridx :gridy :gridwidth :gridheight
                                       :weightx :weighty :anchor :fill
                                       :insets :ipadx :ipady]))]
                              `(.add ~component (new GridBagConstraints
                                                     ~@constraint-values)))))
                        ret)))))
              ret)))))
    

    Thanks to amalloy, user100464, and kotarak for the help.