/* * Copyright (c) 1997, 2011, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. Oracle designates this * particular file as subject to the "Classpath" exception as provided * by Oracle in the LICENSE file that accompanied this code. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA * or visit www.oracle.com if you need additional information or have any * questions. */ package javax.swing.text.html; import sun.swing.SwingUtilities2; import java.util.*; import java.awt.*; import java.io.*; import java.net.*; import javax.swing.Icon; import javax.swing.ImageIcon; import javax.swing.UIManager; import javax.swing.border.*; import javax.swing.event.ChangeListener; import javax.swing.text.*; /** * Support for defining the visual characteristics of * HTML views being rendered. The StyleSheet is used to * translate the HTML model into visual characteristics. * This enables views to be customized by a look-and-feel, * multiple views over the same model can be rendered * differently, etc. This can be thought of as a CSS * rule repository. The key for CSS attributes is an * object of type CSS.Attribute. The type of the value * is up to the StyleSheet implementation, but the * toString method is required * to return a string representation of CSS value. *

* The primary entry point for HTML View implementations * to get their attributes is the * {@link #getViewAttributes getViewAttributes} * method. This should be implemented to establish the * desired policy used to associate attributes with the view. * Each HTMLEditorKit (i.e. and therefore each associated * JEditorPane) can have its own StyleSheet, but by default one * sheet will be shared by all of the HTMLEditorKit instances. * HTMLDocument instance can also have a StyleSheet, which * holds the document-specific CSS specifications. *

* In order for Views to store less state and therefore be * more lightweight, the StyleSheet can act as a factory for * painters that handle some of the rendering tasks. This allows * implementations to determine what they want to cache * and have the sharing potentially at the level that a * selector is common to multiple views. Since the StyleSheet * may be used by views over multiple documents and typically * the HTML attributes don't effect the selector being used, * the potential for sharing is significant. *

* The rules are stored as named styles, and other information * is stored to translate the context of an element to a * rule quickly. The following code fragment will display * the named styles, and therefore the CSS rules contained. *

 *  
 *   import java.util.*;
 *   import javax.swing.text.*;
 *   import javax.swing.text.html.*;
 *  
 *   public class ShowStyles {
 *  
 *       public static void main(String[] args) {
 *         HTMLEditorKit kit = new HTMLEditorKit();
 *         HTMLDocument doc = (HTMLDocument) kit.createDefaultDocument();
 *         StyleSheet styles = doc.getStyleSheet();
 *  
 *         Enumeration rules = styles.getStyleNames();
 *         while (rules.hasMoreElements()) {
 *             String name = (String) rules.nextElement();
 *             Style rule = styles.getStyle(name);
 *             System.out.println(rule.toString());
 *         }
 *         System.exit(0);
 *       }
 *   }
 *  
 * 
*

* The semantics for when a CSS style should overide visual attributes * defined by an element are not well defined. For example, the html * <body bgcolor=red> makes the body have a red * background. But if the html file also contains the CSS rule * body { background: blue } it becomes less clear as to * what color the background of the body should be. The current * implemention gives visual attributes defined in the element the * highest precedence, that is they are always checked before any styles. * Therefore, in the previous example the background would have a * red color as the body element defines the background color to be red. *

* As already mentioned this supports CSS. We don't support the full CSS * spec. Refer to the javadoc of the CSS class to see what properties * we support. The two major CSS parsing related * concepts we do not currently * support are pseudo selectors, such as A:link { color: red }, * and the important modifier. *

* Note: This implementation is currently * incomplete. It can be replaced with alternative implementations * that are complete. Future versions of this class will provide * better CSS support. * * @author Timothy Prinzing * @author Sunita Mani * @author Sara Swanson * @author Jill Nakata */ public class StyleSheet extends StyleContext { // As the javadoc states, this class maintains a mapping between // a CSS selector (such as p.bar) and a Style. // This consists of a number of parts: // . Each selector is broken down into its constituent simple selectors, // and stored in an inverted graph, for example: // p { color: red } ol p { font-size: 10pt } ul p { font-size: 12pt } // results in the graph: // root // | // p // / \ // ol ul // each node (an instance of SelectorMapping) has an associated // specificity and potentially a Style. // . Every rule that is asked for (either by way of getRule(String) or // getRule(HTML.Tag, Element)) results in a unique instance of // ResolvedStyle. ResolvedStyles contain the AttributeSets from the // SelectorMapping. // . When a new rule is created it is inserted into the graph, and // the AttributeSets of each ResolvedStyles are updated appropriately. // . This class creates special AttributeSets, LargeConversionSet and // SmallConversionSet, that maintain a mapping between StyleConstants // and CSS so that developers that wish to use the StyleConstants // methods can do so. // . When one of the AttributeSets is mutated by way of a // StyleConstants key, all the associated CSS keys are removed. This is // done so that the two representations don't get out of sync. For // example, if the developer adds StyleConsants.BOLD, FALSE to an // AttributeSet that contains HTML.Tag.B, the HTML.Tag.B entry will // be removed. /** * Construct a StyleSheet */ public StyleSheet() { super(); selectorMapping = new SelectorMapping(0); resolvedStyles = new Hashtable(); if (css == null) { css = new CSS(); } } /** * Fetches the style to use to render the given type * of HTML tag. The element given is representing * the tag and can be used to determine the nesting * for situations where the attributes will differ * if nesting inside of elements. * * @param t the type to translate to visual attributes * @param e the element representing the tag; the element * can be used to determine the nesting for situations where * the attributes will differ if nested inside of other * elements * @return the set of CSS attributes to use to render * the tag */ public Style getRule(HTML.Tag t, Element e) { SearchBuffer sb = SearchBuffer.obtainSearchBuffer(); try { // Build an array of all the parent elements. Vector searchContext = sb.getVector(); for (Element p = e; p != null; p = p.getParentElement()) { searchContext.addElement(p); } // Build a fully qualified selector. int n = searchContext.size(); StringBuffer cacheLookup = sb.getStringBuffer(); AttributeSet attr; String eName; Object name; // >= 1 as the HTML.Tag for the 0th element is passed in. for (int counter = n - 1; counter >= 1; counter--) { e = searchContext.elementAt(counter); attr = e.getAttributes(); name = attr.getAttribute(StyleConstants.NameAttribute); eName = name.toString(); cacheLookup.append(eName); if (attr != null) { if (attr.isDefined(HTML.Attribute.ID)) { cacheLookup.append('#'); cacheLookup.append(attr.getAttribute (HTML.Attribute.ID)); } else if (attr.isDefined(HTML.Attribute.CLASS)) { cacheLookup.append('.'); cacheLookup.append(attr.getAttribute (HTML.Attribute.CLASS)); } } cacheLookup.append(' '); } cacheLookup.append(t.toString()); e = searchContext.elementAt(0); attr = e.getAttributes(); if (e.isLeaf()) { // For leafs, we use the second tier attributes. Object testAttr = attr.getAttribute(t); if (testAttr instanceof AttributeSet) { attr = (AttributeSet)testAttr; } else { attr = null; } } if (attr != null) { if (attr.isDefined(HTML.Attribute.ID)) { cacheLookup.append('#'); cacheLookup.append(attr.getAttribute(HTML.Attribute.ID)); } else if (attr.isDefined(HTML.Attribute.CLASS)) { cacheLookup.append('.'); cacheLookup.append(attr.getAttribute (HTML.Attribute.CLASS)); } } Style style = getResolvedStyle(cacheLookup.toString(), searchContext, t); return style; } finally { SearchBuffer.releaseSearchBuffer(sb); } } /** * Fetches the rule that best matches the selector given * in string form. Where selector is a space separated * String of the element names. For example, selector * might be 'html body tr td''

* The attributes of the returned Style will change * as rules are added and removed. That is if you to ask for a rule * with a selector "table p" and a new rule was added with a selector * of "p" the returned Style would include the new attributes from * the rule "p". */ public Style getRule(String selector) { selector = cleanSelectorString(selector); if (selector != null) { Style style = getResolvedStyle(selector); return style; } return null; } /** * Adds a set of rules to the sheet. The rules are expected to * be in valid CSS format. Typically this would be called as * a result of parsing a <style> tag. */ public void addRule(String rule) { if (rule != null) { //tweaks to control display properties //see BasicEditorPaneUI final String baseUnitsDisable = "BASE_SIZE_DISABLE"; final String baseUnits = "BASE_SIZE "; final String w3cLengthUnitsEnable = "W3C_LENGTH_UNITS_ENABLE"; final String w3cLengthUnitsDisable = "W3C_LENGTH_UNITS_DISABLE"; if (rule == baseUnitsDisable) { sizeMap = sizeMapDefault; } else if (rule.startsWith(baseUnits)) { rebaseSizeMap(Integer. parseInt(rule.substring(baseUnits.length()))); } else if (rule == w3cLengthUnitsEnable) { w3cLengthUnits = true; } else if (rule == w3cLengthUnitsDisable) { w3cLengthUnits = false; } else { CssParser parser = new CssParser(); try { parser.parse(getBase(), new StringReader(rule), false, false); } catch (IOException ioe) { } } } } /** * Translates a CSS declaration to an AttributeSet that represents * the CSS declaration. Typically this would be called as a * result of encountering an HTML style attribute. */ public AttributeSet getDeclaration(String decl) { if (decl == null) { return SimpleAttributeSet.EMPTY; } CssParser parser = new CssParser(); return parser.parseDeclaration(decl); } /** * Loads a set of rules that have been specified in terms of * CSS1 grammar. If there are collisions with existing rules, * the newly specified rule will win. * * @param in the stream to read the CSS grammar from * @param ref the reference URL. This value represents the * location of the stream and may be null. All relative * URLs specified in the stream will be based upon this * parameter. */ public void loadRules(Reader in, URL ref) throws IOException { CssParser parser = new CssParser(); parser.parse(ref, in, false, false); } /** * Fetches a set of attributes to use in the view for * displaying. This is basically a set of attributes that * can be used for View.getAttributes. */ public AttributeSet getViewAttributes(View v) { return new ViewAttributeSet(v); } /** * Removes a named style previously added to the document. * * @param nm the name of the style to remove */ public void removeStyle(String nm) { Style aStyle = getStyle(nm); if (aStyle != null) { String selector = cleanSelectorString(nm); String[] selectors = getSimpleSelectors(selector); synchronized(this) { SelectorMapping mapping = getRootSelectorMapping(); for (int i = selectors.length - 1; i >= 0; i--) { mapping = mapping.getChildSelectorMapping(selectors[i], true); } Style rule = mapping.getStyle(); if (rule != null) { mapping.setStyle(null); if (resolvedStyles.size() > 0) { Enumeration values = resolvedStyles.elements(); while (values.hasMoreElements()) { ResolvedStyle style = values.nextElement(); style.removeStyle(rule); } } } } } super.removeStyle(nm); } /** * Adds the rules from the StyleSheet ss to those of * the receiver. ss's rules will override the rules of * any previously added style sheets. An added StyleSheet will never * override the rules of the receiving style sheet. * * @since 1.3 */ public void addStyleSheet(StyleSheet ss) { synchronized(this) { if (linkedStyleSheets == null) { linkedStyleSheets = new Vector(); } if (!linkedStyleSheets.contains(ss)) { int index = 0; if (ss instanceof javax.swing.plaf.UIResource && linkedStyleSheets.size() > 1) { index = linkedStyleSheets.size() - 1; } linkedStyleSheets.insertElementAt(ss, index); linkStyleSheetAt(ss, index); } } } /** * Removes the StyleSheet ss from those of the receiver. * * @since 1.3 */ public void removeStyleSheet(StyleSheet ss) { synchronized(this) { if (linkedStyleSheets != null) { int index = linkedStyleSheets.indexOf(ss); if (index != -1) { linkedStyleSheets.removeElementAt(index); unlinkStyleSheet(ss, index); if (index == 0 && linkedStyleSheets.size() == 0) { linkedStyleSheets = null; } } } } } // // The following is used to import style sheets. // /** * Returns an array of the linked StyleSheets. Will return null * if there are no linked StyleSheets. * * @since 1.3 */ public StyleSheet[] getStyleSheets() { StyleSheet[] retValue; synchronized(this) { if (linkedStyleSheets != null) { retValue = new StyleSheet[linkedStyleSheets.size()]; linkedStyleSheets.copyInto(retValue); } else { retValue = null; } } return retValue; } /** * Imports a style sheet from url. The resulting rules * are directly added to the receiver. If you do not want the rules * to become part of the receiver, create a new StyleSheet and use * addStyleSheet to link it in. * * @since 1.3 */ public void importStyleSheet(URL url) { try { InputStream is; is = url.openStream(); Reader r = new BufferedReader(new InputStreamReader(is)); CssParser parser = new CssParser(); parser.parse(url, r, false, true); r.close(); is.close(); } catch (Throwable e) { // on error we simply have no styles... the html // will look mighty wrong but still function. } } /** * Sets the base. All import statements that are relative, will be * relative to base. * * @since 1.3 */ public void setBase(URL base) { this.base = base; } /** * Returns the base. * * @since 1.3 */ public URL getBase() { return base; } /** * Adds a CSS attribute to the given set. * * @since 1.3 */ public void addCSSAttribute(MutableAttributeSet attr, CSS.Attribute key, String value) { css.addInternalCSSValue(attr, key, value); } /** * Adds a CSS attribute to the given set. * * @since 1.3 */ public boolean addCSSAttributeFromHTML(MutableAttributeSet attr, CSS.Attribute key, String value) { Object iValue = css.getCssValue(key, value); if (iValue != null) { attr.addAttribute(key, iValue); return true; } return false; } // ---- Conversion functionality --------------------------------- /** * Converts a set of HTML attributes to an equivalent * set of CSS attributes. * * @param htmlAttrSet AttributeSet containing the HTML attributes. */ public AttributeSet translateHTMLToCSS(AttributeSet htmlAttrSet) { AttributeSet cssAttrSet = css.translateHTMLToCSS(htmlAttrSet); MutableAttributeSet cssStyleSet = addStyle(null, null); cssStyleSet.addAttributes(cssAttrSet); return cssStyleSet; } /** * Adds an attribute to the given set, and returns * the new representative set. This is reimplemented to * convert StyleConstant attributes to CSS prior to forwarding * to the superclass behavior. The StyleConstants attribute * has no corresponding CSS entry, the StyleConstants attribute * is stored (but will likely be unused). * * @param old the old attribute set * @param key the non-null attribute key * @param value the attribute value * @return the updated attribute set * @see MutableAttributeSet#addAttribute */ public AttributeSet addAttribute(AttributeSet old, Object key, Object value) { if (css == null) { // supers constructor will call this before returning, // and we need to make sure CSS is non null. css = new CSS(); } if (key instanceof StyleConstants) { HTML.Tag tag = HTML.getTagForStyleConstantsKey( (StyleConstants)key); if (tag != null && old.isDefined(tag)) { old = removeAttribute(old, tag); } Object cssValue = css.styleConstantsValueToCSSValue ((StyleConstants)key, value); if (cssValue != null) { Object cssKey = css.styleConstantsKeyToCSSKey ((StyleConstants)key); if (cssKey != null) { return super.addAttribute(old, cssKey, cssValue); } } } return super.addAttribute(old, key, value); } /** * Adds a set of attributes to the element. If any of these attributes * are StyleConstants attributes, they will be converted to CSS prior * to forwarding to the superclass behavior. * * @param old the old attribute set * @param attr the attributes to add * @return the updated attribute set * @see MutableAttributeSet#addAttribute */ public AttributeSet addAttributes(AttributeSet old, AttributeSet attr) { if (!(attr instanceof HTMLDocument.TaggedAttributeSet)) { old = removeHTMLTags(old, attr); } return super.addAttributes(old, convertAttributeSet(attr)); } /** * Removes an attribute from the set. If the attribute is a StyleConstants * attribute, the request will be converted to a CSS attribute prior to * forwarding to the superclass behavior. * * @param old the old set of attributes * @param key the non-null attribute name * @return the updated attribute set * @see MutableAttributeSet#removeAttribute */ public AttributeSet removeAttribute(AttributeSet old, Object key) { if (key instanceof StyleConstants) { HTML.Tag tag = HTML.getTagForStyleConstantsKey( (StyleConstants)key); if (tag != null) { old = super.removeAttribute(old, tag); } Object cssKey = css.styleConstantsKeyToCSSKey((StyleConstants)key); if (cssKey != null) { return super.removeAttribute(old, cssKey); } } return super.removeAttribute(old, key); } /** * Removes a set of attributes for the element. If any of the attributes * is a StyleConstants attribute, the request will be converted to a CSS * attribute prior to forwarding to the superclass behavior. * * @param old the old attribute set * @param names the attribute names * @return the updated attribute set * @see MutableAttributeSet#removeAttributes */ public AttributeSet removeAttributes(AttributeSet old, Enumeration names) { // PENDING: Should really be doing something similar to // removeHTMLTags here, but it is rather expensive to have to // clone names return super.removeAttributes(old, names); } /** * Removes a set of attributes. If any of the attributes * is a StyleConstants attribute, the request will be converted to a CSS * attribute prior to forwarding to the superclass behavior. * * @param old the old attribute set * @param attrs the attributes * @return the updated attribute set * @see MutableAttributeSet#removeAttributes */ public AttributeSet removeAttributes(AttributeSet old, AttributeSet attrs) { if (old != attrs) { old = removeHTMLTags(old, attrs); } return super.removeAttributes(old, convertAttributeSet(attrs)); } /** * Creates a compact set of attributes that might be shared. * This is a hook for subclasses that want to alter the * behavior of SmallAttributeSet. This can be reimplemented * to return an AttributeSet that provides some sort of * attribute conversion. * * @param a The set of attributes to be represented in the * the compact form. */ protected SmallAttributeSet createSmallAttributeSet(AttributeSet a) { return new SmallConversionSet(a); } /** * Creates a large set of attributes that should trade off * space for time. This set will not be shared. This is * a hook for subclasses that want to alter the behavior * of the larger attribute storage format (which is * SimpleAttributeSet by default). This can be reimplemented * to return a MutableAttributeSet that provides some sort of * attribute conversion. * * @param a The set of attributes to be represented in the * the larger form. */ protected MutableAttributeSet createLargeAttributeSet(AttributeSet a) { return new LargeConversionSet(a); } /** * For any StyleConstants key in attr that has an associated HTML.Tag, * it is removed from old. The resulting AttributeSet is then returned. */ private AttributeSet removeHTMLTags(AttributeSet old, AttributeSet attr) { if (!(attr instanceof LargeConversionSet) && !(attr instanceof SmallConversionSet)) { Enumeration names = attr.getAttributeNames(); while (names.hasMoreElements()) { Object key = names.nextElement(); if (key instanceof StyleConstants) { HTML.Tag tag = HTML.getTagForStyleConstantsKey( (StyleConstants)key); if (tag != null && old.isDefined(tag)) { old = super.removeAttribute(old, tag); } } } } return old; } /** * Converts a set of attributes (if necessary) so that * any attributes that were specified as StyleConstants * attributes and have a CSS mapping, will be converted * to CSS attributes. */ AttributeSet convertAttributeSet(AttributeSet a) { if ((a instanceof LargeConversionSet) || (a instanceof SmallConversionSet)) { // known to be converted. return a; } // in most cases, there are no StyleConstants attributes // so we iterate the collection of keys to avoid creating // a new set. Enumeration names = a.getAttributeNames(); while (names.hasMoreElements()) { Object name = names.nextElement(); if (name instanceof StyleConstants) { // we really need to do a conversion, iterate again // building a new set. MutableAttributeSet converted = new LargeConversionSet(); Enumeration keys = a.getAttributeNames(); while (keys.hasMoreElements()) { Object key = keys.nextElement(); Object cssValue = null; if (key instanceof StyleConstants) { // convert the StyleConstants attribute if possible Object cssKey = css.styleConstantsKeyToCSSKey ((StyleConstants)key); if (cssKey != null) { Object value = a.getAttribute(key); cssValue = css.styleConstantsValueToCSSValue ((StyleConstants)key, value); if (cssValue != null) { converted.addAttribute(cssKey, cssValue); } } } if (cssValue == null) { converted.addAttribute(key, a.getAttribute(key)); } } return converted; } } return a; } /** * Large set of attributes that does conversion of requests * for attributes of type StyleConstants. */ class LargeConversionSet extends SimpleAttributeSet { /** * Creates a new attribute set based on a supplied set of attributes. * * @param source the set of attributes */ public LargeConversionSet(AttributeSet source) { super(source); } public LargeConversionSet() { super(); } /** * Checks whether a given attribute is defined. * * @param key the attribute key * @return true if the attribute is defined * @see AttributeSet#isDefined */ public boolean isDefined(Object key) { if (key instanceof StyleConstants) { Object cssKey = css.styleConstantsKeyToCSSKey ((StyleConstants)key); if (cssKey != null) { return super.isDefined(cssKey); } } return super.isDefined(key); } /** * Gets the value of an attribute. * * @param key the attribute name * @return the attribute value * @see AttributeSet#getAttribute */ public Object getAttribute(Object key) { if (key instanceof StyleConstants) { Object cssKey = css.styleConstantsKeyToCSSKey ((StyleConstants)key); if (cssKey != null) { Object value = super.getAttribute(cssKey); if (value != null) { return css.cssValueToStyleConstantsValue ((StyleConstants)key, value); } } } return super.getAttribute(key); } } /** * Small set of attributes that does conversion of requests * for attributes of type StyleConstants. */ class SmallConversionSet extends SmallAttributeSet { /** * Creates a new attribute set based on a supplied set of attributes. * * @param attrs the set of attributes */ public SmallConversionSet(AttributeSet attrs) { super(attrs); } /** * Checks whether a given attribute is defined. * * @param key the attribute key * @return true if the attribute is defined * @see AttributeSet#isDefined */ public boolean isDefined(Object key) { if (key instanceof StyleConstants) { Object cssKey = css.styleConstantsKeyToCSSKey ((StyleConstants)key); if (cssKey != null) { return super.isDefined(cssKey); } } return super.isDefined(key); } /** * Gets the value of an attribute. * * @param key the attribute name * @return the attribute value * @see AttributeSet#getAttribute */ public Object getAttribute(Object key) { if (key instanceof StyleConstants) { Object cssKey = css.styleConstantsKeyToCSSKey ((StyleConstants)key); if (cssKey != null) { Object value = super.getAttribute(cssKey); if (value != null) { return css.cssValueToStyleConstantsValue ((StyleConstants)key, value); } } } return super.getAttribute(key); } } // ---- Resource handling ---------------------------------------- /** * Fetches the font to use for the given set of attributes. */ public Font getFont(AttributeSet a) { return css.getFont(this, a, 12, this); } /** * Takes a set of attributes and turn it into a foreground color * specification. This might be used to specify things * like brighter, more hue, etc. * * @param a the set of attributes * @return the color */ public Color getForeground(AttributeSet a) { Color c = css.getColor(a, CSS.Attribute.COLOR); if (c == null) { return Color.black; } return c; } /** * Takes a set of attributes and turn it into a background color * specification. This might be used to specify things * like brighter, more hue, etc. * * @param a the set of attributes * @return the color */ public Color getBackground(AttributeSet a) { return css.getColor(a, CSS.Attribute.BACKGROUND_COLOR); } /** * Fetches the box formatter to use for the given set * of CSS attributes. */ public BoxPainter getBoxPainter(AttributeSet a) { return new BoxPainter(a, css, this); } /** * Fetches the list formatter to use for the given set * of CSS attributes. */ public ListPainter getListPainter(AttributeSet a) { return new ListPainter(a, this); } /** * Sets the base font size, with valid values between 1 and 7. */ public void setBaseFontSize(int sz) { css.setBaseFontSize(sz); } /** * Sets the base font size from the passed in String. The string * can either identify a specific font size, with legal values between * 1 and 7, or identifiy a relative font size such as +1 or -2. */ public void setBaseFontSize(String size) { css.setBaseFontSize(size); } public static int getIndexOfSize(float pt) { return CSS.getIndexOfSize(pt, sizeMapDefault); } /** * Returns the point size, given a size index. */ public float getPointSize(int index) { return css.getPointSize(index, this); } /** * Given a string such as "+2", "-2", or "2", * returns a point size value. */ public float getPointSize(String size) { return css.getPointSize(size, this); } /** * Converts a color string such as "RED" or "#NNNNNN" to a Color. * Note: This will only convert the HTML3.2 color strings * or a string of length 7; * otherwise, it will return null. */ public Color stringToColor(String string) { return CSS.stringToColor(string); } /** * Returns the ImageIcon to draw in the background for * attr. */ ImageIcon getBackgroundImage(AttributeSet attr) { Object value = attr.getAttribute(CSS.Attribute.BACKGROUND_IMAGE); if (value != null) { return ((CSS.BackgroundImage)value).getImage(getBase()); } return null; } /** * Adds a rule into the StyleSheet. * * @param selector the selector to use for the rule. * This will be a set of simple selectors, and must * be a length of 1 or greater. * @param declaration the set of CSS attributes that * make up the rule. */ void addRule(String[] selector, AttributeSet declaration, boolean isLinked) { int n = selector.length; StringBuilder sb = new StringBuilder(); sb.append(selector[0]); for (int counter = 1; counter < n; counter++) { sb.append(' '); sb.append(selector[counter]); } String selectorName = sb.toString(); Style rule = getStyle(selectorName); if (rule == null) { // Notice how the rule is first created, and it not part of // the synchronized block. It is done like this as creating // a new rule will fire a ChangeEvent. We do not want to be // holding the lock when calling to other objects, it can // result in deadlock. Style altRule = addStyle(selectorName, null); synchronized(this) { SelectorMapping mapping = getRootSelectorMapping(); for (int i = n - 1; i >= 0; i--) { mapping = mapping.getChildSelectorMapping (selector[i], true); } rule = mapping.getStyle(); if (rule == null) { rule = altRule; mapping.setStyle(rule); refreshResolvedRules(selectorName, selector, rule, mapping.getSpecificity()); } } } if (isLinked) { rule = getLinkedStyle(rule); } rule.addAttributes(declaration); } // // The following gaggle of methods is used in maintaing the rules from // the sheet. // /** * Updates the attributes of the rules to reference any related * rules in ss. */ private synchronized void linkStyleSheetAt(StyleSheet ss, int index) { if (resolvedStyles.size() > 0) { Enumeration values = resolvedStyles.elements(); while (values.hasMoreElements()) { ResolvedStyle rule = values.nextElement(); rule.insertExtendedStyleAt(ss.getRule(rule.getName()), index); } } } /** * Removes references to the rules in ss. * index gives the index the StyleSheet was at, that is * how many StyleSheets had been added before it. */ private synchronized void unlinkStyleSheet(StyleSheet ss, int index) { if (resolvedStyles.size() > 0) { Enumeration values = resolvedStyles.elements(); while (values.hasMoreElements()) { ResolvedStyle rule = values.nextElement(); rule.removeExtendedStyleAt(index); } } } /** * Returns the simple selectors that comprise selector. */ /* protected */ String[] getSimpleSelectors(String selector) { selector = cleanSelectorString(selector); SearchBuffer sb = SearchBuffer.obtainSearchBuffer(); Vector selectors = sb.getVector(); int lastIndex = 0; int length = selector.length(); while (lastIndex != -1) { int newIndex = selector.indexOf(' ', lastIndex); if (newIndex != -1) { selectors.addElement(selector.substring(lastIndex, newIndex)); if (++newIndex == length) { lastIndex = -1; } else { lastIndex = newIndex; } } else { selectors.addElement(selector.substring(lastIndex)); lastIndex = -1; } } String[] retValue = new String[selectors.size()]; selectors.copyInto(retValue); SearchBuffer.releaseSearchBuffer(sb); return retValue; } /** * Returns a string that only has one space between simple selectors, * which may be the passed in String. */ /*protected*/ String cleanSelectorString(String selector) { boolean lastWasSpace = true; for (int counter = 0, maxCounter = selector.length(); counter < maxCounter; counter++) { switch(selector.charAt(counter)) { case ' ': if (lastWasSpace) { return _cleanSelectorString(selector); } lastWasSpace = true; break; case '\n': case '\r': case '\t': return _cleanSelectorString(selector); default: lastWasSpace = false; } } if (lastWasSpace) { return _cleanSelectorString(selector); } // It was fine. return selector; } /** * Returns a new String that contains only one space between non * white space characters. */ private String _cleanSelectorString(String selector) { SearchBuffer sb = SearchBuffer.obtainSearchBuffer(); StringBuffer buff = sb.getStringBuffer(); boolean lastWasSpace = true; int lastIndex = 0; char[] chars = selector.toCharArray(); int numChars = chars.length; String retValue = null; try { for (int counter = 0; counter < numChars; counter++) { switch(chars[counter]) { case ' ': if (!lastWasSpace) { lastWasSpace = true; if (lastIndex < counter) { buff.append(chars, lastIndex, 1 + counter - lastIndex); } } lastIndex = counter + 1; break; case '\n': case '\r': case '\t': if (!lastWasSpace) { lastWasSpace = true; if (lastIndex < counter) { buff.append(chars, lastIndex, counter - lastIndex); buff.append(' '); } } lastIndex = counter + 1; break; default: lastWasSpace = false; break; } } if (lastWasSpace && buff.length() > 0) { // Remove last space. buff.setLength(buff.length() - 1); } else if (lastIndex < numChars) { buff.append(chars, lastIndex, numChars - lastIndex); } retValue = buff.toString(); } finally { SearchBuffer.releaseSearchBuffer(sb); } return retValue; } /** * Returns the root selector mapping that all selectors are relative * to. This is an inverted graph of the selectors. */ private SelectorMapping getRootSelectorMapping() { return selectorMapping; } /** * Returns the specificity of the passed in String. It assumes the * passed in string doesn't contain junk, that is each selector is * separated by a space and each selector at most contains one . or one * #. A simple selector has a weight of 1, an id selector has a weight * of 100, and a class selector has a weight of 10000. */ /*protected*/ static int getSpecificity(String selector) { int specificity = 0; boolean lastWasSpace = true; for (int counter = 0, maxCounter = selector.length(); counter < maxCounter; counter++) { switch(selector.charAt(counter)) { case '.': specificity += 100; break; case '#': specificity += 10000; break; case ' ': lastWasSpace = true; break; default: if (lastWasSpace) { lastWasSpace = false; specificity += 1; } } } return specificity; } /** * Returns the style that linked attributes should be added to. This * will create the style if necessary. */ private Style getLinkedStyle(Style localStyle) { // NOTE: This is not synchronized, and the caller of this does // not synchronize. There is the chance for one of the callers to // overwrite the existing resolved parent, but it is quite rare. // The reason this is left like this is because setResolveParent // will fire a ChangeEvent. It is really, REALLY bad for us to // hold a lock when calling outside of us, it may cause a deadlock. Style retStyle = (Style)localStyle.getResolveParent(); if (retStyle == null) { retStyle = addStyle(null, null); localStyle.setResolveParent(retStyle); } return retStyle; } /** * Returns the resolved style for selector. This will * create the resolved style, if necessary. */ private synchronized Style getResolvedStyle(String selector, Vector elements, HTML.Tag t) { Style retStyle = resolvedStyles.get(selector); if (retStyle == null) { retStyle = createResolvedStyle(selector, elements, t); } return retStyle; } /** * Returns the resolved style for selector. This will * create the resolved style, if necessary. */ private synchronized Style getResolvedStyle(String selector) { Style retStyle = resolvedStyles.get(selector); if (retStyle == null) { retStyle = createResolvedStyle(selector); } return retStyle; } /** * Adds mapping to elements. It is added * such that elements will remain ordered by * specificity. */ private void addSortedStyle(SelectorMapping mapping, Vector elements) { int size = elements.size(); if (size > 0) { int specificity = mapping.getSpecificity(); for (int counter = 0; counter < size; counter++) { if (specificity >= elements.elementAt(counter).getSpecificity()) { elements.insertElementAt(mapping, counter); return; } } } elements.addElement(mapping); } /** * Adds parentMapping to styles, and * recursively calls this method if parentMapping has * any child mappings for any of the Elements in elements. */ private synchronized void getStyles(SelectorMapping parentMapping, Vector styles, String[] tags, String[] ids, String[] classes, int index, int numElements, Hashtable alreadyChecked) { // Avoid desending the same mapping twice. if (alreadyChecked.contains(parentMapping)) { return; } alreadyChecked.put(parentMapping, parentMapping); Style style = parentMapping.getStyle(); if (style != null) { addSortedStyle(parentMapping, styles); } for (int counter = index; counter < numElements; counter++) { String tagString = tags[counter]; if (tagString != null) { SelectorMapping childMapping = parentMapping. getChildSelectorMapping(tagString, false); if (childMapping != null) { getStyles(childMapping, styles, tags, ids, classes, counter + 1, numElements, alreadyChecked); } if (classes[counter] != null) { String className = classes[counter]; childMapping = parentMapping.getChildSelectorMapping( tagString + "." + className, false); if (childMapping != null) { getStyles(childMapping, styles, tags, ids, classes, counter + 1, numElements, alreadyChecked); } childMapping = parentMapping.getChildSelectorMapping( "." + className, false); if (childMapping != null) { getStyles(childMapping, styles, tags, ids, classes, counter + 1, numElements, alreadyChecked); } } if (ids[counter] != null) { String idName = ids[counter]; childMapping = parentMapping.getChildSelectorMapping( tagString + "#" + idName, false); if (childMapping != null) { getStyles(childMapping, styles, tags, ids, classes, counter + 1, numElements, alreadyChecked); } childMapping = parentMapping.getChildSelectorMapping( "#" + idName, false); if (childMapping != null) { getStyles(childMapping, styles, tags, ids, classes, counter + 1, numElements, alreadyChecked); } } } } } /** * Creates and returns a Style containing all the rules that match * selector. */ private synchronized Style createResolvedStyle(String selector, String[] tags, String[] ids, String[] classes) { SearchBuffer sb = SearchBuffer.obtainSearchBuffer(); Vector tempVector = sb.getVector(); Hashtable tempHashtable = sb.getHashtable(); // Determine all the Styles that are appropriate, placing them // in tempVector try { SelectorMapping mapping = getRootSelectorMapping(); int numElements = tags.length; String tagString = tags[0]; SelectorMapping childMapping = mapping.getChildSelectorMapping( tagString, false); if (childMapping != null) { getStyles(childMapping, tempVector, tags, ids, classes, 1, numElements, tempHashtable); } if (classes[0] != null) { String className = classes[0]; childMapping = mapping.getChildSelectorMapping( tagString + "." + className, false); if (childMapping != null) { getStyles(childMapping, tempVector, tags, ids, classes, 1, numElements, tempHashtable); } childMapping = mapping.getChildSelectorMapping( "." + className, false); if (childMapping != null) { getStyles(childMapping, tempVector, tags, ids, classes, 1, numElements, tempHashtable); } } if (ids[0] != null) { String idName = ids[0]; childMapping = mapping.getChildSelectorMapping( tagString + "#" + idName, false); if (childMapping != null) { getStyles(childMapping, tempVector, tags, ids, classes, 1, numElements, tempHashtable); } childMapping = mapping.getChildSelectorMapping( "#" + idName, false); if (childMapping != null) { getStyles(childMapping, tempVector, tags, ids, classes, 1, numElements, tempHashtable); } } // Create a new Style that will delegate to all the matching // Styles. int numLinkedSS = (linkedStyleSheets != null) ? linkedStyleSheets.size() : 0; int numStyles = tempVector.size(); AttributeSet[] attrs = new AttributeSet[numStyles + numLinkedSS]; for (int counter = 0; counter < numStyles; counter++) { attrs[counter] = tempVector.elementAt(counter).getStyle(); } // Get the AttributeSet from linked style sheets. for (int counter = 0; counter < numLinkedSS; counter++) { AttributeSet attr = linkedStyleSheets.elementAt(counter).getRule(selector); if (attr == null) { attrs[counter + numStyles] = SimpleAttributeSet.EMPTY; } else { attrs[counter + numStyles] = attr; } } ResolvedStyle retStyle = new ResolvedStyle(selector, attrs, numStyles); resolvedStyles.put(selector, retStyle); return retStyle; } finally { SearchBuffer.releaseSearchBuffer(sb); } } /** * Creates and returns a Style containing all the rules that * matches selector. * * @param elements a Vector of all the Elements * the style is being asked for. The * first Element is the deepest Element, with the last Element * representing the root. * @param t the Tag to use for * the first Element in elements */ private Style createResolvedStyle(String selector, Vector elements, HTML.Tag t) { int numElements = elements.size(); // Build three arrays, one for tags, one for class's, and one for // id's String tags[] = new String[numElements]; String ids[] = new String[numElements]; String classes[] = new String[numElements]; for (int counter = 0; counter < numElements; counter++) { Element e = (Element)elements.elementAt(counter); AttributeSet attr = e.getAttributes(); if (counter == 0 && e.isLeaf()) { // For leafs, we use the second tier attributes. Object testAttr = attr.getAttribute(t); if (testAttr instanceof AttributeSet) { attr = (AttributeSet)testAttr; } else { attr = null; } } if (attr != null) { HTML.Tag tag = (HTML.Tag)attr.getAttribute(StyleConstants. NameAttribute); if (tag != null) { tags[counter] = tag.toString(); } else { tags[counter] = null; } if (attr.isDefined(HTML.Attribute.CLASS)) { classes[counter] = attr.getAttribute (HTML.Attribute.CLASS).toString(); } else { classes[counter] = null; } if (attr.isDefined(HTML.Attribute.ID)) { ids[counter] = attr.getAttribute(HTML.Attribute.ID). toString(); } else { ids[counter] = null; } } else { tags[counter] = ids[counter] = classes[counter] = null; } } tags[0] = t.toString(); return createResolvedStyle(selector, tags, ids, classes); } /** * Creates and returns a Style containing all the rules that match * selector. It is assumed that each simple selector * in selector is separated by a space. */ private Style createResolvedStyle(String selector) { SearchBuffer sb = SearchBuffer.obtainSearchBuffer(); // Will contain the tags, ids, and classes, in that order. Vector elements = sb.getVector(); try { boolean done; int dotIndex = 0; int spaceIndex; int poundIndex = 0; int lastIndex = 0; int length = selector.length(); while (lastIndex < length) { if (dotIndex == lastIndex) { dotIndex = selector.indexOf('.', lastIndex); } if (poundIndex == lastIndex) { poundIndex = selector.indexOf('#', lastIndex); } spaceIndex = selector.indexOf(' ', lastIndex); if (spaceIndex == -1) { spaceIndex = length; } if (dotIndex != -1 && poundIndex != -1 && dotIndex < spaceIndex && poundIndex < spaceIndex) { if (poundIndex < dotIndex) { // #. if (lastIndex == poundIndex) { elements.addElement(""); } else { elements.addElement(selector.substring(lastIndex, poundIndex)); } if ((dotIndex + 1) < spaceIndex) { elements.addElement(selector.substring (dotIndex + 1, spaceIndex)); } else { elements.addElement(null); } if ((poundIndex + 1) == dotIndex) { elements.addElement(null); } else { elements.addElement(selector.substring (poundIndex + 1, dotIndex)); } } else if(poundIndex < spaceIndex) { // .# if (lastIndex == dotIndex) { elements.addElement(""); } else { elements.addElement(selector.substring(lastIndex, dotIndex)); } if ((dotIndex + 1) < poundIndex) { elements.addElement(selector.substring (dotIndex + 1, poundIndex)); } else { elements.addElement(null); } if ((poundIndex + 1) == spaceIndex) { elements.addElement(null); } else { elements.addElement(selector.substring (poundIndex + 1, spaceIndex)); } } dotIndex = poundIndex = spaceIndex + 1; } else if (dotIndex != -1 && dotIndex < spaceIndex) { // . if (dotIndex == lastIndex) { elements.addElement(""); } else { elements.addElement(selector.substring(lastIndex, dotIndex)); } if ((dotIndex + 1) == spaceIndex) { elements.addElement(null); } else { elements.addElement(selector.substring(dotIndex + 1, spaceIndex)); } elements.addElement(null); dotIndex = spaceIndex + 1; } else if (poundIndex != -1 && poundIndex < spaceIndex) { // # if (poundIndex == lastIndex) { elements.addElement(""); } else { elements.addElement(selector.substring(lastIndex, poundIndex)); } elements.addElement(null); if ((poundIndex + 1) == spaceIndex) { elements.addElement(null); } else { elements.addElement(selector.substring(poundIndex + 1, spaceIndex)); } poundIndex = spaceIndex + 1; } else { // id elements.addElement(selector.substring(lastIndex, spaceIndex)); elements.addElement(null); elements.addElement(null); } lastIndex = spaceIndex + 1; } // Create the tag, id, and class arrays. int total = elements.size(); int numTags = total / 3; String[] tags = new String[numTags]; String[] ids = new String[numTags]; String[] classes = new String[numTags]; for (int index = 0, eIndex = total - 3; index < numTags; index++, eIndex -= 3) { tags[index] = elements.elementAt(eIndex); classes[index] = elements.elementAt(eIndex + 1); ids[index] = elements.elementAt(eIndex + 2); } return createResolvedStyle(selector, tags, ids, classes); } finally { SearchBuffer.releaseSearchBuffer(sb); } } /** * Should be invoked when a new rule is added that did not previously * exist. Goes through and refreshes the necessary resolved * rules. */ private synchronized void refreshResolvedRules(String selectorName, String[] selector, Style newStyle, int specificity) { if (resolvedStyles.size() > 0) { Enumeration values = resolvedStyles.elements(); while (values.hasMoreElements()) { ResolvedStyle style = values.nextElement(); if (style.matches(selectorName)) { style.insertStyle(newStyle, specificity); } } } } /** * A temporary class used to hold a Vector, a StringBuffer and a * Hashtable. This is used to avoid allocing a lot of garbage when * searching for rules. Use the static method obtainSearchBuffer and * releaseSearchBuffer to get a SearchBuffer, and release it when * done. */ private static class SearchBuffer { /** A stack containing instances of SearchBuffer. Used in getting * rules. */ static Stack searchBuffers = new Stack(); // A set of temporary variables that can be used in whatever way. Vector vector = null; StringBuffer stringBuffer = null; Hashtable hashtable = null; /** * Returns an instance of SearchBuffer. Be sure and issue * a releaseSearchBuffer when done with it. */ static SearchBuffer obtainSearchBuffer() { SearchBuffer sb; try { if(!searchBuffers.empty()) { sb = searchBuffers.pop(); } else { sb = new SearchBuffer(); } } catch (EmptyStackException ese) { sb = new SearchBuffer(); } return sb; } /** * Adds sb to the stack of SearchBuffers that can * be used. */ static void releaseSearchBuffer(SearchBuffer sb) { sb.empty(); searchBuffers.push(sb); } StringBuffer getStringBuffer() { if (stringBuffer == null) { stringBuffer = new StringBuffer(); } return stringBuffer; } Vector getVector() { if (vector == null) { vector = new Vector(); } return vector; } Hashtable getHashtable() { if (hashtable == null) { hashtable = new Hashtable(); } return hashtable; } void empty() { if (stringBuffer != null) { stringBuffer.setLength(0); } if (vector != null) { vector.removeAllElements(); } if (hashtable != null) { hashtable.clear(); } } } static final Border noBorder = new EmptyBorder(0,0,0,0); /** * Class to carry out some of the duties of * CSS formatting. Implementations of this * class enable views to present the CSS formatting * while not knowing anything about how the CSS values * are being cached. *

* As a delegate of Views, this object is responsible for * the insets of a View and making sure the background * is maintained according to the CSS attributes. */ public static class BoxPainter implements Serializable { BoxPainter(AttributeSet a, CSS css, StyleSheet ss) { this.ss = ss; this.css = css; border = getBorder(a); binsets = border.getBorderInsets(null); topMargin = getLength(CSS.Attribute.MARGIN_TOP, a); bottomMargin = getLength(CSS.Attribute.MARGIN_BOTTOM, a); leftMargin = getLength(CSS.Attribute.MARGIN_LEFT, a); rightMargin = getLength(CSS.Attribute.MARGIN_RIGHT, a); bg = ss.getBackground(a); if (ss.getBackgroundImage(a) != null) { bgPainter = new BackgroundImagePainter(a, css, ss); } } /** * Fetches a border to render for the given attributes. * PENDING(prinz) This is pretty badly hacked at the * moment. */ Border getBorder(AttributeSet a) { return new CSSBorder(a); } /** * Fetches the color to use for borders. This will either be * the value specified by the border-color attribute (which * is not inherited), or it will default to the color attribute * (which is inherited). */ Color getBorderColor(AttributeSet a) { Color color = css.getColor(a, CSS.Attribute.BORDER_COLOR); if (color == null) { color = css.getColor(a, CSS.Attribute.COLOR); if (color == null) { return Color.black; } } return color; } /** * Fetches the inset needed on a given side to * account for the margin, border, and padding. * * @param side The size of the box to fetch the * inset for. This can be View.TOP, * View.LEFT, View.BOTTOM, or View.RIGHT. * @param v the view making the request. This is * used to get the AttributeSet, and may be used to * resolve percentage arguments. * @exception IllegalArgumentException for an invalid direction */ public float getInset(int side, View v) { AttributeSet a = v.getAttributes(); float inset = 0; switch(side) { case View.LEFT: inset += getOrientationMargin(HorizontalMargin.LEFT, leftMargin, a, isLeftToRight(v)); inset += binsets.left; inset += getLength(CSS.Attribute.PADDING_LEFT, a); break; case View.RIGHT: inset += getOrientationMargin(HorizontalMargin.RIGHT, rightMargin, a, isLeftToRight(v)); inset += binsets.right; inset += getLength(CSS.Attribute.PADDING_RIGHT, a); break; case View.TOP: inset += topMargin; inset += binsets.top; inset += getLength(CSS.Attribute.PADDING_TOP, a); break; case View.BOTTOM: inset += bottomMargin; inset += binsets.bottom; inset += getLength(CSS.Attribute.PADDING_BOTTOM, a); break; default: throw new IllegalArgumentException("Invalid side: " + side); } return inset; } /** * Paints the CSS box according to the attributes * given. This should paint the border, padding, * and background. * * @param g the rendering surface. * @param x the x coordinate of the allocated area to * render into. * @param y the y coordinate of the allocated area to * render into. * @param w the width of the allocated area to render into. * @param h the height of the allocated area to render into. * @param v the view making the request. This is * used to get the AttributeSet, and may be used to * resolve percentage arguments. */ public void paint(Graphics g, float x, float y, float w, float h, View v) { // PENDING(prinz) implement real rendering... which would // do full set of border and background capabilities. // remove margin float dx = 0; float dy = 0; float dw = 0; float dh = 0; AttributeSet a = v.getAttributes(); boolean isLeftToRight = isLeftToRight(v); float localLeftMargin = getOrientationMargin(HorizontalMargin.LEFT, leftMargin, a, isLeftToRight); float localRightMargin = getOrientationMargin(HorizontalMargin.RIGHT, rightMargin, a, isLeftToRight); if (!(v instanceof HTMLEditorKit.HTMLFactory.BodyBlockView)) { dx = localLeftMargin; dy = topMargin; dw = -(localLeftMargin + localRightMargin); dh = -(topMargin + bottomMargin); } if (bg != null) { g.setColor(bg); g.fillRect((int) (x + dx), (int) (y + dy), (int) (w + dw), (int) (h + dh)); } if (bgPainter != null) { bgPainter.paint(g, x + dx, y + dy, w + dw, h + dh, v); } x += localLeftMargin; y += topMargin; w -= localLeftMargin + localRightMargin; h -= topMargin + bottomMargin; if (border instanceof BevelBorder) { //BevelBorder does not support border width int bw = (int) getLength(CSS.Attribute.BORDER_TOP_WIDTH, a); for (int i = bw - 1; i >= 0; i--) { border.paintBorder(null, g, (int) x + i, (int) y + i, (int) w - 2 * i, (int) h - 2 * i); } } else { border.paintBorder(null, g, (int) x, (int) y, (int) w, (int) h); } } float getLength(CSS.Attribute key, AttributeSet a) { return css.getLength(a, key, ss); } static boolean isLeftToRight(View v) { boolean ret = true; if (isOrientationAware(v)) { Container container; if (v != null && (container = v.getContainer()) != null) { ret = container.getComponentOrientation().isLeftToRight(); } } return ret; } /* * only certain tags are concerned about orientation *

, ,