/*
 * @(#)Metacity.java	1.23 04/08/31
 *
 * Copyright 2004 Sun Microsystems, Inc. All rights reserved.
 * SUN PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
 */
package com.sun.java.swing.plaf.gtk;

import com.sun.java.swing.SwingUtilities2;

import javax.swing.plaf.synth.*;

import java.awt.*;
import java.awt.geom.*;
import java.awt.image.*;
import java.io.*;
import java.net.*;
import java.security.*;
import java.util.*;

import javax.swing.*;
import javax.swing.border.*;

import javax.xml.parsers.*;
import org.xml.sax.SAXException;
import org.w3c.dom.*;

/**
 * @version 1.23, 08/31/04
 */
class Metacity implements SynthConstants {
    // Tutorial:
    // http://developer.gnome.org/doc/tutorials/metacity/metacity-themes.html

    // Themes:
    // http://art.gnome.org/theme_list.php?category=metacity

    static Metacity INSTANCE;

    private static final String[] themeNames = {
	getUserTheme(),
        "blueprint",
	"Bluecurve",
	"Crux",
	"SwingFallbackTheme"
    };


    static {
	for (String themeName : themeNames) {
	    if (themeName != null) {
		try {
		    INSTANCE = new Metacity(themeName);
		} catch (FileNotFoundException ex) {
		} catch (IOException ex) {
		    logError(themeName, ex);
		} catch (ParserConfigurationException ex) {
		    logError(themeName, ex);
		} catch (SAXException ex) {
		    logError(themeName, ex);
		}
	    }
	    if (INSTANCE != null) {
		break;
	    }
	}
	if (INSTANCE == null) {
	    throw new Error("Could not find any installed metacity theme, and fallback failed");
	}
    }

    private static boolean errorLogged = false;
    private static DocumentBuilder documentBuilder;
    private static Document xmlDoc;
    private static String userHome;

    private Node frame_style_set;
    private Map<String, Object> frameGeometry;
    private Map<String, Map<String, Object>> frameGeometries;

    private LayoutManager titlePaneLayout = new TitlePaneLayout();

    private ColorizeImageFilter imageFilter = new ColorizeImageFilter();
    private URL themeDir = null;
    private SynthContext context;
    private String themeName;

    private ArithmeticExpressionEvaluator aee = new ArithmeticExpressionEvaluator();
    private Map<String, Integer> variables;

    // Reusable clip shape object
    private RoundRectClipShape roundedClipShape;

    protected Metacity(String themeName) throws IOException, ParserConfigurationException, SAXException {
	this.themeName = themeName;
	themeDir = getThemeDir(themeName);
	if (themeDir != null) {
	    URL themeURL = new URL(themeDir, "metacity-theme-1.xml");
	    xmlDoc = getXMLDoc(themeURL);
	    if (xmlDoc == null) {
		throw new IOException(themeURL.toString());
	    }
	} else {
	    throw new FileNotFoundException(themeName);
	}

	// Initialize constants
	variables = new HashMap();
	NodeList nodes = xmlDoc.getElementsByTagName("constant");
	int n = nodes.getLength();
	for (int i = 0; i < n; i++) {
	    Node node = nodes.item(i);
	    String name = getStringAttr(node, "name");
	    if (name != null) {
		String value = getStringAttr(node, "value");
		if (value != null) {
		    try {
			variables.put(name, Integer.parseInt(value));
		    } catch (NumberFormatException ex) {
			logError(themeName, ex);
			// Ignore bad value
		    }
		}
	    }
	}

	// Cache frame geometries
	frameGeometries = new HashMap();
	nodes = xmlDoc.getElementsByTagName("frame_geometry");
	n = nodes.getLength();
	for (int i = 0; i < n; i++) {
	    Node node = nodes.item(i);
	    String name = getStringAttr(node, "name");
	    if (name != null) {
		HashMap<String, Object> gm = new HashMap();
		frameGeometries.put(name, gm);

		String parentGM = getStringAttr(node, "parent");
		if (parentGM != null) {
		    gm.putAll(frameGeometries.get(parentGM));
		}

		gm.put("has_title",
		       Boolean.valueOf(getBooleanAttr(node, "has_title",            true)));
		gm.put("rounded_top_left",
		       Boolean.valueOf(getBooleanAttr(node, "rounded_top_left",     false)));
		gm.put("rounded_top_right",
		       Boolean.valueOf(getBooleanAttr(node, "rounded_top_right",    false)));
		gm.put("rounded_bottom_left",
		       Boolean.valueOf(getBooleanAttr(node, "rounded_bottom_left",  false)));
		gm.put("rounded_bottom_right",
		       Boolean.valueOf(getBooleanAttr(node, "rounded_bottom_right", false)));
		
		NodeList childNodes = node.getChildNodes();
		int nc = childNodes.getLength();
		for (int j = 0; j < nc; j++) {
		    Node child = childNodes.item(j);
		    if (child.getNodeType() == Node.ELEMENT_NODE) {
			name = child.getNodeName();
			Object value = null;
			if ("distance".equals(name)) {
			    value = new Integer(getIntAttr(child, "value", 0));
			} else if ("border".equals(name)) {	
			    value = new Insets(getIntAttr(child, "top", 0),
					       getIntAttr(child, "left", 0),
					       getIntAttr(child, "bottom", 0),
					       getIntAttr(child, "right", 0));
			} else if ("aspect_ratio".equals(name)) {
			    value = new Float(getFloatAttr(child, "value", 1.0F));
			} else {
			    logError(themeName, "Unknown Metacity frame geometry value type: "+name);
			}
			String childName = getStringAttr(child, "name");
			if (childName != null && value != null) {
			    gm.put(childName, value);
			}
		    }
		}
	    }
	}
	frameGeometry = frameGeometries.get("normal");
    }


    public static LayoutManager getTitlePaneLayout() {
	return INSTANCE.titlePaneLayout;
    }

    private Shape getRoundedClipShape(int x, int y, int w, int h,
				      int arcw, int arch, int corners) {
	if (roundedClipShape == null) {
	    roundedClipShape = new RoundRectClipShape();
	}
	roundedClipShape.setRoundedRect(x, y, w, h, arcw, arch, corners);

	return roundedClipShape;
    }

    void paintButtonBackground(SynthContext context, Graphics g, int x, int y, int w, int h) {
	this.context = context;
	JButton button = (JButton)context.getComponent();
	String buttonName = button.getName();
	int buttonState = context.getComponentState();

	JComponent titlePane = (JComponent)button.getParent();
	Container titlePaneParent = titlePane.getParent();

	JInternalFrame jif;
	if (titlePaneParent instanceof JInternalFrame) {
	    jif = (JInternalFrame)titlePaneParent;
	} else if (titlePaneParent instanceof JInternalFrame.JDesktopIcon) {
	    jif = ((JInternalFrame.JDesktopIcon)titlePaneParent).getInternalFrame();
	} else {
	    return;
	}

	boolean active = jif.isSelected();
	button.setOpaque(false);

	String state = "normal";
	if ((buttonState & PRESSED) != 0) {
	    state = "pressed";
	} else if ((buttonState & MOUSE_OVER) != 0) {
	    state = "prelight";
	}

	String function = null;
	String location = null;
	boolean left_corner  = false;
	boolean right_corner = false;


	if (buttonName == "InternalFrameTitlePane.menuButton") {
	    function = "menu";
	    location = "left_left";
	    left_corner = true;
	} else if (buttonName == "InternalFrameTitlePane.iconifyButton") {
	    function = "minimize";
	    int nButtons = ((jif.isIconifiable() ? 1 : 0) +
			    (jif.isMaximizable() ? 1 : 0) +
			    (jif.isClosable() ? 1 : 0));
	    right_corner = (nButtons == 1);
	    switch (nButtons) {
	      case 1: location = "right_right"; break;
	      case 2: location = "right_middle"; break;
	      case 3: location = "right_left"; break;
	    }
	} else if (buttonName == "InternalFrameTitlePane.maximizeButton") {
	    function = "maximize";
	    right_corner = !jif.isClosable();
	    location = jif.isClosable() ? "right_middle" : "right_right";
	} else if (buttonName == "InternalFrameTitlePane.closeButton") {
	    function = "close";
	    right_corner = true;
	    location = "right_right";
	}

	Node frame = getNode(frame_style_set, "frame", new String[] {
	    "focus", (active ? "yes" : "no"),
	    "state", (jif.isMaximum() ? "maximized" : "normal")
	});

	if (function != null && frame != null) {
	    Node frame_style = getNode("frame_style", new String[] {
		"name", getStringAttr(frame, "style")
	    });
	    if (frame_style != null) {
		setFrameGeometry(titlePane,
				 frameGeometries.get(getStringAttr(frame_style, "geometry")));


		Shape oldClip = g.getClip();
		if ((right_corner && getBoolean("rounded_top_right", false)) ||
		    (left_corner  && getBoolean("rounded_top_left", false))) { 

		    Point buttonLoc = button.getLocation();
		    if (right_corner) { 
			g.setClip(getRoundedClipShape(0, 0, w, h,
						      12, 12, RoundRectClipShape.TOP_RIGHT));
		    } else {
			g.setClip(getRoundedClipShape(0, 0, w, h,
						      11, 11, RoundRectClipShape.TOP_LEFT));
		    }
		}
		drawButton(frame_style, location+"_background", state, g, w, h, jif);
		drawButton(frame_style, function, state, g, w, h, jif);
		g.setClip(oldClip);
	    }
	}
    }

