| /* |
| * 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 |
| * <code>toString</code> method is required |
| * to return a string representation of CSS value. |
| * <p> |
| * 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. |
| * <p> |
| * 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. |
| * <p> |
| * 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. |
| * <code><pre> |
| * |
| * 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); |
| * } |
| * } |
| * |
| * </pre></code> |
| * <p> |
| * The semantics for when a CSS style should overide visual attributes |
| * defined by an element are not well defined. For example, the html |
| * <code><body bgcolor=red></code> makes the body have a red |
| * background. But if the html file also contains the CSS rule |
| * <code>body { background: blue }</code> 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. |
| * <p> |
| * 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 <code>A:link { color: red }</code>, |
| * and the <code>important</code> modifier. |
| * <p> |
| * <font color="red">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.</font> |
| * |
| * @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<String, ResolvedStyle>(); |
| 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<Element> 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 <code>selector</code> is a space separated |
| * String of the element names. For example, <code>selector</code> |
| * might be 'html body tr td''<p> |
| * 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<ResolvedStyle> values = resolvedStyles.elements(); |
| while (values.hasMoreElements()) { |
| ResolvedStyle style = values.nextElement(); |
| style.removeStyle(rule); |
| } |
| } |
| } |
| } |
| } |
| super.removeStyle(nm); |
| } |
| |
| /** |
| * Adds the rules from the StyleSheet <code>ss</code> to those of |
| * the receiver. <code>ss's</code> 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<StyleSheet>(); |
| } |
| 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 <code>ss</code> 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 <code>url</code>. 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 <code>base</code>. |
| * |
| * @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 |
| * <code>attr</code>. |
| */ |
| 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 <code>ss</code>. |
| */ |
| private synchronized void linkStyleSheetAt(StyleSheet ss, int index) { |
| if (resolvedStyles.size() > 0) { |
| Enumeration<ResolvedStyle> values = resolvedStyles.elements(); |
| while (values.hasMoreElements()) { |
| ResolvedStyle rule = values.nextElement(); |
| rule.insertExtendedStyleAt(ss.getRule(rule.getName()), |
| index); |
| } |
| } |
| } |
| |
| /** |
| * Removes references to the rules in <code>ss</code>. |
| * <code>index</code> 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<ResolvedStyle> 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<String> 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 <code>selector</code>. 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 <code>selector</code>. 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 <code>mapping</code> to <code>elements</code>. It is added |
| * such that <code>elements</code> will remain ordered by |
| * specificity. |
| */ |
| private void addSortedStyle(SelectorMapping mapping, Vector<SelectorMapping> 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 <code>parentMapping</code> to <code>styles</code>, and |
| * recursively calls this method if <code>parentMapping</code> has |
| * any child mappings for any of the Elements in <code>elements</code>. |
| */ |
| private synchronized void getStyles(SelectorMapping parentMapping, |
| Vector<SelectorMapping> styles, |
| String[] tags, String[] ids, String[] classes, |
| int index, int numElements, |
| Hashtable<SelectorMapping, SelectorMapping> 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 |
| * <code>selector</code>. |
| */ |
| private synchronized Style createResolvedStyle(String selector, |
| String[] tags, |
| String[] ids, String[] classes) { |
| SearchBuffer sb = SearchBuffer.obtainSearchBuffer(); |
| Vector<SelectorMapping> tempVector = sb.getVector(); |
| Hashtable<SelectorMapping, SelectorMapping> 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 <code>selector</code>. |
| * |
| * @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 <code>elements</code> |
| */ |
| 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 |
| * <code>selector</code>. It is assumed that each simple selector |
| * in <code>selector</code> 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<String> 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<ResolvedStyle> 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<SearchBuffer> searchBuffers = new Stack<SearchBuffer>(); |
| // 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 <code>sb</code> 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. |
| * <p> |
| * 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 |
| * <dir>, <menu>, <ul>, <ol> |
| * for all others we return true. It is implemented this way |
| * for performance purposes |
| */ |
| static boolean isOrientationAware(View v) { |
| boolean ret = false; |
| AttributeSet attr; |
| Object obj; |
| if (v != null |
| && (attr = v.getElement().getAttributes()) != null |
| && (obj = attr.getAttribute(StyleConstants.NameAttribute)) instanceof HTML.Tag |
| && (obj == HTML.Tag.DIR |
| || obj == HTML.Tag.MENU |
| || obj == HTML.Tag.UL |
| || obj == HTML.Tag.OL)) { |
| ret = true; |
| } |
| |
| return ret; |
| } |
| |
| static enum HorizontalMargin { LEFT, RIGHT } |
| |
| /** |
| * for <dir>, <menu>, <ul> etc. |
| * margins are Left-To-Right/Right-To-Left depended. |
| * see 5088268 for more details |
| * margin-(left|right)-(ltr|rtl) were introduced to describe it |
| * if margin-(left|right) is present we are to use it. |
| * |
| * @param side The horizontal side to fetch margin for |
| * This can be HorizontalMargin.LEFT or HorizontalMargin.RIGHT |
| * @param cssMargin margin from css |
| * @param a AttributeSet for the View we getting margin for |
| * @param isLeftToRight |
| * @return orientation depended margin |
| */ |
| float getOrientationMargin(HorizontalMargin side, float cssMargin, |
| AttributeSet a, boolean isLeftToRight) { |
| float margin = cssMargin; |
| float orientationMargin = cssMargin; |
| Object cssMarginValue = null; |
| switch (side) { |
| case RIGHT: |
| { |
| orientationMargin = (isLeftToRight) ? |
| getLength(CSS.Attribute.MARGIN_RIGHT_LTR, a) : |
| getLength(CSS.Attribute.MARGIN_RIGHT_RTL, a); |
| cssMarginValue = a.getAttribute(CSS.Attribute.MARGIN_RIGHT); |
| } |
| break; |
| case LEFT : |
| { |
| orientationMargin = (isLeftToRight) ? |
| getLength(CSS.Attribute.MARGIN_LEFT_LTR, a) : |
| getLength(CSS.Attribute.MARGIN_LEFT_RTL, a); |
| cssMarginValue = a.getAttribute(CSS.Attribute.MARGIN_LEFT); |
| } |
| break; |
| } |
| |
| if (cssMarginValue == null |
| && orientationMargin != Integer.MIN_VALUE) { |
| margin = orientationMargin; |
| } |
| return margin; |
| } |
| |
| float topMargin; |
| float bottomMargin; |
| float leftMargin; |
| float rightMargin; |
| // Bitmask, used to indicate what margins are relative: |
| // bit 0 for top, 1 for bottom, 2 for left and 3 for right. |
| short marginFlags; |
| Border border; |
| Insets binsets; |
| CSS css; |
| StyleSheet ss; |
| Color bg; |
| BackgroundImagePainter bgPainter; |
| } |
| |
| /** |
| * Class to carry out some of the duties of CSS list |
| * formatting. Implementations of this |
| * class enable views to present the CSS formatting |
| * while not knowing anything about how the CSS values |
| * are being cached. |
| */ |
| public static class ListPainter implements Serializable { |
| |
| ListPainter(AttributeSet attr, StyleSheet ss) { |
| this.ss = ss; |
| /* Get the image to use as a list bullet */ |
| String imgstr = (String)attr.getAttribute(CSS.Attribute. |
| LIST_STYLE_IMAGE); |
| type = null; |
| if (imgstr != null && !imgstr.equals("none")) { |
| String tmpstr = null; |
| try { |
| StringTokenizer st = new StringTokenizer(imgstr, "()"); |
| if (st.hasMoreTokens()) |
| tmpstr = st.nextToken(); |
| if (st.hasMoreTokens()) |
| tmpstr = st.nextToken(); |
| URL u = new URL(tmpstr); |
| img = new ImageIcon(u); |
| } catch (MalformedURLException e) { |
| if (tmpstr != null && ss != null && ss.getBase() != null) { |
| try { |
| URL u = new URL(ss.getBase(), tmpstr); |
| img = new ImageIcon(u); |
| } catch (MalformedURLException murle) { |
| img = null; |
| } |
| } |
| else { |
| img = null; |
| } |
| } |
| } |
| |
| /* Get the type of bullet to use in the list */ |
| if (img == null) { |
| type = (CSS.Value)attr.getAttribute(CSS.Attribute. |
| LIST_STYLE_TYPE); |
| } |
| start = 1; |
| |
| paintRect = new Rectangle(); |
| } |
| |
| /** |
| * Returns a string that represents the value |
| * of the HTML.Attribute.TYPE attribute. |
| * If this attributes is not defined, then |
| * then the type defaults to "disc" unless |
| * the tag is on Ordered list. In the case |
| * of the latter, the default type is "decimal". |
| */ |
| private CSS.Value getChildType(View childView) { |
| CSS.Value childtype = (CSS.Value)childView.getAttributes(). |
| getAttribute(CSS.Attribute.LIST_STYLE_TYPE); |
| |
| if (childtype == null) { |
| if (type == null) { |
| // Parent view. |
| View v = childView.getParent(); |
| HTMLDocument doc = (HTMLDocument)v.getDocument(); |
| if (doc.matchNameAttribute(v.getElement().getAttributes(), |
| HTML.Tag.OL)) { |
| childtype = CSS.Value.DECIMAL; |
| } else { |
| childtype = CSS.Value.DISC; |
| } |
| } else { |
| childtype = type; |
| } |
| } |
| return childtype; |
| } |
| |
| /** |
| * Obtains the starting index from <code>parent</code>. |
| */ |
| private void getStart(View parent) { |
| checkedForStart = true; |
| Element element = parent.getElement(); |
| if (element != null) { |
| AttributeSet attr = element.getAttributes(); |
| Object startValue; |
| if (attr != null && attr.isDefined(HTML.Attribute.START) && |
| (startValue = attr.getAttribute |
| (HTML.Attribute.START)) != null && |
| (startValue instanceof String)) { |
| |
| try { |
| start = Integer.parseInt((String)startValue); |
| } |
| catch (NumberFormatException nfe) {} |
| } |
| } |
| } |
| |
| /** |
| * Returns an integer that should be used to render the child at |
| * <code>childIndex</code> with. The retValue will usually be |
| * <code>childIndex</code> + 1, unless <code>parentView</code> |
| * has some Views that do not represent LI's, or one of the views |
| * has a HTML.Attribute.START specified. |
| */ |
| private int getRenderIndex(View parentView, int childIndex) { |
| if (!checkedForStart) { |
| getStart(parentView); |
| } |
| int retIndex = childIndex; |
| for (int counter = childIndex; counter >= 0; counter--) { |
| AttributeSet as = parentView.getElement().getElement(counter). |
| getAttributes(); |
| if (as.getAttribute(StyleConstants.NameAttribute) != |
| HTML.Tag.LI) { |
| retIndex--; |
| } else if (as.isDefined(HTML.Attribute.VALUE)) { |
| Object value = as.getAttribute(HTML.Attribute.VALUE); |
| if (value != null && |
| (value instanceof String)) { |
| try { |
| int iValue = Integer.parseInt((String)value); |
| return retIndex - counter + iValue; |
| } |
| catch (NumberFormatException nfe) {} |
| } |
| } |
| } |
| return retIndex + start; |
| } |
| |
| /** |
| * Paints the CSS list decoration according to the |
| * attributes given. |
| * |
| * @param g the rendering surface. |
| * @param x the x coordinate of the list item allocation |
| * @param y the y coordinate of the list item allocation |
| * @param w the width of the list item allocation |
| * @param h the height of the list item allocation |
| * @param v the allocated area to paint into. |
| * @param item which list item is being painted. This |
| * is a number greater than or equal to 0. |
| */ |
| public void paint(Graphics g, float x, float y, float w, float h, View v, int item) { |
| View cv = v.getView(item); |
| Container host = v.getContainer(); |
| Object name = cv.getElement().getAttributes().getAttribute |
| (StyleConstants.NameAttribute); |
| // Only draw something if the View is a list item. This won't |
| // be the case for comments. |
| if (!(name instanceof HTML.Tag) || |
| name != HTML.Tag.LI) { |
| return; |
| } |
| // deside on what side draw bullets, etc. |
| isLeftToRight = |
| host.getComponentOrientation().isLeftToRight(); |
| |
| // How the list indicator is aligned is not specified, it is |
| // left up to the UA. IE and NS differ on this behavior. |
| // This is closer to NS where we align to the first line of text. |
| // If the child is not text we draw the indicator at the |
| // origin (0). |
| float align = 0; |
| if (cv.getViewCount() > 0) { |
| View pView = cv.getView(0); |
| Object cName = pView.getElement().getAttributes(). |
| getAttribute(StyleConstants.NameAttribute); |
| if ((cName == HTML.Tag.P || cName == HTML.Tag.IMPLIED) && |
| pView.getViewCount() > 0) { |
| paintRect.setBounds((int)x, (int)y, (int)w, (int)h); |
| Shape shape = cv.getChildAllocation(0, paintRect); |
| if (shape != null && (shape = pView.getView(0). |
| getChildAllocation(0, shape)) != null) { |
| Rectangle rect = (shape instanceof Rectangle) ? |
| (Rectangle)shape : shape.getBounds(); |
| |
| align = pView.getView(0).getAlignment(View.Y_AXIS); |
| y = rect.y; |
| h = rect.height; |
| } |
| } |
| } |
| |
| // set the color of a decoration |
| Color c = (host.isEnabled() |
| ? (ss != null |
| ? ss.getForeground(cv.getAttributes()) |
| : host.getForeground()) |
| : UIManager.getColor("textInactiveText")); |
| g.setColor(c); |
| |
| if (img != null) { |
| drawIcon(g, (int) x, (int) y, (int) w, (int) h, align, host); |
| return; |
| } |
| CSS.Value childtype = getChildType(cv); |
| Font font = ((StyledDocument)cv.getDocument()). |
| getFont(cv.getAttributes()); |
| if (font != null) { |
| g.setFont(font); |
| } |
| if (childtype == CSS.Value.SQUARE || childtype == CSS.Value.CIRCLE |
| || childtype == CSS.Value.DISC) { |
| drawShape(g, childtype, (int) x, (int) y, |
| (int) w, (int) h, align); |
| } else if (childtype == CSS.Value.DECIMAL) { |
| drawLetter(g, '1', (int) x, (int) y, (int) w, (int) h, align, |
| getRenderIndex(v, item)); |
| } else if (childtype == CSS.Value.LOWER_ALPHA) { |
| drawLetter(g, 'a', (int) x, (int) y, (int) w, (int) h, align, |
| getRenderIndex(v, item)); |
| } else if (childtype == CSS.Value.UPPER_ALPHA) { |
| drawLetter(g, 'A', (int) x, (int) y, (int) w, (int) h, align, |
| getRenderIndex(v, item)); |
| } else if (childtype == CSS.Value.LOWER_ROMAN) { |
| drawLetter(g, 'i', (int) x, (int) y, (int) w, (int) h, align, |
| getRenderIndex(v, item)); |
| } else if (childtype == CSS.Value.UPPER_ROMAN) { |
| drawLetter(g, 'I', (int) x, (int) y, (int) w, (int) h, align, |
| getRenderIndex(v, item)); |
| } |
| } |
| |
| /** |
| * Draws the bullet icon specified by the list-style-image argument. |
| * |
| * @param g the graphics context |
| * @param ax x coordinate to place the bullet |
| * @param ay y coordinate to place the bullet |
| * @param aw width of the container the bullet is placed in |
| * @param ah height of the container the bullet is placed in |
| * @param align preferred alignment factor for the child view |
| */ |
| void drawIcon(Graphics g, int ax, int ay, int aw, int ah, |
| float align, Component c) { |
| // Align to bottom of icon. |
| int gap = isLeftToRight ? - (img.getIconWidth() + bulletgap) : |
| (aw + bulletgap); |
| int x = ax + gap; |
| int y = Math.max(ay, ay + (int)(align * ah) -img.getIconHeight()); |
| |
| img.paintIcon(c, g, x, y); |
| } |
| |
| /** |
| * Draws the graphical bullet item specified by the type argument. |
| * |
| * @param g the graphics context |
| * @param type type of bullet to draw (circle, square, disc) |
| * @param ax x coordinate to place the bullet |
| * @param ay y coordinate to place the bullet |
| * @param aw width of the container the bullet is placed in |
| * @param ah height of the container the bullet is placed in |
| * @param align preferred alignment factor for the child view |
| */ |
| void drawShape(Graphics g, CSS.Value type, int ax, int ay, int aw, |
| int ah, float align) { |
| // Align to bottom of shape. |
| int gap = isLeftToRight ? - (bulletgap + 8) : (aw + bulletgap); |
| int x = ax + gap; |
| int y = Math.max(ay, ay + (int)(align * ah) - 8); |
| |
| if (type == CSS.Value.SQUARE) { |
| g.drawRect(x, y, 8, 8); |
| } else if (type == CSS.Value.CIRCLE) { |
| g.drawOval(x, y, 8, 8); |
| } else { |
| g.fillOval(x, y, 8, 8); |
| } |
| } |
| |
| /** |
| * Draws the letter or number for an ordered list. |
| * |
| * @param g the graphics context |
| * @param letter type of ordered list to draw |
| * @param ax x coordinate to place the bullet |
| * @param ay y coordinate to place the bullet |
| * @param aw width of the container the bullet is placed in |
| * @param ah height of the container the bullet is placed in |
| * @param index position of the list item in the list |
| */ |
| void drawLetter(Graphics g, char letter, int ax, int ay, int aw, |
| int ah, float align, int index) { |
| String str = formatItemNum(index, letter); |
| str = isLeftToRight ? str + "." : "." + str; |
| FontMetrics fm = SwingUtilities2.getFontMetrics(null, g); |
| int stringwidth = SwingUtilities2.stringWidth(null, fm, str); |
| int gap = isLeftToRight ? - (stringwidth + bulletgap) : |
| (aw + bulletgap); |
| int x = ax + gap; |
| int y = Math.max(ay + fm.getAscent(), ay + (int)(ah * align)); |
| SwingUtilities2.drawString(null, g, str, x, y); |
| } |
| |
| /** |
| * Converts the item number into the ordered list number |
| * (i.e. 1 2 3, i ii iii, a b c, etc. |
| * |
| * @param itemNum number to format |
| * @param type type of ordered list |
| */ |
| String formatItemNum(int itemNum, char type) { |
| String numStyle = "1"; |
| |
| boolean uppercase = false; |
| |
| String formattedNum; |
| |
| switch (type) { |
| case '1': |
| default: |
| formattedNum = String.valueOf(itemNum); |
| break; |
| |
| case 'A': |
| uppercase = true; |
| // fall through |
| case 'a': |
| formattedNum = formatAlphaNumerals(itemNum); |
| break; |
| |
| case 'I': |
| uppercase = true; |
| // fall through |
| case 'i': |
| formattedNum = formatRomanNumerals(itemNum); |
| } |
| |
| if (uppercase) { |
| formattedNum = formattedNum.toUpperCase(); |
| } |
| |
| return formattedNum; |
| } |
| |
| /** |
| * Converts the item number into an alphabetic character |
| * |
| * @param itemNum number to format |
| */ |
| String formatAlphaNumerals(int itemNum) { |
| String result; |
| |
| if (itemNum > 26) { |
| result = formatAlphaNumerals(itemNum / 26) + |
| formatAlphaNumerals(itemNum % 26); |
| } else { |
| // -1 because item is 1 based. |
| result = String.valueOf((char)('a' + itemNum - 1)); |
| } |
| |
| return result; |
| } |
| |
| /* list of roman numerals */ |
| static final char romanChars[][] = { |
| {'i', 'v'}, |
| {'x', 'l' }, |
| {'c', 'd' }, |
| {'m', '?' }, |
| }; |
| |
| /** |
| * Converts the item number into a roman numeral |
| * |
| * @param num number to format |
| */ |
| String formatRomanNumerals(int num) { |
| return formatRomanNumerals(0, num); |
| } |
| |
| /** |
| * Converts the item number into a roman numeral |
| * |
| * @param num number to format |
| */ |
| String formatRomanNumerals(int level, int num) { |
| if (num < 10) { |
| return formatRomanDigit(level, num); |
| } else { |
| return formatRomanNumerals(level + 1, num / 10) + |
| formatRomanDigit(level, num % 10); |
| } |
| } |
| |
| |
| /** |
| * Converts the item number into a roman numeral |
| * |
| * @param level position |
| * @param digit digit to format |
| */ |
| String formatRomanDigit(int level, int digit) { |
| String result = ""; |
| if (digit == 9) { |
| result = result + romanChars[level][0]; |
| result = result + romanChars[level + 1][0]; |
| return result; |
| } else if (digit == 4) { |
| result = result + romanChars[level][0]; |
| result = result + romanChars[level][1]; |
| return result; |
| } else if (digit >= 5) { |
| result = result + romanChars[level][1]; |
| digit -= 5; |
| } |
| |
| for (int i = 0; i < digit; i++) { |
| result = result + romanChars[level][0]; |
| } |
| |
| return result; |
| } |
| |
| private Rectangle paintRect; |
| private boolean checkedForStart; |
| private int start; |
| private CSS.Value type; |
| URL imageurl; |
| private StyleSheet ss = null; |
| Icon img = null; |
| private int bulletgap = 5; |
| private boolean isLeftToRight; |
| } |
| |
| |
| /** |
| * Paints the background image. |
| */ |
| static class BackgroundImagePainter implements Serializable { |
| ImageIcon backgroundImage; |
| float hPosition; |
| float vPosition; |
| // bit mask: 0 for repeat x, 1 for repeat y, 2 for horiz relative, |
| // 3 for vert relative |
| short flags; |
| // These are used when painting, updatePaintCoordinates updates them. |
| private int paintX; |
| private int paintY; |
| private int paintMaxX; |
| private int paintMaxY; |
| |
| BackgroundImagePainter(AttributeSet a, CSS css, StyleSheet ss) { |
| backgroundImage = ss.getBackgroundImage(a); |
| // Determine the position. |
| CSS.BackgroundPosition pos = (CSS.BackgroundPosition)a.getAttribute |
| (CSS.Attribute.BACKGROUND_POSITION); |
| if (pos != null) { |
| hPosition = pos.getHorizontalPosition(); |
| vPosition = pos.getVerticalPosition(); |
| if (pos.isHorizontalPositionRelativeToSize()) { |
| flags |= 4; |
| } |
| else if (pos.isHorizontalPositionRelativeToSize()) { |
| hPosition *= css.getFontSize(a, 12, ss); |
| } |
| if (pos.isVerticalPositionRelativeToSize()) { |
| flags |= 8; |
| } |
| else if (pos.isVerticalPositionRelativeToFontSize()) { |
| vPosition *= css.getFontSize(a, 12, ss); |
| } |
| } |
| // Determine any repeating values. |
| CSS.Value repeats = (CSS.Value)a.getAttribute(CSS.Attribute. |
| BACKGROUND_REPEAT); |
| if (repeats == null || repeats == CSS.Value.BACKGROUND_REPEAT) { |
| flags |= 3; |
| } |
| else if (repeats == CSS.Value.BACKGROUND_REPEAT_X) { |
| flags |= 1; |
| } |
| else if (repeats == CSS.Value.BACKGROUND_REPEAT_Y) { |
| flags |= 2; |
| } |
| } |
| |
| void paint(Graphics g, float x, float y, float w, float h, View v) { |
| Rectangle clip = g.getClipRect(); |
| if (clip != null) { |
| // Constrain the clip so that images don't draw outside the |
| // legal bounds. |
| g.clipRect((int)x, (int)y, (int)w, (int)h); |
| } |
| if ((flags & 3) == 0) { |
| // no repeating |
| int width = backgroundImage.getIconWidth(); |
| int height = backgroundImage.getIconWidth(); |
| if ((flags & 4) == 4) { |
| paintX = (int)(x + w * hPosition - |
| (float)width * hPosition); |
| } |
| else { |
| paintX = (int)x + (int)hPosition; |
| } |
| if ((flags & 8) == 8) { |
| paintY = (int)(y + h * vPosition - |
| (float)height * vPosition); |
| } |
| else { |
| paintY = (int)y + (int)vPosition; |
| } |
| if (clip == null || |
| !((paintX + width <= clip.x) || |
| (paintY + height <= clip.y) || |
| (paintX >= clip.x + clip.width) || |
| (paintY >= clip.y + clip.height))) { |
| backgroundImage.paintIcon(null, g, paintX, paintY); |
| } |
| } |
| else { |
| int width = backgroundImage.getIconWidth(); |
| int height = backgroundImage.getIconHeight(); |
| if (width > 0 && height > 0) { |
| paintX = (int)x; |
| paintY = (int)y; |
| paintMaxX = (int)(x + w); |
| paintMaxY = (int)(y + h); |
| if (updatePaintCoordinates(clip, width, height)) { |
| while (paintX < paintMaxX) { |
| int ySpot = paintY; |
| while (ySpot < paintMaxY) { |
| backgroundImage.paintIcon(null, g, paintX, |
| ySpot); |
| ySpot += height; |
| } |
| paintX += width; |
| } |
| } |
| } |
| } |
| if (clip != null) { |
| // Reset clip. |
| g.setClip(clip.x, clip.y, clip.width, clip.height); |
| } |
| } |
| |
| private boolean updatePaintCoordinates |
| (Rectangle clip, int width, int height){ |
| if ((flags & 3) == 1) { |
| paintMaxY = paintY + 1; |
| } |
| else if ((flags & 3) == 2) { |
| paintMaxX = paintX + 1; |
| } |
| if (clip != null) { |
| if ((flags & 3) == 1 && ((paintY + height <= clip.y) || |
| (paintY > clip.y + clip.height))) { |
| // not visible. |
| return false; |
| } |
| if ((flags & 3) == 2 && ((paintX + width <= clip.x) || |
| (paintX > clip.x + clip.width))) { |
| // not visible. |
| return false; |
| } |
| if ((flags & 1) == 1) { |
| if ((clip.x + clip.width) < paintMaxX) { |
| if ((clip.x + clip.width - paintX) % width == 0) { |
| paintMaxX = clip.x + clip.width; |
| } |
| else { |
| paintMaxX = ((clip.x + clip.width - paintX) / |
| width + 1) * width + paintX; |
| } |
| } |
| if (clip.x > paintX) { |
| paintX = (clip.x - paintX) / width * width + paintX; |
| } |
| } |
| if ((flags & 2) == 2) { |
| if ((clip.y + clip.height) < paintMaxY) { |
| if ((clip.y + clip.height - paintY) % height == 0) { |
| paintMaxY = clip.y + clip.height; |
| } |
| else { |
| paintMaxY = ((clip.y + clip.height - paintY) / |
| height + 1) * height + paintY; |
| } |
| } |
| if (clip.y > paintY) { |
| paintY = (clip.y - paintY) / height * height + paintY; |
| } |
| } |
| } |
| // Valid |
| return true; |
| } |
| } |
| |
| |
| /** |
| * A subclass of MuxingAttributeSet that translates between |
| * CSS and HTML and StyleConstants. The AttributeSets used are |
| * the CSS rules that match the Views Elements. |
| */ |
| class ViewAttributeSet extends MuxingAttributeSet { |
| ViewAttributeSet(View v) { |
| host = v; |
| |
| // PENDING(prinz) fix this up to be a more realistic |
| // implementation. |
| Document doc = v.getDocument(); |
| SearchBuffer sb = SearchBuffer.obtainSearchBuffer(); |
| Vector<AttributeSet> muxList = sb.getVector(); |
| try { |
| if (doc instanceof HTMLDocument) { |
| StyleSheet styles = StyleSheet.this; |
| Element elem = v.getElement(); |
| AttributeSet a = elem.getAttributes(); |
| AttributeSet htmlAttr = styles.translateHTMLToCSS(a); |
| |
| if (htmlAttr.getAttributeCount() != 0) { |
| muxList.addElement(htmlAttr); |
| } |
| if (elem.isLeaf()) { |
| Enumeration keys = a.getAttributeNames(); |
| while (keys.hasMoreElements()) { |
| Object key = keys.nextElement(); |
| if (key instanceof HTML.Tag) { |
| if (key == HTML.Tag.A) { |
| Object o = a.getAttribute(key); |
| /** |
| In the case of an A tag, the css rules |
| apply only for tags that have their |
| href attribute defined and not for |
| anchors that only have their name attributes |
| defined, i.e anchors that function as |
| destinations. Hence we do not add the |
| attributes for that latter kind of |
| anchors. When CSS2 support is added, |
| it will be possible to specificity this |
| kind of conditional behaviour in the |
| stylesheet. |
| **/ |
| if (o != null && o instanceof AttributeSet) { |
| AttributeSet attr = (AttributeSet)o; |
| if (attr.getAttribute(HTML.Attribute.HREF) == null) { |
| continue; |
| } |
| } |
| } |
| AttributeSet cssRule = styles.getRule((HTML.Tag) key, elem); |
| if (cssRule != null) { |
| muxList.addElement(cssRule); |
| } |
| } |
| } |
| } else { |
| HTML.Tag t = (HTML.Tag) a.getAttribute |
| (StyleConstants.NameAttribute); |
| AttributeSet cssRule = styles.getRule(t, elem); |
| if (cssRule != null) { |
| muxList.addElement(cssRule); |
| } |
| } |
| } |
| AttributeSet[] attrs = new AttributeSet[muxList.size()]; |
| muxList.copyInto(attrs); |
| setAttributes(attrs); |
| } |
| finally { |
| SearchBuffer.releaseSearchBuffer(sb); |
| } |
| } |
| |
| // --- AttributeSet methods ---------------------------- |
| |
| /** |
| * Checks whether a given attribute is defined. |
| * This will convert the key over to CSS if the |
| * key is a StyleConstants key that has a CSS |
| * mapping. |
| * |
| * @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) { |
| key = cssKey; |
| } |
| } |
| return super.isDefined(key); |
| } |
| |
| /** |
| * Gets the value of an attribute. If the requested |
| * attribute is a StyleConstants attribute that has |
| * a CSS mapping, the request will be converted. |
| * |
| * @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 = doGetAttribute(cssKey); |
| if (value instanceof CSS.CssValue) { |
| return ((CSS.CssValue)value).toStyleConstants |
| ((StyleConstants)key, host); |
| } |
| } |
| } |
| return doGetAttribute(key); |
| } |
| |
| Object doGetAttribute(Object key) { |
| Object retValue = super.getAttribute(key); |
| if (retValue != null) { |
| return retValue; |
| } |
| // didn't find it... try parent if it's a css attribute |
| // that is inherited. |
| if (key instanceof CSS.Attribute) { |
| CSS.Attribute css = (CSS.Attribute) key; |
| if (css.isInherited()) { |
| AttributeSet parent = getResolveParent(); |
| if (parent != null) |
| return parent.getAttribute(key); |
| } |
| } |
| return null; |
| } |
| |
| /** |
| * If not overriden, the resolving parent defaults to |
| * the parent element. |
| * |
| * @return the attributes from the parent |
| * @see AttributeSet#getResolveParent |
| */ |
| public AttributeSet getResolveParent() { |
| if (host == null) { |
| return null; |
| } |
| View parent = host.getParent(); |
| return (parent != null) ? parent.getAttributes() : null; |
| } |
| |
| /** View created for. */ |
| View host; |
| } |
| |
| |
| /** |
| * A subclass of MuxingAttributeSet that implements Style. Currently |
| * the MutableAttributeSet methods are unimplemented, that is they |
| * do nothing. |
| */ |
| // PENDING(sky): Decide what to do with this. Either make it |
| // contain a SimpleAttributeSet that modify methods are delegated to, |
| // or change getRule to return an AttributeSet and then don't make this |
| // implement Style. |
| static class ResolvedStyle extends MuxingAttributeSet implements |
| Serializable, Style { |
| ResolvedStyle(String name, AttributeSet[] attrs, int extendedIndex) { |
| super(attrs); |
| this.name = name; |
| this.extendedIndex = extendedIndex; |
| } |
| |
| /** |
| * Inserts a Style into the receiver so that the styles the |
| * receiver represents are still ordered by specificity. |
| * <code>style</code> will be added before any extended styles, that |
| * is before extendedIndex. |
| */ |
| synchronized void insertStyle(Style style, int specificity) { |
| AttributeSet[] attrs = getAttributes(); |
| int maxCounter = attrs.length; |
| int counter = 0; |
| for (;counter < extendedIndex; counter++) { |
| if (specificity > getSpecificity(((Style)attrs[counter]). |
| getName())) { |
| break; |
| } |
| } |
| insertAttributeSetAt(style, counter); |
| extendedIndex++; |
| } |
| |
| /** |
| * Removes a previously added style. This will do nothing if |
| * <code>style</code> is not referenced by the receiver. |
| */ |
| synchronized void removeStyle(Style style) { |
| AttributeSet[] attrs = getAttributes(); |
| |
| for (int counter = attrs.length - 1; counter >= 0; counter--) { |
| if (attrs[counter] == style) { |
| removeAttributeSetAt(counter); |
| if (counter < extendedIndex) { |
| extendedIndex--; |
| } |
| break; |
| } |
| } |
| } |
| |
| /** |
| * Adds <code>s</code> as one of the Attributesets to look up |
| * attributes in. |
| */ |
| synchronized void insertExtendedStyleAt(Style attr, int index) { |
| insertAttributeSetAt(attr, extendedIndex + index); |
| } |
| |
| /** |
| * Adds <code>s</code> as one of the AttributeSets to look up |
| * attributes in. It will be the AttributeSet last checked. |
| */ |
| synchronized void addExtendedStyle(Style attr) { |
| insertAttributeSetAt(attr, getAttributes().length); |
| } |
| |
| /** |
| * Removes the style at <code>index</code> + |
| * <code>extendedIndex</code>. |
| */ |
| synchronized void removeExtendedStyleAt(int index) { |
| removeAttributeSetAt(extendedIndex + index); |
| } |
| |
| /** |
| * Returns true if the receiver matches <code>selector</code>, where |
| * a match is defined by the CSS rule matching. |
| * Each simple selector must be separated by a single space. |
| */ |
| protected boolean matches(String selector) { |
| int sLast = selector.length(); |
| |
| if (sLast == 0) { |
| return false; |
| } |
| int thisLast = name.length(); |
| int sCurrent = selector.lastIndexOf(' '); |
| int thisCurrent = name.lastIndexOf(' '); |
| if (sCurrent >= 0) { |
| sCurrent++; |
| } |
| if (thisCurrent >= 0) { |
| thisCurrent++; |
| } |
| if (!matches(selector, sCurrent, sLast, thisCurrent, thisLast)) { |
| return false; |
| } |
| while (sCurrent != -1) { |
| sLast = sCurrent - 1; |
| sCurrent = selector.lastIndexOf(' ', sLast - 1); |
| if (sCurrent >= 0) { |
| sCurrent++; |
| } |
| boolean match = false; |
| while (!match && thisCurrent != -1) { |
| thisLast = thisCurrent - 1; |
| thisCurrent = name.lastIndexOf(' ', thisLast - 1); |
| if (thisCurrent >= 0) { |
| thisCurrent++; |
| } |
| match = matches(selector, sCurrent, sLast, thisCurrent, |
| thisLast); |
| } |
| if (!match) { |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| /** |
| * Returns true if the substring of the receiver, in the range |
| * thisCurrent, thisLast matches the substring of selector in |
| * the ranme sCurrent to sLast based on CSS selector matching. |
| */ |
| boolean matches(String selector, int sCurrent, int sLast, |
| int thisCurrent, int thisLast) { |
| sCurrent = Math.max(sCurrent, 0); |
| thisCurrent = Math.max(thisCurrent, 0); |
| int thisDotIndex = boundedIndexOf(name, '.', thisCurrent, |
| thisLast); |
| int thisPoundIndex = boundedIndexOf(name, '#', thisCurrent, |
| thisLast); |
| int sDotIndex = boundedIndexOf(selector, '.', sCurrent, sLast); |
| int sPoundIndex = boundedIndexOf(selector, '#', sCurrent, sLast); |
| if (sDotIndex != -1) { |
| // Selector has a '.', which indicates name must match it, |
| // or if the '.' starts the selector than name must have |
| // the same class (doesn't matter what element name). |
| if (thisDotIndex == -1) { |
| return false; |
| } |
| if (sCurrent == sDotIndex) { |
| if ((thisLast - thisDotIndex) != (sLast - sDotIndex) || |
| !selector.regionMatches(sCurrent, name, thisDotIndex, |
| (thisLast - thisDotIndex))) { |
| return false; |
| } |
| } |
| else { |
| // Has to fully match. |
| if ((sLast - sCurrent) != (thisLast - thisCurrent) || |
| !selector.regionMatches(sCurrent, name, thisCurrent, |
| (thisLast - thisCurrent))) { |
| return false; |
| } |
| } |
| return true; |
| } |
| if (sPoundIndex != -1) { |
| // Selector has a '#', which indicates name must match it, |
| // or if the '#' starts the selector than name must have |
| // the same id (doesn't matter what element name). |
| if (thisPoundIndex == -1) { |
| return false; |
| } |
| if (sCurrent == sPoundIndex) { |
| if ((thisLast - thisPoundIndex) !=(sLast - sPoundIndex) || |
| !selector.regionMatches(sCurrent, name, thisPoundIndex, |
| (thisLast - thisPoundIndex))) { |
| return false; |
| } |
| } |
| else { |
| // Has to fully match. |
| if ((sLast - sCurrent) != (thisLast - thisCurrent) || |
| !selector.regionMatches(sCurrent, name, thisCurrent, |
| (thisLast - thisCurrent))) { |
| return false; |
| } |
| } |
| return true; |
| } |
| if (thisDotIndex != -1) { |
| // Reciever references a class, just check element name. |
| return (((thisDotIndex - thisCurrent) == (sLast - sCurrent)) && |
| selector.regionMatches(sCurrent, name, thisCurrent, |
| thisDotIndex - thisCurrent)); |
| } |
| if (thisPoundIndex != -1) { |
| // Reciever references an id, just check element name. |
| return (((thisPoundIndex - thisCurrent) ==(sLast - sCurrent))&& |
| selector.regionMatches(sCurrent, name, thisCurrent, |
| thisPoundIndex - thisCurrent)); |
| } |
| // Fail through, no classes or ides, just check string. |
| return (((thisLast - thisCurrent) == (sLast - sCurrent)) && |
| selector.regionMatches(sCurrent, name, thisCurrent, |
| thisLast - thisCurrent)); |
| } |
| |
| /** |
| * Similiar to String.indexOf, but allows an upper bound |
| * (this is slower in that it will still check string starting at |
| * start. |
| */ |
| int boundedIndexOf(String string, char search, int start, |
| int end) { |
| int retValue = string.indexOf(search, start); |
| if (retValue >= end) { |
| return -1; |
| } |
| return retValue; |
| } |
| |
| public void addAttribute(Object name, Object value) {} |
| public void addAttributes(AttributeSet attributes) {} |
| public void removeAttribute(Object name) {} |
| public void removeAttributes(Enumeration<?> names) {} |
| public void removeAttributes(AttributeSet attributes) {} |
| public void setResolveParent(AttributeSet parent) {} |
| public String getName() {return name;} |
| public void addChangeListener(ChangeListener l) {} |
| public void removeChangeListener(ChangeListener l) {} |
| public ChangeListener[] getChangeListeners() { |
| return new ChangeListener[0]; |
| } |
| |
| /** The name of the Style, which is the selector. |
| * This will NEVER change! |
| */ |
| String name; |
| /** Start index of styles coming from other StyleSheets. */ |
| private int extendedIndex; |
| } |
| |
| |
| /** |
| * SelectorMapping contains a specifitiy, as an integer, and an associated |
| * Style. It can also reference children <code>SelectorMapping</code>s, |
| * so that it behaves like a tree. |
| * <p> |
| * This is not thread safe, it is assumed the caller will take the |
| * necessary precations if this is to be used in a threaded environment. |
| */ |
| static class SelectorMapping implements Serializable { |
| public SelectorMapping(int specificity) { |
| this.specificity = specificity; |
| } |
| |
| /** |
| * Returns the specificity this mapping represents. |
| */ |
| public int getSpecificity() { |
| return specificity; |
| } |
| |
| /** |
| * Sets the Style associated with this mapping. |
| */ |
| public void setStyle(Style style) { |
| this.style = style; |
| } |
| |
| /** |
| * Returns the Style associated with this mapping. |
| */ |
| public Style getStyle() { |
| return style; |
| } |
| |
| /** |
| * Returns the child mapping identified by the simple selector |
| * <code>selector</code>. If a child mapping does not exist for |
| *<code>selector</code>, and <code>create</code> is true, a new |
| * one will be created. |
| */ |
| public SelectorMapping getChildSelectorMapping(String selector, |
| boolean create) { |
| SelectorMapping retValue = null; |
| |
| if (children != null) { |
| retValue = children.get(selector); |
| } |
| else if (create) { |
| children = new HashMap<String, SelectorMapping>(7); |
| } |
| if (retValue == null && create) { |
| int specificity = getChildSpecificity(selector); |
| |
| retValue = createChildSelectorMapping(specificity); |
| children.put(selector, retValue); |
| } |
| return retValue; |
| } |
| |
| /** |
| * Creates a child <code>SelectorMapping</code> with the specified |
| * <code>specificity</code>. |
| */ |
| protected SelectorMapping createChildSelectorMapping(int specificity) { |
| return new SelectorMapping(specificity); |
| } |
| |
| /** |
| * Returns the specificity for the child selector |
| * <code>selector</code>. |
| */ |
| protected int getChildSpecificity(String selector) { |
| // class (.) 100 |
| // id (#) 10000 |
| char firstChar = selector.charAt(0); |
| int specificity = getSpecificity(); |
| |
| if (firstChar == '.') { |
| specificity += 100; |
| } |
| else if (firstChar == '#') { |
| specificity += 10000; |
| } |
| else { |
| specificity += 1; |
| if (selector.indexOf('.') != -1) { |
| specificity += 100; |
| } |
| if (selector.indexOf('#') != -1) { |
| specificity += 10000; |
| } |
| } |
| return specificity; |
| } |
| |
| /** |
| * The specificity for this selector. |
| */ |
| private int specificity; |
| /** |
| * Style for this selector. |
| */ |
| private Style style; |
| /** |
| * Any sub selectors. Key will be String, and value will be |
| * another SelectorMapping. |
| */ |
| private HashMap<String, SelectorMapping> children; |
| } |
| |
| |
| // ---- Variables --------------------------------------------- |
| |
| final static int DEFAULT_FONT_SIZE = 3; |
| |
| private CSS css; |
| |
| /** |
| * An inverted graph of the selectors. |
| */ |
| private SelectorMapping selectorMapping; |
| |
| /** Maps from selector (as a string) to Style that includes all |
| * relevant styles. */ |
| private Hashtable<String, ResolvedStyle> resolvedStyles; |
| |
| /** Vector of StyleSheets that the rules are to reference. |
| */ |
| private Vector<StyleSheet> linkedStyleSheets; |
| |
| /** Where the style sheet was found. Used for relative imports. */ |
| private URL base; |
| |
| |
| /** |
| * Default parser for CSS specifications that get loaded into |
| * the StyleSheet.<p> |
| * This class is NOT thread safe, do not ask it to parse while it is |
| * in the middle of parsing. |
| */ |
| class CssParser implements CSSParser.CSSParserCallback { |
| |
| /** |
| * Parses the passed in CSS declaration into an AttributeSet. |
| */ |
| public AttributeSet parseDeclaration(String string) { |
| try { |
| return parseDeclaration(new StringReader(string)); |
| } catch (IOException ioe) {} |
| return null; |
| } |
| |
| /** |
| * Parses the passed in CSS declaration into an AttributeSet. |
| */ |
| public AttributeSet parseDeclaration(Reader r) throws IOException { |
| parse(base, r, true, false); |
| return declaration.copyAttributes(); |
| } |
| |
| /** |
| * Parse the given CSS stream |
| */ |
| public void parse(URL base, Reader r, boolean parseDeclaration, |
| boolean isLink) throws IOException { |
| this.base = base; |
| this.isLink = isLink; |
| this.parsingDeclaration = parseDeclaration; |
| declaration.removeAttributes(declaration); |
| selectorTokens.removeAllElements(); |
| selectors.removeAllElements(); |
| propertyName = null; |
| parser.parse(r, this, parseDeclaration); |
| } |
| |
| // |
| // CSSParserCallback methods, public to implement the interface. |
| // |
| |
| /** |
| * Invoked when a valid @import is encountered, will call |
| * <code>importStyleSheet</code> if a |
| * <code>MalformedURLException</code> is not thrown in creating |
| * the URL. |
| */ |
| public void handleImport(String importString) { |
| URL url = CSS.getURL(base, importString); |
| if (url != null) { |
| importStyleSheet(url); |
| } |
| } |
| |
| /** |
| * A selector has been encountered. |
| */ |
| public void handleSelector(String selector) { |
| //class and index selectors are case sensitive |
| if (!(selector.startsWith(".") |
| || selector.startsWith("#"))) { |
| selector = selector.toLowerCase(); |
| } |
| int length = selector.length(); |
| |
| if (selector.endsWith(",")) { |
| if (length > 1) { |
| selector = selector.substring(0, length - 1); |
| selectorTokens.addElement(selector); |
| } |
| addSelector(); |
| } |
| else if (length > 0) { |
| selectorTokens.addElement(selector); |
| } |
| } |
| |
| /** |
| * Invoked when the start of a rule is encountered. |
| */ |
| public void startRule() { |
| if (selectorTokens.size() > 0) { |
| addSelector(); |
| } |
| propertyName = null; |
| } |
| |
| /** |
| * Invoked when a property name is encountered. |
| */ |
| public void handleProperty(String property) { |
| propertyName = property; |
| } |
| |
| /** |
| * Invoked when a property value is encountered. |
| */ |
| public void handleValue(String value) { |
| if (propertyName != null && value != null && value.length() > 0) { |
| CSS.Attribute cssKey = CSS.getAttribute(propertyName); |
| if (cssKey != null) { |
| // There is currently no mechanism to determine real |
| // base that style sheet was loaded from. For the time |
| // being, this maps for LIST_STYLE_IMAGE, which appear |
| // to be the only one that currently matters. A more |
| // general mechanism is definately needed. |
| if (cssKey == CSS.Attribute.LIST_STYLE_IMAGE) { |
| if (value != null && !value.equals("none")) { |
| URL url = CSS.getURL(base, value); |
| |
| if (url != null) { |
| value = url.toString(); |
| } |
| } |
| } |
| addCSSAttribute(declaration, cssKey, value); |
| } |
| propertyName = null; |
| } |
| } |
| |
| /** |
| * Invoked when the end of a rule is encountered. |
| */ |
| public void endRule() { |
| int n = selectors.size(); |
| for (int i = 0; i < n; i++) { |
| String[] selector = selectors.elementAt(i); |
| if (selector.length > 0) { |
| StyleSheet.this.addRule(selector, declaration, isLink); |
| } |
| } |
| declaration.removeAttributes(declaration); |
| selectors.removeAllElements(); |
| } |
| |
| private void addSelector() { |
| String[] selector = new String[selectorTokens.size()]; |
| selectorTokens.copyInto(selector); |
| selectors.addElement(selector); |
| selectorTokens.removeAllElements(); |
| } |
| |
| |
| Vector<String[]> selectors = new Vector<String[]>(); |
| Vector<String> selectorTokens = new Vector<String>(); |
| /** Name of the current property. */ |
| String propertyName; |
| MutableAttributeSet declaration = new SimpleAttributeSet(); |
| /** True if parsing a declaration, that is the Reader will not |
| * contain a selector. */ |
| boolean parsingDeclaration; |
| /** True if the attributes are coming from a linked/imported style. */ |
| boolean isLink; |
| /** Where the CSS stylesheet lives. */ |
| URL base; |
| CSSParser parser = new CSSParser(); |
| } |
| |
| void rebaseSizeMap(int base) { |
| final int minimalFontSize = 4; |
| sizeMap = new int[sizeMapDefault.length]; |
| for (int i = 0; i < sizeMapDefault.length; i++) { |
| sizeMap[i] = Math.max(base * sizeMapDefault[i] / |
| sizeMapDefault[CSS.baseFontSizeIndex], |
| minimalFontSize); |
| } |
| |
| } |
| |
| int[] getSizeMap() { |
| return sizeMap; |
| } |
| boolean isW3CLengthUnits() { |
| return w3cLengthUnits; |
| } |
| |
| /** |
| * The HTML/CSS size model has seven slots |
| * that one can assign sizes to. |
| */ |
| static final int sizeMapDefault[] = { 8, 10, 12, 14, 18, 24, 36 }; |
| |
| private int sizeMap[] = sizeMapDefault; |
| private boolean w3cLengthUnits = false; |
| } |