1 /*
   2  * Copyright (c) 2002, 2018, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 package com.sun.java.swing.plaf.gtk;
  26 
  27 import sun.swing.SwingUtilities2;
  28 import com.sun.java.swing.plaf.gtk.GTKConstants.ArrowType;
  29 import com.sun.java.swing.plaf.gtk.GTKConstants.ShadowType;
  30 
  31 import javax.swing.plaf.ColorUIResource;
  32 import javax.swing.plaf.basic.BasicInternalFrameTitlePane;
  33 import javax.swing.plaf.synth.*;
  34 
  35 import java.awt.*;
  36 import java.awt.geom.*;
  37 import java.awt.image.*;
  38 import java.io.*;
  39 import java.net.*;
  40 import java.security.*;
  41 import java.util.*;
  42 
  43 import javax.swing.*;
  44 
  45 import javax.xml.parsers.*;
  46 import org.xml.sax.SAXException;
  47 import org.w3c.dom.*;
  48 
  49 /**
  50  */
  51 class Metacity implements SynthConstants {
  52     // Tutorial:
  53     // http://developer.gnome.org/doc/tutorials/metacity/metacity-themes.html
  54 
  55     // Themes:
  56     // http://art.gnome.org/theme_list.php?category=metacity
  57 
  58     static Metacity INSTANCE;
  59 
  60     private static final String[] themeNames = {
  61         getUserTheme(),
  62         "blueprint",
  63         "Bluecurve",
  64         "Crux",
  65         "SwingFallbackTheme"
  66     };
  67 
  68     static {
  69         for (String themeName : themeNames) {
  70             if (themeName != null) {
  71             try {
  72                 INSTANCE = new Metacity(themeName);
  73             } catch (FileNotFoundException ex) {
  74             } catch (IOException ex) {
  75                 logError(themeName, ex);
  76             } catch (ParserConfigurationException ex) {
  77                 logError(themeName, ex);
  78             } catch (SAXException ex) {
  79                 logError(themeName, ex);
  80             }
  81             }
  82             if (INSTANCE != null) {
  83             break;
  84             }
  85         }
  86         if (INSTANCE == null) {
  87             throw new Error("Could not find any installed metacity theme, and fallback failed");
  88         }
  89     }
  90 
  91     private static boolean errorLogged = false;
  92     private static DocumentBuilder documentBuilder;
  93     private static Document xmlDoc;
  94     private static String userHome;
  95 
  96     private Node frame_style_set;
  97     private Map<String, Object> frameGeometry;
  98     private Map<String, Map<String, Object>> frameGeometries;
  99 
 100     private LayoutManager titlePaneLayout = new TitlePaneLayout();
 101 
 102     private ColorizeImageFilter imageFilter = new ColorizeImageFilter();
 103     private URL themeDir = null;
 104     private SynthContext context;
 105     private String themeName;
 106 
 107     private ArithmeticExpressionEvaluator aee = new ArithmeticExpressionEvaluator();
 108     private Map<String, Integer> variables;
 109 
 110     // Reusable clip shape object
 111     private RoundRectClipShape roundedClipShape;
 112 
 113     protected Metacity(String themeName) throws IOException, ParserConfigurationException, SAXException {
 114         this.themeName = themeName;
 115         themeDir = getThemeDir(themeName);
 116         if (themeDir != null) {
 117             URL themeURL = new URL(themeDir, "metacity-theme-1.xml");
 118             xmlDoc = getXMLDoc(themeURL);
 119             if (xmlDoc == null) {
 120                 throw new IOException(themeURL.toString());
 121             }
 122         } else {
 123             throw new FileNotFoundException(themeName);
 124         }
 125 
 126         // Initialize constants
 127         variables = new HashMap<String, Integer>();
 128         NodeList nodes = xmlDoc.getElementsByTagName("constant");
 129         int n = nodes.getLength();
 130         for (int i = 0; i < n; i++) {
 131             Node node = nodes.item(i);
 132             String name = getStringAttr(node, "name");
 133             if (name != null) {
 134                 String value = getStringAttr(node, "value");
 135                 if (value != null) {
 136                     try {
 137                         variables.put(name, Integer.parseInt(value));
 138                     } catch (NumberFormatException ex) {
 139                         logError(themeName, ex);
 140                         // Ignore bad value
 141                     }
 142                 }
 143             }
 144         }
 145 
 146         // Cache frame geometries
 147         frameGeometries = new HashMap<String, Map<String, Object>>();
 148         nodes = xmlDoc.getElementsByTagName("frame_geometry");
 149         n = nodes.getLength();
 150         for (int i = 0; i < n; i++) {
 151             Node node = nodes.item(i);
 152             String name = getStringAttr(node, "name");
 153             if (name != null) {
 154                 HashMap<String, Object> gm = new HashMap<String, Object>();
 155                 frameGeometries.put(name, gm);
 156 
 157                 String parentGM = getStringAttr(node, "parent");
 158                 if (parentGM != null) {
 159                     gm.putAll(frameGeometries.get(parentGM));
 160                 }
 161 
 162                 gm.put("has_title",
 163                        Boolean.valueOf(getBooleanAttr(node, "has_title",            true)));
 164                 gm.put("rounded_top_left",
 165                        Boolean.valueOf(getBooleanAttr(node, "rounded_top_left",     false)));
 166                 gm.put("rounded_top_right",
 167                        Boolean.valueOf(getBooleanAttr(node, "rounded_top_right",    false)));
 168                 gm.put("rounded_bottom_left",
 169                        Boolean.valueOf(getBooleanAttr(node, "rounded_bottom_left",  false)));
 170                 gm.put("rounded_bottom_right",
 171                        Boolean.valueOf(getBooleanAttr(node, "rounded_bottom_right", false)));
 172 
 173                 NodeList childNodes = node.getChildNodes();
 174                 int nc = childNodes.getLength();
 175                 for (int j = 0; j < nc; j++) {
 176                     Node child = childNodes.item(j);
 177                     if (child.getNodeType() == Node.ELEMENT_NODE) {
 178                         name = child.getNodeName();
 179                         Object value = null;
 180                         if ("distance".equals(name)) {
 181                             value = Integer.valueOf(getIntAttr(child, "value", 0));
 182                         } else if ("border".equals(name)) {
 183                             value = new Insets(getIntAttr(child, "top", 0),
 184                                                getIntAttr(child, "left", 0),
 185                                                getIntAttr(child, "bottom", 0),
 186                                                getIntAttr(child, "right", 0));
 187                         } else if ("aspect_ratio".equals(name)) {
 188                             value = Float.valueOf(getFloatAttr(child, "value", 1.0F));
 189                         } else {
 190                             logError(themeName, "Unknown Metacity frame geometry value type: "+name);
 191                         }
 192                         String childName = getStringAttr(child, "name");
 193                         if (childName != null && value != null) {
 194                             gm.put(childName, value);
 195                         }
 196                     }
 197                 }
 198             }
 199         }
 200         frameGeometry = frameGeometries.get("normal");
 201     }
 202 
 203 
 204     public static LayoutManager getTitlePaneLayout() {
 205         return INSTANCE.titlePaneLayout;
 206     }
 207 
 208     private Shape getRoundedClipShape(int x, int y, int w, int h,
 209                                       int arcw, int arch, int corners) {
 210         if (roundedClipShape == null) {
 211             roundedClipShape = new RoundRectClipShape();
 212         }
 213         roundedClipShape.setRoundedRect(x, y, w, h, arcw, arch, corners);
 214 
 215         return roundedClipShape;
 216     }
 217 
 218     void paintButtonBackground(SynthContext context, Graphics g, int x, int y, int w, int h) {
 219         updateFrameGeometry(context);
 220 
 221         this.context = context;
 222         JButton button = (JButton)context.getComponent();
 223         String buttonName = button.getName();
 224         int buttonState = context.getComponentState();
 225 
 226         JComponent titlePane = (JComponent)button.getParent();
 227         Container titlePaneParent = titlePane.getParent();
 228 
 229         JInternalFrame jif = findInternalFrame(titlePaneParent);
 230         if (jif == null) {
 231             return;
 232         }
 233 
 234         boolean active = jif.isSelected();
 235         button.setOpaque(false);
 236 
 237         String state = "normal";
 238         if ((buttonState & PRESSED) != 0) {
 239             state = "pressed";
 240         } else if ((buttonState & MOUSE_OVER) != 0) {
 241             state = "prelight";
 242         }
 243 
 244         String function = null;
 245         String location = null;
 246         boolean left_corner  = false;
 247         boolean right_corner = false;
 248 
 249 
 250         if (buttonName == "InternalFrameTitlePane.menuButton") {
 251             function = "menu";
 252             location = "left_left";
 253             left_corner = true;
 254         } else if (buttonName == "InternalFrameTitlePane.iconifyButton") {
 255             function = "minimize";
 256             int nButtons = ((jif.isIconifiable() ? 1 : 0) +
 257                             (jif.isMaximizable() ? 1 : 0) +
 258                             (jif.isClosable() ? 1 : 0));
 259             right_corner = (nButtons == 1);
 260             switch (nButtons) {
 261               case 1: location = "right_right"; break;
 262               case 2: location = "right_middle"; break;
 263               case 3: location = "right_left"; break;
 264             }
 265         } else if (buttonName == "InternalFrameTitlePane.maximizeButton") {
 266             function = "maximize";
 267             right_corner = !jif.isClosable();
 268             location = jif.isClosable() ? "right_middle" : "right_right";
 269         } else if (buttonName == "InternalFrameTitlePane.closeButton") {
 270             function = "close";
 271             right_corner = true;
 272             location = "right_right";
 273         }
 274 
 275         Node frame = getNode(frame_style_set, "frame", new String[] {
 276             "focus", (active ? "yes" : "no"),
 277             "state", (jif.isMaximum() ? "maximized" : "normal")
 278         });
 279 
 280         if (function != null && frame != null) {
 281             Node frame_style = getNode("frame_style", new String[] {
 282                 "name", getStringAttr(frame, "style")
 283             });
 284             if (frame_style != null) {
 285                 Shape oldClip = g.getClip();
 286                 if ((right_corner && getBoolean("rounded_top_right", false)) ||
 287                     (left_corner  && getBoolean("rounded_top_left", false))) {
 288 
 289                     Point buttonLoc = button.getLocation();
 290                     if (right_corner) {
 291                         g.setClip(getRoundedClipShape(0, 0, w, h,
 292                                                       12, 12, RoundRectClipShape.TOP_RIGHT));
 293                     } else {
 294                         g.setClip(getRoundedClipShape(0, 0, w, h,
 295                                                       11, 11, RoundRectClipShape.TOP_LEFT));
 296                     }
 297 
 298                     Rectangle clipBounds = oldClip.getBounds();
 299                     g.clipRect(clipBounds.x, clipBounds.y,
 300                                clipBounds.width, clipBounds.height);
 301                 }
 302                 drawButton(frame_style, location+"_background", state, g, w, h, jif);
 303                 drawButton(frame_style, function, state, g, w, h, jif);
 304                 g.setClip(oldClip);
 305             }
 306         }
 307     }
 308 
 309     protected void drawButton(Node frame_style, String function, String state,
 310                             Graphics g, int w, int h, JInternalFrame jif) {
 311         Node buttonNode = getNode(frame_style, "button",
 312                                   new String[] { "function", function, "state", state });
 313         if (buttonNode == null && !state.equals("normal")) {
 314             buttonNode = getNode(frame_style, "button",
 315                                  new String[] { "function", function, "state", "normal" });
 316         }
 317         if (buttonNode != null) {
 318             Node draw_ops;
 319             String draw_ops_name = getStringAttr(buttonNode, "draw_ops");
 320             if (draw_ops_name != null) {
 321                 draw_ops = getNode("draw_ops", new String[] { "name", draw_ops_name });
 322             } else {
 323                 draw_ops = getNode(buttonNode, "draw_ops", null);
 324             }
 325             variables.put("width",  w);
 326             variables.put("height", h);
 327             draw(draw_ops, g, jif);
 328         }
 329     }
 330 
 331     JInternalFrame findInternalFrame(Component comp) {
 332         if (comp.getParent() instanceof BasicInternalFrameTitlePane) {
 333             comp = comp.getParent();
 334         }
 335         if (comp instanceof JInternalFrame) {
 336             return  (JInternalFrame)comp;
 337         } else if (comp instanceof JInternalFrame.JDesktopIcon) {
 338             return ((JInternalFrame.JDesktopIcon)comp).getInternalFrame();
 339         }
 340         assert false : "cannot find the internal frame";
 341         return null;
 342     }
 343 
 344     void paintFrameBorder(SynthContext context, Graphics g, int x0, int y0, int width, int height) {
 345         updateFrameGeometry(context);
 346 
 347         this.context = context;
 348         JComponent comp = context.getComponent();
 349         JComponent titlePane = findChild(comp, "InternalFrame.northPane");
 350 
 351         if (titlePane == null) {
 352             return;
 353         }
 354 
 355         JInternalFrame jif = findInternalFrame(comp);
 356         if (jif == null) {
 357             return;
 358         }
 359 
 360         boolean active = jif.isSelected();
 361         Font oldFont = g.getFont();
 362         g.setFont(titlePane.getFont());
 363         g.translate(x0, y0);
 364 
 365         Rectangle titleRect = calculateTitleArea(jif);
 366         JComponent menuButton = findChild(titlePane, "InternalFrameTitlePane.menuButton");
 367 
 368         Icon frameIcon = jif.getFrameIcon();
 369         variables.put("mini_icon_width",
 370                       (frameIcon != null) ? frameIcon.getIconWidth()  : 0);
 371         variables.put("mini_icon_height",
 372                       (frameIcon != null) ? frameIcon.getIconHeight() : 0);
 373         variables.put("title_width",  calculateTitleTextWidth(g, jif));
 374         FontMetrics fm = SwingUtilities2.getFontMetrics(jif, g);
 375         variables.put("title_height", fm.getAscent() + fm.getDescent());
 376 
 377         // These don't seem to apply here, but the Galaxy theme uses them. Not sure why.
 378         variables.put("icon_width",  32);
 379         variables.put("icon_height", 32);
 380 
 381         if (frame_style_set != null) {
 382             Node frame = getNode(frame_style_set, "frame", new String[] {
 383                 "focus", (active ? "yes" : "no"),
 384                 "state", (jif.isMaximum() ? "maximized" : "normal")
 385             });
 386 
 387             if (frame != null) {
 388                 Node frame_style = getNode("frame_style", new String[] {
 389                     "name", getStringAttr(frame, "style")
 390                 });
 391                 if (frame_style != null) {
 392                     Shape oldClip = g.getClip();
 393                     boolean roundTopLeft     = getBoolean("rounded_top_left",     false);
 394                     boolean roundTopRight    = getBoolean("rounded_top_right",    false);
 395                     boolean roundBottomLeft  = getBoolean("rounded_bottom_left",  false);
 396                     boolean roundBottomRight = getBoolean("rounded_bottom_right", false);
 397 
 398                     if (roundTopLeft || roundTopRight || roundBottomLeft || roundBottomRight) {
 399                         jif.setOpaque(false);
 400 
 401                         g.setClip(getRoundedClipShape(0, 0, width, height, 12, 12,
 402                                         (roundTopLeft     ? RoundRectClipShape.TOP_LEFT     : 0) |
 403                                         (roundTopRight    ? RoundRectClipShape.TOP_RIGHT    : 0) |
 404                                         (roundBottomLeft  ? RoundRectClipShape.BOTTOM_LEFT  : 0) |
 405                                         (roundBottomRight ? RoundRectClipShape.BOTTOM_RIGHT : 0)));
 406                     }
 407 
 408                     Rectangle clipBounds = oldClip.getBounds();
 409                     g.clipRect(clipBounds.x, clipBounds.y,
 410                                clipBounds.width, clipBounds.height);
 411 
 412                     int titleHeight = titlePane.getHeight();
 413 
 414                     boolean minimized = jif.isIcon();
 415                     Insets insets = getBorderInsets(context, null);
 416 
 417                     int leftTitlebarEdge   = getInt("left_titlebar_edge");
 418                     int rightTitlebarEdge  = getInt("right_titlebar_edge");
 419                     int topTitlebarEdge    = getInt("top_titlebar_edge");
 420                     int bottomTitlebarEdge = getInt("bottom_titlebar_edge");
 421 
 422                     if (!minimized) {
 423                         drawPiece(frame_style, g, "entire_background",
 424                                   0, 0, width, height, jif);
 425                     }
 426                     drawPiece(frame_style, g, "titlebar",
 427                               0, 0, width, titleHeight, jif);
 428                     drawPiece(frame_style, g, "titlebar_middle",
 429                               leftTitlebarEdge, topTitlebarEdge,
 430                               width - leftTitlebarEdge - rightTitlebarEdge,
 431                               titleHeight - topTitlebarEdge - bottomTitlebarEdge,
 432                               jif);
 433                     drawPiece(frame_style, g, "left_titlebar_edge",
 434                               0, 0, leftTitlebarEdge, titleHeight, jif);
 435                     drawPiece(frame_style, g, "right_titlebar_edge",
 436                               width - rightTitlebarEdge, 0,
 437                               rightTitlebarEdge, titleHeight, jif);
 438                     drawPiece(frame_style, g, "top_titlebar_edge",
 439                               0, 0, width, topTitlebarEdge, jif);
 440                     drawPiece(frame_style, g, "bottom_titlebar_edge",
 441                               0, titleHeight - bottomTitlebarEdge,
 442                               width, bottomTitlebarEdge, jif);
 443                     drawPiece(frame_style, g, "title",
 444                               titleRect.x, titleRect.y, titleRect.width, titleRect.height, jif);
 445                     if (!minimized) {
 446                         drawPiece(frame_style, g, "left_edge",
 447                                   0, titleHeight, insets.left, height-titleHeight, jif);
 448                         drawPiece(frame_style, g, "right_edge",
 449                                   width-insets.right, titleHeight, insets.right, height-titleHeight, jif);
 450                         drawPiece(frame_style, g, "bottom_edge",
 451                                   0, height - insets.bottom, width, insets.bottom, jif);
 452                         drawPiece(frame_style, g, "overlay",
 453                                   0, 0, width, height, jif);
 454                     }
 455                     g.setClip(oldClip);
 456                 }
 457             }
 458         }
 459         g.translate(-x0, -y0);
 460         g.setFont(oldFont);
 461     }
 462 
 463 
 464 
 465     private static class Privileged implements PrivilegedAction<Object> {
 466         private static int GET_THEME_DIR  = 0;
 467         private static int GET_USER_THEME = 1;
 468         private static int GET_IMAGE      = 2;
 469         private int type;
 470         private Object arg;
 471 
 472         public Object doPrivileged(int type, Object arg) {
 473             this.type = type;
 474             this.arg = arg;
 475             return AccessController.doPrivileged(this);
 476         }
 477 
 478         public Object run() {
 479             if (type == GET_THEME_DIR) {
 480                 String sep = File.separator;
 481                 String[] dirs = new String[] {
 482                     userHome + sep + ".themes",
 483                     System.getProperty("swing.metacitythemedir"),
 484                     "/usr/X11R6/share/themes",
 485                     "/usr/X11R6/share/gnome/themes",
 486                     "/usr/local/share/themes",
 487                     "/usr/local/share/gnome/themes",
 488                     "/usr/share/themes",
 489                     "/usr/gnome/share/themes",  // Debian/Redhat/Solaris
 490                     "/opt/gnome2/share/themes"  // SuSE
 491                 };
 492 
 493                 URL themeDir = null;
 494                 for (int i = 0; i < dirs.length; i++) {
 495                     // System property may not be set so skip null directories.
 496                     if (dirs[i] == null) {
 497                         continue;
 498                     }
 499                     File dir =
 500                         new File(dirs[i] + sep + arg + sep + "metacity-1");
 501                     if (new File(dir, "metacity-theme-1.xml").canRead()) {
 502                         try {
 503                             themeDir = dir.toURI().toURL();
 504                         } catch (MalformedURLException ex) {
 505                             themeDir = null;
 506                         }
 507                         break;
 508                     }
 509                 }
 510                 if (themeDir == null) {
 511                     String filename = "resources/metacity/" + arg +
 512                         "/metacity-1/metacity-theme-1.xml";
 513                     URL url = getClass().getResource(filename);
 514                     if (url != null) {
 515                         String str = url.toString();
 516                         try {
 517                             themeDir = new URL(str.substring(0, str.lastIndexOf('/'))+"/");
 518                         } catch (MalformedURLException ex) {
 519                             themeDir = null;
 520                         }
 521                     }
 522                 }
 523                 return themeDir;
 524             } else if (type == GET_USER_THEME) {
 525                 try {
 526                     // Set userHome here because we need the privilege
 527                     userHome = System.getProperty("user.home");
 528 
 529                     String theme = System.getProperty("swing.metacitythemename");
 530                     if (theme != null) {
 531                         return theme;
 532                     }
 533                     // Note: this is a small file (< 1024 bytes) so it's not worth
 534                     // starting an XML parser or even to use a buffered reader.
 535                     URL url = new URL(new File(userHome).toURI().toURL(),
 536                                       ".gconf/apps/metacity/general/%25gconf.xml");
 537                     // Pending: verify character encoding spec for gconf
 538                     Reader reader = new InputStreamReader(url.openStream(), "ISO-8859-1");
 539                     char[] buf = new char[1024];
 540                     StringBuilder sb = new StringBuilder();
 541                     int n;
 542                     while ((n = reader.read(buf)) >= 0) {
 543                         sb.append(buf, 0, n);
 544                     }
 545                     reader.close();
 546                     String str = sb.toString();
 547                     if (str != null) {
 548                         String strLowerCase = str.toLowerCase();
 549                         int i = strLowerCase.indexOf("<entry name=\"theme\"");
 550                         if (i >= 0) {
 551                             i = strLowerCase.indexOf("<stringvalue>", i);
 552                             if (i > 0) {
 553                                 i += "<stringvalue>".length();
 554                                 int i2 = str.indexOf('<', i);
 555                                 return str.substring(i, i2);
 556                             }
 557                         }
 558                     }
 559                 } catch (MalformedURLException ex) {
 560                     // OK to just ignore. We'll use a fallback theme.
 561                 } catch (IOException ex) {
 562                     // OK to just ignore. We'll use a fallback theme.
 563                 }
 564                 return null;
 565             } else if (type == GET_IMAGE) {
 566                 return new ImageIcon((URL)arg).getImage();
 567             } else {
 568                 return null;
 569             }
 570         }
 571     }
 572 
 573     private static URL getThemeDir(String themeName) {
 574         return (URL)new Privileged().doPrivileged(Privileged.GET_THEME_DIR, themeName);
 575     }
 576 
 577     private static String getUserTheme() {
 578         return (String)new Privileged().doPrivileged(Privileged.GET_USER_THEME, null);
 579     }
 580 
 581     protected void tileImage(Graphics g, Image image, int x0, int y0, int w, int h, float[] alphas) {
 582         Graphics2D g2 = (Graphics2D)g;
 583         Composite oldComp = g2.getComposite();
 584 
 585         int sw = image.getWidth(null);
 586         int sh = image.getHeight(null);
 587         int y = y0;
 588         while (y < y0 + h) {
 589             sh = Math.min(sh, y0 + h - y);
 590             int x = x0;
 591             while (x < x0 + w) {
 592                 float f = (alphas.length - 1.0F) * x / (x0 + w);
 593                 int i = (int)f;
 594                 f -= (int)f;
 595                 float alpha = (1-f) * alphas[i];
 596                 if (i+1 < alphas.length) {
 597                     alpha += f * alphas[i+1];
 598                 }
 599                 g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, alpha));
 600                 int swm = Math.min(sw, x0 + w - x);
 601                 g.drawImage(image, x, y, x+swm, y+sh, 0, 0, swm, sh, null);
 602                 x += swm;
 603             }
 604             y += sh;
 605         }
 606         g2.setComposite(oldComp);
 607     }
 608 
 609     private HashMap<String, Image> images = new HashMap<String, Image>();
 610 
 611     protected Image getImage(String key, Color c) {
 612         Image image = images.get(key+"-"+c.getRGB());
 613         if (image == null) {
 614             image = imageFilter.colorize(getImage(key), c);
 615             if (image != null) {
 616                 images.put(key+"-"+c.getRGB(), image);
 617             }
 618         }
 619         return image;
 620     }
 621 
 622     protected Image getImage(String key) {
 623         Image image = images.get(key);
 624         if (image == null) {
 625             if (themeDir != null) {
 626                 try {
 627                     URL url = new URL(themeDir, key);
 628                     image = (Image)new Privileged().doPrivileged(Privileged.GET_IMAGE, url);
 629                 } catch (MalformedURLException ex) {
 630                     //log("Bad image url: "+ themeDir + "/" + key);
 631                 }
 632             }
 633             if (image != null) {
 634                 images.put(key, image);
 635             }
 636         }
 637         return image;
 638     }
 639 
 640     private class ColorizeImageFilter extends RGBImageFilter {
 641         double cr, cg, cb;
 642 
 643         public ColorizeImageFilter() {
 644             canFilterIndexColorModel = true;
 645         }
 646 
 647         public void setColor(Color color) {
 648             cr = color.getRed()   / 255.0;
 649             cg = color.getGreen() / 255.0;
 650             cb = color.getBlue()  / 255.0;
 651         }
 652 
 653         public Image colorize(Image fromImage, Color c) {
 654             setColor(c);
 655             ImageProducer producer = new FilteredImageSource(fromImage.getSource(), this);
 656             return new ImageIcon(context.getComponent().createImage(producer)).getImage();
 657         }
 658 
 659         public int filterRGB(int x, int y, int rgb) {
 660             // Assume all rgb values are shades of gray
 661             double grayLevel = 2 * (rgb & 0xff) / 255.0;
 662             double r, g, b;
 663 
 664             if (grayLevel <= 1.0) {
 665                 r = cr * grayLevel;
 666                 g = cg * grayLevel;
 667                 b = cb * grayLevel;
 668             } else {
 669                 grayLevel -= 1.0;
 670                 r = cr + (1.0 - cr) * grayLevel;
 671                 g = cg + (1.0 - cg) * grayLevel;
 672                 b = cb + (1.0 - cb) * grayLevel;
 673             }
 674 
 675             return ((rgb & 0xff000000) +
 676                     (((int)(r * 255)) << 16) +
 677                     (((int)(g * 255)) << 8) +
 678                     (int)(b * 255));
 679         }
 680     }
 681 
 682     protected static JComponent findChild(JComponent parent, String name) {
 683         int n = parent.getComponentCount();
 684         for (int i = 0; i < n; i++) {
 685             JComponent c = (JComponent)parent.getComponent(i);
 686             if (name.equals(c.getName())) {
 687                 return c;
 688             }
 689         }
 690         return null;
 691     }
 692 
 693 
 694     protected class TitlePaneLayout implements LayoutManager {
 695         public void addLayoutComponent(String name, Component c) {}
 696         public void removeLayoutComponent(Component c) {}
 697         public Dimension preferredLayoutSize(Container c)  {
 698             return minimumLayoutSize(c);
 699         }
 700 
 701         public Dimension minimumLayoutSize(Container c) {
 702             JComponent titlePane = (JComponent)c;
 703             Container titlePaneParent = titlePane.getParent();
 704             JInternalFrame frame;
 705             if (titlePaneParent instanceof JInternalFrame) {
 706                 frame = (JInternalFrame)titlePaneParent;
 707             } else if (titlePaneParent instanceof JInternalFrame.JDesktopIcon) {
 708                 frame = ((JInternalFrame.JDesktopIcon)titlePaneParent).getInternalFrame();
 709             } else {
 710                 return null;
 711             }
 712 
 713             Dimension buttonDim = calculateButtonSize(titlePane);
 714             Insets title_border  = (Insets)getFrameGeometry().get("title_border");
 715             Insets button_border = (Insets)getFrameGeometry().get("button_border");
 716 
 717             // Calculate width.
 718             int width = getInt("left_titlebar_edge") + buttonDim.width + getInt("right_titlebar_edge");
 719             if (title_border != null) {
 720                 width += title_border.left + title_border.right;
 721             }
 722             if (frame.isClosable()) {
 723                 width += buttonDim.width;
 724             }
 725             if (frame.isMaximizable()) {
 726                 width += buttonDim.width;
 727             }
 728             if (frame.isIconifiable()) {
 729                 width += buttonDim.width;
 730             }
 731             FontMetrics fm = frame.getFontMetrics(titlePane.getFont());
 732             String frameTitle = frame.getTitle();
 733             int title_w = frameTitle != null ? SwingUtilities2.stringWidth(
 734                                frame, fm, frameTitle) : 0;
 735             int title_length = frameTitle != null ? frameTitle.length() : 0;
 736 
 737             // Leave room for three characters in the title.
 738             if (title_length > 3) {
 739                 int subtitle_w = SwingUtilities2.stringWidth(
 740                     frame, fm, frameTitle.substring(0, 3) + "...");
 741                 width += (title_w < subtitle_w) ? title_w : subtitle_w;
 742             } else {
 743                 width += title_w;
 744             }
 745 
 746             // Calculate height.
 747             int titleHeight = fm.getHeight() + getInt("title_vertical_pad");
 748             if (title_border != null) {
 749                 titleHeight += title_border.top + title_border.bottom;
 750             }
 751             int buttonHeight = buttonDim.height;
 752             if (button_border != null) {
 753                 buttonHeight += button_border.top + button_border.bottom;
 754             }
 755             int height = Math.max(buttonHeight, titleHeight);
 756 
 757             return new Dimension(width, height);
 758         }
 759 
 760         public void layoutContainer(Container c) {
 761             JComponent titlePane = (JComponent)c;
 762             Container titlePaneParent = titlePane.getParent();
 763             JInternalFrame frame;
 764             if (titlePaneParent instanceof JInternalFrame) {
 765                 frame = (JInternalFrame)titlePaneParent;
 766             } else if (titlePaneParent instanceof JInternalFrame.JDesktopIcon) {
 767                 frame = ((JInternalFrame.JDesktopIcon)titlePaneParent).getInternalFrame();
 768             } else {
 769                 return;
 770             }
 771             Map<String, Object> gm = getFrameGeometry();
 772 
 773             int w = titlePane.getWidth();
 774             int h = titlePane.getHeight();
 775 
 776             JComponent menuButton     = findChild(titlePane, "InternalFrameTitlePane.menuButton");
 777             JComponent minimizeButton = findChild(titlePane, "InternalFrameTitlePane.iconifyButton");
 778             JComponent maximizeButton = findChild(titlePane, "InternalFrameTitlePane.maximizeButton");
 779             JComponent closeButton    = findChild(titlePane, "InternalFrameTitlePane.closeButton");
 780 
 781             Insets button_border = (Insets)gm.get("button_border");
 782             Dimension buttonDim = calculateButtonSize(titlePane);
 783 
 784             int y = (button_border != null) ? button_border.top : 0;
 785             if (titlePaneParent.getComponentOrientation().isLeftToRight()) {
 786                 int x = getInt("left_titlebar_edge");
 787 
 788                 menuButton.setBounds(x, y, buttonDim.width, buttonDim.height);
 789 
 790                 x = w - buttonDim.width - getInt("right_titlebar_edge");
 791                 if (button_border != null) {
 792                     x -= button_border.right;
 793                 }
 794 
 795                 if (frame.isClosable()) {
 796                     closeButton.setBounds(x, y, buttonDim.width, buttonDim.height);
 797                     x -= buttonDim.width;
 798                 }
 799 
 800                 if (frame.isMaximizable()) {
 801                     maximizeButton.setBounds(x, y, buttonDim.width, buttonDim.height);
 802                     x -= buttonDim.width;
 803                 }
 804 
 805                 if (frame.isIconifiable()) {
 806                     minimizeButton.setBounds(x, y, buttonDim.width, buttonDim.height);
 807                 }
 808             } else {
 809                 int x = w - buttonDim.width - getInt("right_titlebar_edge");
 810 
 811                 menuButton.setBounds(x, y, buttonDim.width, buttonDim.height);
 812 
 813                 x = getInt("left_titlebar_edge");
 814                 if (button_border != null) {
 815                     x += button_border.left;
 816                 }
 817 
 818                 if (frame.isClosable()) {
 819                     closeButton.setBounds(x, y, buttonDim.width, buttonDim.height);
 820                     x += buttonDim.width;
 821                 }
 822 
 823                 if (frame.isMaximizable()) {
 824                     maximizeButton.setBounds(x, y, buttonDim.width, buttonDim.height);
 825                     x += buttonDim.width;
 826                 }
 827 
 828                 if (frame.isIconifiable()) {
 829                     minimizeButton.setBounds(x, y, buttonDim.width, buttonDim.height);
 830                 }
 831             }
 832         }
 833     } // end TitlePaneLayout
 834 
 835     protected Map<String, Object> getFrameGeometry() {
 836         return frameGeometry;
 837     }
 838 
 839     protected void setFrameGeometry(JComponent titlePane, Map<String, Object> gm) {
 840         this.frameGeometry = gm;
 841         if (getInt("top_height") == 0 && titlePane != null) {
 842             gm.put("top_height", Integer.valueOf(titlePane.getHeight()));
 843         }
 844     }
 845 
 846     protected int getInt(String key) {
 847         Integer i = (Integer)frameGeometry.get(key);
 848         if (i == null) {
 849             i = variables.get(key);
 850         }
 851         return (i != null) ? i.intValue() : 0;
 852     }
 853 
 854     protected boolean getBoolean(String key, boolean fallback) {
 855         Boolean b = (Boolean)frameGeometry.get(key);
 856         return (b != null) ? b.booleanValue() : fallback;
 857     }
 858 
 859 
 860     protected void drawArc(Node node, Graphics g) {
 861         NamedNodeMap attrs = node.getAttributes();
 862         Color color = parseColor(getStringAttr(attrs, "color"));
 863         int x = aee.evaluate(getStringAttr(attrs, "x"));
 864         int y = aee.evaluate(getStringAttr(attrs, "y"));
 865         int w = aee.evaluate(getStringAttr(attrs, "width"));
 866         int h = aee.evaluate(getStringAttr(attrs, "height"));
 867         int start_angle = aee.evaluate(getStringAttr(attrs, "start_angle"));
 868         int extent_angle = aee.evaluate(getStringAttr(attrs, "extent_angle"));
 869         boolean filled = getBooleanAttr(node, "filled", false);
 870         if (getInt("width") == -1) {
 871             x -= w;
 872         }
 873         if (getInt("height") == -1) {
 874             y -= h;
 875         }
 876         g.setColor(color);
 877         if (filled) {
 878             g.fillArc(x, y, w, h, start_angle, extent_angle);
 879         } else {
 880             g.drawArc(x, y, w, h, start_angle, extent_angle);
 881         }
 882     }
 883 
 884     protected void drawLine(Node node, Graphics g) {
 885         NamedNodeMap attrs = node.getAttributes();
 886         Color color = parseColor(getStringAttr(attrs, "color"));
 887         int x1 = aee.evaluate(getStringAttr(attrs, "x1"));
 888         int y1 = aee.evaluate(getStringAttr(attrs, "y1"));
 889         int x2 = aee.evaluate(getStringAttr(attrs, "x2"));
 890         int y2 = aee.evaluate(getStringAttr(attrs, "y2"));
 891         int lineWidth = aee.evaluate(getStringAttr(attrs, "width"), 1);
 892         g.setColor(color);
 893         if (lineWidth != 1) {
 894             Graphics2D g2d = (Graphics2D)g;
 895             Stroke stroke = g2d.getStroke();
 896             g2d.setStroke(new BasicStroke((float)lineWidth));
 897             g2d.drawLine(x1, y1, x2, y2);
 898             g2d.setStroke(stroke);
 899         } else {
 900             g.drawLine(x1, y1, x2, y2);
 901         }
 902     }
 903 
 904     protected void drawRectangle(Node node, Graphics g) {
 905         NamedNodeMap attrs = node.getAttributes();
 906         Color color = parseColor(getStringAttr(attrs, "color"));
 907         boolean filled = getBooleanAttr(node, "filled", false);
 908         int x = aee.evaluate(getStringAttr(attrs, "x"));
 909         int y = aee.evaluate(getStringAttr(attrs, "y"));
 910         int w = aee.evaluate(getStringAttr(attrs, "width"));
 911         int h = aee.evaluate(getStringAttr(attrs, "height"));
 912         g.setColor(color);
 913         if (getInt("width") == -1) {
 914             x -= w;
 915         }
 916         if (getInt("height") == -1) {
 917             y -= h;
 918         }
 919         if (filled) {
 920             g.fillRect(x, y, w, h);
 921         } else {
 922             g.drawRect(x, y, w, h);
 923         }
 924     }
 925 
 926     protected void drawTile(Node node, Graphics g, JInternalFrame jif) {
 927         NamedNodeMap attrs = node.getAttributes();
 928         int x0 = aee.evaluate(getStringAttr(attrs, "x"));
 929         int y0 = aee.evaluate(getStringAttr(attrs, "y"));
 930         int w = aee.evaluate(getStringAttr(attrs, "width"));
 931         int h = aee.evaluate(getStringAttr(attrs, "height"));
 932         int tw = aee.evaluate(getStringAttr(attrs, "tile_width"));
 933         int th = aee.evaluate(getStringAttr(attrs, "tile_height"));
 934         int width  = getInt("width");
 935         int height = getInt("height");
 936         if (width == -1) {
 937             x0 -= w;
 938         }
 939         if (height == -1) {
 940             y0 -= h;
 941         }
 942         Shape oldClip = g.getClip();
 943         if (g instanceof Graphics2D) {
 944             ((Graphics2D)g).clip(new Rectangle(x0, y0, w, h));
 945         }
 946         variables.put("width",  tw);
 947         variables.put("height", th);
 948 
 949         Node draw_ops = getNode("draw_ops", new String[] { "name", getStringAttr(node, "name") });
 950 
 951         int y = y0;
 952         while (y < y0 + h) {
 953             int x = x0;
 954             while (x < x0 + w) {
 955                 g.translate(x, y);
 956                 draw(draw_ops, g, jif);
 957                 g.translate(-x, -y);
 958                 x += tw;
 959             }
 960             y += th;
 961         }
 962 
 963         variables.put("width",  width);
 964         variables.put("height", height);
 965         g.setClip(oldClip);
 966     }
 967 
 968     protected void drawTint(Node node, Graphics g) {
 969         NamedNodeMap attrs = node.getAttributes();
 970         Color color = parseColor(getStringAttr(attrs, "color"));
 971         float alpha = Float.parseFloat(getStringAttr(attrs, "alpha"));
 972         int x = aee.evaluate(getStringAttr(attrs, "x"));
 973         int y = aee.evaluate(getStringAttr(attrs, "y"));
 974         int w = aee.evaluate(getStringAttr(attrs, "width"));
 975         int h = aee.evaluate(getStringAttr(attrs, "height"));
 976         if (getInt("width") == -1) {
 977             x -= w;
 978         }
 979         if (getInt("height") == -1) {
 980             y -= h;
 981         }
 982         if (g instanceof Graphics2D) {
 983             Graphics2D g2 = (Graphics2D)g;
 984             Composite oldComp = g2.getComposite();
 985             AlphaComposite ac = AlphaComposite.getInstance(AlphaComposite.SRC_OVER, alpha);
 986             g2.setComposite(ac);
 987             g2.setColor(color);
 988             g2.fillRect(x, y, w, h);
 989             g2.setComposite(oldComp);
 990         }
 991     }
 992 
 993     protected void drawTitle(Node node, Graphics g, JInternalFrame jif) {
 994         NamedNodeMap attrs = node.getAttributes();
 995         String colorStr = getStringAttr(attrs, "color");
 996         int i = colorStr.indexOf("gtk:fg[");
 997         if (i > 0) {
 998             colorStr = colorStr.substring(0, i) + "gtk:text[" + colorStr.substring(i+7);
 999         }
1000         Color color = parseColor(colorStr);
1001         int x = aee.evaluate(getStringAttr(attrs, "x"));
1002         int y = aee.evaluate(getStringAttr(attrs, "y"));
1003 
1004         String title = jif.getTitle();
1005         if (title != null) {
1006             FontMetrics fm = SwingUtilities2.getFontMetrics(jif, g);
1007             title = SwingUtilities2.clipStringIfNecessary(jif, fm, title,
1008                          calculateTitleArea(jif).width);
1009             g.setColor(color);
1010             SwingUtilities2.drawString(jif, g, title, x, y + fm.getAscent());
1011         }
1012     }
1013 
1014     protected Dimension calculateButtonSize(JComponent titlePane) {
1015         int buttonHeight = getInt("button_height");
1016         if (buttonHeight == 0) {
1017             buttonHeight = titlePane.getHeight();
1018             if (buttonHeight == 0) {
1019                 buttonHeight = 13;
1020             } else {
1021                 Insets button_border = (Insets)frameGeometry.get("button_border");
1022                 if (button_border != null) {
1023                     buttonHeight -= (button_border.top + button_border.bottom);
1024                 }
1025             }
1026         }
1027         int buttonWidth = getInt("button_width");
1028         if (buttonWidth == 0) {
1029             buttonWidth = buttonHeight;
1030             Float aspect_ratio = (Float)frameGeometry.get("aspect_ratio");
1031             if (aspect_ratio != null) {
1032                 buttonWidth = (int)(buttonHeight / aspect_ratio.floatValue());
1033             }
1034         }
1035         return new Dimension(buttonWidth, buttonHeight);
1036     }
1037 
1038     protected Rectangle calculateTitleArea(JInternalFrame jif) {
1039         JComponent titlePane = findChild(jif, "InternalFrame.northPane");
1040         Dimension buttonDim = calculateButtonSize(titlePane);
1041         Insets title_border = (Insets)frameGeometry.get("title_border");
1042         Insets button_border = (Insets)getFrameGeometry().get("button_border");
1043 
1044         Rectangle r = new Rectangle();
1045         r.x = getInt("left_titlebar_edge");
1046         r.y = 0;
1047         r.height = titlePane.getHeight();
1048         if (title_border != null) {
1049             r.x += title_border.left;
1050             r.y += title_border.top;
1051             r.height -= (title_border.top + title_border.bottom);
1052         }
1053 
1054         if (titlePane.getParent().getComponentOrientation().isLeftToRight()) {
1055             r.x += buttonDim.width;
1056             if (button_border != null) {
1057                 r.x += button_border.left;
1058             }
1059             r.width = titlePane.getWidth() - r.x - getInt("right_titlebar_edge");
1060             if (jif.isClosable()) {
1061                 r.width -= buttonDim.width;
1062             }
1063             if (jif.isMaximizable()) {
1064                 r.width -= buttonDim.width;
1065             }
1066             if (jif.isIconifiable()) {
1067                 r.width -= buttonDim.width;
1068             }
1069         } else {
1070             if (jif.isClosable()) {
1071                 r.x += buttonDim.width;
1072             }
1073             if (jif.isMaximizable()) {
1074                 r.x += buttonDim.width;
1075             }
1076             if (jif.isIconifiable()) {
1077                 r.x += buttonDim.width;
1078             }
1079             r.width = titlePane.getWidth() - r.x - getInt("right_titlebar_edge")
1080                     - buttonDim.width;
1081             if (button_border != null) {
1082                 r.x -= button_border.right;
1083             }
1084         }
1085         if (title_border != null) {
1086             r.width -= title_border.right;
1087         }
1088         return r;
1089     }
1090 
1091 
1092     protected int calculateTitleTextWidth(Graphics g, JInternalFrame jif) {
1093         String title = jif.getTitle();
1094         if (title != null) {
1095             Rectangle r = calculateTitleArea(jif);
1096             return Math.min(SwingUtilities2.stringWidth(jif,
1097                      SwingUtilities2.getFontMetrics(jif, g), title), r.width);
1098         }
1099         return 0;
1100     }
1101 
1102     protected void setClip(Node node, Graphics g) {
1103         NamedNodeMap attrs = node.getAttributes();
1104         int x = aee.evaluate(getStringAttr(attrs, "x"));
1105         int y = aee.evaluate(getStringAttr(attrs, "y"));
1106         int w = aee.evaluate(getStringAttr(attrs, "width"));
1107         int h = aee.evaluate(getStringAttr(attrs, "height"));
1108         if (getInt("width") == -1) {
1109             x -= w;
1110         }
1111         if (getInt("height") == -1) {
1112             y -= h;
1113         }
1114         if (g instanceof Graphics2D) {
1115             ((Graphics2D)g).clip(new Rectangle(x, y, w, h));
1116         }
1117     }
1118 
1119     protected void drawGTKArrow(Node node, Graphics g) {
1120         NamedNodeMap attrs = node.getAttributes();
1121         String arrow    = getStringAttr(attrs, "arrow");
1122         String shadow   = getStringAttr(attrs, "shadow");
1123         String stateStr = getStringAttr(attrs, "state").toUpperCase();
1124         int x = aee.evaluate(getStringAttr(attrs, "x"));
1125         int y = aee.evaluate(getStringAttr(attrs, "y"));
1126         int w = aee.evaluate(getStringAttr(attrs, "width"));
1127         int h = aee.evaluate(getStringAttr(attrs, "height"));
1128 
1129         int state = -1;
1130         if ("NORMAL".equals(stateStr)) {
1131             state = ENABLED;
1132         } else if ("SELECTED".equals(stateStr)) {
1133             state = SELECTED;
1134         } else if ("INSENSITIVE".equals(stateStr)) {
1135             state = DISABLED;
1136         } else if ("PRELIGHT".equals(stateStr)) {
1137             state = MOUSE_OVER;
1138         }
1139 
1140         ShadowType shadowType = null;
1141         if ("in".equals(shadow)) {
1142             shadowType = ShadowType.IN;
1143         } else if ("out".equals(shadow)) {
1144             shadowType = ShadowType.OUT;
1145         } else if ("etched_in".equals(shadow)) {
1146             shadowType = ShadowType.ETCHED_IN;
1147         } else if ("etched_out".equals(shadow)) {
1148             shadowType = ShadowType.ETCHED_OUT;
1149         } else if ("none".equals(shadow)) {
1150             shadowType = ShadowType.NONE;
1151         }
1152 
1153         ArrowType direction = null;
1154         if ("up".equals(arrow)) {
1155             direction = ArrowType.UP;
1156         } else if ("down".equals(arrow)) {
1157             direction = ArrowType.DOWN;
1158         } else if ("left".equals(arrow)) {
1159             direction = ArrowType.LEFT;
1160         } else if ("right".equals(arrow)) {
1161             direction = ArrowType.RIGHT;
1162         }
1163 
1164         GTKPainter.INSTANCE.paintMetacityElement(context, g, state,
1165                 "metacity-arrow", x, y, w, h, shadowType, direction);
1166     }
1167 
1168     protected void drawGTKBox(Node node, Graphics g) {
1169         NamedNodeMap attrs = node.getAttributes();
1170         String shadow   = getStringAttr(attrs, "shadow");
1171         String stateStr = getStringAttr(attrs, "state").toUpperCase();
1172         int x = aee.evaluate(getStringAttr(attrs, "x"));
1173         int y = aee.evaluate(getStringAttr(attrs, "y"));
1174         int w = aee.evaluate(getStringAttr(attrs, "width"));
1175         int h = aee.evaluate(getStringAttr(attrs, "height"));
1176 
1177         int state = -1;
1178         if ("NORMAL".equals(stateStr)) {
1179             state = ENABLED;
1180         } else if ("SELECTED".equals(stateStr)) {
1181             state = SELECTED;
1182         } else if ("INSENSITIVE".equals(stateStr)) {
1183             state = DISABLED;
1184         } else if ("PRELIGHT".equals(stateStr)) {
1185             state = MOUSE_OVER;
1186         }
1187 
1188         ShadowType shadowType = null;
1189         if ("in".equals(shadow)) {
1190             shadowType = ShadowType.IN;
1191         } else if ("out".equals(shadow)) {
1192             shadowType = ShadowType.OUT;
1193         } else if ("etched_in".equals(shadow)) {
1194             shadowType = ShadowType.ETCHED_IN;
1195         } else if ("etched_out".equals(shadow)) {
1196             shadowType = ShadowType.ETCHED_OUT;
1197         } else if ("none".equals(shadow)) {
1198             shadowType = ShadowType.NONE;
1199         }
1200         GTKPainter.INSTANCE.paintMetacityElement(context, g, state,
1201                 "metacity-box", x, y, w, h, shadowType, null);
1202     }
1203 
1204     protected void drawGTKVLine(Node node, Graphics g) {
1205         NamedNodeMap attrs = node.getAttributes();
1206         String stateStr = getStringAttr(attrs, "state").toUpperCase();
1207 
1208         int x  = aee.evaluate(getStringAttr(attrs, "x"));
1209         int y1 = aee.evaluate(getStringAttr(attrs, "y1"));
1210         int y2 = aee.evaluate(getStringAttr(attrs, "y2"));
1211 
1212         int state = -1;
1213         if ("NORMAL".equals(stateStr)) {
1214             state = ENABLED;
1215         } else if ("SELECTED".equals(stateStr)) {
1216             state = SELECTED;
1217         } else if ("INSENSITIVE".equals(stateStr)) {
1218             state = DISABLED;
1219         } else if ("PRELIGHT".equals(stateStr)) {
1220             state = MOUSE_OVER;
1221         }
1222 
1223         GTKPainter.INSTANCE.paintMetacityElement(context, g, state,
1224                 "metacity-vline", x, y1, 1, y2 - y1, null, null);
1225     }
1226 
1227     protected void drawGradient(Node node, Graphics g) {
1228         NamedNodeMap attrs = node.getAttributes();
1229         String type = getStringAttr(attrs, "type");
1230         float alpha = getFloatAttr(node, "alpha", -1F);
1231         int x = aee.evaluate(getStringAttr(attrs, "x"));
1232         int y = aee.evaluate(getStringAttr(attrs, "y"));
1233         int w = aee.evaluate(getStringAttr(attrs, "width"));
1234         int h = aee.evaluate(getStringAttr(attrs, "height"));
1235         if (getInt("width") == -1) {
1236             x -= w;
1237         }
1238         if (getInt("height") == -1) {
1239             y -= h;
1240         }
1241 
1242         // Get colors from child nodes
1243         Node[] colorNodes = getNodesByName(node, "color");
1244         Color[] colors = new Color[colorNodes.length];
1245         for (int i = 0; i < colorNodes.length; i++) {
1246             colors[i] = parseColor(getStringAttr(colorNodes[i], "value"));
1247         }
1248 
1249         boolean horizontal = ("diagonal".equals(type) || "horizontal".equals(type));
1250         boolean vertical   = ("diagonal".equals(type) || "vertical".equals(type));
1251 
1252         if (g instanceof Graphics2D) {
1253             Graphics2D g2 = (Graphics2D)g;
1254             Composite oldComp = g2.getComposite();
1255             if (alpha >= 0F) {
1256                 g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, alpha));
1257             }
1258             int n = colors.length - 1;
1259             for (int i = 0; i < n; i++) {
1260                 g2.setPaint(new GradientPaint(x + (horizontal ? (i*w/n) : 0),
1261                                               y + (vertical   ? (i*h/n) : 0),
1262                                               colors[i],
1263                                               x + (horizontal ? ((i+1)*w/n) : 0),
1264                                               y + (vertical   ? ((i+1)*h/n) : 0),
1265                                               colors[i+1]));
1266                 g2.fillRect(x + (horizontal ? (i*w/n) : 0),
1267                             y + (vertical   ? (i*h/n) : 0),
1268                             (horizontal ? (w/n) : w),
1269                             (vertical   ? (h/n) : h));
1270             }
1271             g2.setComposite(oldComp);
1272         }
1273     }
1274 
1275     protected void drawImage(Node node, Graphics g) {
1276         NamedNodeMap attrs = node.getAttributes();
1277         String filename = getStringAttr(attrs, "filename");
1278         String colorizeStr = getStringAttr(attrs, "colorize");
1279         Color colorize = (colorizeStr != null) ? parseColor(colorizeStr) : null;
1280         String alpha = getStringAttr(attrs, "alpha");
1281         Image object = (colorize != null) ? getImage(filename, colorize) : getImage(filename);
1282         variables.put("object_width",  object.getWidth(null));
1283         variables.put("object_height", object.getHeight(null));
1284         String fill_type = getStringAttr(attrs, "fill_type");
1285         int x = aee.evaluate(getStringAttr(attrs, "x"));
1286         int y = aee.evaluate(getStringAttr(attrs, "y"));
1287         int w = aee.evaluate(getStringAttr(attrs, "width"));
1288         int h = aee.evaluate(getStringAttr(attrs, "height"));
1289         if (getInt("width") == -1) {
1290             x -= w;
1291         }
1292         if (getInt("height") == -1) {
1293             y -= h;
1294         }
1295 
1296         if (alpha != null) {
1297             if ("tile".equals(fill_type)) {
1298                 StringTokenizer tokenizer = new StringTokenizer(alpha, ":");
1299                 float[] alphas = new float[tokenizer.countTokens()];
1300                 for (int i = 0; i < alphas.length; i++) {
1301                     alphas[i] = Float.parseFloat(tokenizer.nextToken());
1302                 }
1303                 tileImage(g, object, x, y, w, h, alphas);
1304             } else {
1305                 float a = Float.parseFloat(alpha);
1306                 if (g instanceof Graphics2D) {
1307                     Graphics2D g2 = (Graphics2D)g;
1308                     Composite oldComp = g2.getComposite();
1309                     g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, a));
1310                     g2.drawImage(object, x, y, w, h, null);
1311                     g2.setComposite(oldComp);
1312                 }
1313             }
1314         } else {
1315             g.drawImage(object, x, y, w, h, null);
1316         }
1317     }
1318 
1319     protected void drawIcon(Node node, Graphics g, JInternalFrame jif) {
1320         Icon icon = jif.getFrameIcon();
1321         if (icon == null) {
1322             return;
1323         }
1324 
1325         NamedNodeMap attrs = node.getAttributes();
1326         String alpha = getStringAttr(attrs, "alpha");
1327         int x = aee.evaluate(getStringAttr(attrs, "x"));
1328         int y = aee.evaluate(getStringAttr(attrs, "y"));
1329         int w = aee.evaluate(getStringAttr(attrs, "width"));
1330         int h = aee.evaluate(getStringAttr(attrs, "height"));
1331         if (getInt("width") == -1) {
1332             x -= w;
1333         }
1334         if (getInt("height") == -1) {
1335             y -= h;
1336         }
1337 
1338         if (alpha != null) {
1339             float a = Float.parseFloat(alpha);
1340             if (g instanceof Graphics2D) {
1341                 Graphics2D g2 = (Graphics2D)g;
1342                 Composite oldComp = g2.getComposite();
1343                 g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, a));
1344                 icon.paintIcon(jif, g, x, y);
1345                 g2.setComposite(oldComp);
1346             }
1347         } else {
1348             icon.paintIcon(jif, g, x, y);
1349         }
1350     }
1351 
1352     protected void drawInclude(Node node, Graphics g, JInternalFrame jif) {
1353         int oldWidth  = getInt("width");
1354         int oldHeight = getInt("height");
1355 
1356         NamedNodeMap attrs = node.getAttributes();
1357         int x = aee.evaluate(getStringAttr(attrs, "x"),       0);
1358         int y = aee.evaluate(getStringAttr(attrs, "y"),       0);
1359         int w = aee.evaluate(getStringAttr(attrs, "width"),  -1);
1360         int h = aee.evaluate(getStringAttr(attrs, "height"), -1);
1361 
1362         if (w != -1) {
1363             variables.put("width",  w);
1364         }
1365         if (h != -1) {
1366             variables.put("height", h);
1367         }
1368 
1369         Node draw_ops = getNode("draw_ops", new String[] {
1370             "name", getStringAttr(node, "name")
1371         });
1372         g.translate(x, y);
1373         draw(draw_ops, g, jif);
1374         g.translate(-x, -y);
1375 
1376         if (w != -1) {
1377             variables.put("width",  oldWidth);
1378         }
1379         if (h != -1) {
1380             variables.put("height", oldHeight);
1381         }
1382     }
1383 
1384     protected void draw(Node draw_ops, Graphics g, JInternalFrame jif) {
1385         if (draw_ops != null) {
1386             NodeList nodes = draw_ops.getChildNodes();
1387             if (nodes != null) {
1388                 Shape oldClip = g.getClip();
1389                 for (int i = 0; i < nodes.getLength(); i++) {
1390                     Node child = nodes.item(i);
1391                     if (child.getNodeType() == Node.ELEMENT_NODE) {
1392                         try {
1393                             String name = child.getNodeName();
1394                             if ("include".equals(name)) {
1395                                 drawInclude(child, g, jif);
1396                             } else if ("arc".equals(name)) {
1397                                 drawArc(child, g);
1398                             } else if ("clip".equals(name)) {
1399                                 setClip(child, g);
1400                             } else if ("gradient".equals(name)) {
1401                                 drawGradient(child, g);
1402                             } else if ("gtk_arrow".equals(name)) {
1403                                 drawGTKArrow(child, g);
1404                             } else if ("gtk_box".equals(name)) {
1405                                 drawGTKBox(child, g);
1406                             } else if ("gtk_vline".equals(name)) {
1407                                 drawGTKVLine(child, g);
1408                             } else if ("image".equals(name)) {
1409                                 drawImage(child, g);
1410                             } else if ("icon".equals(name)) {
1411                                 drawIcon(child, g, jif);
1412                             } else if ("line".equals(name)) {
1413                                 drawLine(child, g);
1414                             } else if ("rectangle".equals(name)) {
1415                                 drawRectangle(child, g);
1416                             } else if ("tint".equals(name)) {
1417                                 drawTint(child, g);
1418                             } else if ("tile".equals(name)) {
1419                                 drawTile(child, g, jif);
1420                             } else if ("title".equals(name)) {
1421                                 drawTitle(child, g, jif);
1422                             } else {
1423                                 System.err.println("Unknown Metacity drawing op: "+child);
1424                             }
1425                         } catch (NumberFormatException ex) {
1426                             logError(themeName, ex);
1427                         }
1428                     }
1429                 }
1430                 g.setClip(oldClip);
1431             }
1432         }
1433     }
1434 
1435     protected void drawPiece(Node frame_style, Graphics g, String position, int x, int y,
1436                              int width, int height, JInternalFrame jif) {
1437         Node piece = getNode(frame_style, "piece", new String[] { "position", position });
1438         if (piece != null) {
1439             Node draw_ops;
1440             String draw_ops_name = getStringAttr(piece, "draw_ops");
1441             if (draw_ops_name != null) {
1442                 draw_ops = getNode("draw_ops", new String[] { "name", draw_ops_name });
1443             } else {
1444                 draw_ops = getNode(piece, "draw_ops", null);
1445             }
1446             variables.put("width",  width);
1447             variables.put("height", height);
1448             g.translate(x, y);
1449             draw(draw_ops, g, jif);
1450             g.translate(-x, -y);
1451         }
1452     }
1453 
1454 
1455     Insets getBorderInsets(SynthContext context, Insets insets) {
1456         updateFrameGeometry(context);
1457 
1458         if (insets == null) {
1459             insets = new Insets(0, 0, 0, 0);
1460         }
1461         insets.top    = ((Insets)frameGeometry.get("title_border")).top;
1462         insets.bottom = getInt("bottom_height");
1463         insets.left   = getInt("left_width");
1464         insets.right  = getInt("right_width");
1465         return insets;
1466     }
1467 
1468 
1469     private void updateFrameGeometry(SynthContext context) {
1470         this.context = context;
1471         JComponent comp = context.getComponent();
1472         JComponent titlePane = findChild(comp, "InternalFrame.northPane");
1473 
1474         JInternalFrame jif = findInternalFrame(comp);
1475         if (jif == null) {
1476             return;
1477         }
1478 
1479         if (frame_style_set == null) {
1480             Node window = getNode("window", new String[]{"type", "normal"});
1481 
1482             if (window != null) {
1483                 frame_style_set = getNode("frame_style_set",
1484                         new String[] {"name", getStringAttr(window, "style_set")});
1485             }
1486 
1487             if (frame_style_set == null) {
1488                 frame_style_set = getNode("frame_style_set", new String[] {"name", "normal"});
1489             }
1490         }
1491 
1492         if (frame_style_set != null) {
1493             Node frame = getNode(frame_style_set, "frame", new String[] {
1494                 "focus", (jif.isSelected() ? "yes" : "no"),
1495                 "state", (jif.isMaximum() ? "maximized" : "normal")
1496             });
1497 
1498             if (frame != null) {
1499                 Node frame_style = getNode("frame_style", new String[] {
1500                     "name", getStringAttr(frame, "style")
1501                 });
1502                 if (frame_style != null) {
1503                     Map<String, Object> gm = frameGeometries.get(getStringAttr(frame_style, "geometry"));
1504 
1505                     setFrameGeometry(titlePane, gm);
1506                 }
1507             }
1508         }
1509     }
1510 
1511 
1512     protected static void logError(String themeName, Exception ex) {
1513         logError(themeName, ex.toString());
1514     }
1515 
1516     protected static void logError(String themeName, String msg) {
1517         if (!errorLogged) {
1518             System.err.println("Exception in Metacity for theme \""+themeName+"\": "+msg);
1519             errorLogged = true;
1520         }
1521     }
1522 
1523 
1524     // XML Parsing
1525 
1526 
1527     protected static Document getXMLDoc(final URL xmlFile)
1528                                 throws IOException,
1529                                        ParserConfigurationException,
1530                                        SAXException {
1531         if (documentBuilder == null) {
1532             documentBuilder =
1533                 DocumentBuilderFactory.newInstance().newDocumentBuilder();
1534         }
1535         InputStream inputStream =
1536             AccessController.doPrivileged(new PrivilegedAction<InputStream>() {
1537                 public InputStream run() {
1538                     try {
1539                         return new BufferedInputStream(xmlFile.openStream());
1540                     } catch (IOException ex) {
1541                         return null;
1542                     }
1543                 }
1544             });
1545 
1546         Document doc = null;
1547         if (inputStream != null) {
1548             doc = documentBuilder.parse(inputStream);
1549         }
1550         return doc;
1551     }
1552 
1553 
1554     protected Node[] getNodesByName(Node parent, String name) {
1555         NodeList nodes = parent.getChildNodes(); // ElementNode
1556         int n = nodes.getLength();
1557         ArrayList<Node> list = new ArrayList<Node>();
1558         for (int i=0; i < n; i++) {
1559             Node node = nodes.item(i);
1560             if (name.equals(node.getNodeName())) {
1561                 list.add(node);
1562             }
1563         }
1564         return list.toArray(new Node[list.size()]);
1565     }
1566 
1567 
1568 
1569     protected Node getNode(String tagName, String[] attrs) {
1570         NodeList nodes = xmlDoc.getElementsByTagName(tagName);
1571         return (nodes != null) ? getNode(nodes, tagName, attrs) : null;
1572     }
1573 
1574     protected Node getNode(Node parent, String name, String[] attrs) {
1575         Node node = null;
1576         NodeList nodes = parent.getChildNodes();
1577         if (nodes != null) {
1578             node = getNode(nodes, name, attrs);
1579         }
1580         if (node == null) {
1581             String inheritFrom = getStringAttr(parent, "parent");
1582             if (inheritFrom != null) {
1583                 Node inheritFromNode = getNode(parent.getParentNode(),
1584                                                parent.getNodeName(),
1585                                                new String[] { "name", inheritFrom });
1586                 if (inheritFromNode != null) {
1587                     node = getNode(inheritFromNode, name, attrs);
1588                 }
1589             }
1590         }
1591         return node;
1592     }
1593 
1594     protected Node getNode(NodeList nodes, String name, String[] attrs) {
1595         int n = nodes.getLength();
1596         for (int i=0; i < n; i++) {
1597             Node node = nodes.item(i);
1598             if (name.equals(node.getNodeName())) {
1599                 if (attrs != null) {
1600                     NamedNodeMap nodeAttrs = node.getAttributes();
1601                     if (nodeAttrs != null) {
1602                         boolean matches = true;
1603                         int nAttrs = attrs.length / 2;
1604                         for (int a = 0; a < nAttrs; a++) {
1605                             String aName  = attrs[a * 2];
1606                             String aValue = attrs[a * 2 + 1];
1607                             Node attr = nodeAttrs.getNamedItem(aName);
1608                             if (attr == null ||
1609                                 aValue != null && !aValue.equals(attr.getNodeValue())) {
1610                                 matches = false;
1611                                 break;
1612                             }
1613                         }
1614                         if (matches) {
1615                             return node;
1616                         }
1617                     }
1618                 } else {
1619                     return node;
1620                 }
1621             }
1622         }
1623         return null;
1624     }
1625 
1626     protected String getStringAttr(Node node, String name) {
1627         String value = null;
1628         NamedNodeMap attrs = node.getAttributes();
1629         if (attrs != null) {
1630             value = getStringAttr(attrs, name);
1631             if (value == null) {
1632                 String inheritFrom = getStringAttr(attrs, "parent");
1633                 if (inheritFrom != null) {
1634                     Node inheritFromNode = getNode(node.getParentNode(),
1635                                                    node.getNodeName(),
1636                                                    new String[] { "name", inheritFrom });
1637                     if (inheritFromNode != null) {
1638                         value = getStringAttr(inheritFromNode, name);
1639                     }
1640                 }
1641             }
1642         }
1643         return value;
1644     }
1645 
1646     protected String getStringAttr(NamedNodeMap attrs, String name) {
1647         Node item = attrs.getNamedItem(name);
1648         return (item != null) ? item.getNodeValue() : null;
1649     }
1650 
1651     protected boolean getBooleanAttr(Node node, String name, boolean fallback) {
1652         String str = getStringAttr(node, name);
1653         if (str != null) {
1654             return Boolean.valueOf(str).booleanValue();
1655         }
1656         return fallback;
1657     }
1658 
1659     protected int getIntAttr(Node node, String name, int fallback) {
1660         String str = getStringAttr(node, name);
1661         int value = fallback;
1662         if (str != null) {
1663             try {
1664                 value = Integer.parseInt(str);
1665             } catch (NumberFormatException ex) {
1666                 logError(themeName, ex);
1667             }
1668         }
1669         return value;
1670     }
1671 
1672     protected float getFloatAttr(Node node, String name, float fallback) {
1673         String str = getStringAttr(node, name);
1674         float value = fallback;
1675         if (str != null) {
1676             try {
1677                 value = Float.parseFloat(str);
1678             } catch (NumberFormatException ex) {
1679                 logError(themeName, ex);
1680             }
1681         }
1682         return value;
1683     }
1684 
1685 
1686 
1687     protected Color parseColor(String str) {
1688         StringTokenizer tokenizer = new StringTokenizer(str, "/");
1689         int n = tokenizer.countTokens();
1690         if (n > 1) {
1691             String function = tokenizer.nextToken();
1692             if ("shade".equals(function)) {
1693                 assert (n == 3);
1694                 Color c = parseColor2(tokenizer.nextToken());
1695                 float alpha = Float.parseFloat(tokenizer.nextToken());
1696                 return GTKColorType.adjustColor(c, 1.0F, alpha, alpha);
1697             } else if ("blend".equals(function)) {
1698                 assert (n == 4);
1699                 Color  bg = parseColor2(tokenizer.nextToken());
1700                 Color  fg = parseColor2(tokenizer.nextToken());
1701                 float alpha = Float.parseFloat(tokenizer.nextToken());
1702                 if (alpha > 1.0f) {
1703                     alpha = 1.0f / alpha;
1704                 }
1705 
1706                 return new Color((int)(bg.getRed() + ((fg.getRed() - bg.getRed()) * alpha)),
1707                                  (int)(bg.getRed() + ((fg.getRed() - bg.getRed()) * alpha)),
1708                                  (int)(bg.getRed() + ((fg.getRed() - bg.getRed()) * alpha)));
1709             } else {
1710                 System.err.println("Unknown Metacity color function="+str);
1711                 return null;
1712             }
1713         } else {
1714             return parseColor2(str);
1715         }
1716     }
1717 
1718     protected Color parseColor2(String str) {
1719         Color c = null;
1720         if (str.startsWith("gtk:")) {
1721             int i1 = str.indexOf('[');
1722             if (i1 > 3) {
1723                 String typeStr = str.substring(4, i1).toLowerCase();
1724                 int i2 = str.indexOf(']');
1725                 if (i2 > i1+1) {
1726                     String stateStr = str.substring(i1+1, i2).toUpperCase();
1727                     int state = -1;
1728                     if ("ACTIVE".equals(stateStr)) {
1729                         state = PRESSED;
1730                     } else if ("INSENSITIVE".equals(stateStr)) {
1731                         state = DISABLED;
1732                     } else if ("NORMAL".equals(stateStr)) {
1733                         state = ENABLED;
1734                     } else if ("PRELIGHT".equals(stateStr)) {
1735                         state = MOUSE_OVER;
1736                     } else if ("SELECTED".equals(stateStr)) {
1737                         state = SELECTED;
1738                     }
1739                     ColorType type = null;
1740                     if ("fg".equals(typeStr)) {
1741                         type = GTKColorType.FOREGROUND;
1742                     } else if ("bg".equals(typeStr)) {
1743                         type = GTKColorType.BACKGROUND;
1744                     } else if ("base".equals(typeStr)) {
1745                         type = GTKColorType.TEXT_BACKGROUND;
1746                     } else if ("text".equals(typeStr)) {
1747                         type = GTKColorType.TEXT_FOREGROUND;
1748                     } else if ("dark".equals(typeStr)) {
1749                         type = GTKColorType.DARK;
1750                     } else if ("light".equals(typeStr)) {
1751                         type = GTKColorType.LIGHT;
1752                     }
1753                     if (state >= 0 && type != null) {
1754                         c = ((GTKStyle)context.getStyle()).getGTKColor(context, state, type);
1755                     }
1756                 }
1757             }
1758         }
1759         if (c == null) {
1760             c = parseColorString(str);
1761         }
1762         return c;
1763     }
1764 
1765     private static Color parseColorString(String str) {
1766         if (str.charAt(0) == '#') {
1767             str = str.substring(1);
1768 
1769             int i = str.length();
1770 
1771             if (i < 3 || i > 12 || (i % 3) != 0) {
1772                 return null;
1773             }
1774 
1775             i /= 3;
1776 
1777             int r;
1778             int g;
1779             int b;
1780 
1781             try {
1782                 r = Integer.parseInt(str.substring(0, i), 16);
1783                 g = Integer.parseInt(str.substring(i, i * 2), 16);
1784                 b = Integer.parseInt(str.substring(i * 2, i * 3), 16);
1785             } catch (NumberFormatException nfe) {
1786                 return null;
1787             }
1788 
1789             if (i == 4) {
1790                 return new ColorUIResource(r / 65535.0f, g / 65535.0f, b / 65535.0f);
1791             } else if (i == 1) {
1792                 return new ColorUIResource(r / 15.0f, g / 15.0f, b / 15.0f);
1793             } else if (i == 2) {
1794                 return new ColorUIResource(r, g, b);
1795             } else {
1796                 return new ColorUIResource(r / 4095.0f, g / 4095.0f, b / 4095.0f);
1797             }
1798         } else {
1799             return XColors.lookupColor(str);
1800         }
1801     }
1802 
1803     class ArithmeticExpressionEvaluator {
1804         private PeekableStringTokenizer tokenizer;
1805 
1806         int evaluate(String expr) {
1807             tokenizer = new PeekableStringTokenizer(expr, " \t+-*/%()", true);
1808             return Math.round(expression());
1809         }
1810 
1811         int evaluate(String expr, int fallback) {
1812             return (expr != null) ? evaluate(expr) : fallback;
1813         }
1814 
1815         public float expression() {
1816             float value = getTermValue();
1817             boolean done = false;
1818             while (!done && tokenizer.hasMoreTokens()) {
1819                 String next = tokenizer.peek();
1820                 if ("+".equals(next) ||
1821                     "-".equals(next) ||
1822                     "`max`".equals(next) ||
1823                     "`min`".equals(next)) {
1824                     tokenizer.nextToken();
1825                     float value2 = getTermValue();
1826                     if ("+".equals(next)) {
1827                         value += value2;
1828                     } else if ("-".equals(next)) {
1829                         value -= value2;
1830                     } else if ("`max`".equals(next)) {
1831                         value = Math.max(value, value2);
1832                     } else if ("`min`".equals(next)) {
1833                         value = Math.min(value, value2);
1834                     }
1835                 } else {
1836                     done = true;
1837                 }
1838             }
1839             return value;
1840         }
1841 
1842         public float getTermValue() {
1843             float value = getFactorValue();
1844             boolean done = false;
1845             while (!done && tokenizer.hasMoreTokens()) {
1846                 String next = tokenizer.peek();
1847                 if ("*".equals(next) || "/".equals(next) || "%".equals(next)) {
1848                     tokenizer.nextToken();
1849                     float value2 = getFactorValue();
1850                     if ("*".equals(next)) {
1851                         value *= value2;
1852                     } else if ("/".equals(next)) {
1853                         value /= value2;
1854                     } else {
1855                         value %= value2;
1856                     }
1857                 } else {
1858                     done = true;
1859                 }
1860             }
1861             return value;
1862         }
1863 
1864         public float getFactorValue() {
1865             float value;
1866             if ("(".equals(tokenizer.peek())) {
1867                 tokenizer.nextToken();
1868                 value = expression();
1869                 tokenizer.nextToken(); // skip right paren
1870             } else {
1871                 String token = tokenizer.nextToken();
1872                 if (Character.isDigit(token.charAt(0))) {
1873                     value = Float.parseFloat(token);
1874                 } else {
1875                     Integer i = variables.get(token);
1876                     if (i == null) {
1877                         i = (Integer)getFrameGeometry().get(token);
1878                     }
1879                     if (i == null) {
1880                         logError(themeName, "Variable \"" + token + "\" not defined");
1881                         return 0;
1882                     }
1883                     value = (i != null) ? i.intValue() : 0F;
1884                 }
1885             }
1886             return value;
1887         }
1888 
1889 
1890     }
1891 
1892     static class PeekableStringTokenizer extends StringTokenizer {
1893         String token = null;
1894 
1895         public PeekableStringTokenizer(String str, String delim,
1896                                        boolean returnDelims) {
1897             super(str, delim, returnDelims);
1898             peek();
1899         }
1900 
1901         public String peek() {
1902             if (token == null) {
1903                 token = nextToken();
1904             }
1905             return token;
1906         }
1907 
1908         public boolean hasMoreTokens() {
1909             return (token != null || super.hasMoreTokens());
1910         }
1911 
1912         public String nextToken() {
1913             if (token != null) {
1914                 String t = token;
1915                 token = null;
1916                 if (hasMoreTokens()) {
1917                     peek();
1918                 }
1919                 return t;
1920             } else {
1921                 String token = super.nextToken();
1922                 while ((token.equals(" ") || token.equals("\t"))
1923                        && hasMoreTokens()) {
1924                     token = super.nextToken();
1925                 }
1926                 return token;
1927             }
1928         }
1929     }
1930 
1931 
1932     static class RoundRectClipShape extends RectangularShape {
1933         static final int TOP_LEFT = 1;
1934         static final int TOP_RIGHT = 2;
1935         static final int BOTTOM_LEFT = 4;
1936         static final int BOTTOM_RIGHT = 8;
1937 
1938         int x;
1939         int y;
1940         int width;
1941         int height;
1942         int arcwidth;
1943         int archeight;
1944         int corners;
1945 
1946         public RoundRectClipShape() {
1947         }
1948 
1949         public RoundRectClipShape(int x, int y, int w, int h,
1950                                   int arcw, int arch, int corners) {
1951             setRoundedRect(x, y, w, h, arcw, arch, corners);
1952         }
1953 
1954         public void setRoundedRect(int x, int y, int w, int h,
1955                                    int arcw, int arch, int corners) {
1956             this.corners = corners;
1957             this.x = x;
1958             this.y = y;
1959             this.width = w;
1960             this.height = h;
1961             this.arcwidth = arcw;
1962             this.archeight = arch;
1963         }
1964 
1965         public double getX() {
1966             return (double)x;
1967         }
1968 
1969         public double getY() {
1970             return (double)y;
1971         }
1972 
1973         public double getWidth() {
1974             return (double)width;
1975         }
1976 
1977         public double getHeight() {
1978             return (double)height;
1979         }
1980 
1981         public double getArcWidth() {
1982             return (double)arcwidth;
1983         }
1984 
1985         public double getArcHeight() {
1986             return (double)archeight;
1987         }
1988 
1989         public boolean isEmpty() {
1990             return false;  // Not called
1991         }
1992 
1993         public Rectangle2D getBounds2D() {
1994             return null;  // Not called
1995         }
1996 
1997         public int getCornerFlags() {
1998             return corners;
1999         }
2000 
2001         public void setFrame(double x, double y, double w, double h) {
2002             // Not called
2003         }
2004 
2005         public boolean contains(double x, double y) {
2006             return false;  // Not called
2007         }
2008 
2009         private int classify(double coord, double left, double right, double arcsize) {
2010             return 0;  // Not called
2011         }
2012 
2013         public boolean intersects(double x, double y, double w, double h) {
2014             return false;  // Not called
2015         }
2016 
2017         public boolean contains(double x, double y, double w, double h) {
2018             return false;  // Not called
2019         }
2020 
2021         public PathIterator getPathIterator(AffineTransform at) {
2022             return new RoundishRectIterator(this, at);
2023         }
2024 
2025 
2026         static class RoundishRectIterator implements PathIterator {
2027             double x, y, w, h, aw, ah;
2028             AffineTransform affine;
2029             int index;
2030 
2031             double[][] ctrlpts;
2032             int[] types;
2033 
2034             private static final double angle = Math.PI / 4.0;
2035             private static final double a = 1.0 - Math.cos(angle);
2036             private static final double b = Math.tan(angle);
2037             private static final double c = Math.sqrt(1.0 + b * b) - 1 + a;
2038             private static final double cv = 4.0 / 3.0 * a * b / c;
2039             private static final double acv = (1.0 - cv) / 2.0;
2040 
2041             // For each array:
2042             //     4 values for each point {v0, v1, v2, v3}:
2043             //         point = (x + v0 * w + v1 * arcWidth,
2044             //                  y + v2 * h + v3 * arcHeight);
2045             private static final double[][] CtrlPtTemplate = {
2046                 {  0.0,  0.0,  1.0,  0.0 },     /* BOTTOM LEFT corner */
2047                 {  0.0,  0.0,  1.0, -0.5 },     /* BOTTOM LEFT arc start */
2048                 {  0.0,  0.0,  1.0, -acv,       /* BOTTOM LEFT arc curve */
2049                    0.0,  acv,  1.0,  0.0,
2050                    0.0,  0.5,  1.0,  0.0 },
2051                 {  1.0,  0.0,  1.0,  0.0 },     /* BOTTOM RIGHT corner */
2052                 {  1.0, -0.5,  1.0,  0.0 },     /* BOTTOM RIGHT arc start */
2053                 {  1.0, -acv,  1.0,  0.0,       /* BOTTOM RIGHT arc curve */
2054                    1.0,  0.0,  1.0, -acv,
2055                    1.0,  0.0,  1.0, -0.5 },
2056                 {  1.0,  0.0,  0.0,  0.0 },     /* TOP RIGHT corner */
2057                 {  1.0,  0.0,  0.0,  0.5 },     /* TOP RIGHT arc start */
2058                 {  1.0,  0.0,  0.0,  acv,       /* TOP RIGHT arc curve */
2059                    1.0, -acv,  0.0,  0.0,
2060                    1.0, -0.5,  0.0,  0.0 },
2061                 {  0.0,  0.0,  0.0,  0.0 },     /* TOP LEFT corner */
2062                 {  0.0,  0.5,  0.0,  0.0 },     /* TOP LEFT arc start */
2063                 {  0.0,  acv,  0.0,  0.0,       /* TOP LEFT arc curve */
2064                    0.0,  0.0,  0.0,  acv,
2065                    0.0,  0.0,  0.0,  0.5 },
2066                 {},                             /* Closing path element */
2067             };
2068             private static final int[] CornerFlags = {
2069                 RoundRectClipShape.BOTTOM_LEFT,
2070                 RoundRectClipShape.BOTTOM_RIGHT,
2071                 RoundRectClipShape.TOP_RIGHT,
2072                 RoundRectClipShape.TOP_LEFT,
2073             };
2074 
2075             RoundishRectIterator(RoundRectClipShape rr, AffineTransform at) {
2076                 this.x = rr.getX();
2077                 this.y = rr.getY();
2078                 this.w = rr.getWidth();
2079                 this.h = rr.getHeight();
2080                 this.aw = Math.min(w, Math.abs(rr.getArcWidth()));
2081                 this.ah = Math.min(h, Math.abs(rr.getArcHeight()));
2082                 this.affine = at;
2083                 if (w < 0 || h < 0) {
2084                     // Don't draw anything...
2085                     ctrlpts = new double[0][];
2086                     types = new int[0];
2087                 } else {
2088                     int corners = rr.getCornerFlags();
2089                     int numedges = 5;  // 4xCORNER_POINT, CLOSE
2090                     for (int i = 1; i < 0x10; i <<= 1) {
2091                         // Add one for each corner that has a curve
2092                         if ((corners & i) != 0) numedges++;
2093                     }
2094                     ctrlpts = new double[numedges][];
2095                     types = new int[numedges];
2096                     int j = 0;
2097                     for (int i = 0; i < 4; i++) {
2098                         types[j] = SEG_LINETO;
2099                         if ((corners & CornerFlags[i]) == 0) {
2100                             ctrlpts[j++] = CtrlPtTemplate[i*3+0];
2101                         } else {
2102                             ctrlpts[j++] = CtrlPtTemplate[i*3+1];
2103                             types[j] = SEG_CUBICTO;
2104                             ctrlpts[j++] = CtrlPtTemplate[i*3+2];
2105                         }
2106                     }
2107                     types[j] = SEG_CLOSE;
2108                     ctrlpts[j++] = CtrlPtTemplate[12];
2109                     types[0] = SEG_MOVETO;
2110                 }
2111             }
2112 
2113             public int getWindingRule() {
2114                 return WIND_NON_ZERO;
2115             }
2116 
2117             public boolean isDone() {
2118                 return index >= ctrlpts.length;
2119             }
2120 
2121             public void next() {
2122                 index++;
2123             }
2124 
2125             public int currentSegment(float[] coords) {
2126                 if (isDone()) {
2127                     throw new NoSuchElementException("roundrect iterator out of bounds");
2128                 }
2129                 double[] ctrls = ctrlpts[index];
2130                 int nc = 0;
2131                 for (int i = 0; i < ctrls.length; i += 4) {
2132                     coords[nc++] = (float) (x + ctrls[i + 0] * w + ctrls[i + 1] * aw);
2133                     coords[nc++] = (float) (y + ctrls[i + 2] * h + ctrls[i + 3] * ah);
2134                 }
2135                 if (affine != null) {
2136                     affine.transform(coords, 0, coords, 0, nc / 2);
2137                 }
2138                 return types[index];
2139             }
2140 
2141             public int currentSegment(double[] coords) {
2142                 if (isDone()) {
2143                     throw new NoSuchElementException("roundrect iterator out of bounds");
2144                 }
2145                 double[] ctrls = ctrlpts[index];
2146                 int nc = 0;
2147                 for (int i = 0; i < ctrls.length; i += 4) {
2148                     coords[nc++] = x + ctrls[i + 0] * w + ctrls[i + 1] * aw;
2149                     coords[nc++] = y + ctrls[i + 2] * h + ctrls[i + 3] * ah;
2150                 }
2151                 if (affine != null) {
2152                     affine.transform(coords, 0, coords, 0, nc / 2);
2153                 }
2154                 return types[index];
2155             }
2156         }
2157     }
2158 }