    protected void drawButton(Node frame_style, String function, String state,
			    Graphics g, int w, int h, JInternalFrame jif) {
	Node buttonNode = getNode(frame_style, "button",
				  new String[] { "function", function, "state", state });
	if (buttonNode == null && !state.equals("normal")) {
	    buttonNode = getNode(frame_style, "button",
				 new String[] { "function", function, "state", "normal" });
	}
	if (buttonNode != null) {
	    Node draw_ops;
	    String draw_ops_name = getStringAttr(buttonNode, "draw_ops");
	    if (draw_ops_name != null) {
		draw_ops = getNode("draw_ops", new String[] { "name", draw_ops_name });
	    } else {
		draw_ops = getNode(buttonNode, "draw_ops", null);
	    }
	    variables.put("width",  w);
	    variables.put("height", h);
	    draw(draw_ops, g, jif);
	}
    }

    void paintFrameBorder(SynthContext context, Graphics g, int x0, int y0, int width, int height) {
	this.context = context;
	JComponent comp = context.getComponent();
	JComponent titlePane = findChild(comp, "InternalFrame.northPane");

	if (titlePane == null) {
	    return;
	}

        JInternalFrame jif = null;
        if (comp instanceof JInternalFrame) {
            jif = (JInternalFrame)comp;
	} else if (comp instanceof JInternalFrame.JDesktopIcon) {
	    jif = ((JInternalFrame.JDesktopIcon)comp).getInternalFrame();
	} else {
	    return;
        }

	boolean active = jif.isSelected();
	Font oldFont = g.getFont();
	g.setFont(titlePane.getFont());
	g.translate(x0, y0);

	Rectangle titleRect = calculateTitleArea(jif);
	JComponent menuButton = findChild(titlePane, "InternalFrameTitlePane.menuButton");

	Icon frameIcon = jif.getFrameIcon();
	variables.put("mini_icon_width",
		      (frameIcon != null) ? frameIcon.getIconWidth()  : 0);
	variables.put("mini_icon_height",
		      (frameIcon != null) ? frameIcon.getIconHeight() : 0);
	variables.put("title_width",  calculateTitleTextWidth(g, jif));
	FontMetrics fm = SwingUtilities2.getFontMetrics(jif, g);
	variables.put("title_height", fm.getAscent() + fm.getDescent());

	// These don't seem to apply here, but the Galaxy theme uses them. Not sure why.
	variables.put("icon_width",  32);
	variables.put("icon_height", 32);


	if (frame_style_set == null) {
	    frame_style_set = getNode("frame_style_set", new String[] { "name", "normal" });
	}

	if (frame_style_set != null) {
	    Node frame = getNode(frame_style_set, "frame", new String[] {
		"focus", (active ? "yes" : "no"),
		"state", (jif.isMaximum() ? "maximized" : "normal")
	    });

	    if (frame != null) {
		Node frame_style = getNode("frame_style", new String[] {
		    "name", getStringAttr(frame, "style")
		});
		if (frame_style != null) {
		    Map gm = frameGeometries.get(getStringAttr(frame_style, "geometry"));
		    setFrameGeometry(titlePane, gm);
		    Shape oldClip = g.getClip();
		    boolean roundTopLeft     = getBoolean("rounded_top_left",     false);
		    boolean roundTopRight    = getBoolean("rounded_top_right",    false);
		    boolean roundBottomLeft  = getBoolean("rounded_bottom_left",  false);
		    boolean roundBottomRight = getBoolean("rounded_bottom_right", false);

		    if (roundTopLeft || roundTopRight || roundBottomLeft || roundBottomRight) {
			jif.setOpaque(false);

			g.setClip(getRoundedClipShape(0, 0, width, height, 12, 12,
					(roundTopLeft     ? RoundRectClipShape.TOP_LEFT     : 0) |
					(roundTopRight    ? RoundRectClipShape.TOP_RIGHT    : 0) |
					(roundBottomLeft  ? RoundRectClipShape.BOTTOM_LEFT  : 0) |
					(roundBottomRight ? RoundRectClipShape.BOTTOM_RIGHT : 0)));
		    }

		    int titleHeight = titlePane.getHeight();

		    boolean minimized = jif.isIcon();
		    Insets insets = getBorderInsets(context, null);

		    int leftTitlebarEdge   = getInt("left_titlebar_edge");
		    int rightTitlebarEdge  = getInt("right_titlebar_edge");
		    int topTitlebarEdge    = getInt("top_titlebar_edge");
		    int bottomTitlebarEdge = getInt("bottom_titlebar_edge");

		    if (!minimized) {
			drawPiece(frame_style, g, "entire_background",
				  0, 0, width, height, jif);
		    }
		    drawPiece(frame_style, g, "titlebar",
			      0, 0, width, titleHeight, jif);
		    drawPiece(frame_style, g, "titlebar_middle",
			      leftTitlebarEdge, topTitlebarEdge,
			      width - leftTitlebarEdge - rightTitlebarEdge,
			      titleHeight - topTitlebarEdge - bottomTitlebarEdge,
			      jif);
		    drawPiece(frame_style, g, "left_titlebar_edge",
			      0, 0, leftTitlebarEdge, titleHeight, jif);
		    drawPiece(frame_style, g, "right_titlebar_edge",
			      width - rightTitlebarEdge, 0,
			      rightTitlebarEdge, titleHeight, jif);
		    drawPiece(frame_style, g, "top_titlebar_edge",
			      0, 0, width, topTitlebarEdge, jif);
		    drawPiece(frame_style, g, "bottom_titlebar_edge",
			      0, titleHeight - bottomTitlebarEdge,
			      width, bottomTitlebarEdge, jif);
		    drawPiece(frame_style, g, "title",
			      titleRect.x, titleRect.y, titleRect.width, titleRect.height, jif);
		    if (!minimized) {
			drawPiece(frame_style, g, "left_edge",
				  0, titleHeight, insets.left, height-titleHeight, jif);
			drawPiece(frame_style, g, "right_edge",
				  width-insets.right, titleHeight, insets.right, height-titleHeight, jif);
			drawPiece(frame_style, g, "bottom_edge",
				  0, height - insets.bottom, width, insets.bottom, jif);
			drawPiece(frame_style, g, "overlay",
				  0, 0, width, height, jif);
		    }
		    g.setClip(oldClip);
		}
	    }
	}
	g.translate(-x0, -y0);
	g.setFont(oldFont);
    }



    private static class Privileged implements PrivilegedAction { 
	private static int GET_THEME_DIR  = 0;
	private static int GET_USER_THEME = 1;
	private static int GET_IMAGE      = 2;
	private int type;
	private Object arg;

	public Object doPrivileged(int type, Object arg) {
	    this.type = type;
	    this.arg = arg;
	    return AccessController.doPrivileged(this);
	}

