/* Alloy Analyzer 4 -- Copyright (c) 2006-2009, Felix Chang * * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files * (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF * OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ package edu.mit.csail.sdg.alloy4viz; import java.awt.BorderLayout; import java.awt.Color; import java.io.IOException; import java.util.LinkedHashMap; import java.util.Set; import java.util.TreeSet; import javax.swing.Icon; import javax.swing.JPanel; import javax.swing.JScrollPane; import edu.mit.csail.sdg.alloy4.ConstSet; import edu.mit.csail.sdg.alloy4.MailBug; import edu.mit.csail.sdg.alloy4.OurCheckbox; import edu.mit.csail.sdg.alloy4.OurUtil; import edu.mit.csail.sdg.alloy4graph.DotColor; import edu.mit.csail.sdg.alloy4graph.DotPalette; import edu.mit.csail.sdg.alloy4graph.DotShape; import edu.mit.csail.sdg.alloy4graph.DotStyle; /** Mutable; this stores an unprojected model as well as the current theme customization. * * <p><b>Thread Safety:</b> Can be called only by the AWT event thread. */ public final class VizState { /** Construct a new VizState (with default theme settings) for the given instance; if world!=null, it is the root of the AST. */ public VizState(AlloyInstance originalInstance) { this.originalInstance = originalInstance; this.currentModel = originalInstance.model; resetTheme(); loadInstance(originalInstance); } /** Make a copy of an existing VizState object. */ public VizState(VizState old) { originalInstance = old.originalInstance; currentModel = old.currentModel; projectedTypes = new TreeSet<AlloyType>(old.projectedTypes); useOriginalNames = old.useOriginalNames; hidePrivate = old.hidePrivate; hideMeta = old.hideMeta; fontSize = old.fontSize; nodePalette = old.nodePalette; edgePalette = old.edgePalette; nodeColor.putAll(old.nodeColor); nodeStyle.putAll(old.nodeStyle); nodeVisible.putAll(old.nodeVisible); label.putAll(old.label); number.putAll(old.number); hideUnconnected.putAll(old.hideUnconnected); showAsAttr.putAll(old.showAsAttr); showAsLabel.putAll(old.showAsLabel); shape.putAll(old.shape); weight.putAll(old.weight); attribute.putAll(old.attribute); mergeArrows.putAll(old.mergeArrows); constraint.putAll(old.constraint); layoutBack.putAll(old.layoutBack); edgeColor.putAll(old.edgeColor); edgeStyle.putAll(old.edgeStyle); edgeVisible.putAll(old.edgeVisible); changedSinceLastSave = false; } /** Clears the current theme. */ public void resetTheme() { currentModel = originalInstance.model; projectedTypes.clear(); useOriginalNames = false; hidePrivate = true; hideMeta = true; fontSize = 12; nodePalette = DotPalette.CLASSIC; edgePalette = DotPalette.CLASSIC; nodeColor.clear(); nodeColor.put(null, DotColor.WHITE); nodeStyle.clear(); nodeStyle.put(null, DotStyle.SOLID); nodeVisible.clear(); nodeVisible.put(null, true); label.clear(); label.put(null, ""); number.clear(); number.put(null, true); hideUnconnected.clear(); hideUnconnected.put(null, false); showAsAttr.clear(); showAsAttr.put(null, false); showAsLabel.clear(); showAsLabel.put(null, true); shape.clear(); shape.put(null, DotShape.ELLIPSE); weight.clear(); weight.put(null, 0); attribute.clear(); attribute.put(null, false); mergeArrows.clear(); mergeArrows.put(null, true); constraint.clear(); constraint.put(null, true); layoutBack.clear(); layoutBack.put(null, false); edgeColor.clear(); edgeColor.put(null, DotColor.MAGIC); edgeStyle.clear(); edgeStyle.put(null, DotStyle.SOLID); edgeVisible.clear(); edgeVisible.put(null, true); // Provide some nice defaults for "Int" and "seq/Int" type AlloyType sigint=AlloyType.INT; label.put(sigint,""); number.put(sigint,true); hideUnconnected.put(sigint,true); AlloyType seqidx=AlloyType.SEQINT; label.put(seqidx,""); number.put(seqidx,true); hideUnconnected.put(seqidx,true); // Provide some nice defaults for meta model stuff AlloyType set=AlloyType.SET; AlloyRelation ext=AlloyRelation.EXTENDS, in=AlloyRelation.IN; shape.put(null,DotShape.BOX); nodeColor.put(null,DotColor.YELLOW); nodeStyle.put(null,DotStyle.SOLID); shape.put(set,DotShape.ELLIPSE); nodeColor.put(set,DotColor.BLUE); label.put(set,""); edgeColor.put(ext,DotColor.BLACK); weight.put(ext,100); layoutBack.put(ext,true); edgeColor.put(in,DotColor.BLACK); weight.put(in,100); layoutBack.put(in,true); // Done cache.clear(); changedSinceLastSave=false; } /** Load a new instance into this VizState object (the input argument is treated as a new unprojected instance); * if world!=null, it is the root of the AST */ public void loadInstance(AlloyInstance unprojectedInstance) { this.originalInstance=unprojectedInstance; for (AlloyType t:getProjectedTypes()) if (!unprojectedInstance.model.hasType(t)) projectedTypes.remove(t); currentModel = StaticProjector.project(unprojectedInstance.model, projectedTypes); cache.clear(); } /** Erase the current theme customizations and then load it from a file. * @throws IOException - if an error occurred */ public void loadPaletteXML(String filename) throws IOException { resetTheme(); StaticThemeReaderWriter.readAlloy(filename,this); cache.clear(); changedSinceLastSave=false; } /** Saves the current theme to a file (which will be overwritten if it exists already). * @throws IOException - if an error occurred */ public void savePaletteXML(String filename) throws IOException { StaticThemeReaderWriter.writeAlloy(filename,this); changedSinceLastSave=false; } /** Caches previously generated graphs. */ private LinkedHashMap<AlloyProjection,JPanel> cache=new LinkedHashMap<AlloyProjection,JPanel>(); /** Generate a VizGraphPanel for a given projection choice, using the current settings. */ public JPanel getGraph(AlloyProjection projectionChoice) { JPanel ans = cache.get(projectionChoice); if (ans!=null) return ans; AlloyInstance inst = originalInstance; try { ans = StaticGraphMaker.produceGraph(inst, this, projectionChoice); cache.put(projectionChoice, ans); } catch(Throwable ex) { String msg = "An error has occurred: " + ex + "\n\nStackTrace:\n" + MailBug.dump(ex) + "\n"; JScrollPane scroll = OurUtil.scrollpane(OurUtil.textarea(msg, 0, 0, false, false)); ans = new JPanel(); ans.setLayout(new BorderLayout()); ans.add(scroll, BorderLayout.CENTER); ans.setBackground(Color.WHITE); } ans.setBorder(null); return ans; } /** True if the theme has been modified since last save. */ private boolean changedSinceLastSave=false; /** True if the theme has been modified since last save. */ public boolean changedSinceLastSave() { return changedSinceLastSave; } /** Sets the "changed since last save" flag, then flush any cached generated graphs. */ private void change() { changedSinceLastSave=true; cache.clear(); } /** If oldValue is different from newValue, then sets the "changed since last save" flag and flush the cache. */ private void changeIf(Object oldValue, Object newValue) { if (oldValue==null) { if (newValue==null) return; } else { if (oldValue.equals(newValue)) return; } change(); } /*============================================================================================*/ /** If x is an AlloyType, x is not univ, then return its parent (which could be univ); * If x is an AlloySet, then return x's type; * All else, return null. */ private AlloyType parent(AlloyElement x, AlloyModel model) { if (x instanceof AlloySet) return ((AlloySet)x).getType(); if (x instanceof AlloyType) return model.getSuperType((AlloyType)x); return null; } /*============================================================================================*/ /** The original unprojected instance. */ private AlloyInstance originalInstance; /** Returns the original unprojected model. */ public AlloyInstance getOriginalInstance() { return originalInstance; } /** Returns the original unprojected model. */ public AlloyModel getOriginalModel() { return originalInstance.model; } /*============================================================================================*/ /** The current (possibly projected) model. */ private AlloyModel currentModel; /** Returns the current (possibly projected) model. */ public AlloyModel getCurrentModel() { return currentModel; } /*============================================================================================*/ /** The set of types we are currently projecting over. */ private Set<AlloyType> projectedTypes = new TreeSet<AlloyType>(); /** Gets an unmodifiable copy of the set of types we are currently projecting over. */ public ConstSet<AlloyType> getProjectedTypes() { return ConstSet.make(projectedTypes); } /** Returns true iff the type is not univ, and it is a toplevel type. */ public boolean canProject(final AlloyType type) { return isTopLevel(type); } /** Returns true iff the type is not univ, and it is a toplevel type. */ public boolean isTopLevel(final AlloyType type) { return AlloyType.UNIV.equals(originalInstance.model.getSuperType(type)); } /** Adds type to the list of projected types if it's a toplevel type. */ public void project(AlloyType type) { if (canProject(type)) if (projectedTypes.add(type)) { currentModel = StaticProjector.project(originalInstance.model, projectedTypes); change(); } } /** Removes type from the list of projected types if it is currently projected. */ public void deproject(AlloyType type) { if (projectedTypes.remove(type)) { currentModel = StaticProjector.project(originalInstance.model, projectedTypes); change(); } } /** Removes every entry from the list of projected types. */ public void deprojectAll() { if (projectedTypes.size()>0) { projectedTypes.clear(); currentModel = StaticProjector.project(originalInstance.model, projectedTypes); change(); } } /*============================================================================================*/ /** Whether to use the original atom names. */ private boolean useOriginalNames = false; /** Returns whether we will use original atom names. */ public boolean useOriginalName() { return useOriginalNames; } /** Sets whether we will use original atom names or not. */ public void useOriginalName(Boolean newValue) { if (newValue!=null && useOriginalNames!=newValue) { change(); useOriginalNames=newValue; } } /*============================================================================================*/ /** Whether to hide private sigs/fields/relations. */ private boolean hidePrivate = false; /** Returns whether we will hide private sigs/fields/relations. */ public boolean hidePrivate() { return hidePrivate; } /** Sets whether we will hide private sigs/fields/relations. */ public void hidePrivate(Boolean newValue) { if (newValue!=null && hidePrivate!=newValue) { change(); hidePrivate=newValue; } } /*============================================================================================*/ /** Whether to hide meta sigs/fields/relations. */ private boolean hideMeta = true; /** Returns whether we will hide meta sigs/fields/relations. */ public boolean hideMeta() { return hideMeta; } /** Sets whether we will hide meta sigs/fields/relations. */ public void hideMeta(Boolean newValue) { if (newValue!=null && hideMeta!=newValue) { change(); hideMeta=newValue; } } /*============================================================================================*/ /** The graph's font size. */ private int fontSize = 12; /** Returns the font size. */ public int getFontSize() { return fontSize; } /** Sets the font size. */ public void setFontSize(int n) { if (fontSize!=n && fontSize>0) { change(); fontSize=n; } } /*============================================================================================*/ /** The default node palette. */ private DotPalette nodePalette; /** Gets the default node palette. */ public DotPalette getNodePalette() { return nodePalette; } /** Sets the default node palette. */ public void setNodePalette(DotPalette x) { if (nodePalette!=x && x!=null) {change(); nodePalette=x;} } /*============================================================================================*/ /** The default edge palette. */ private DotPalette edgePalette; /** Gets the default edge palette. */ public DotPalette getEdgePalette() { return edgePalette; } /** Sets the default edge palette. */ public void setEdgePalette(DotPalette x) { if (edgePalette!=x && x!=null) {change(); edgePalette=x;} } /*============================================================================================*/ // An important invariant to maintain: every map here must map null to a nonnull value. public final MInt weight = new MInt(); public final MString label = new MString(); public final MMap<DotColor> nodeColor = new MMap<DotColor>(); public final MMap<DotColor> edgeColor = new MMap<DotColor>(); public final MMap<DotStyle> nodeStyle = new MMap<DotStyle>(); public final MMap<DotStyle> edgeStyle = new MMap<DotStyle>(); public final MMap<DotShape> shape = new MMap<DotShape>(); public final MMap<Boolean> attribute = new MMap<Boolean>(true, false); public final MMap<Boolean> mergeArrows = new MMap<Boolean>(true, false); public final MMap<Boolean> constraint = new MMap<Boolean>(true, false); public final MMap<Boolean> layoutBack = new MMap<Boolean>(true, false); public final MMap<Boolean> edgeVisible = new MMap<Boolean>(true, false); public final MMap<Boolean> nodeVisible = new MMap<Boolean>(true, false); public final MMap<Boolean> number = new MMap<Boolean>(true, false); public final MMap<Boolean> hideUnconnected = new MMap<Boolean>(true, false); public final MMap<Boolean> showAsAttr = new MMap<Boolean>(true, false); public final MMap<Boolean> showAsLabel = new MMap<Boolean>(true, false); public final class MInt { private final LinkedHashMap<AlloyElement,Integer> map = new LinkedHashMap<AlloyElement,Integer>(); private MInt() { } private void clear() { map.clear(); change(); } private void putAll(MInt x) { map.putAll(x.map); change(); } public int get(AlloyElement x) { Integer ans=map.get(x); if (ans==null) return 0; else return ans; } public void put(AlloyElement x, Integer v) { if (v==null || v<0) v=0; changeIf(map.put(x,v), v); } } public final class MString { private final LinkedHashMap<AlloyElement,String> map = new LinkedHashMap<AlloyElement,String>(); private MString() { } private void clear() { map.clear(); change(); } private void putAll(MString x) { map.putAll(x.map); change(); } public String get(AlloyElement x) { String ans=map.get(x); if (ans==null) ans=x.getName().trim(); return ans; } public void put(AlloyElement x, String v) { if (x==null && v==null) v=""; if (x!=null && x.getName().equals(v)) v=null; changeIf(map.put(x,v), v); } } public final class MMap<T> { private final LinkedHashMap<AlloyElement,T> map = new LinkedHashMap<AlloyElement,T>(); private final T onValue; private final T offValue; private MMap() { onValue=null; offValue=null; } private MMap(T on, T off) { this.onValue=on; this.offValue=off; } private void clear() { map.clear(); change(); } private void putAll(MMap<T> x) { map.putAll(x.map); change(); } public T get(AlloyElement obj) { return map.get(obj); } public T resolve(AlloyElement obj) { AlloyModel m = currentModel; for(AlloyElement x=obj; ;x=parent(x,m)) { T v=map.get(x); if (v!=null) return v; } } /** Set the value for the given object; can be "null" to mean "inherit" */ public void put(AlloyElement obj, T value) { if (obj==null && value==null) return; Object old = map.put(obj, value); if ((old==null && value!=null) || (old!=null && !old.equals(value))) change(); } OurCheckbox pick(String label, String tooltip) { return new OurCheckbox(label, tooltip, (Boolean.TRUE.equals(get(null)) ? OurCheckbox.ON : OurCheckbox.OFF)) { private static final long serialVersionUID = 0; public Icon do_action() { T old = get(null); boolean ans = (old!=null && old.equals(onValue)); MMap.this.put(null, ans ? offValue : onValue); return ans ? OFF : ON; } }; } OurCheckbox pick(final AlloyElement obj, final String label, final String tooltip) { T a = get(obj), b = resolve(obj); Icon icon = a==null ? (Boolean.TRUE.equals(b) ? OurCheckbox.INH_ON : OurCheckbox.INH_OFF) : (Boolean.TRUE.equals(a) ? OurCheckbox.ALL_ON : OurCheckbox.ALL_OFF); return new OurCheckbox(label, tooltip, icon) { private static final long serialVersionUID = 0; public Icon do_action() { T a = get(obj); if (a==null) a=onValue; else if (a.equals(onValue)) a=offValue; else a=null; MMap.this.put(obj, a); return a==null ? (Boolean.TRUE.equals(resolve(obj)) ? INH_ON : INH_OFF) : (Boolean.TRUE.equals(a) ? ALL_ON : ALL_OFF); } }; } } // Reads the value for that type/set/relation. // If x==null, then we guarantee the return value is nonnull // If x!=null, then it may return null (which means "inherited") // (Note: "label" and "weight" will never return null) // Reads the value for that atom based on an existing AlloyInstance; return value is never null. public DotColor nodeColor (AlloyAtom a, AlloyInstance i) { for(AlloySet s:i.atom2sets(a)) {DotColor v=nodeColor.get(s); if (v!=null) return v;} return nodeColor.resolve (a.getType()); } public DotStyle nodeStyle (AlloyAtom a, AlloyInstance i) { for(AlloySet s:i.atom2sets(a)) {DotStyle v=nodeStyle.get(s); if (v!=null) return v;} return nodeStyle.resolve (a.getType()); } public DotShape shape (AlloyAtom a, AlloyInstance i) { for(AlloySet s:i.atom2sets(a)) {DotShape v=shape.get(s); if (v!=null) return v;} return shape.resolve (a.getType()); } public boolean nodeVisible (AlloyAtom a, AlloyInstance i) { // If it's in 1 or more set, then TRUE if at least one of them is TRUE. // If it's in 0 set, then travel up the chain of AlloyType and return the first non-null value. if (i.atom2sets(a).size()>0) { for(AlloySet s:i.atom2sets(a)) if (nodeVisible.resolve(s)) return true; return false; } return nodeVisible.resolve(a.getType()); } }