You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
3339 lines
124 KiB
3339 lines
124 KiB
/*
|
|
* Copyright (c) 1997, 2013, Oracle and/or its affiliates. All rights reserved.
|
|
* ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*/
|
|
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.
|
|
* <pre><code>
|
|
*
|
|
* 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);
|
|
* }
|
|
* }
|
|
*
|
|
* </code></pre>
|
|
* <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
|
|
* implementation 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 identify 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 maintaining 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) {
|
|
// Receiver references a class, just check element name.
|
|
return (((thisDotIndex - thisCurrent) == (sLast - sCurrent)) &&
|
|
selector.regionMatches(sCurrent, name, thisCurrent,
|
|
thisDotIndex - thisCurrent));
|
|
}
|
|
if (thisPoundIndex != -1) {
|
|
// Receiver 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));
|
|
}
|
|
|
|
/**
|
|
* Similar 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;
|
|
}
|