	public Object run() {
	    if (type == GET_THEME_DIR) {
		String sep = File.separator;
		String[] dirs = new String[] {
		    userHome + sep + ".themes",
		    System.getProperty("swing.metacitythemedir"),
		    "/usr/share/themes",
		    "/usr/gnome/share/themes",  // Debian/Redhat/Solaris
                    "/opt/gnome2/share/themes"  // SuSE
		};

		URL themeDir = null;
		for (int i = 0; i < dirs.length; i++) {
                    // System property may not be set so skip null directories.
                    if (dirs[i] == null) {
                        continue;
                    }
                    File dir =
                        new File(dirs[i] + sep + arg + sep + "metacity-1");
		    if (new File(dir, "metacity-theme-1.xml").canRead()) {
			try {
			    themeDir = dir.toURL();
			} catch (MalformedURLException ex) {
			    themeDir = null;
			}
			break;
		    }
		}
		if (themeDir == null) {
		    String filename = "resources/metacity/" + arg +
                        "/metacity-1/metacity-theme-1.xml";
		    URL url = getClass().getResource(filename);
		    if (url != null) {
			String str = url.toString();
			try {
			    themeDir = new URL(str.substring(0, str.lastIndexOf('/'))+"/");
			} catch (MalformedURLException ex) {
			    themeDir = null;
			}
		    }
		}
		return themeDir;
	    } else if (type == GET_USER_THEME) {
		try {
		    // Set userHome here because we need the privilege
		    userHome = System.getProperty("user.home");

		    String theme = System.getProperty("swing.metacitythemename");
		    if (theme != null) {
			return theme;
		    }
		    // Note: this is a small file (< 1024 bytes) so it's not worth
		    // starting an XML parser or even to use a buffered reader.
		    URL url = new URL(new File(userHome).toURL(),
				      ".gconf/apps/metacity/general/%25gconf.xml");
		    // Pending: verify character encoding spec for gconf
		    Reader reader = new InputStreamReader(url.openStream(), "ISO-8859-1");
		    char[] buf = new char[1024];
		    StringBuffer strBuf = new StringBuffer();
		    int n;
		    while ((n = reader.read(buf)) >= 0) {
			strBuf.append(buf, 0, n);
		    }	    
		    reader.close();
		    String str = strBuf.toString();
		    if (str != null) {
			String strLowerCase = str.toLowerCase();
			int i = strLowerCase.indexOf("<entry name=\"theme\"");
			if (i >= 0) {
			    i = strLowerCase.indexOf("<stringvalue>", i);
			    if (i > 0) {
				i += "<stringvalue>".length();
				int i2 = str.indexOf("<", i);
				return str.substring(i, i2);
			    }
			}
		    }
		} catch (MalformedURLException ex) {
		    // OK to just ignore. We'll use a fallback theme.
		} catch (IOException ex) {
		    // OK to just ignore. We'll use a fallback theme.
		}
		return null;
	    } else if (type == GET_IMAGE) {
		return new ImageIcon((URL)arg).getImage();
	    } else {
		return null;
	    }
	}
    }

    private static URL getThemeDir(String themeName) {
	return (URL)new Privileged().doPrivileged(Privileged.GET_THEME_DIR, themeName);
    }

    private static String getUserTheme() {
	return (String)new Privileged().doPrivileged(Privileged.GET_USER_THEME, null);
    }

