/*
* Copyright 2006 - 2013
* Stefan Balev <stefan.balev@graphstream-project.org>
* Julien Baudry <julien.baudry@graphstream-project.org>
* Antoine Dutot <antoine.dutot@graphstream-project.org>
* Yoann Pigné <yoann.pigne@graphstream-project.org>
* Guilhelm Savin <guilhelm.savin@graphstream-project.org>
*
* This file is part of GraphStream <http://graphstream-project.org>.
*
* GraphStream is a library whose purpose is to handle static or dynamic
* graph, create them from scratch, file or any source and display them.
*
* This program is free software distributed under the terms of two licenses, the
* CeCILL-C license that fits European law, and the GNU Lesser General Public
* License. You can use, modify and/ or redistribute the software under the terms
* of the CeCILL-C license as circulated by CEA, CNRS and INRIA at the following
* URL <http://www.cecill.info> or under the terms of the GNU LGPL as published by
* the Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
* PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* The fact that you are presently reading this means that you have had
* knowledge of the CeCILL-C and LGPL licenses and that you accept their terms.
*/
package org.graphstream.ui.graphicGraph.stylesheet;
import java.awt.Color;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.StringReader;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import org.graphstream.graph.Edge;
import org.graphstream.graph.Element;
import org.graphstream.graph.Graph;
import org.graphstream.graph.Node;
import org.graphstream.ui.graphicGraph.GraphicSprite;
import org.graphstream.ui.graphicGraph.stylesheet.StyleConstants.StrokeMode;
import org.graphstream.ui.graphicGraph.stylesheet.parser.StyleSheetParser;
import org.graphstream.util.parser.ParseException;
/**
* Implementation of the style sheets that can be stored in the graphic graph.
*
* @author Antoine Dutot
*/
public class StyleSheet {
// Attributes
/**
* The top-level default rule.
*/
public Rule defaultRule;
/**
* The default, id and class rules for graphs.
*/
public NameSpace graphRules = new NameSpace(Selector.Type.GRAPH);
/**
* The default, id and class rules for nodes.
*/
public NameSpace nodeRules = new NameSpace(Selector.Type.NODE);
/**
* The default, id and class rules for edges.
*/
public NameSpace edgeRules = new NameSpace(Selector.Type.EDGE);
/**
* The default, id and class rules for sprites.
*/
public NameSpace spriteRules = new NameSpace(Selector.Type.SPRITE);
/**
* Set of listeners.
*/
public ArrayList<StyleSheetListener> listeners = new ArrayList<StyleSheetListener>();
// Constructors
/**
* New style sheet initialised to defaults.
*/
public StyleSheet() {
initRules();
}
// Access
/**
* The default rule for graphs.
*
* @return A rule.
*/
public Rule getDefaultGraphRule() {
return graphRules.defaultRule;
}
/**
* The default rule for nodes.
*
* @return A rule.
*/
public Rule getDefaultNodeRule() {
return nodeRules.defaultRule;
}
/**
* The default rule for edges.
*
* @return A rule.
*/
public Rule getDefaultEdgeRule() {
return edgeRules.defaultRule;
}
/**
* The default rule for sprites.
*
* @return A rule.
*/
public Rule getDefaultSpriteRule() {
return spriteRules.defaultRule;
}
/**
* The default style for graphs.
*
* @return A style.
*/
public Style getDefaultGraphStyle() {
return getDefaultGraphRule().getStyle();
}
/**
* The default style for nodes.
*
* @return A style.
*/
public Style getDefaultNodeStyle() {
return getDefaultNodeRule().getStyle();
}
/**
* The default style for edges.
*
* @return A style.
*/
public Style getDefaultEdgeStyle() {
return getDefaultEdgeRule().getStyle();
}
/**
* The default style for sprites.
*
* @return A style.
*/
public Style getDefaultSpriteStyle() {
return getDefaultSpriteRule().getStyle();
}
/**
* All the rules (default, specific and class) that apply to graphs.
*
* @return The set of rules for graphs.
*/
public NameSpace getGraphStyleNameSpace() {
return graphRules;
}
/**
* All the rules (default, specific and class) that apply to nodes.
*
* @return The set of rules for nodes.
*/
public NameSpace getNodeStyleNameSpace() {
return nodeRules;
}
/**
* All the rules (default, specific and class) that apply to edges.
*
* @return The set of rules for edges.
*/
public NameSpace getEdgeStyleNameSpace() {
return edgeRules;
}
/**
* All the rules (default, specific and class) that apply to sprites.
*
* @return The set of rules for sprites.
*/
public NameSpace getSpriteStyleNameSpace() {
return spriteRules;
}
/**
* Get the rules that match a given element.
*
* First a rule for the identifier of the element is looked for. It is
* looked for in its name space (nodes for Node element, etc.) If it is not
* found, the default rule for this kind of element is used. This rule is
* pushed at start of the returned array of rules.
*
* After a rule for the element is found, then the various classes the
* element pertains to are looked at and each class rule found is added in
* order in the returned array.
*
* @param element
* The element a rules are searched for.
* @return A set of rules matching the element, with the main rule at index
* 0.
*/
public ArrayList<Rule> getRulesFor(Element element) {
ArrayList<Rule> rules = null;
if (element instanceof Graph) {
rules = graphRules.getRulesFor(element);
} else if (element instanceof Node) {
rules = nodeRules.getRulesFor(element);
} else if (element instanceof Edge) {
rules = edgeRules.getRulesFor(element);
} else if (element instanceof GraphicSprite) {
rules = spriteRules.getRulesFor(element);
} else {
rules = new ArrayList<Rule>();
rules.add(defaultRule);
}
return rules;
}
/**
* Compute the name of the style group and element will pertain to knowing
* its styling rules.
*
* @param element
* The element.
* @param rules
* The styling rules.
* @return The unique identifier of the style group for the element.
* @see #getRulesFor(Element)
*/
public String getStyleGroupIdFor(Element element, ArrayList<Rule> rules) {
StringBuilder builder = new StringBuilder();
if (element instanceof Graph) {
builder.append("g");
} else if (element instanceof Node) {
builder.append("n");
} else if (element instanceof Edge) {
builder.append("e");
} else if (element instanceof GraphicSprite) {
builder.append("s");
} else {
throw new RuntimeException("What ?");
}
if (rules.get(0).selector.getId() != null) {
builder.append('_');
builder.append(rules.get(0).selector.getId());
}
int n = rules.size();
if (n > 1) {
builder.append('(');
builder.append(rules.get(1).selector.getClazz());
for (int i = 2; i < n; i++) {
builder.append(',');
builder.append(rules.get(i).selector.getClazz());
}
builder.append(')');
}
return builder.toString();
}
// Commands
/**
* Create the default rules. This method is the place to set defaults for
* specific element types. This is here that the edge width is reset to one,
* since the default width is larger. The default z index that is different
* for every class of element is also set here.
*/
protected void initRules() {
defaultRule = new Rule(new Selector(Selector.Type.ANY), null);
defaultRule.getStyle().setDefaults();
graphRules.defaultRule = new Rule(new Selector(Selector.Type.GRAPH),
defaultRule);
nodeRules.defaultRule = new Rule(new Selector(Selector.Type.NODE),
defaultRule);
edgeRules.defaultRule = new Rule(new Selector(Selector.Type.EDGE),
defaultRule);
spriteRules.defaultRule = new Rule(new Selector(Selector.Type.SPRITE),
defaultRule);
graphRules.defaultRule.getStyle().setValue("padding",
new Values(Style.Units.PX, 30));
edgeRules.defaultRule.getStyle().setValue("shape",
StyleConstants.Shape.LINE);
edgeRules.defaultRule.getStyle().setValue("size",
new Values(Style.Units.PX, 1));
edgeRules.defaultRule.getStyle().setValue("z-index", new Integer(1));
nodeRules.defaultRule.getStyle().setValue("z-index", new Integer(2));
spriteRules.defaultRule.getStyle().setValue("z-index", new Integer(3));
Colors colors = new Colors();
colors.add(Color.WHITE);
graphRules.defaultRule.getStyle().setValue("fill-color", colors);
graphRules.defaultRule.getStyle().setValue("stroke-mode",
StrokeMode.NONE);
for (StyleSheetListener listener : listeners) {
listener.styleAdded(defaultRule, defaultRule);
listener.styleAdded(graphRules.defaultRule, graphRules.defaultRule);
listener.styleAdded(nodeRules.defaultRule, nodeRules.defaultRule);
listener.styleAdded(edgeRules.defaultRule, edgeRules.defaultRule);
listener.styleAdded(spriteRules.defaultRule,
spriteRules.defaultRule);
}
// for( StyleSheetListener listener: listeners )
// listener.styleAdded( defaultRule, defaultRule );
// for( StyleSheetListener listener: listeners )
// listener.styleAdded( graphRules.defaultRule, graphRules.defaultRule
// );
// for( StyleSheetListener listener: listeners )
// listener.styleAdded( nodeRules.defaultRule, nodeRules.defaultRule );
// for( StyleSheetListener listener: listeners )
// listener.styleAdded( edgeRules.defaultRule, edgeRules.defaultRule );
// for( StyleSheetListener listener: listeners )
// listener.styleAdded( spriteRules.defaultRule, spriteRules.defaultRule
// );
}
/**
* Add a listener for style events. You never receive events for default
* rules and styles. You receive events only for the rules and styles that
* are added after this listener is registered.
*
* @param listener
* The new listener.
*/
public void addListener(StyleSheetListener listener) {
listeners.add(listener);
}
/**
* Remove a previously registered listener.
*
* @param listener
* The listener to remove.
*/
public void removeListener(StyleSheetListener listener) {
int index = listeners.indexOf(listener);
if (index >= 0)
listeners.remove(index);
}
/**
* Clear all specific rules and initialise the default rules. The listeners
* are not changed.
*/
public void clear() {
graphRules.clear();
nodeRules.clear();
edgeRules.clear();
spriteRules.clear();
initRules();
for (StyleSheetListener listener : listeners)
listener.styleSheetCleared();
}
/**
* Parse a style sheet from a file. The style sheet will complete the
* previously parsed style sheets.
*
* @param fileName
* Name of the file containing the style sheet.
* @throws IOException
* For any kind of I/O error or parse error.
*/
public void parseFromFile(String fileName) throws IOException {
parse(new InputStreamReader(new BufferedInputStream(
new FileInputStream(fileName))));
}
/**
* Parse a style sheet from an URL. The style sheet will complete the
* previously parsed style sheets. First, this method will search the URL as
* SystemRessource, then as a file and if there is no match, just try to
* create an URL object giving the URL as constructor's parameter.
*
* @param url
* Name of the file containing the style sheet.
* @throws IOException
* For any kind of I/O error or parse error.
*/
public void parseFromURL(String url) throws IOException {
URL u = StyleSheet.class.getClassLoader().getResource(url);
if (u == null) {
File f = new File(url);
if (f.exists())
u = f.toURI().toURL();
else
u = new URL(url);
}
parse(new InputStreamReader(u.openStream()));
}
/**
* Parse a style sheet from a string. The style sheet will complete the
* previously parsed style sheets.
*
* @param styleSheet
* The string containing the whole style sheet.
* @throws IOException
* For any kind of I/O error or parse error.
*/
public void parseFromString(String styleSheet) throws IOException {
parse(new StringReader(styleSheet));
}
/**
* Parse only one style, create a rule with the given selector, and add this
* rule.
*
* @param select
* The elements for which this style must apply.
* @param styleString
* The style string to parse.
*/
public void parseStyleFromString(Selector select, String styleString)
throws IOException {
StyleSheetParser parser = new StyleSheetParser(this, new StringReader(
styleString));
Style style = new Style();
try {
parser.stylesStart(style);
} catch (ParseException e) {
throw new IOException(e.getMessage());
}
Rule rule = new Rule(select);
rule.setStyle(style);
addRule(rule);
}
/**
* Load a style sheet from an attribute value, the value can either be the
* contents of the whole style sheet, or begin by "url". If it starts with
* "url", it must then contain between parenthesis the string of the URL to
* load. For example:
*
* <pre>
* url('file:///some/path/on/the/file/system')
* </pre>
*
* Or
*
* <pre>
* url('http://some/web/url')
* </pre>
*
* The loaded style sheet will be merged with the styles already present in
* the style sheet.
*
* @param styleSheetValue
* The style sheet name of content.
* @throws IOException
* If the loading or parsing of the style sheet failed.
*/
public void load(String styleSheetValue) throws IOException {
if (styleSheetValue.startsWith("url")) {
// Extract the part between '(' and ')'.
int beg = styleSheetValue.indexOf('(');
int end = styleSheetValue.lastIndexOf(')');
if (beg >= 0 && end > beg)
styleSheetValue = styleSheetValue.substring(beg + 1, end);
styleSheetValue = styleSheetValue.trim();
// Remove the quotes (') or (").
if (styleSheetValue.startsWith("'")) {
beg = 0;
end = styleSheetValue.lastIndexOf('\'');
if (beg >= 0 && end > beg)
styleSheetValue = styleSheetValue.substring(beg + 1, end);
}
styleSheetValue = styleSheetValue.trim();
if (styleSheetValue.startsWith("\"")) {
beg = 0;
end = styleSheetValue.lastIndexOf('"');
if (beg >= 0 && end > beg)
styleSheetValue = styleSheetValue.substring(beg + 1, end);
}
// That's it.
parseFromURL(styleSheetValue);
} else // Parse from string, the value is considered to be the style
// sheet contents.
{
parseFromString(styleSheetValue);
}
}
/**
* Parse the style sheet from the given reader.
*
* @param reader
* The reader pointing at the style sheet.
* @throws IOException
* For any kind of I/O error or parse error.
*/
protected void parse(Reader reader) throws IOException {
StyleSheetParser parser = new StyleSheetParser(this, reader);
try {
parser.start();
} catch (ParseException e) {
throw new IOException(e.getMessage());
}
}
/**
* Add a new rule with its style. If the rule selector is just GRAPH, NODE,
* EDGE or SPRITE, the default corresponding rules make a copy (or
* augmentation) of its style. Else if an id or class is specified the rules
* are added (or changed/augmented if the id or class was already set) and
* their parent is set to the default graph, node, edge or sprite rules. If
* this is an event rule (or meta-class rule), its sibling rule (the same
* rule without the meta-class) is searched and created if not found and the
* event rule is added as an alternative to it.
*
* @param newRule
* The new rule.
*/
public void addRule(Rule newRule) {
Rule oldRule = null;
switch (newRule.selector.getType()) {
case ANY:
throw new RuntimeException(
"The ANY selector should never be used, it is created automatically.");
case GRAPH:
oldRule = graphRules.addRule(newRule);
break;
case NODE:
oldRule = nodeRules.addRule(newRule);
break;
case EDGE:
oldRule = edgeRules.addRule(newRule);
break;
case SPRITE:
oldRule = spriteRules.addRule(newRule);
break;
default:
throw new RuntimeException("Ho ho ho ?");
}
for (StyleSheetListener listener : listeners)
listener.styleAdded(oldRule, newRule);
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append("StyleSheet : {\n");
builder.append(" default styles:\n");
builder.append(defaultRule.toString(1));
builder.append(graphRules.toString(1));
builder.append(nodeRules.toString(1));
builder.append(edgeRules.toString(1));
builder.append(spriteRules.toString(1));
return builder.toString();
}
// Nested classes
/**
* A name space is a tuple (default rule, id rule set, class rule set).
*
* <p>
* The name space defines a default rule for a kind of elements, a set of
* rules for this kind of elements with a given identifier, and a set or
* rules for this kind of elements with a given class.
* </p>
*/
public class NameSpace {
// Attribute
/**
* The kind of elements in this name space.
*/
public Selector.Type type;
/**
* The default rule for this kind of elements.
*/
public Rule defaultRule;
/**
* The set of rules for this kind of elements with a given identifier.
*/
public HashMap<String, Rule> byId = new HashMap<String, Rule>();
/**
* The set of rules for this kind of elements with a given class.
*/
public HashMap<String, Rule> byClass = new HashMap<String, Rule>();
// Constructor
public NameSpace(Selector.Type type) {
this.type = type;
}
// Access
/**
* The kind of elements this name space applies rules to.
*
* @return A type of element (node, edge, sprite, graph).
*/
public Selector.Type getGraphElementType() {
return type;
}
/**
* Number of specific (id) rules.
*
* @return The number of rules that apply to elements by their
* identifiers.
*/
public int getIdRulesCount() {
return byId.size();
}
/**
* Number of specific (class) rules.
*
* @return The number of rules that apply to elements by their classes.
*/
public int getClassRulesCount() {
return byClass.size();
}
/**
* Get the rules that match a given element. The rules are returned in a
* given order. The array always contain the "main" rule that matches
* the element. This rule is either a default rule for the kind of
* element given or the rule that matches its identifier if there is
* one. Then class rules the element has can be appended to this array
* in order.
*
* @return an array of rules that match the element, with the main rule
* at index 0.
*/
protected ArrayList<Rule> getRulesFor(Element element) {
Rule rule = byId.get(element.getId());
ArrayList<Rule> rules = new ArrayList<Rule>();
if (rule != null)
rules.add(rule);
else
rules.add(defaultRule);
getClassRules(element, rules);
if (rules.isEmpty())
rules.add(defaultRule);
return rules;
}
/**
* Search if the given element has classes attributes and fill the given
* array with the set of rules that match these classes.
*
* @param element
* The element for which classes must be found.
* @param rules
* The rule array to fill.
*/
protected void getClassRules(Element element, ArrayList<Rule> rules) {
Object o = element.getAttribute("ui.class");
if (o != null) {
if (o instanceof Object[]) {
for (Object s : (Object[]) o) {
if (s instanceof CharSequence) {
Rule rule = byClass.get((CharSequence) s);
if (rule != null)
rules.add(rule);
}
}
} else if (o instanceof CharSequence) {
String classList = ((CharSequence) o).toString().trim();
String[] classes = classList.split("\\s*,\\s*");
for (String c : classes) {
Rule rule = byClass.get(c);
if (rule != null)
rules.add(rule);
}
} else {
throw new RuntimeException(
"Oups ! class attribute is of type "
+ o.getClass().getName());
}
}
}
// Command
/**
* Remove all styles.
*/
protected void clear() {
defaultRule = null;
byId.clear();
byClass.clear();
}
/**
* Add a new rule.
*
* <p>
* Several cases can occur :
* </p>
*
* <ul>
* <li>The rule to add has an ID or class and the rule does not yet
* exists and is not an event rule : add it directly.</li>
* <li>If the rule has an ID or class but the rule already exists,
* augment to already existing rule.</li>
* <li>If the rule has no ID or class and is not an event, augment the
* default style.</li>
* <li>If the rule is an event, the corresponding normal rule is
* searched, if it does not exists, it is created then or else, the
* event is added to the found rule.</li>
* </ul>
*
* @param newRule
* The rule to add or copy.
* @return It the rule added augments an existing rule, this existing
* rule is returned, else null is returned.
*/
protected Rule addRule(Rule newRule) {
Rule oldRule = null;
if (newRule.selector.getPseudoClass() != null) {
oldRule = addEventRule(newRule);
} else if (newRule.selector.getId() != null) {
oldRule = byId.get(newRule.selector.getId());
if (oldRule != null) {
oldRule.getStyle().augment(newRule.getStyle());
} else {
byId.put(newRule.selector.getId(), newRule);
newRule.getStyle().reparent(defaultRule);
}
} else if (newRule.selector.getClazz() != null) {
oldRule = byClass.get(newRule.selector.getClazz());
if (oldRule != null) {
oldRule.getStyle().augment(newRule.getStyle());
} else {
byClass.put(newRule.selector.getClazz(), newRule);
newRule.getStyle().reparent(defaultRule);
}
} else {
oldRule = defaultRule;
defaultRule.getStyle().augment(newRule.getStyle());
newRule = defaultRule;
}
// That's it.
return oldRule;
}
protected Rule addEventRule(Rule newRule) {
Rule parentRule = null;
if (newRule.selector.getId() != null) {
parentRule = byId.get(newRule.selector.getId());
if (parentRule == null) {
parentRule = addRule(new Rule(new Selector(
newRule.selector.getType(),
newRule.selector.getId(),
newRule.selector.getClazz())));
}
} else if (newRule.selector.getClazz() != null) {
parentRule = byClass.get(newRule.selector.getClazz());
if (parentRule == null) {
parentRule = addRule(new Rule(new Selector(
newRule.selector.getType(),
newRule.selector.getId(),
newRule.selector.getClazz())));
}
} else {
parentRule = defaultRule;
}
newRule.getStyle().reparent(parentRule);
parentRule.getStyle().addAlternateStyle(
newRule.selector.getPseudoClass(), newRule);
return parentRule;
}
@Override
public String toString() {
return toString(-1);
}
public String toString(int level) {
String prefix = "";
if (level > 0) {
for (int i = 0; i < level; i++)
prefix += " ";
}
StringBuilder builder = new StringBuilder();
builder.append(String
.format("%s%s default style :%n", prefix, type));
builder.append(defaultRule.toString(level + 1));
toStringRules(level, builder, byId,
String.format("%s%s id styles", prefix, type));
toStringRules(level, builder, byClass,
String.format("%s%s class styles", prefix, type));
return builder.toString();
}
protected void toStringRules(int level, StringBuilder builder,
HashMap<String, Rule> rules, String title) {
builder.append(title);
builder.append(String.format(" :%n"));
for (Rule rule : rules.values())
builder.append(rule.toString(level + 1));
}
}
}