/* 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.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.GridLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.AdjustmentEvent;
import java.awt.event.AdjustmentListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.JButton;
import javax.swing.JComboBox;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JSplitPane;
import javax.swing.JTextArea;
import javax.swing.SwingConstants;
import javax.swing.border.Border;
import javax.swing.border.EmptyBorder;
import edu.mit.csail.sdg.alloy4.ConstList;
import edu.mit.csail.sdg.alloy4.OurBorder;
import edu.mit.csail.sdg.alloy4.OurCombobox;
import edu.mit.csail.sdg.alloy4.OurUtil;
import edu.mit.csail.sdg.alloy4.Util;
import edu.mit.csail.sdg.alloy4graph.GraphViewer;
/** GUI panel that houses the actual graph, as well as any projection comboboxes.
*
* <p><b>Thread Safety:</b> Can be called only by the AWT event thread.
*/
public final class VizGraphPanel extends JPanel {
/** This ensures the class can be serialized reliably. */
private static final long serialVersionUID = 0;
/** This is the current customization settings. */
private final VizState vizState;
/** Whether the user wants to see the DOT source code or not. */
private boolean seeDot=false;
/** The current GraphViewer (or null if we are not looking at a GraphViewer) */
private GraphViewer viewer=null;
/** The scrollpane containing the upperhalf of the panel (showing the graph) */
private final JScrollPane diagramScrollPanel;
/** The upperhalf of the panel (showing the graph). */
private final JPanel graphPanel;
/** The lowerhalf of the panel (showing the comboboxes for choosing the projected atoms). */
private final JPanel navPanel;
/** The splitpane separating the graphPanel and the navPanel. */
private final JSplitPane split;
/** The current projection choice; null if no projection is in effect. */
private AlloyProjection currentProjection=null;
/** This is the list of TypePanel(s) we've already constructed. */
private final Map<AlloyType,TypePanel> type2panel = new TreeMap<AlloyType,TypePanel>();
/** Inner class that displays a combo box of possible projection atom choices. */
private final class TypePanel extends JPanel {
/** This ensures the class can be serialized reliably. */
private static final long serialVersionUID = 0;
/** The type being projected. */
private final AlloyType type;
/** The list of atoms; can be an empty list if there are no atoms in this type to be projected. */
private final List<AlloyAtom> atoms;
/** The list of atom names; atomnames.empty() iff atoms.isEmpty() */
private final String[] atomnames;
/** The combo box showing the possible atoms to choose from. */
private final JComboBox atomCombo;
/** True if this TypePanel object does not need to be rebuilt. */
private boolean upToDate(AlloyType type, List<AlloyAtom> atoms) {
if (!this.type.equals(type)) return false;
atoms = new ArrayList<AlloyAtom>(atoms);
Collections.sort(atoms);
if (!this.atoms.equals(atoms)) return false;
for(int i=0; i<this.atoms.size(); i++) {
String n = this.atoms.get(i).getVizName(vizState,true);
if (!atomnames[i].equals(n)) return false;
}
return true;
}
/** Constructs a new TypePanel.
* @param type - the type being projected
* @param atoms - the list of possible projection atom choices
*/
private TypePanel(AlloyType type, List<AlloyAtom> atoms, AlloyAtom initialValue) {
super();
final JButton left, right;
int initialIndex=0;
this.type=type;
atoms=new ArrayList<AlloyAtom>(atoms);
Collections.sort(atoms);
this.atoms=ConstList.make(atoms);
setLayout(new BoxLayout(this, BoxLayout.X_AXIS));
setBorder(null);
this.atomnames=new String[this.atoms.size()];
for(int i=0; i<this.atoms.size(); i++) {
atomnames[i]=this.atoms.get(i).getVizName(vizState,true);
if (this.atoms.get(i).equals(initialValue)) initialIndex=i;
}
add(left = new JButton("<<"));
add(Box.createRigidArea(new Dimension(2,2)));
add(atomCombo = new OurCombobox(atomnames.length<1 ? new String[]{" "} : atomnames));
add(Box.createRigidArea(new Dimension(2,2)));
add(right = new JButton(">>"));
left.setVerticalAlignment(SwingConstants.CENTER);
right.setVerticalAlignment(SwingConstants.CENTER);
Dimension dim = atomCombo.getPreferredSize();
int idealWidth = Util.onMac() ? 120 : 80;
if (dim.width<idealWidth) { dim.width=idealWidth+20; atomCombo.setMinimumSize(dim); atomCombo.setPreferredSize(dim); }
left.setEnabled(initialIndex>0);
right.setEnabled(initialIndex<atomnames.length-1);
atomCombo.setSelectedIndex(initialIndex);
if (Util.onMac()) atomCombo.setBorder(BorderFactory.createEmptyBorder(4,1,0,1));
left.addActionListener(new ActionListener() {
public final void actionPerformed(ActionEvent e) {
int curIndex = atomCombo.getSelectedIndex();
if (curIndex > 0) atomCombo.setSelectedIndex(curIndex-1);
}
});
right.addActionListener(new ActionListener() {
public final void actionPerformed(ActionEvent e) {
int curIndex = atomCombo.getSelectedIndex();
if (curIndex < atomCombo.getItemCount()-1) atomCombo.setSelectedIndex(curIndex+1);
}
});
atomCombo.addActionListener(new ActionListener() {
public final void actionPerformed(ActionEvent e) {
left.setEnabled(atomCombo.getSelectedIndex() > 0);
right.setEnabled(atomCombo.getSelectedIndex() < atomnames.length-1);
remakeAll();
VizGraphPanel.this.getParent().invalidate();
VizGraphPanel.this.getParent().repaint();
}
});
}
/** Returns the entire list of atoms; could be an empty set. */
public List<AlloyAtom> getAlloyAtoms() { return atoms; }
/** Returns the currently-selected atom; returns null if the list is empty. */
public AlloyAtom getAlloyAtom() {
int i=atomCombo.getSelectedIndex();
if (i>=0 && i<atoms.size()) return atoms.get(i); else return null;
}
/** Returns the AlloyType associated with this TypePanel. */
public AlloyType getAlloyType() { return type; }
}
/** Create a splitpane showing the graph on top, as well as projection comboboxes on the bottom.
* @param vizState - the current visualization settings
* @param seeDot - true if we want to see the DOT source code, false if we want it rendered as a graph
*/
public VizGraphPanel(VizState vizState, boolean seeDot) {
Border b = new EmptyBorder(0, 0, 0, 0);
OurUtil.make(this, Color.BLACK, Color.WHITE, b);
this.seeDot = seeDot;
this.vizState = vizState;
setLayout(new GridLayout());
setMaximumSize(new Dimension(Short.MAX_VALUE, Short.MAX_VALUE));
navPanel = new JPanel();
JScrollPane navscroll = OurUtil.scrollpane(navPanel);
graphPanel = OurUtil.make(new JPanel(), Color.BLACK, Color.WHITE, b);
graphPanel.addMouseListener(new MouseAdapter() {
@Override public void mousePressed(MouseEvent ev) {
// We let Ctrl+LeftClick bring up the popup menu, just like RightClick,
// since many Mac mouses do not have a right button.
if (viewer==null) return;
else if (ev.getButton()==MouseEvent.BUTTON3) { }
else if (ev.getButton()==MouseEvent.BUTTON1 && ev.isControlDown()) { }
else return;
viewer.alloyPopup(graphPanel, ev.getX(), ev.getY());
}
});
diagramScrollPanel = OurUtil.scrollpane(graphPanel, new OurBorder(true,true,true,false));
diagramScrollPanel.getVerticalScrollBar().addAdjustmentListener(new AdjustmentListener() {
public void adjustmentValueChanged(AdjustmentEvent e) {
diagramScrollPanel.invalidate(); diagramScrollPanel.repaint(); diagramScrollPanel.validate();
}
});
diagramScrollPanel.getHorizontalScrollBar().addAdjustmentListener(new AdjustmentListener() {
public void adjustmentValueChanged(AdjustmentEvent e) {
diagramScrollPanel.invalidate(); diagramScrollPanel.repaint(); diagramScrollPanel.validate();
}
});
split = OurUtil.splitpane(JSplitPane.VERTICAL_SPLIT, diagramScrollPanel, navscroll, 0);
split.setResizeWeight(1.0);
split.setDividerSize(0);
add(split);
remakeAll();
}
/** Regenerate the comboboxes and the graph. */
public void remakeAll() {
Map<AlloyType,AlloyAtom> map=new LinkedHashMap<AlloyType,AlloyAtom>();
navPanel.removeAll();
for (AlloyType type: vizState.getProjectedTypes()) {
List<AlloyAtom> atoms=vizState.getOriginalInstance().type2atoms(type);
TypePanel tp = type2panel.get(type);
if (tp!=null && tp.getAlloyAtom()!=null && !atoms.contains(tp.getAlloyAtom())) tp=null;
if (tp!=null && tp.getAlloyAtom()==null && atoms.size()>0) tp=null;
if (tp!=null && !tp.upToDate(type,atoms)) tp=null;
if (tp==null) type2panel.put(type, tp=new TypePanel(type, atoms, null));
navPanel.add(tp);
map.put(tp.getAlloyType(), tp.getAlloyAtom());
}
currentProjection = new AlloyProjection(map);
JPanel graph = vizState.getGraph(currentProjection);
if (seeDot && (graph instanceof GraphViewer)) {
viewer = null;
JTextArea txt = OurUtil.textarea(graph.toString(), 10, 10, false, true, getFont());
diagramScrollPanel.setViewportView(txt);
} else {
if (graph instanceof GraphViewer) viewer=(GraphViewer)graph; else viewer=null;
graphPanel.removeAll();
graphPanel.add(graph);
diagramScrollPanel.setViewportView(graphPanel);
diagramScrollPanel.invalidate(); diagramScrollPanel.repaint(); diagramScrollPanel.validate();
}
}
/** Changes the font. */
@Override public void setFont(Font font) {
super.setFont(font);
if (diagramScrollPanel!=null) diagramScrollPanel.getViewport().getView().setFont(font);
}
/** Changes whether we are seeing the DOT source or not. */
public void seeDot(boolean yesOrNo) {
if (seeDot==yesOrNo) return;
seeDot=yesOrNo;
remakeAll();
}
/** Retrieves the actual GraphViewer object that contains the graph (or null if the graph hasn't loaded yet) */
public GraphViewer alloyGetViewer() { return viewer; }
/** We override the paint method to auto-resize the divider. */
@Override public void paint(Graphics g) {
super.paint(g);
split.setDividerLocation(
split.getSize().height
- split.getInsets().bottom
- split.getDividerSize()
- split.getRightComponent().getPreferredSize().height
);
}
}