blob: 3192427f6da28993d1e19d77ef86e8ca2179494f [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.netbeans.modules.css.visual;
import java.awt.Component;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.beans.PropertyEditor;
import java.beans.PropertyEditorSupport;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.logging.Level;
import javax.swing.DefaultCellEditor;
import javax.swing.JLabel;
import javax.swing.JTable;
import javax.swing.SwingUtilities;
import javax.swing.table.TableCellRenderer;
import javax.swing.text.BadLocationException;
import javax.swing.text.Document;
import org.netbeans.editor.BaseDocument;
import org.netbeans.editor.Utilities;
import org.netbeans.modules.css.lib.api.properties.FixedTextGrammarElement;
import org.netbeans.modules.css.lib.api.properties.GrammarElementVisitor;
import org.netbeans.modules.css.lib.api.properties.GroupGrammarElement;
import org.netbeans.modules.css.lib.api.properties.Properties;
import org.netbeans.modules.css.lib.api.properties.PropertyCategory;
import org.netbeans.modules.css.lib.api.properties.PropertyDefinition;
import org.netbeans.modules.css.lib.api.properties.ResolvedProperty;
import org.netbeans.modules.css.lib.api.properties.Token;
import org.netbeans.modules.css.lib.api.properties.UnitGrammarElement;
import org.netbeans.modules.css.model.api.*;
import org.netbeans.modules.css.visual.api.DeclarationInfo;
import org.netbeans.modules.parsing.api.Snapshot;
import org.openide.explorer.propertysheet.ExPropertyEditor;
import org.openide.explorer.propertysheet.PropertyEnv;
import org.openide.filesystems.FileObject;
import org.openide.nodes.AbstractNode;
import org.openide.nodes.Children;
import org.openide.nodes.Node;
import org.openide.nodes.PropertySupport;
import org.openide.util.Exceptions;
import org.openide.util.Lookup;
import org.openide.util.Mutex;
import org.openide.util.NbBundle;
import org.openide.util.RequestProcessor.Task;
/**
* A node representing a CSS rule with no children. The node properties
* represents the css rule properties.
*
* @author marekfukala
*/
@NbBundle.Messages({
"rule.properties=Properties",
"rule.properties.description=Properties of the css rule",
"rule.properties.add.declaration.tooltip=Enter a value to add this property to the selected rule",
"rule.global.set.displayname=All Categories",
"rule.global.set.tooltip=Properties from All Categories"
})
public class RuleEditorNode extends AbstractNode {
private static String COLOR_CODE_GRAY = "777777";
private static String COLOR_CODE_RED = "ff7777";
public static String NONE_PROPERTY_NAME = "<none>";
private String filterText;
private PropertySetsInfo propertySetsInfo;
private RuleEditorPanel panel;
private Map<PropertyDefinition, PropertyDeclaration> addedDeclarations = new HashMap<>();
private Rule lastRule;
//cache the model.canApplyChanges() as it is very costly operation
private boolean readOnlyMode;
public RuleEditorNode(RuleEditorPanel panel) {
super(new RuleChildren());
this.panel = panel;
}
public Model getModel() {
return panel.getModel();
}
public boolean isReadOnlyMode() {
return readOnlyMode;
}
public FileObject getFileObject() {
return getModel().getLookup().lookup(FileObject.class);
}
public Rule getRule() {
return panel.getRule();
}
public boolean isShowAllProperties() {
return panel.getViewMode().isShowAllProperties();
}
public boolean isShowCategories() {
return panel.getViewMode().isShowCategories();
}
public boolean isAddPropertyMode() {
return panel.isAddPropertyMode();
}
//called by the RuleEditorPanel when user types into the filter text field
void setFilterText(String prefix) {
this.filterText = prefix;
fireContextChanged(true); //recreate the property sets
}
//called by the RuleEditorPanel when any of the properties affecting
//the PropertySet-s generation changes.
public void fireContextChanged(boolean forceRefresh) {
boolean oldReadOnlyModel = readOnlyMode;
readOnlyMode = getModel() == null || !getModel().canApplyChanges();
if(oldReadOnlyModel != readOnlyMode) {
//refresh the PS as the read only mode changes
forceRefresh = true;
}
try {
PropertySetsInfo oldInfo = getCachedPropertySetsInfo();
PropertySetsInfo newInfo = createPropertySetsInfo();
PropertyCategoryPropertySet[] oldSets = oldInfo.getSets();
PropertyCategoryPropertySet[] newSets = newInfo.getSets();
if (!forceRefresh) {
//the client doesn't require the property sets to be really recreated,
//we may try to update them only if possible
//compare old and new sets, if they contain same sets with same properties,
//then update the PropertyDefinition-s so they contain reference to the current
//css model vertion.
//
//if there's a new PropertySet or one of the PropertySets contains more or less
//properties than the original, then do not do the incremental update but
//refresh the PropertySets completely.
update:
{
//check if the "created declaration" flag has changed or not
if(oldInfo.isCreatedDeclaration() != newInfo.isCreatedDeclaration()) {
break update; //dpn't merge
}
//old DeclarationProperty to new value map
if (oldSets.length == newSets.length) {
for (int i = 0; i < oldSets.length; i++) {
PropertyCategoryPropertySet o = oldSets[i];
PropertyCategoryPropertySet n = newSets[i];
Map<PropertyDeclaration, DeclarationProperty> om = o.declaration2PropertyMap;
Map<PropertyDeclaration, DeclarationProperty> nm = n.declaration2PropertyMap;
if (om.size() != nm.size()) {
break update;
}
//same number of declarations
//notice: the same order of the properties as in the last model
//is ensured by the getUniquePropertyName() method which adds
//index of the property in the rule to its name.
//create declaration name -> declaration maps se we may compare
//(as the css source model elements do not comparable by equals/hashcode)
Map<String, PropertyDeclaration> oName2DeclarationMap = new HashMap<>();
for (PropertyDeclaration d : om.keySet()) {
if (lastRule.getModel() != d.getModel()) {
break update; // Issue 234155
}
oName2DeclarationMap.put(PropertyUtils.getDeclarationId(lastRule, d), d);
}
Map<String, PropertyDeclaration> nName2DeclarationMap = new HashMap<>();
for (PropertyDeclaration d : nm.keySet()) {
nName2DeclarationMap.put(PropertyUtils.getDeclarationId(getRule(), d), d);
}
//compare the names of the properties in the old and new map,
//they must be the same otherwise we wont' marge but recreate
//the whole property sets
Collection<String> oldNames = oName2DeclarationMap.keySet();
Collection<String> newNames = nName2DeclarationMap.keySet();
Collection<String> comp = new HashSet<>(oldNames);
if (comp.retainAll(newNames)) { //assumption: the collections size are the same
break update; //canot merge - the collections differ
}
for (Entry<String, PropertyDeclaration> entry : oName2DeclarationMap.entrySet()) {
String declarationName = entry.getKey();
PropertyDeclaration oldD = entry.getValue();
PropertyDeclaration newD = nName2DeclarationMap.get(declarationName);
//update the existing DeclarationProperty with the fresh
//Declaration object from the new model instance
DeclarationProperty declarationProperty = om.get(oldD);
declarationProperty.updateDeclaration(newD);
//also update the declaration2PropertyMap itself
//as we now use new Declaration object
om.remove(oldD);
om.put(newD, declarationProperty);
}
}
return;
}
}
}
//refresh the sets completely
propertySetsInfo = newInfo;
firePropertySetsChange(oldSets, newSets);
} finally {
this.lastRule = getRule();
}
}
void fireDeclarationInfoChanged(PropertyDeclaration declaration, DeclarationInfo declarationInfo) {
DeclarationProperty dp = getDeclarationProperty(declaration);
if (dp != null) {
dp.setDeclarationInfo(declarationInfo);
}
}
DeclarationProperty getDeclarationProperty(PropertyDeclaration declaration) {
for (PropertyCategoryPropertySet set : getCachedPropertySetsInfo().getSets()) {
DeclarationProperty declarationProperty = set.getDeclarationProperty(declaration);
if (declarationProperty != null) {
return declarationProperty;
}
}
return null;
}
@Override
public synchronized PropertySet[] getPropertySets() {
this.lastRule = getRule();
return getCachedPropertySetsInfo().getSets();
}
private synchronized PropertySetsInfo getCachedPropertySetsInfo() {
if (propertySetsInfo == null) {
propertySetsInfo = createPropertySetsInfo();
}
return propertySetsInfo;
}
private boolean matchesFilterText(String text) {
if (filterText == null) {
return true;
} else {
return text.contains(filterText);
}
}
private Collection<PropertyDefinition> filterByPrefix(Collection<PropertyDefinition> defs) {
Collection<PropertyDefinition> filtered = new ArrayList<>();
for (PropertyDefinition pd : defs) {
if (matchesFilterText(pd.getName())) {
filtered.add(pd);
}
}
return filtered;
}
/**
* Creates property sets of the node.
*
* @return property sets of the node.
*/
private PropertySetsInfo createPropertySetsInfo() {
if (getModel() == null || getRule() == null) {
return new PropertySetsInfo(new PropertyCategoryPropertySet[0], panel.getCreatedDeclaration() != null);
}
Collection<PropertyCategoryPropertySet> sets = new ArrayList<>();
List<PropertyDeclaration> declarations = PropertyUtils.getPropertyDeclarations(getRule());
FileObject file = getFileObject();
if (isShowCategories()) {
//create property sets for property categories
Map<PropertyDefinition, PropertyDeclaration> created = new HashMap<>();
Map<PropertyCategory, List<PropertyDeclaration>> categoryToDeclarationsMap = new EnumMap<>(PropertyCategory.class);
for (PropertyDeclaration d : declarations) {
if (addedDeclarations.containsValue(d)) {
continue; //skip those added declarations
}
//check the declaration
org.netbeans.modules.css.model.api.Property property = d.getProperty();
PropertyValue propertyValue = d.getPropertyValue();
if (property != null && propertyValue != null) {
if (matchesFilterText(property.getContent().toString())) {
PropertyDefinition def = Properties.getPropertyDefinition(property.getContent().toString());
String declarationId = PropertyUtils.getDeclarationId(getRule(), d);
if(panel.getCreatedDeclarationsIdsList().contains(declarationId)) {
created.put(def, d);
}
PropertyCategory category;
if (def != null) {
category = def.getPropertyCategory();
} else {
category = PropertyCategory.UNKNOWN;
}
List<PropertyDeclaration> values = categoryToDeclarationsMap.get(category);
if (values == null) {
values = new LinkedList<>();
categoryToDeclarationsMap.put(category, values);
}
values.add(d);
}
}
}
Map<PropertyCategory, PropertyCategoryPropertySet> propertySetsMap = new EnumMap<>(PropertyCategory.class);
for (Entry<PropertyCategory, List<PropertyDeclaration>> entry : categoryToDeclarationsMap.entrySet()) {
List<PropertyDeclaration> categoryDeclarations = entry.getValue();
if(isShowAllProperties()) {
//remove the "just created"
categoryDeclarations.removeAll(created.values());
}
//sort alpha
Collections.sort(categoryDeclarations, PropertyUtils.getDeclarationsComparator());
PropertyCategoryPropertySet propertyCategoryPropertySet = new PropertyCategoryPropertySet(entry.getKey());
propertyCategoryPropertySet.addAll(categoryDeclarations);
propertySetsMap.put(entry.getKey(), propertyCategoryPropertySet);
sets.add(propertyCategoryPropertySet);
}
if (isShowAllProperties()) {
//Show all properties
for (PropertyCategory cat : PropertyCategory.values()) {
//now add all the remaining properties
List<PropertyDefinition> allInCat = new LinkedList<>(filterByPrefix(getCategoryProperties(cat)));
if (allInCat.isEmpty()) {
continue; //skip empty categories (when filtering)
}
Collections.sort(allInCat, PropertyUtils.getPropertyDefinitionsComparator());
PropertyCategoryPropertySet propertySet = propertySetsMap.get(cat);
if (propertySet == null) {
propertySet = new PropertyCategoryPropertySet(cat);
sets.add(propertySet);
}
//remove already used
for (PropertyDeclaration d : propertySet.getDeclarations()) {
PropertyDefinition def = Properties.getPropertyDefinition(d.getProperty().getContent().toString());
allInCat.remove(def);
}
//add the rest of unused properties to the property set
for (PropertyDefinition pd : allInCat) {
PropertyDeclaration alreadyAdded = addedDeclarations.get(pd);
if(alreadyAdded == null) {
alreadyAdded = created.get(pd);
}
if (alreadyAdded != null) {
propertySet.add(alreadyAdded, true);
} else {
propertySet.add(file, pd);
}
}
}
}
} else {
//not showCategories
//just create one top level property set for virtual category (the items actually don't belong to the category)
PropertyCategoryPropertySet set = new PropertyCategoryPropertySet(PropertyCategory.DEFAULT);
if(!isShowAllProperties()) {
//set properties only view
List<PropertyDeclaration> filtered = new ArrayList<>();
for (PropertyDeclaration d : declarations) {
if (addedDeclarations.containsValue(d)) {
continue; //skip those added declarations
}
String declarationId = PropertyUtils.getDeclarationId(getRule(), d);
if(panel.getCreatedDeclarationsIdsList().contains(declarationId)) {
//created declaration--ignore filter
filtered.add(d);
} else {
//check the declaration
org.netbeans.modules.css.model.api.Property property = d.getProperty();
PropertyValue propertyValue = d.getPropertyValue();
if (property != null && propertyValue != null) {
if (matchesFilterText(property.getContent().toString())) {
filtered.add(d);
}
}
}
}
//sort aplha
Comparator<PropertyDeclaration> comparator = PropertyUtils.createDeclarationsComparator(getRule(), panel.getCreatedDeclarationsIdsList());
Collections.sort(filtered, comparator);
set.addAll(filtered);
//do NOT show all properties
//Add the fake "Add Property" FeatureDescriptor at the end of the set
if(!readOnlyMode && panel.getCreatedDeclaration() == null) {
//do not add the "Add Property" item when we are editing value of the just added property
set.add_Add_Property_FeatureDescriptor();
}
} else {
//all properties view
List<PropertyDeclaration> filteredExisting = new ArrayList<>();
Map<PropertyDefinition, PropertyDeclaration> filteredCreated = new HashMap<>();
for (PropertyDeclaration d : declarations) {
if (addedDeclarations.containsValue(d)) {
continue; //skip those added declarations
}
String declarationId = PropertyUtils.getDeclarationId(getRule(), d);
if(panel.getCreatedDeclarationsIdsList().contains(declarationId)) {
//created declaration--ignore filter
filteredCreated.put(d.getResolvedProperty().getPropertyDefinition(), d);
} else {
//check the declaration
org.netbeans.modules.css.model.api.Property property = d.getProperty();
PropertyValue propertyValue = d.getPropertyValue();
if (property != null && propertyValue != null) {
if (matchesFilterText(property.getContent().toString())) {
filteredExisting.add(d);
}
}
}
}
set.addAll(filteredExisting);
List<PropertyDefinition> all = new ArrayList<>(filterByPrefix(Properties.getPropertyDefinitions(file, true)));
Collections.sort(all, PropertyUtils.getPropertyDefinitionsComparator());
//remove already used
for (PropertyDeclaration d : set.getDeclarations()) {
PropertyDefinition def = Properties.getPropertyDefinition(d.getProperty().getContent().toString());
all.remove(def);
}
//add the rest of unused properties to the property set
for (PropertyDefinition pd : all) {
//boz<i' gula's<:
PropertyDeclaration alreadyAdded = addedDeclarations.get(pd); //added in "ADD PROPERTY MODE"
if(alreadyAdded == null) {
alreadyAdded = filteredCreated.get(pd); //added in normal mode
}
if (alreadyAdded != null) {
set.add(alreadyAdded, true);
} else {
set.add(file, pd);
}
}
}
//overrride the default descriptions
set.setDisplayName(Bundle.rule_global_set_displayname());
set.setShortDescription(Bundle.rule_global_set_tooltip());
sets.add(set);
}
return new PropertySetsInfo(sets.toArray(new PropertyCategoryPropertySet[0]), panel.getCreatedDeclaration() != null);
}
/**
* Returns a list of *visible* properties with this category.
*/
public List<PropertyDefinition> getCategoryProperties(PropertyCategory cat) {
Collection<PropertyDefinition> defs = Properties.getPropertyDefinitions(getModel().getLookup().lookup(FileObject.class), true);
List<PropertyDefinition> defsInCat = new ArrayList<>();
for (PropertyDefinition d : defs) {
if (d.getPropertyCategory() == cat) {
defsInCat.add(d);
}
}
return defsInCat;
}
private String getPropertyDisplayName(PropertyDeclaration declaration) {
return declaration.getProperty().getContent().toString();
}
public void applyModelChanges() {
final Model model = getModel();
model.runReadTask(new Model.ModelTask() {
@Override
public void run(StyleSheet styleSheet) {
try {
model.applyChanges();
} catch (IOException | BadLocationException ex) {
Exceptions.printStackTrace(ex);
}
}
});
}
class PropertyCategoryPropertySet extends PropertySet {
private List<Property> properties = new ArrayList<>();
private Map<PropertyDeclaration, DeclarationProperty> declaration2PropertyMap = new HashMap<>();
public PropertyCategoryPropertySet(PropertyCategory propertyCategory) {
super(propertyCategory.name(), //NOI18N
propertyCategory.getDisplayName(),
propertyCategory.getShortDescription());
}
public void add_Add_Property_FeatureDescriptor() {
properties.add(create_Add_Property_Feature_Descriptor());
}
public void add(PropertyDeclaration declaration, boolean markAsModified) {
DeclarationProperty property = createDeclarationProperty(declaration, markAsModified);
declaration2PropertyMap.put(declaration, property);
properties.add(property);
}
public void addAll(Collection<PropertyDeclaration> declarations) {
for (PropertyDeclaration d : declarations) {
add(d, false);
}
}
public Collection<PropertyDeclaration> getDeclarations() {
return declaration2PropertyMap.keySet();
}
public DeclarationProperty getDeclarationProperty(PropertyDeclaration declaration) {
return declaration2PropertyMap.get(declaration);
}
public void add(FileObject context, PropertyDefinition propertyDefinition) {
properties.add(createPropertyDefinitionProperty(context, propertyDefinition));
}
@Override
public Property<String>[] getProperties() {
return properties.toArray(new Property[]{});
}
}
private Property createPropertyDefinitionProperty(FileObject context, PropertyDefinition definition) {
PropertyDefinition pmodel = Properties.getPropertyDefinition(definition.getName());
return new PropertyDefinitionProperty(definition, createPropertyValueEditor(context, pmodel, null, false));
}
private PropertyValuesEditor createPropertyValueEditor(FileObject context, PropertyDefinition pmodel, PropertyDeclaration declaration, boolean addNoneProperty) {
final Collection<UnitGrammarElement> unitElements = new ArrayList<>();
final Collection<FixedTextGrammarElement> fixedElements = new ArrayList<>();
if (pmodel != null) {
GroupGrammarElement rootElement = pmodel.getGrammarElement(context);
rootElement.accept(new GrammarElementVisitor() {
@Override
public void visit(UnitGrammarElement element) {
unitElements.add(element);
}
@Override
public void visit(FixedTextGrammarElement element) {
fixedElements.add(element);
}
});
}
return new PropertyValuesEditor(panel, pmodel, getModel(), fixedElements, unitElements, declaration, addNoneProperty);
}
private abstract class AbstractPDP<T> extends PropertySupport<T> {
private PropertyDefinition def;
private PropertyEditor editor;
public AbstractPDP(PropertyDefinition def, PropertyEditor editor, String name, Class<T> type, String displayName, String shortDescription, boolean canR, boolean canW) {
super(name, type, displayName, shortDescription, canR, canW);
this.def = def;
this.editor = editor;
}
public AbstractPDP(PropertyDefinition def, PropertyEditor editor, Class<T> clazz, String shortDescription) {
super(def.getName(),
clazz,
def.getName(),
shortDescription,
true,
getRule().isValid() && !readOnlyMode);
this.def = def;
this.editor = editor;
}
@Override
public PropertyEditor getPropertyEditor() {
return editor;
}
@Override
public T getValue() throws IllegalAccessException, InvocationTargetException {
return getEmptyValue();
}
@Override
public void setValue(T val) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException {
if (getEmptyValue().equals(val)) {
return; //no change
}
//add a new declaration to the rule
ElementFactory factory = getModel().getElementFactory();
Rule rule = getRule();
Declarations declarations = rule.getDeclarations();
if (declarations == null) {
//empty rule, create declarations node as well
declarations = factory.createDeclarations();
rule.setDeclarations(declarations);
}
org.netbeans.modules.css.model.api.Property property = factory.createProperty(def.getName());
Expression expr = factory.createExpression(convertToString(val));
PropertyValue value = factory.createPropertyValue(expr);
PropertyDeclaration newPropertyDeclaration = factory.createPropertyDeclaration(property, value, false);
Declaration newDeclaration = factory.createDeclaration();
newDeclaration.setPropertyDeclaration(newPropertyDeclaration);
declarations.addDeclaration(newDeclaration);
//save the model to the source
if (!isAddPropertyMode()) {
panel.setCreatedDeclaration(rule, newPropertyDeclaration);
applyModelChanges();
} else {
//add property mode - just refresh the content
addedDeclarations.put(def, newPropertyDeclaration); //remember what we've added during this dialog cycle
fireContextChanged(true);
}
}
protected abstract String convertToString(T val);
protected abstract T getEmptyValue();
}
private static String EMPTY_STRING = "";
private class PlainPDP extends AbstractPDP<String> {
public PlainPDP(PropertyDefinition def, PropertyEditor editor, String shortDescription) {
super(def, editor, String.class, shortDescription);
}
@Override
protected String convertToString(String val) {
return val;
}
@Override
protected String getEmptyValue() {
return EMPTY_STRING;
}
}
private class PropertyDefinitionProperty extends PlainPDP {
public PropertyDefinitionProperty(PropertyDefinition def, PropertyEditor editor) {
super(def,
editor,
Bundle.rule_properties_add_declaration_tooltip());
}
}
private DeclarationProperty createDeclarationProperty(PropertyDeclaration declaration, boolean markAsModified) {
ResolvedProperty resolvedProperty = declaration.getResolvedProperty();
PropertyDefinition propertyDefinition = resolvedProperty != null ? resolvedProperty.getPropertyDefinition() : null;
return new DeclarationProperty(declaration,
PropertyUtils.getDeclarationId(getRule(), declaration),
getPropertyDisplayName(declaration),
markAsModified,
createPropertyValueEditor(getFileObject(), propertyDefinition, declaration, true));
}
@NbBundle.Messages({
"property.set.at.prefix=Set at ",
"property.value.unexpected.token={0}, unexpected character(s) \"{1}\" found",
"property.value.not.resolved={0}, error in property value",
"property.erroneous={0}, erroneous property",
"property.unknown={0}, unknown property",
"property.inactive={0}, not affecting the selected element",
"property.overridden={0}, overridden by another property",
"property.description={0}",
"property.no.file=No File"
})
public class DeclarationProperty extends PropertySupport {
private final String propertyName;
private final PropertyEditor editor;
private final boolean markAsModified;
private PropertyDeclaration propertyDeclaration;
private DeclarationInfo info;
private String shortDescription;
private String valueSet;
private String locationPrefix;
public DeclarationProperty(PropertyDeclaration declaration, String propertyName, String propertyDisplayName, boolean markAsModified, PropertyEditor editor) {
super(propertyName,
String.class,
propertyDisplayName,
null, true, !readOnlyMode && getRule().isValid());
this.propertyName = propertyName;
this.propertyDeclaration = declaration;
this.markAsModified = markAsModified;
this.editor = editor;
checkForErrors();
//one may set a custom inplace editor by
//setValue("inplaceEditor", new MyInplaceEditor());
}
public PropertyDeclaration getDeclaration() {
return propertyDeclaration;
}
/**
* Updates the {@link #info} field to {@link DeclarationInfo#ERRONEOUS}
* if the active declaration contains errors.
*/
private void checkForErrors() {
//suppress the errors for just added property
//it doesn't have the value yet, but this doesn't mean
//we want to mark it as erroneous while adding the value
if (getDeclaration().equals(panel.getCreatedDeclaration())) {
return;
}
String property = propertyDeclaration.getProperty().getContent().toString().trim();
PropertyDefinition model = Properties.getPropertyDefinition(property);
if (model == null) {
//flag as unknown
info = DeclarationInfo.ERRONEOUS;
shortDescription = Bundle.property_unknown(getLocationPrefix());
return ;
}
//so we have a property model...
//but before checking the property value ensure we are not trying
//to do so for vendor specific property. Values of these properties
//are not supposed to be checked as the grammars are not very much
//up-to-date and reliable.
if(Properties.isVendorSpecificProperty(model)) {
shortDescription = Bundle.property_description(getLocationPrefix());
return ;
}
PropertyValue value = propertyDeclaration.getPropertyValue();
if (value != null) {
Expression expression = value.getExpression();
CharSequence content = expression != null ? expression.getContent() : "";
ResolvedProperty rp = new ResolvedProperty(getFileObject(), model, content);
if (!rp.isResolved()) {
List<Token> unresolvedTokens = rp.getUnresolvedTokens();
if(unresolvedTokens.isEmpty()) {
//no value token/s
info = DeclarationInfo.ERRONEOUS;
shortDescription = Bundle.property_value_not_resolved(getLocationPrefix());
return ;
}
//we have some unresolved token,
//lets check if the token is vendor specific value token
Token unexpectedToken = unresolvedTokens.iterator().next();
String unexpectedText = unexpectedToken.image().toString();
if(!org.netbeans.modules.css.editor.module.spi.Utilities.isVendorSpecificPropertyValueToken(getFileObject(), unexpectedText)) {
//no, it seems to be a common value token
shortDescription = Bundle.property_value_unexpected_token(getLocationPrefix(), unexpectedText);
return;
}
}
}
//else everything seems to be all right
shortDescription = Bundle.property_description(getLocationPrefix());
}
/**
* Returns the file:line prefix for the tooltip
*/
private CharSequence getLocationPrefix() {
if (locationPrefix == null) {
final StringBuilder sb = new StringBuilder();
sb.append(Bundle.property_set_at_prefix());
Model model = getModel();
Lookup lookup = model.getLookup();
FileObject file = lookup.lookup(FileObject.class);
if (file == null) {
sb.append(Bundle.property_no_file());
} else {
sb.append(file.getNameExt());
}
Snapshot snap = lookup.lookup(Snapshot.class);
final Document doc = lookup.lookup(Document.class);
if (snap != null && doc != null) {
PropertyDeclaration decl = getDeclaration();
int ast_from = decl.getStartOffset();
if (ast_from != -1) {
//source element, not virtual which is not persisted yet
final int doc_from = snap.getOriginalOffset(ast_from);
if (doc_from != -1) {
doc.render(new Runnable() {
@Override
public void run() {
try {
int lineOffset = 1 + Utilities.getLineOffset((BaseDocument) doc, doc_from);
sb.append(':');
sb.append(lineOffset);
} catch (BadLocationException ex) {
//no-op
}
}
});
}
}
}
locationPrefix = sb.toString();
}
return locationPrefix;
}
private void updateDeclaration(PropertyDeclaration declaration) {
assert PropertyUtils.getDeclarationId(getRule(), declaration).equals(propertyName);
//update the declaration
String oldValue = getValue();
this.propertyDeclaration = declaration;
String newValue = getValue();
locationPrefix = null; //reset the prefix as it was computed for the original declaration
/* Reset DeclarationInfo to default state (null) as the contract
* doesn't require/expect the RuleEditorController.setDeclarationInfo(...)
* to be called for each "plain" declaration with null DeclarationInfo argument.
*/
DeclarationInfo oldInfo = info;
info = null;
String oldShortDescription = shortDescription;
//possibly set the DeclarationInfo to ERRONEOUS
checkForErrors();
if (!shortDescription.equals(oldShortDescription)) {
fireShortDescriptionChange(oldShortDescription, shortDescription);
}
//now we need to fire property name property change with some
//change so call setDeclarationInfo() which does property change
//from null to current value and hence forces the PS to repaint
//the property
if (info != oldInfo) {
//DeclarationInfo has changed
setDeclarationInfo(info);
} else {
//no change to DeclarationInfo
setDisplayName(getHtmlDisplayName());
//and fire property change to the node
//this will trigger the property name and value repaint
firePropertyChange(propertyName, oldValue, newValue);
}
}
@Override
public String getShortDescription() {
return shortDescription;
}
@Override
public PropertyEditor getPropertyEditor() {
return editor;
}
public void setDeclarationInfo(DeclarationInfo info) {
if(this.info == info) {
return ; //no change
}
this.info = info;
setDisplayName(getHtmlDisplayName());
//tooltip update
String oldShortDescription = shortDescription;
switch(info) {
case ERRONEOUS:
shortDescription = Bundle.property_erroneous(getLocationPrefix());
break;
case INACTIVE:
shortDescription = Bundle.property_inactive(getLocationPrefix());
break;
case OVERRIDDEN:
shortDescription = Bundle.property_overridden(getLocationPrefix());
break;
}
fireShortDescriptionChange(oldShortDescription, shortDescription);
//force the property repaint - stupid way but there's
//doesn't seem to be any better way
firePropertyChange(propertyName, null, getValue());
}
private boolean isOverridden() {
return info != null && info == DeclarationInfo.OVERRIDDEN;
}
private boolean isInactive() {
return info != null && info == DeclarationInfo.INACTIVE;
}
private boolean isErroneous() {
return info != null && info == DeclarationInfo.ERRONEOUS;
}
@Override
public String getHtmlDisplayName() {
StringBuilder b = new StringBuilder();
String color = null;
boolean bold = false;
boolean strike = false;
if (isShowAllProperties()) {
if (isAddPropertyMode() && !markAsModified) {
color = COLOR_CODE_GRAY;
} else {
bold = true;
}
}
if (isOverridden()) {
strike = true;
}
if (isInactive()) {
color = COLOR_CODE_GRAY;
strike = true;
}
if (isErroneous()) {
strike = true;
color = COLOR_CODE_RED;
}
//render
if (bold) {
b.append("<b>");//NOI18N
}
if (strike) {
b.append("<s>"); //use <del>?
}
if (color != null) {
b.append("<font color="); //NOI18N
b.append(color);
b.append(">"); //NOI18N
}
b.append(getPropertyDisplayName(propertyDeclaration));
if (color != null) {
b.append("</font>"); //NOI18N
}
if (strike) {
b.append("</s>"); //use <del>?
}
if (bold) {
b.append("</b>");//NOI18N
}
return b.toString();
}
@Override
public String getValue() {
if (valueSet != null) {
return valueSet;
}
PropertyValue val = propertyDeclaration.getPropertyValue();
return val == null ? null : val.getExpression().getContent().toString().trim();
}
@Override
public void setValue(final Object o) {
assert SwingUtilities.isEventDispatchThread();
final String asString = (String) o;
if (asString == null || asString.isEmpty()) {
return;
}
String currentValue = getValue();
if (asString.equals(currentValue)) {
//same value, ignore
return;
}
this.valueSet = asString;
SAVE_CHANGE_TASK.schedule(200);
}
private Task SAVE_CHANGE_TASK = RuleEditorPanel.RP.create(new Runnable() {
@Override
public void run() {
Mutex.EVENT.readAccess(new Runnable() {
@Override
public void run() {
//all the access to valueSet field is safe as
//the field is only set in setValue() which always
//runs id EDT
//The tasks may schedule in such way that more than one tasks
//runs after the setValue(...) method called.
//In such case the first task sets the valueSet field to null
//and the other tasks cannot rule (they do not have anything
//to do anyway) so just quit in such case.
if (valueSet == null) {
return;
}
Model model = getModel();
model.runWriteTask(new Model.ModelTask() {
@Override
public void run(StyleSheet styleSheet) {
if (NONE_PROPERTY_NAME.equals(valueSet)) {
//remove the whole declaration
Declaration declaration = (Declaration) propertyDeclaration.getParent();
Declarations declarations = (Declarations)declaration.getParent();
declaration.removeElement(propertyDeclaration);
declarations.removeDeclaration(declaration);
} else {
//update the value
RuleEditorPanel.LOG.log(Level.FINE, "updating property to {0}", valueSet);
propertyDeclaration.getPropertyValue().getExpression().setContent(valueSet);
}
}
});
if (!isAddPropertyMode()) {
//save changes
applyModelChanges();
//the model save request will cause the source model's
//Model.CHANGES_APPLIED_TO_DOCUMENT property change event fired
//and the RuleEditorPanel's listener will SYNCHRONOUSLY
//refresh the css source model.
//
//...so now we have a new instance of model reflecting
//the changes made by the writetask above
}
valueSet = null;
}
});
}
});
}
private Property create_Add_Property_Feature_Descriptor() {
// return ADD_PROPERTY_FD;
//TODO put back the shared instance once Standa fixes the multiple setValue(...) calls so the innser property state can be removed.
return new AddPropertyFD();
}
private Property ADD_PROPERTY_FD = new AddPropertyFD();
@NbBundle.Messages({
"AddProperty.displayName.html=<html><body><b>Add Property</b></body></html>",
"AddProperty.displayName=Add Property",
"AddProperty.shortDescription=Click here to add a new property."
})
private class AddPropertyFD extends Property<String> {
private String valueSet;
public AddPropertyFD() {
super(String.class);
setName(AddPropertyFD.class.getSimpleName());
setDisplayName(Bundle.AddProperty_displayName());
setShortDescription(Bundle.AddProperty_shortDescription());
}
@Override
public PropertyEditor getPropertyEditor() {
return new AddPropertyPropertyEditor(this);
}
@Override
public boolean canRead() {
return true;
}
@Override
public String getValue() throws IllegalAccessException, InvocationTargetException {
return "";
}
@Override
public boolean canWrite() {
return false;
}
//called from AddPropertyPropertyEditor when a value is entered
@Override
public void setValue(final String propertyName) {
if (propertyName == null) {
return;
}
if(propertyName.trim().isEmpty()) {
return ; //ignore no value
}
if(valueSet != null) {
RuleEditorPanel.LOG.log(Level.WARNING, "Trying to set property value more than once!, relaxing...");
return ;
}
valueSet = propertyName;
//1.create the property
//2.select the corresponding row in the PS
final Model model = getModel();
final Rule rule = getRule();
model.runWriteTask(new Model.ModelTask() {
@Override
public void run(StyleSheet styleSheet) {
//add the new declaration to the model.
//the declaration is not complete - the value is missing and it is necessary to
//enter in the PS otherwise the model become invalid.
ModelUtils utils = new ModelUtils(model);
Declarations decls = rule.getDeclarations();
if (decls == null) {
decls = model.getElementFactory().createDeclarations();
rule.setDeclarations(decls);
}
PropertyDeclaration pdeclarationElement = utils.createPropertyDeclaration(propertyName + ":");
Declaration declarationElement = model.getElementFactory().createDeclaration();
declarationElement.setPropertyDeclaration(pdeclarationElement);
decls.addDeclaration(declarationElement);
//do not save the model (apply changes) - once the write task finishes
//the embedded property sheet will be refreshed from the modified model.
//remember the created declaration so once the model change is fired
//and the property sheet is refreshed, we can find and select the corresponding
//FeatureDescriptor
panel.setCreatedDeclaration(rule, pdeclarationElement);
}
});
}
}
private class AddPropertyPropertyEditor extends PropertyEditorSupport implements ExPropertyEditor {
private AddPropertyFD property;
public AddPropertyPropertyEditor(AddPropertyFD property) {
this.property = property;
}
@Override
public void attachEnv(PropertyEnv env) {
env.getFeatureDescriptor().setValue("custom.cell.renderer",
new AddPropertyCellRendererComponent()); //NOI18N
env.getFeatureDescriptor().setValue("custom.cell.editor",
new AddPropertyCellEditorComponent(new AutocompleteJComboBox(getFileObject()), property)); //NOI18N
}
}
private class AddPropertyCellRendererComponent implements TableCellRenderer {
@Override
public Component getTableCellRendererComponent(JTable jtable, Object o, boolean bln, boolean bln1, int i, int i1) {
return new JLabel(Bundle.AddProperty_displayName_html());
}
}
private class AddPropertyCellEditorComponent extends DefaultCellEditor {
private AutocompleteJComboBox editor;
private AddPropertyFD property;
private boolean cancelled;
public AddPropertyCellEditorComponent(AutocompleteJComboBox jcb, AddPropertyFD addFDProperty) {
super(jcb);
this.property = addFDProperty;
this.editor = jcb;
this.editor.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent ae) {
if(!cancelled) {
property.setValue((String)editor.getSelectedItem());
}
}
});
}
@Override
public Component getTableCellEditorComponent(JTable jtable, Object o, boolean bln, int i, int i1) {
editor.setSelectedIndex( -1 );
return editor;
}
@Override
protected void fireEditingCanceled() {
cancelled = true;
super.fireEditingCanceled();
}
}
/**
* Empty children keys
*/
private static class RuleChildren extends Children.Keys {
@Override
protected Node[] createNodes(Object key) {
return new Node[]{};
}
}
private static class PropertySetsInfo {
private final PropertyCategoryPropertySet[] sets;
private final boolean createdDeclaration;
public PropertySetsInfo(PropertyCategoryPropertySet[] sets, boolean createdDeclaration) {
this.sets = sets;
this.createdDeclaration = createdDeclaration;
}
public PropertyCategoryPropertySet[] getSets() {
return sets;
}
/**
* Returns true if the propertysets were created when there was
* "created declaration" set in the RuleEditorPanel.
*/
public boolean isCreatedDeclaration() {
return createdDeclaration;
}
}
}