    protected void tileImage(Graphics g, Image image, int x0, int y0, int w, int h, float[] alphas) {
	Graphics2D g2 = (Graphics2D)g;
	Composite oldComp = g2.getComposite();

	int sw = image.getWidth(null);
	int sh = image.getHeight(null);
	int y = y0;
	while (y < y0 + h) {
	    sh = Math.min(sh, y0 + h - y);
	    int x = x0;
	    while (x < x0 + w) {
		float f = (alphas.length - 1.0F) * x / (x0 + w);
		int i = (int)f;
		f -= (int)f;
		float alpha = (1-f) * alphas[i];
		if (i+1 < alphas.length) {
		    alpha += f * alphas[i+1];
		}
		g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, alpha));
		int swm = Math.min(sw, x0 + w - x);
		g.drawImage(image, x, y, x+swm, y+sh, 0, 0, swm, sh, null);
		x += swm;
	    }
	    y += sh;
	}
	g2.setComposite(oldComp);
    }

    private HashMap<String, Image> images = new HashMap();

    protected Image getImage(String key, Color c) {
	Image image = images.get(key+"-"+c.getRGB());
	if (image == null) {
	    image = imageFilter.colorize(getImage(key), c);
	    if (image != null) {
		images.put(key+"-"+c.getRGB(), image);
	    }
	}
	return image;
    }

    protected Image getImage(String key) {
	Image image = images.get(key);
	if (image == null) {
	    if (themeDir != null) {
		try {
		    URL url = new URL(themeDir, key);
		    image = (Image)new Privileged().doPrivileged(Privileged.GET_IMAGE, url);
		} catch (MalformedURLException ex) {
		    //log("Bad image url: "+ themeDir + "/" + key);
		}
	    }
	    if (image != null) {
		images.put(key, image);
	    }
	}
	return image;
    }

    private class ColorizeImageFilter extends RGBImageFilter {
	double cr, cg, cb;

	public ColorizeImageFilter() {
	    canFilterIndexColorModel = true;
	}

	public void setColor(Color color) {
	    cr = color.getRed()   / 255.0;
	    cg = color.getGreen() / 255.0;
	    cb = color.getBlue()  / 255.0;
	}

	public Image colorize(Image fromImage, Color c) {
	    setColor(c);
	    ImageProducer producer = new FilteredImageSource(fromImage.getSource(), this);
	    return new ImageIcon(context.getComponent().createImage(producer)).getImage();
	}

	public int filterRGB(int x, int y, int rgb) {
	    // Assume all rgb values are shades of gray
	    double grayLevel = 2 * (rgb & 0xff) / 255.0;
	    double r, g, b;

	    if (grayLevel <= 1.0) {
		r = cr * grayLevel;
		g = cg * grayLevel;
		b = cb * grayLevel;
            } else {
		grayLevel -= 1.0;
		r = cr + (1.0 - cr) * grayLevel;
		g = cg + (1.0 - cg) * grayLevel;
		b = cb + (1.0 - cb) * grayLevel;
            }

	    return ((rgb & 0xff000000) +
		    (((int)(r * 255)) << 16) +
		    (((int)(g * 255)) << 8) +
		    (int)(b * 255));
	}
    }

    protected static JComponent findChild(JComponent parent, String name) {
	int n = parent.getComponentCount();
	for (int i = 0; i < n; i++) {
	    JComponent c = (JComponent)parent.getComponent(i);
	    if (name.equals(c.getName())) {
		return c;
	    }
	}
	return null;
    }


    protected class TitlePaneLayout implements LayoutManager {
        public void addLayoutComponent(String name, Component c) {}
        public void removeLayoutComponent(Component c) {}    
        public Dimension preferredLayoutSize(Container c)  {
	    return minimumLayoutSize(c);
	}
    
        public Dimension minimumLayoutSize(Container c) {
	    JComponent titlePane = (JComponent)c;
	    Container titlePaneParent = titlePane.getParent();
	    JInternalFrame frame;
	    if (titlePaneParent instanceof JInternalFrame) {
		frame = (JInternalFrame)titlePaneParent;
	    } else if (titlePaneParent instanceof JInternalFrame.JDesktopIcon) {
		frame = ((JInternalFrame.JDesktopIcon)titlePaneParent).getInternalFrame();
	    } else {
		return null;
	    }

	    Dimension buttonDim = calculateButtonSize(titlePane);
	    Insets title_border  = (Insets)getFrameGeometry().get("title_border");
	    Insets button_border = (Insets)getFrameGeometry().get("button_border");

            // Calculate width.
            int width = getInt("left_titlebar_edge") + buttonDim.width + getInt("right_titlebar_edge");
	    if (title_border != null) {
		width += title_border.left + title_border.right;
	    }
            if (frame.isClosable()) {
                width += buttonDim.width;
            }
            if (frame.isMaximizable()) {
                width += buttonDim.width;
            }
            if (frame.isIconifiable()) {
                width += buttonDim.width;
            }
            FontMetrics fm = frame.getFontMetrics(titlePane.getFont());
            String frameTitle = frame.getTitle();
            int title_w = frameTitle != null ? SwingUtilities2.stringWidth(
                               frame, fm, frameTitle) : 0;
            int title_length = frameTitle != null ? frameTitle.length() : 0;

            // Leave room for three characters in the title.
            if (title_length > 3) {
                int subtitle_w = SwingUtilities2.stringWidth(
                    frame, fm, frameTitle.substring(0, 3) + "...");
                width += (title_w < subtitle_w) ? title_w : subtitle_w;
            } else {
                width += title_w;
            }

            // Calculate height.
	    int titleHeight = fm.getHeight() + getInt("title_vertical_pad");
	    if (title_border != null) {
		titleHeight += title_border.top + title_border.bottom;
	    }
	    int buttonHeight = buttonDim.height;
	    if (button_border != null) {
		buttonHeight += button_border.top + button_border.bottom;
	    }
            int height = Math.max(buttonHeight, titleHeight);

            return new Dimension(width, height);
	}
    
        public void layoutContainer(Container c) {
	    JComponent titlePane = (JComponent)c;
	    Container titlePaneParent = titlePane.getParent();
	    JInternalFrame frame;
	    if (titlePaneParent instanceof JInternalFrame) {
		frame = (JInternalFrame)titlePaneParent;
	    } else if (titlePaneParent instanceof JInternalFrame.JDesktopIcon) {
		frame = ((JInternalFrame.JDesktopIcon)titlePaneParent).getInternalFrame();
	    } else {
		return;
	    }
	    Map gm = getFrameGeometry();

            int w = titlePane.getWidth();
            int h = titlePane.getHeight();

	    JComponent menuButton     = findChild(titlePane, "InternalFrameTitlePane.menuButton");
	    JComponent minimizeButton = findChild(titlePane, "InternalFrameTitlePane.iconifyButton");
	    JComponent maximizeButton = findChild(titlePane, "InternalFrameTitlePane.maximizeButton");
	    JComponent closeButton    = findChild(titlePane, "InternalFrameTitlePane.closeButton");

	    int buttonGap = 0;

	    Insets button_border = (Insets)gm.get("button_border");
	    Dimension buttonDim = calculateButtonSize(titlePane);

            int x = getInt("left_titlebar_edge");
	    int y = (button_border != null) ? button_border.top : 0;

            menuButton.setBounds(x, y, buttonDim.width, buttonDim.height);

            x = w - buttonDim.width - getInt("right_titlebar_edge");
	    if (button_border != null) {
		x -= button_border.right;
	    }

            if (frame.isClosable()) {
                closeButton.setBounds(x, y, buttonDim.width, buttonDim.height);
                x -= (buttonDim.width + buttonGap);
            } 

            if (frame.isMaximizable()) {
                maximizeButton.setBounds(x, y, buttonDim.width, buttonDim.height);
                x -= (buttonDim.width + buttonGap);
            }

            if (frame.isIconifiable()) {
                minimizeButton.setBounds(x, y, buttonDim.width, buttonDim.height);
            } 
        }
    } // end TitlePaneLayout

    protected Map getFrameGeometry() {
	return frameGeometry;
    }

    protected void setFrameGeometry(JComponent titlePane, Map gm) {
	this.frameGeometry = gm;
	if (getInt("top_height") == 0) {
	    gm.put("top_height", new Integer(titlePane.getHeight()));
	}
    }

    protected int getInt(String key) {
	Integer i = (Integer)frameGeometry.get(key);
	if (i == null) {
	    i = variables.get(key);
	}
	return (i != null) ? i.intValue() : 0;
    }

    protected boolean getBoolean(String key, boolean fallback) {
	Boolean b = (Boolean)frameGeometry.get(key);
	return (b != null) ? b.booleanValue() : fallback;
    }


    protected void drawArc(Node node, Graphics g) {
	NamedNodeMap attrs = node.getAttributes();
	Color color = parseColor(getStringAttr(attrs, "color"));
	int x = aee.evaluate(getStringAttr(attrs, "x"));
	int y = aee.evaluate(getStringAttr(attrs, "y"));
	int w = aee.evaluate(getStringAttr(attrs, "width"));
	int h = aee.evaluate(getStringAttr(attrs, "height"));
	int start_angle = aee.evaluate(getStringAttr(attrs, "start_angle"));
	int extent_angle = aee.evaluate(getStringAttr(attrs, "extent_angle"));
	boolean filled = getBooleanAttr(node, "filled", false);
	if (getInt("width") == -1) {
	    x -= w;
	}
	if (getInt("height") == -1) {
	    y -= h;
	}
	g.setColor(color);
	if (filled) {
	    g.fillArc(x, y, w, h, start_angle, extent_angle);
	} else {
	    g.drawArc(x, y, w, h, start_angle, extent_angle);
	}
    }

    protected void drawLine(Node node, Graphics g) {
	NamedNodeMap attrs = node.getAttributes();
	Color color = parseColor(getStringAttr(attrs, "color"));
	int x1 = aee.evaluate(getStringAttr(attrs, "x1"));
	int y1 = aee.evaluate(getStringAttr(attrs, "y1"));
	int x2 = aee.evaluate(getStringAttr(attrs, "x2"));
	int y2 = aee.evaluate(getStringAttr(attrs, "y2"));
	int lineWidth = aee.evaluate(getStringAttr(attrs, "width"), 1);
	g.setColor(color);
	if (lineWidth != 1) {
	    Graphics2D g2d = (Graphics2D)g;
	    Stroke stroke = g2d.getStroke();
	    g2d.setStroke(new BasicStroke((float)lineWidth));
	    g2d.drawLine(x1, y1, x2, y2);
	    g2d.setStroke(stroke);
	} else {
	    g.drawLine(x1, y1, x2, y2);
	}
    }

    protected void drawRectangle(Node node, Graphics g) {
	NamedNodeMap attrs = node.getAttributes();
	Color color = parseColor(getStringAttr(attrs, "color"));
	boolean filled = getBooleanAttr(node, "filled", false);
	int x = aee.evaluate(getStringAttr(attrs, "x"));
	int y = aee.evaluate(getStringAttr(attrs, "y"));
	int w = aee.evaluate(getStringAttr(attrs, "width"));
	int h = aee.evaluate(getStringAttr(attrs, "height"));
	g.setColor(color);
	if (getInt("width") == -1) {
	    x -= w;
	}
	if (getInt("height") == -1) {
	    y -= h;
	}
	if (filled) {
	    g.fillRect(x, y, w, h);
	} else {
	    g.drawRect(x, y, w, h);
	}
    }

    protected void drawTile(Node node, Graphics g, JInternalFrame jif) {
	NamedNodeMap attrs = node.getAttributes();
	int x0 = aee.evaluate(getStringAttr(attrs, "x"));
	int y0 = aee.evaluate(getStringAttr(attrs, "y"));
	int w = aee.evaluate(getStringAttr(attrs, "width"));
	int h = aee.evaluate(getStringAttr(attrs, "height"));
	int tw = aee.evaluate(getStringAttr(attrs, "tile_width"));
	int th = aee.evaluate(getStringAttr(attrs, "tile_height"));
	int width  = getInt("width");
	int height = getInt("height");
	if (width == -1) {
	    x0 -= w;
	}
	if (height == -1) {
	    y0 -= h;
	}
	Shape oldClip = g.getClip();
	if (g instanceof Graphics2D) {
	    ((Graphics2D)g).clip(new Rectangle(x0, y0, w, h));
	}
	variables.put("width",  tw);
	variables.put("height", th);

	Node draw_ops = getNode("draw_ops", new String[] { "name", getStringAttr(node, "name") });
	
	int y = y0;
	while (y < y0 + h) {
	    int x = x0;
	    while (x < x0 + w) {
		g.translate(x, y);
		draw(draw_ops, g, jif);
		g.translate(-x, -y);
		x += tw;
	    }
	    y += th;
	}

	variables.put("width",  width);
	variables.put("height", height);
	g.setClip(oldClip);
    }

    protected void drawTint(Node node, Graphics g) {
	NamedNodeMap attrs = node.getAttributes();
	Color color = parseColor(getStringAttr(attrs, "color"));
	float alpha = Float.parseFloat(getStringAttr(attrs, "alpha"));
	int x = aee.evaluate(getStringAttr(attrs, "x"));
	int y = aee.evaluate(getStringAttr(attrs, "y"));
	int w = aee.evaluate(getStringAttr(attrs, "width"));
	int h = aee.evaluate(getStringAttr(attrs, "height"));
	if (getInt("width") == -1) {
	    x -= w;
	}
	if (getInt("height") == -1) {
	    y -= h;
	}
	if (g instanceof Graphics2D) {
	    Graphics2D g2 = (Graphics2D)g;
	    Composite oldComp = g2.getComposite();
	    AlphaComposite ac = AlphaComposite.getInstance(AlphaComposite.SRC_OVER, alpha);
	    g2.setComposite(ac);
	    g2.setColor(color);
	    g2.fillRect(x, y, w, h);
	    g2.setComposite(oldComp);
	}
    }

    protected void drawTitle(Node node, Graphics g, JInternalFrame jif) {
	NamedNodeMap attrs = node.getAttributes();
	String colorStr = getStringAttr(attrs, "color");
	int i = colorStr.indexOf("gtk:fg[");
	if (i > 0) {
	    colorStr = colorStr.substring(0, i) + "gtk:text[" + colorStr.substring(i+7);
	}
	Color color = parseColor(colorStr);
	int x = aee.evaluate(getStringAttr(attrs, "x"));
	int y = aee.evaluate(getStringAttr(attrs, "y"));

	String title = jif.getTitle();
        if (title != null) {
            FontMetrics fm = SwingUtilities2.getFontMetrics(jif, g);
	    if (jif.getComponentOrientation().isLeftToRight()) {
		title = SwingUtilities2.clipStringIfNecessary(jif, fm, title,
                             calculateTitleTextWidth(g, jif));
	    }
	    g.setColor(color);
            SwingUtilities2.drawString(jif, g, title, x, y + fm.getAscent());
        }
    }

    protected Dimension calculateButtonSize(JComponent titlePane) {
	int buttonHeight = getInt("button_height");
	if (buttonHeight == 0) {
	    buttonHeight = titlePane.getHeight();
	    if (buttonHeight == 0) {
		buttonHeight = 13;
	    } else {
		Insets button_border = (Insets)frameGeometry.get("button_border");
		if (button_border != null) {
		    buttonHeight -= (button_border.top + button_border.bottom);
		}
	    }
	}
	int buttonWidth = getInt("button_width");
	if (buttonWidth == 0) {
	    buttonWidth = buttonHeight;
	    Float aspect_ratio = (Float)frameGeometry.get("aspect_ratio");
	    if (aspect_ratio != null) {
		buttonWidth = (int)(buttonHeight / aspect_ratio.floatValue());
	    }
	}
	return new Dimension(buttonWidth, buttonHeight);
    }

    protected Rectangle calculateTitleArea(JInternalFrame jif) {
	JComponent titlePane = findChild(jif, "InternalFrame.northPane");
	Dimension buttonDim = calculateButtonSize(titlePane);
	Insets title_border = (Insets)frameGeometry.get("title_border");
	Rectangle r = new Rectangle();

	r.x = getInt("left_titlebar_edge") + buttonDim.width;
	r.y = 0;
	r.height = titlePane.getHeight();
	if (title_border != null) {
	    r.x += title_border.left;
	    r.y += title_border.top;
	    r.height -= (title_border.top + title_border.bottom);
	}

	r.width = titlePane.getWidth() - r.x - getInt("right_titlebar_edge");
	if (jif.isClosable()) {
	    r.width -= buttonDim.width;
	}
	if (jif.isMaximizable()) {
	    r.width -= buttonDim.width;
	}
	if (jif.isIconifiable()) {
	    r.width -= buttonDim.width;
	}
	if (title_border != null) {
	    r.width -= title_border.right;
	}
	return r;
    }


    protected int calculateTitleTextWidth(Graphics g, JInternalFrame jif) {
	String title = jif.getTitle();
	if (title != null) {
	    Rectangle r = calculateTitleArea(jif);
	    return Math.min(SwingUtilities2.stringWidth(jif,
                     SwingUtilities2.getFontMetrics(jif, g), title), r.width);
	}
	return 0;
    }

    protected void setClip(Node node, Graphics g) {
	NamedNodeMap attrs = node.getAttributes();
	int x = aee.evaluate(getStringAttr(attrs, "x"));
	int y = aee.evaluate(getStringAttr(attrs, "y"));
	int w = aee.evaluate(getStringAttr(attrs, "width"));
	int h = aee.evaluate(getStringAttr(attrs, "height"));
	if (getInt("width") == -1) {
	    x -= w;
	}
	if (getInt("height") == -1) {
	    y -= h;
	}
	if (g instanceof Graphics2D) {
	    ((Graphics2D)g).clip(new Rectangle(x, y, w, h));
	}
    }

    protected void drawGTKArrow(Node node, Graphics g) {
	NamedNodeMap attrs = node.getAttributes();
	String arrow    = getStringAttr(attrs, "arrow");
	String shadow   = getStringAttr(attrs, "shadow");
	String stateStr = getStringAttr(attrs, "state").toUpperCase();
	int x = aee.evaluate(getStringAttr(attrs, "x"));
	int y = aee.evaluate(getStringAttr(attrs, "y"));
	int w = aee.evaluate(getStringAttr(attrs, "width"));
	int h = aee.evaluate(getStringAttr(attrs, "height"));

	int state = -1;
	if ("NORMAL".equals(stateStr)) {
	    state = ENABLED;
	} else if ("SELECTED".equals(stateStr)) {
	    state = SELECTED;
	} else if ("INSENSITIVE".equals(stateStr)) {
	    state = DISABLED;
	} else if ("PRELIGHT".equals(stateStr)) {
	    state = MOUSE_OVER;
	}

	int shadowType = -1;
	if ("in".equals(shadow)) {
	    shadowType = GTKConstants.SHADOW_IN;
	} else if ("out".equals(shadow)) {
	    shadowType = GTKConstants.SHADOW_OUT;
	} else if ("etched_in".equals(shadow)) {
	    shadowType = GTKConstants.SHADOW_ETCHED_IN;
	} else if ("etched_out".equals(shadow)) {
	    shadowType = GTKConstants.SHADOW_ETCHED_OUT;
	} else if ("none".equals(shadow)) {
	    shadowType = GTKConstants.SHADOW_NONE;
	}
	int direction = -1;
	if ("up".equals(arrow)) {
	    direction = GTKConstants.ARROW_UP;
	} else if ("down".equals(arrow)) {
	    direction = GTKConstants.ARROW_DOWN;
	} else if ("left".equals(arrow)) {
	    direction = GTKConstants.ARROW_LEFT;
	} else if ("right".equals(arrow)) {
	    direction = GTKConstants.ARROW_RIGHT;
	}
	GTKEngine engine = ((GTKStyle)context.getStyle()).getEngine(context);
	engine.paintArrow(context, g, state, shadowType,
			  direction, null, x, y, w, h);
    }

    protected void drawGTKBox(Node node, Graphics g) {
	NamedNodeMap attrs = node.getAttributes();
	String shadow   = getStringAttr(attrs, "shadow");
	String stateStr = getStringAttr(attrs, "state").toUpperCase();
	int x = aee.evaluate(getStringAttr(attrs, "x"));
	int y = aee.evaluate(getStringAttr(attrs, "y"));
	int w = aee.evaluate(getStringAttr(attrs, "width"));
	int h = aee.evaluate(getStringAttr(attrs, "height"));

	int state = -1;
	if ("NORMAL".equals(stateStr)) {
	    state = ENABLED;
	} else if ("SELECTED".equals(stateStr)) {
	    state = SELECTED;
	} else if ("INSENSITIVE".equals(stateStr)) {
	    state = DISABLED;
	} else if ("PRELIGHT".equals(stateStr)) {
	    state = MOUSE_OVER;
	}

	int shadowType = -1;
	if ("in".equals(shadow)) {
	    shadowType = GTKConstants.SHADOW_IN;
	} else if ("out".equals(shadow)) {
	    shadowType = GTKConstants.SHADOW_OUT;
	} else if ("etched_in".equals(shadow)) {
	    shadowType = GTKConstants.SHADOW_ETCHED_IN;
	} else if ("etched_out".equals(shadow)) {
	    shadowType = GTKConstants.SHADOW_ETCHED_OUT;
	} else if ("none".equals(shadow)) {
	    shadowType = GTKConstants.SHADOW_NONE;
	}
	GTKEngine.INSTANCE.paintBox(context, g, state, shadowType,
				    null, x, y, w, h);
    }

    protected void drawGTKVLine(Node node, Graphics g) {
	NamedNodeMap attrs = node.getAttributes();
	String stateStr = getStringAttr(attrs, "state").toUpperCase();

	int x  = aee.evaluate(getStringAttr(attrs, "x"));
	int y1 = aee.evaluate(getStringAttr(attrs, "y1"));
	int y2 = aee.evaluate(getStringAttr(attrs, "y2"));

	int state = -1;
	if ("NORMAL".equals(stateStr)) {
	    state = ENABLED;
	} else if ("SELECTED".equals(stateStr)) {
	    state = SELECTED;
	} else if ("INSENSITIVE".equals(stateStr)) {
	    state = DISABLED;
	} else if ("PRELIGHT".equals(stateStr)) {
	    state = MOUSE_OVER;
	}

	GTKEngine.INSTANCE.paintVline(context, g, state, null, x, y1, 1, y2-y1);
    }

    protected void drawGradient(Node node, Graphics g) {
	NamedNodeMap attrs = node.getAttributes();
	String type = getStringAttr(attrs, "type");
	float alpha = getFloatAttr(node, "alpha", -1F);
	int x = aee.evaluate(getStringAttr(attrs, "x"));
	int y = aee.evaluate(getStringAttr(attrs, "y"));
	int w = aee.evaluate(getStringAttr(attrs, "width"));
	int h = aee.evaluate(getStringAttr(attrs, "height"));
	if (getInt("width") == -1) {
	    x -= w;
	}
	if (getInt("height") == -1) {
	    y -= h;
	}

	// Get colors from child nodes
	Node[] colorNodes = getNodesByName(node, "color");
	Color[] colors = new Color[colorNodes.length];
	for (int i = 0; i < colorNodes.length; i++) {
	    colors[i] = parseColor(getStringAttr(colorNodes[i], "value"));
	}

	boolean horizontal = ("diagonal".equals(type) || "horizontal".equals(type));
	boolean vertical   = ("diagonal".equals(type) || "vertical".equals(type));

	if (g instanceof Graphics2D) {
	    Graphics2D g2 = (Graphics2D)g;
	    Composite oldComp = g2.getComposite();
	    if (alpha >= 0F) {
		g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, alpha));
	    }
	    int n = colors.length - 1;
	    for (int i = 0; i < n; i++) {
		g2.setPaint(new GradientPaint(x + (horizontal ? (i*w/n) : 0),
					      y + (vertical   ? (i*h/n) : 0),
					      colors[i],
					      x + (horizontal ? ((i+1)*w/n) : 0),
					      y + (vertical   ? ((i+1)*h/n) : 0),
					      colors[i+1]));
		g2.fillRect(x + (horizontal ? (i*w/n) : 0),
			    y + (vertical   ? (i*h/n) : 0),
			    (horizontal ? (w/n) : w),
			    (vertical   ? (h/n) : h));
	    }
	    g2.setComposite(oldComp);
	}
    }

    protected void drawImage(Node node, Graphics g) {
	NamedNodeMap attrs = node.getAttributes();
	String filename = getStringAttr(attrs, "filename");
	String colorizeStr = getStringAttr(attrs, "colorize");
	Color colorize = (colorizeStr != null) ? parseColor(colorizeStr) : null;
	String alpha = getStringAttr(attrs, "alpha");
	Image object = (colorize != null) ? getImage(filename, colorize) : getImage(filename);
	variables.put("object_width",  object.getWidth(null));
	variables.put("object_height", object.getHeight(null));
	String fill_type = getStringAttr(attrs, "fill_type"); 
	int x = aee.evaluate(getStringAttr(attrs, "x"));
	int y = aee.evaluate(getStringAttr(attrs, "y"));
	int w = aee.evaluate(getStringAttr(attrs, "width"));
	int h = aee.evaluate(getStringAttr(attrs, "height"));
	if (getInt("width") == -1) {
	    x -= w;
	}
	if (getInt("height") == -1) {
	    y -= h;
	}

	if (alpha != null) {
	    if ("tile".equals(fill_type)) {
		StringTokenizer tokenizer = new StringTokenizer(alpha, ":");
		float[] alphas = new float[tokenizer.countTokens()];
		for (int i = 0; i < alphas.length; i++) {
		    alphas[i] = Float.parseFloat(tokenizer.nextToken());
		}
		tileImage(g, object, x, y, w, h, alphas);
	    } else {
		float a = Float.parseFloat(alpha);
		if (g instanceof Graphics2D) {
		    Graphics2D g2 = (Graphics2D)g;
		    Composite oldComp = g2.getComposite();
		    g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, a));
		    g2.drawImage(object, x, y, w, h, null);
		    g2.setComposite(oldComp);
		}
	    }
	} else {
	    g.drawImage(object, x, y, w, h, null);
	}
    }

    protected void drawIcon(Node node, Graphics g, JInternalFrame jif) {
	Icon icon = jif.getFrameIcon();
	if (icon == null) {
	    return;
	}

	NamedNodeMap attrs = node.getAttributes();
	String alpha = getStringAttr(attrs, "alpha");
	int x = aee.evaluate(getStringAttr(attrs, "x"));
	int y = aee.evaluate(getStringAttr(attrs, "y"));
	int w = aee.evaluate(getStringAttr(attrs, "width"));
	int h = aee.evaluate(getStringAttr(attrs, "height"));
	if (getInt("width") == -1) {
	    x -= w;
	}
	if (getInt("height") == -1) {
	    y -= h;
	}

	if (alpha != null) {
	    float a = Float.parseFloat(alpha);
	    if (g instanceof Graphics2D) {
		Graphics2D g2 = (Graphics2D)g;
		Composite oldComp = g2.getComposite();
		g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, a));
		icon.paintIcon(jif, g, x, y);
		g2.setComposite(oldComp);
	    }
	} else {
	    icon.paintIcon(jif, g, x, y);
	}
    }

    protected void drawInclude(Node node, Graphics g, JInternalFrame jif) {
	int oldWidth  = getInt("width");
	int oldHeight = getInt("height");

	NamedNodeMap attrs = node.getAttributes();
	int x = aee.evaluate(getStringAttr(attrs, "x"),       0);
	int y = aee.evaluate(getStringAttr(attrs, "y"),       0);
	int w = aee.evaluate(getStringAttr(attrs, "width"),  -1);
	int h = aee.evaluate(getStringAttr(attrs, "height"), -1);

	if (w != -1) {
	    variables.put("width",  w);
	}
	if (h != -1) {
	    variables.put("height", h);
	}

	Node draw_ops = getNode("draw_ops", new String[] {
	    "name", getStringAttr(node, "name")
	});
	g.translate(x, y);
	draw(draw_ops, g, jif);
	g.translate(-x, -y);

	if (w != -1) {
	    variables.put("width",  oldWidth);
	}
	if (h != -1) {
	    variables.put("height", oldHeight);
	}
    }    

    protected void draw(Node draw_ops, Graphics g, JInternalFrame jif) {
	if (draw_ops != null) {
	    NodeList nodes = draw_ops.getChildNodes();
	    if (nodes != null) {
		Shape oldClip = g.getClip();
		for (int i = 0; i < nodes.getLength(); i++) {
		    Node child = nodes.item(i);
		    if (child.getNodeType() == Node.ELEMENT_NODE) {
			try {
			    String name = child.getNodeName();
			    if ("include".equals(name)) {
				drawInclude(child, g, jif);
			    } else if ("arc".equals(name)) {
				drawArc(child, g);
			    } else if ("clip".equals(name)) {
				setClip(child, g);
			    } else if ("gradient".equals(name)) {
				drawGradient(child, g);
			    } else if ("gtk_arrow".equals(name)) {
				drawGTKArrow(child, g);
			    } else if ("gtk_box".equals(name)) {
				drawGTKBox(child, g);
			    } else if ("gtk_vline".equals(name)) {
				drawGTKVLine(child, g);
			    } else if ("image".equals(name)) {
				drawImage(child, g);
			    } else if ("icon".equals(name)) {
				drawIcon(child, g, jif);
			    } else if ("line".equals(name)) {
				drawLine(child, g);
			    } else if ("rectangle".equals(name)) {
				drawRectangle(child, g);
			    } else if ("tint".equals(name)) {
				drawTint(child, g);
			    } else if ("tile".equals(name)) {
				drawTile(child, g, jif);
			    } else if ("title".equals(name)) {
				drawTitle(child, g, jif);
			    } else {
				System.err.println("Unknown Metacity drawing op: "+child);
			    }
			} catch (NumberFormatException ex) {
			    logError(themeName, ex);
			}
		    }
		}
		g.setClip(oldClip);
	    }
	}
    }

    protected void drawPiece(Node frame_style, Graphics g, String position, int x, int y,
			     int width, int height, JInternalFrame jif) {
	Node piece = getNode(frame_style, "piece", new String[] { "position", position });
	if (piece != null) {
	    Node draw_ops;
	    String draw_ops_name = getStringAttr(piece, "draw_ops");
	    if (draw_ops_name != null) {
		draw_ops = getNode("draw_ops", new String[] { "name", draw_ops_name });
	    } else {
		draw_ops = getNode(piece, "draw_ops", null);
	    }
	    variables.put("width",  width);
	    variables.put("height", height);
	    g.translate(x, y);
	    draw(draw_ops, g, jif);
	    g.translate(-x, -y);
	}
    }


    Insets getBorderInsets(SynthContext context, Insets insets) {
	if (insets == null) {
	    insets = new Insets(0, 0, 0, 0);
	}
	insets.top    = ((Insets)frameGeometry.get("title_border")).top;
	insets.bottom = getInt("bottom_height");
	insets.left   = getInt("left_width");
	insets.right  = getInt("right_width");
	return insets;
    }


    protected static void logError(String themeName, Exception ex) {
	logError(themeName, ex.toString());
    }

    protected static void logError(String themeName, String msg) {
	if (!errorLogged) {
	    System.err.println("Exception in Metacity for theme \""+themeName+"\": "+msg);
	    errorLogged = true;
	}
    }


    // XML Parsing


    protected static Document getXMLDoc(final URL xmlFile)
				throws IOException,
                                       ParserConfigurationException,
                                       SAXException {
	if (documentBuilder == null) {
	    documentBuilder =
                DocumentBuilderFactory.newInstance().newDocumentBuilder();
	}
	InputStream inputStream =
            (InputStream)AccessController.doPrivileged(new PrivilegedAction() {
                public Object run() {
                    try { 
                        return new BufferedInputStream(xmlFile.openStream());
                    } catch (IOException ex) {
                        return null;
                    }
                }
            });

        Document doc = null;
        if (inputStream != null) {
	    doc = documentBuilder.parse(inputStream);
        }
        return doc;
    }


    protected Node[] getNodesByName(Node parent, String name) {
	NodeList nodes = parent.getChildNodes(); // ElementNode
	int n = nodes.getLength();
	ArrayList<Node> list = new ArrayList();
	for (int i=0; i < n; i++) {
	    Node node = nodes.item(i);
	    if (name.equals(node.getNodeName())) {
		list.add(node);
	    }
	}
	return list.toArray(new Node[list.size()]);
    }



    protected Node getNode(String tagName, String[] attrs) {
	NodeList nodes = xmlDoc.getElementsByTagName(tagName);
	return (nodes != null) ? getNode(nodes, tagName, attrs) : null;
    }

    protected Node getNode(Node parent, String name, String[] attrs) {
	Node node = null;
	NodeList nodes = parent.getChildNodes();
	if (nodes != null) {
	    node = getNode(nodes, name, attrs);
	}
	if (node == null) {
	    String inheritFrom = getStringAttr(parent, "parent");
	    if (inheritFrom != null) {
		Node inheritFromNode = getNode(parent.getParentNode(),
					       parent.getNodeName(),
					       new String[] { "name", inheritFrom });
		if (inheritFromNode != null) {
		    node = getNode(inheritFromNode, name, attrs);
		}
	    }
	}
	return node;
    }

    protected Node getNode(NodeList nodes, String name, String[] attrs) {
	int n = nodes.getLength();
	for (int i=0; i < n; i++) {
	    Node node = nodes.item(i);
	    if (name.equals(node.getNodeName())) {
		if (attrs != null) {
		    NamedNodeMap nodeAttrs = node.getAttributes();
		    if (nodeAttrs != null) {
			boolean matches = true;
			int nAttrs = attrs.length / 2;
			for (int a = 0; a < nAttrs; a++) {
			    String aName  = attrs[a * 2];
			    String aValue = attrs[a * 2 + 1];
			    Node attr = nodeAttrs.getNamedItem(aName);
			    if (attr == null || !aValue.equals((String)attr.getNodeValue())) {
				matches = false;
				break;
			    }
			}
			if (matches) {
			    return node;
			}
		    }
		} else {
		    return node;
		}
	    }
	}
	return null;
    }

    protected String getStringAttr(Node node, String name) {
	String value = null;
	NamedNodeMap attrs = node.getAttributes();
	if (attrs != null) {
	    value = getStringAttr(attrs, name);
	    if (value == null) {
		String inheritFrom = getStringAttr(attrs, "parent");
		if (inheritFrom != null) {
		    Node inheritFromNode = getNode(node.getParentNode(),
						   node.getNodeName(),
						   new String[] { "name", inheritFrom });
		    if (inheritFromNode != null) {
			value = getStringAttr(inheritFromNode, name);
		    }
		}
	    }
	}
	return value;
    }

    protected String getStringAttr(NamedNodeMap attrs, String name) {
	Node item = attrs.getNamedItem(name);
	return (item != null) ? (String)item.getNodeValue() : null;
    }

    protected boolean getBooleanAttr(Node node, String name, boolean fallback) {
	String str = getStringAttr(node, name);
	if (str != null) {
	    return Boolean.valueOf(str).booleanValue();
	}
	return fallback;
    }

    protected int getIntAttr(Node node, String name, int fallback) {
	String str = getStringAttr(node, name);
	int value = fallback;
	if (str != null) {
	    try {
		value = Integer.parseInt(str);
	    } catch (NumberFormatException ex) {
		logError(themeName, ex);
	    }
	}
	return value;
    }

    protected float getFloatAttr(Node node, String name, float fallback) {
	String str = getStringAttr(node, name);
	float value = fallback;
	if (str != null) {
	    try {
		value = Float.parseFloat(str);
	    } catch (NumberFormatException ex) {
		logError(themeName, ex);
	    }
	}
	return value;
    }



    protected Color parseColor(String str) {
	StringTokenizer tokenizer = new StringTokenizer(str, "/");
	int n = tokenizer.countTokens();
	if (n > 1) {
	    String function = tokenizer.nextToken();
	    if ("shade".equals(function)) {
		assert (n == 3);
		Color c = parseColor2(tokenizer.nextToken());
		float alpha = Float.parseFloat(tokenizer.nextToken());
		return GTKColorType.adjustColor(c, 1.0F, alpha, alpha);
	    } else if ("blend".equals(function)) {
		assert (n == 4);
		Color bg = parseColor2(tokenizer.nextToken());
		Color fg = parseColor2(tokenizer.nextToken());
		float alpha = Float.parseFloat(tokenizer.nextToken());
		return new Color((int)(bg.getRed() + ((fg.getRed() - bg.getRed()) * alpha)),
				 (int)(bg.getRed() + ((fg.getRed() - bg.getRed()) * alpha)),
				 (int)(bg.getRed() + ((fg.getRed() - bg.getRed()) * alpha)));
	    } else {
		System.err.println("Unknown Metacity color function="+str);
		return null;
	    }
	} else {
	    return parseColor2(str);
	}
    }

    protected Color parseColor2(String str) {
	Color c = null;
	if (str.startsWith("gtk:")) {
	    int i1 = str.indexOf('[');
	    if (i1 > 3) {
		String typeStr = str.substring(4, i1).toLowerCase();
		int i2 = str.indexOf(']');
		if (i2 > i1+1) {
		    String stateStr = str.substring(i1+1, i2).toUpperCase();
		    int state = -1;
		    if ("ACTIVE".equals(stateStr)) {
			state = PRESSED;
		    } else if ("INSENSITIVE".equals(stateStr)) {
			state = DISABLED;
		    } else if ("NORMAL".equals(stateStr)) {
			state = ENABLED;
		    } else if ("PRELIGHT".equals(stateStr)) {
			state = MOUSE_OVER;
		    } else if ("SELECTED".equals(stateStr)) {
			state = SELECTED;
		    }
		    ColorType type = null;
		    if ("fg".equals(typeStr)) {
			type = GTKColorType.FOREGROUND;
		    } else if ("bg".equals(typeStr)) {
			type = GTKColorType.BACKGROUND;
		    } else if ("base".equals(typeStr)) {
			type = GTKColorType.TEXT_BACKGROUND;
		    } else if ("text".equals(typeStr)) {
			type = GTKColorType.TEXT_FOREGROUND;
		    } else if ("dark".equals(typeStr)) {
			type = GTKColorType.DARK;
		    } else if ("light".equals(typeStr)) {
			type = GTKColorType.LIGHT;
		    }
		    if (state >= 0 && type != null) {
			c = ((GTKStyle)context.getStyle()).getGTKColor(context.getComponent(),
								       context.getRegion(),
								       state, type);
		    }
		}
	    }
	}
	if (c == null) {
	    c = GTKParser.parseColorString(str);
	}
	return c;
    }

    class ArithmeticExpressionEvaluator {
	private PeekableStringTokenizer tokenizer;

	int evaluate(String expr) {
	    tokenizer = new PeekableStringTokenizer(expr, " \t+-*/%()", true);
	    return Math.round(expression());
	}

	int evaluate(String expr, int fallback) {
	    return (expr != null) ? evaluate(expr) : fallback;
	}

	public float expression() {
	    float value = getTermValue();
	    boolean done = false;
	    while (!done && tokenizer.hasMoreTokens()) {
		String next = tokenizer.peek();
		if ("+".equals(next) ||
		    "-".equals(next) ||
		    "`max`".equals(next) ||
		    "`min`".equals(next)) {
		    tokenizer.nextToken();
		    float value2 = getTermValue();
		    if ("+".equals(next)) {
			value += value2;
		    } else if ("-".equals(next)) {
			value -= value2;
		    } else if ("`max`".equals(next)) {
			value = Math.max(value, value2);
		    } else if ("`min`".equals(next)) {
			value = Math.min(value, value2);
		    }
		} else {
		    done = true;
		}
	    }
	    return value;
	}

	public float getTermValue() {
	    float value = getFactorValue();
	    boolean done = false;
	    while (!done && tokenizer.hasMoreTokens()) {
		String next = tokenizer.peek();
		if ("*".equals(next) || "/".equals(next) || "%".equals(next)) {
		    tokenizer.nextToken();
		    float value2 = getFactorValue();
		    if ("*".equals(next)) { 
			value *= value2;
		    } else if ("/".equals(next)) { 
			value /= value2;
		    } else {
			value %= value2;
		    }
		} else {
		    done = true;
		}
	    }
	    return value;
	}

	public float getFactorValue() {
	    float value;
	    if ("(".equals(tokenizer.peek())) {
		tokenizer.nextToken();
		value = expression();
		tokenizer.nextToken(); // skip right paren
	    } else {
		String token = tokenizer.nextToken();
		if (Character.isDigit(token.charAt(0))) {
		    value = Float.parseFloat(token);
		} else {
		    Integer i = variables.get(token);
		    if (i == null) {
			i = (Integer)getFrameGeometry().get(token);
		    }
		    if (i == null) {
			logError(themeName, "Variable \"" + token + "\" not defined");
			return 0;
		    }
		    value = (i != null) ? i.intValue() : 0F;
		}
	    }
	    return value;
	}


    }

    static class PeekableStringTokenizer extends StringTokenizer {
	String token = null;

	public PeekableStringTokenizer(String str, String delim,
				       boolean returnDelims) {
	    super(str, delim, returnDelims);
	    peek();
	}

	public String peek() {
	    if (token == null) {
		token = nextToken();
	    }
	    return token;
	}

	public boolean hasMoreTokens() {
	    return (token != null || super.hasMoreTokens());
	}

	public String nextToken() {
	    if (token != null) {
		String t = token;
		token = null;
		if (hasMoreTokens()) {
		    peek();
		}
		return t;
	    } else {
		String token = super.nextToken();
		while ((token.equals(" ") || token.equals("\t"))
		       && hasMoreTokens()) {
		    token = super.nextToken();
		}
		return token;
	    }
	}
    }


    static class RoundRectClipShape extends RectangularShape {
	static final int TOP_LEFT = 1;
	static final int TOP_RIGHT = 2;
	static final int BOTTOM_LEFT = 4;
	static final int BOTTOM_RIGHT = 8;

	int x;
	int y;
	int width;
	int height;
	int arcwidth;
	int archeight;
	int corners;

	public RoundRectClipShape() {
	}

	public RoundRectClipShape(int x, int y, int w, int h,
				  int arcw, int arch, int corners) {
	    setRoundedRect(x, y, w, h, arcw, arch, corners);
	}

	public void setRoundedRect(int x, int y, int w, int h,
				   int arcw, int arch, int corners) {
	    this.corners = corners;
	    this.x = x;
	    this.y = y;
	    this.width = w;
	    this.height = h;
	    this.arcwidth = arcw;
	    this.archeight = arch;
	}

	public double getX() {
	    return (double)x;
	}

	public double getY() {
	    return (double)y;
	}

	public double getWidth() {
	    return (double)width;
	}

	public double getHeight() {
	    return (double)height;
	}

	public double getArcWidth() {
	    return (double)arcwidth;
	}

	public double getArcHeight() {
	    return (double)archeight;
	}

	public boolean isEmpty() {
	    return false;  // Not called
	}

	public Rectangle2D getBounds2D() {
	    return null;  // Not called
	}

	public int getCornerFlags() {
	    return corners;
	}

	public void setFrame(double x, double y, double w, double h) {
	    // Not called
	}

	public boolean contains(double x, double y) {
	    return false;  // Not called
	}

	private int classify(double coord, double left, double right, double arcsize) {
	    return 0;  // Not called
	}

	public boolean intersects(double x, double y, double w, double h) {
	    return false;  // Not called
	}

	public boolean contains(double x, double y, double w, double h) {
	    return false;  // Not called
	}

	public PathIterator getPathIterator(AffineTransform at) {
	    return new RoundishRectIterator(this, at);
	}


	static class RoundishRectIterator implements PathIterator {
	    double x, y, w, h, aw, ah;
	    AffineTransform affine;
	    int index;

	    double ctrlpts[][];
	    int types[];

	    private static final double angle = Math.PI / 4.0;
	    private static final double a = 1.0 - Math.cos(angle);
	    private static final double b = Math.tan(angle);
	    private static final double c = Math.sqrt(1.0 + b * b) - 1 + a;
	    private static final double cv = 4.0 / 3.0 * a * b / c;
	    private static final double acv = (1.0 - cv) / 2.0;

	    // For each array:
	    //     4 values for each point {v0, v1, v2, v3}:
	    //         point = (x + v0 * w + v1 * arcWidth,
	    //                  y + v2 * h + v3 * arcHeight);
	    private static final double CtrlPtTemplate[][] = {
		{  0.0,  0.0,  1.0,  0.0 },	/* BOTTOM LEFT corner */
		{  0.0,  0.0,  1.0, -0.5 },	/* BOTTOM LEFT arc start */
		{  0.0,  0.0,  1.0, -acv,	/* BOTTOM LEFT arc curve */
		   0.0,  acv,  1.0,  0.0,
		   0.0,  0.5,  1.0,  0.0 },
		{  1.0,  0.0,  1.0,  0.0 },	/* BOTTOM RIGHT corner */
		{  1.0, -0.5,  1.0,  0.0 },	/* BOTTOM RIGHT arc start */
		{  1.0, -acv,  1.0,  0.0,	/* BOTTOM RIGHT arc curve */
		   1.0,  0.0,  1.0, -acv,
		   1.0,  0.0,  1.0, -0.5 },
		{  1.0,  0.0,  0.0,  0.0 },	/* TOP RIGHT corner */
		{  1.0,  0.0,  0.0,  0.5 },	/* TOP RIGHT arc start */
		{  1.0,  0.0,  0.0,  acv,	/* TOP RIGHT arc curve */
		   1.0, -acv,  0.0,  0.0,
		   1.0, -0.5,  0.0,  0.0 },
		{  0.0,  0.0,  0.0,  0.0 },	/* TOP LEFT corner */
		{  0.0,  0.5,  0.0,  0.0 },	/* TOP LEFT arc start */
		{  0.0,  acv,  0.0,  0.0,	/* TOP LEFT arc curve */
		   0.0,  0.0,  0.0,  acv,
		   0.0,  0.0,  0.0,  0.5 },
		{},				/* Closing path element */
	    };
	    private static final int CornerFlags[] = {
		RoundRectClipShape.BOTTOM_LEFT,
		RoundRectClipShape.BOTTOM_RIGHT,
		RoundRectClipShape.TOP_RIGHT,
		RoundRectClipShape.TOP_LEFT,
	    };

	    RoundishRectIterator(RoundRectClipShape rr, AffineTransform at) {
		this.x = rr.getX();
		this.y = rr.getY();
		this.w = rr.getWidth();
		this.h = rr.getHeight();
		this.aw = Math.min(w, Math.abs(rr.getArcWidth()));
		this.ah = Math.min(h, Math.abs(rr.getArcHeight()));
		this.affine = at;
		if (w < 0 || h < 0) {
		    // Don't draw anything...
		    ctrlpts = new double[0][];
		    types = new int[0];
		} else {
		    int corners = rr.getCornerFlags();
		    int numedges = 5;  // 4xCORNER_POINT, CLOSE
		    for (int i = 1; i < 0x10; i <<= 1) {
			// Add one for each corner that has a curve
			if ((corners & i) != 0) numedges++;
		    }
		    ctrlpts = new double[numedges][];
		    types = new int[numedges];
		    int j = 0;
		    for (int i = 0; i < 4; i++) {
			types[j] = SEG_LINETO;
			if ((corners & CornerFlags[i]) == 0) {
			    ctrlpts[j++] = CtrlPtTemplate[i*3+0];
			} else {
			    ctrlpts[j++] = CtrlPtTemplate[i*3+1];
			    types[j] = SEG_CUBICTO;
			    ctrlpts[j++] = CtrlPtTemplate[i*3+2];
			}
		    }
		    types[j] = SEG_CLOSE;
		    ctrlpts[j++] = CtrlPtTemplate[12];
		    types[0] = SEG_MOVETO;
		}
	    }

	    public int getWindingRule() {
		return WIND_NON_ZERO;
	    }

	    public boolean isDone() {
		return index >= ctrlpts.length;
	    }

	    public void next() {
		index++;
	    }

	    public int currentSegment(float[] coords) {
		if (isDone()) {
		    throw new NoSuchElementException("roundrect iterator out of bounds");
		}
		double ctrls[] = ctrlpts[index];
		int nc = 0;
		for (int i = 0; i < ctrls.length; i += 4) {
		    coords[nc++] = (float) (x + ctrls[i + 0] * w + ctrls[i + 1] * aw);
		    coords[nc++] = (float) (y + ctrls[i + 2] * h + ctrls[i + 3] * ah);
		}
		if (affine != null) {
		    affine.transform(coords, 0, coords, 0, nc / 2);
		}
		return types[index];
	    }

	    public int currentSegment(double[] coords) {
		return 0;  // Not called 
	    }
	}
    }
}

