/*
* Copyright (c) 2008, SQL Power Group Inc.
*
* This file is part of Wabit.
*
* Wabit is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* Wabit 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package ca.sqlpower.swingui.querypen;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.event.ActionEvent;
import java.awt.geom.Point2D;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import javax.swing.AbstractAction;
import javax.swing.ImageIcon;
import javax.swing.JCheckBox;
import javax.swing.JEditorPane;
import javax.swing.JLabel;
import org.apache.log4j.Logger;
import ca.sqlpower.object.SPObject;
import ca.sqlpower.object.SPVariableHelper;
import ca.sqlpower.query.Container;
import ca.sqlpower.query.ContainerChildEvent;
import ca.sqlpower.query.ContainerChildListener;
import ca.sqlpower.query.Item;
import edu.umd.cs.piccolo.PCanvas;
import edu.umd.cs.piccolo.PNode;
import edu.umd.cs.piccolo.event.PBasicInputEventHandler;
import edu.umd.cs.piccolo.event.PDragSequenceEventHandler;
import edu.umd.cs.piccolo.event.PInputEvent;
import edu.umd.cs.piccolo.nodes.PPath;
import edu.umd.cs.piccolo.util.PBounds;
import edu.umd.cs.piccolo.util.PPickPath;
import edu.umd.cs.piccolox.event.PNotification;
import edu.umd.cs.piccolox.event.PNotificationCenter;
import edu.umd.cs.piccolox.event.PSelectionEventHandler;
import edu.umd.cs.piccolox.nodes.PClip;
import edu.umd.cs.piccolox.nodes.PStyledText;
import edu.umd.cs.piccolox.pswing.PSwing;
/**
* This container pane displays a list of values stored in its model. The elements displayed
* in a container pane can be broken into groups and will be separated by a line for each
* group.
*
* @param <C> The type of object this container is displaying.
*/
public class ContainerPane extends PNode implements CleanupPNode {
private static Logger logger = Logger.getLogger(ContainerPane.class);
/**
* Stores true when the OS is MAC
*/
private static final boolean MAC_OS_X = (System.getProperty("os.name").
toLowerCase().startsWith("mac os x"));
private static final ImageIcon CLOSE_ICON = new ImageIcon(
ContainerPane.class.getResource("close-16.png"));
/**
* The size of the border to place around the text in this container pane
* for readability.
*/
private static final int BORDER_SIZE = 5;
/**
* The size of separators between different fields.
*/
private static final int SEPARATOR_SIZE = 5;
/**
* The stroke size of the lines in the container pane.
*/
private static final float STROKE_SIZE = 2;
private final Container model;
/**
* The outer rectangle of this component. All parts of this component should
* be within this rectangle and it should be resized if the components
* inside are changed.
*/
private PPath outerRect;
/**
* The pane that contains the current state of the mouse for that this component
* is attached to.
*/
private QueryPen queryPen;
/**
* The canvas this component is being drawn on.
*/
private PCanvas canvas;
/**
* This is the Text for the Where ColumnHeader. We need to store the variable so we can change its position when the column names or headers get resized
*/
private PStyledText whereHeader;
/**
* this is a checkBox in the header which checks all the items checkBoxes
*/
private PSwing swingCheckBox;
/**
* All of the {@link PStyledText} objects that represent an object in the model.
*/
private List<UnmodifiableItemPNode> containedItems;
/**
* The PPath lines that separate the header from the columns and
* different groups of columns.
*/
private List<PPath> separatorLines;
/**
* These listeners will fire a change event when an element on this object
* is changed that affects the resulting generated query.
*/
private final Collection<PropertyChangeListener> queryChangeListeners;
/**
* A listener to properly display the alias and column name when the
* {@link EditablePStyledText} is switching from edit to non-edit mode and
* back. This listener for the nameEditor will show only the alias when the
* alias is being edited. When the alias is not being edited it will show
* the alias and column name, in brackets, if an alias is specified.
* Otherwise only the column name will be displayed.
*/
private EditStyledTextListener editingTextListener = new EditStyledTextListener() {
/**
* Tracks if we are in an editing state or not. Used to keep the
* editingStopped method from running only once per stop edit (some
* cases the editingStopped can be called from multiple places on the
* same stopEditing).
*/
private boolean editing = false;
public void editingStopping() {
if (editing) {
createAliasName();
}
editing = false;
}
public void editingStarting() {
editing = true;
if (model.getAlias() != null && model.getAlias().length() > 0) {
modelNameText.getEditorPane().setText(model.getAlias());
logger.debug("Setting editor text to " + model.getAlias());
}
}
};
private void createAliasName() {
JEditorPane nameEditor = modelNameText.getEditorPane();
String name = model.getName();
if (nameEditor.getText() != null && nameEditor.getText().length() > 0 && !nameEditor.getText().equals(name)) {
model.setAlias(nameEditor.getText());
} else {
logger.debug("item name is " + name);
model.setAlias("");
}
setVisibleAliasText();
logger.debug("editor has text " + nameEditor.getText() + " alias is " + model.getAlias());
}
/**
* Refiring the events may no longer be needed.
*/
private PropertyChangeListener guiItemChangeListener = new PropertyChangeListener() {
public void propertyChange(PropertyChangeEvent evt) {
for (PropertyChangeListener l : queryChangeListeners) {
l.propertyChange(evt);
}
}
};
/**
* This listener will resize the bounding box of the container
* when properties of components it is attached to change.
*/
private PropertyChangeListener resizeOnEditChangeListener = new PropertyChangeListener() {
public void propertyChange(PropertyChangeEvent evt) {
repositionWhereAndResize();
}
};
/**
* This listener is added to the Container to listen for changes to the model. This must be removed
* for the component to be disposed properly.
*/
private final PropertyChangeListener containerChangeListener = new PropertyChangeListener() {
public void propertyChange(PropertyChangeEvent evt) {
if (evt.getPropertyName().equals("position")) {
translate(model.getPosition().getX() - getGlobalBounds().getX(),
model.getPosition().getY() - getGlobalBounds().getY());
} else if (evt.getPropertyName().equals("alias")) {
setVisibleAliasText();
}
if (evt.getPropertyName().equals(Container.CONTAINTER_ALIAS_CHANGED)) {
for (PropertyChangeListener l : queryChangeListeners) {
l.propertyChange(evt);
}
}
}
};
private final ContainerChildListener containerChildListener = new ContainerChildListener() {
public void containerChildAdded(ContainerChildEvent evt) {
addItem(evt.getChild());
logger.debug("Added " + (evt.getChild()).getName() + " to the container pane.");
}
public void containerChildRemoved(ContainerChildEvent evt) {
removeItem(evt.getChild());
}
};
private EditablePStyledText modelNameText;
/**
* This is the header that defines which column is the select check boxes,
* which column is the column name and alias, and which column is the where
* clause.
*/
private final PNode header;
/**
* This is the header for column names and aliases.
*/
private PStyledText columnNameHeader;
/**
* A clipping region the background header will be clipped to. This removes
* the lower rounding corners of the background.
*/
private PClip headerBackClip;
/**
* This will give the header a nice gradient background.
*/
private PPath headerBackground;
/**
* This will give the where fields a background colour.
*/
private PNode whereBackground;
/**
* This is the close button in the corner of the panel.
*/
private final PSwing closeButton;
private final SPVariableHelper variablesHelper;
public ContainerPane(QueryPen pen, PCanvas canvas, Container newModel) {
this(pen, canvas, newModel, null);
}
public ContainerPane(QueryPen pen, PCanvas canvas, Container newModel, SPVariableHelper variables) {
model = newModel;
this.variablesHelper = variables;
logger.debug("Container alias is " + model.getAlias());
model.addPropertyChangeListener(containerChangeListener);
model.addChildListener(containerChildListener);
queryChangeListeners = new ArrayList<PropertyChangeListener>();
this.queryPen = pen;
this.canvas = canvas;
containedItems = new ArrayList<UnmodifiableItemPNode>();
separatorLines = new ArrayList<PPath>();
logger.debug("Model name is " + model.getName());
modelNameText = new EditablePStyledText(model.getName(), pen, canvas);
modelNameText.addEditStyledTextListener(editingTextListener);
modelNameText.addPropertyChangeListener(PNode.PROPERTY_BOUNDS, resizeOnEditChangeListener);
modelNameText.addInputEventListener(new PBasicInputEventHandler() {
@Override
public void mousePressed(PInputEvent event){
if(!queryPen.getMultipleSelectEventHandler().isSelected(ContainerPane.this)){
queryPen.getMultipleSelectEventHandler().unselectAll();
}
queryPen.getMultipleSelectEventHandler().select(ContainerPane.this);
}
});
addChild(modelNameText);
header = createColumnHeader();
header.translate(0, modelNameText.getHeight()+ BORDER_SIZE);
addChild(header);
int yLoc = 2;
for (Item item : model.getItems()) {
final UnmodifiableItemPNode newText = createTextLine(item);
newText.translate(0, (modelNameText.getHeight() + BORDER_SIZE) * yLoc+ BORDER_SIZE);
addChild(newText);
containedItems.add(newText);
yLoc++;
}
repositionWhereClauses();
PBounds fullBounds = getFullBounds();
outerRect = PPath.createRoundRectangle(
(float)fullBounds.x - BORDER_SIZE,
(float)fullBounds.y - BORDER_SIZE,
(float)fullBounds.width + BORDER_SIZE * 2,
(float)fullBounds.height + BORDER_SIZE * 2,
QueryPen.CONTAINER_ROUND_CORNER_RADIUS,
QueryPen.CONTAINER_ROUND_CORNER_RADIUS);
outerRect.setStroke(new BasicStroke(STROKE_SIZE));
headerBackground = PPath.createRoundRectangle(
(float)-BORDER_SIZE, (float)-BORDER_SIZE,
(float)outerRect.getWidth() - 1,
(float)(modelNameText.getHeight() + BORDER_SIZE) * 2 + BORDER_SIZE
+ QueryPen.CONTAINER_ROUND_CORNER_RADIUS,
QueryPen.CONTAINER_ROUND_CORNER_RADIUS, QueryPen.CONTAINER_ROUND_CORNER_RADIUS);
headerBackground.setStrokePaint(new Color(0x00ffffff, true));
headerBackClip = new PClip();
headerBackClip.addChild(headerBackground);
float headerClipHeight = (float)(modelNameText.getHeight() + BORDER_SIZE) * 2 + 2 * BORDER_SIZE;
headerBackClip.setPathToRectangle(
(float)outerRect.getX(), (float)outerRect.getY(),
(float)outerRect.getWidth(), headerClipHeight);
headerBackClip.setStrokePaint(new Color(0x00ffffff, true));
whereBackground = new PNode();
whereBackground.translate(
outerRect.getX() + whereHeader.getFullBounds().getX() + BORDER_SIZE,
outerRect.getY() + headerClipHeight);
whereBackground.setWidth(
outerRect.getWidth() - whereHeader.getFullBounds().getX() - STROKE_SIZE - BORDER_SIZE - 1);
whereBackground.setHeight(outerRect.getHeight() - headerClipHeight - STROKE_SIZE - 1);
whereBackground.setPaint(QueryPen.WHERE_BACKGROUND_COLOUR);
addChild(whereBackground);
addChild(headerBackClip);
this.addChild(outerRect);
whereBackground.moveToBack();
headerBackClip.moveToBack();
outerRect.moveToBack();
setBounds(outerRect.getBounds());
translate(-getGlobalBounds().getX(), -getGlobalBounds().getY());
translate(model.getPosition().getX(), model.getPosition().getY());
closeButton = new PSwing(new JLabel(CLOSE_ICON));
addChild(closeButton);
if (MAC_OS_X) {
closeButton.translate(-(BORDER_SIZE + (CLOSE_ICON.getIconWidth() / 2)),
-(BORDER_SIZE + (CLOSE_ICON.getIconHeight() / 2)));
} else {
closeButton.translate(headerBackClip.getWidth() - BORDER_SIZE - (CLOSE_ICON.getIconWidth() / 2),
-(BORDER_SIZE + (CLOSE_ICON.getIconHeight() / 2)));
}
closeButton.addInputEventListener(new PBasicInputEventHandler() {
@Override
public void mousePressed(PInputEvent event) {
queryPen.deleteContainer(ContainerPane.this);
}
});
closeButton.setTransparency(0);
addInputEventListener(new PDragSequenceEventHandler() {
@Override
protected void endDrag(PInputEvent e) {
super.endDrag(e);
model.setPosition(new Point2D.Double(getGlobalBounds().getX(), getGlobalBounds().getY()));
logger.debug("Setting position " + getGlobalBounds().getX() + ", " + getGlobalBounds().getY());
}
});
setVisibleAliasText();
PNotificationCenter.defaultCenter().addListener(this, "setFocusAppearance", PSelectionEventHandler.SELECTION_CHANGED_NOTIFICATION, null);
setFocusAppearance(new PNotification(null, null, null));
}
/**
* Creates a {@link PStyledText} object that is editable by clicking on it
* if it's a column, and not editable if it's a table from which everything
* is being selected.
*/
private UnmodifiableItemPNode createTextLine(Item item) {
final UnmodifiableItemPNode modelNameText;
modelNameText = new UnmodifiableItemPNode(queryPen, canvas, item, this.variablesHelper);
modelNameText.getItemText().addPropertyChangeListener(PNode.PROPERTY_BOUNDS, resizeOnEditChangeListener);
modelNameText.getWherePStyledText().addPropertyChangeListener(PNode.PROPERTY_BOUNDS, resizeOnEditChangeListener);
modelNameText.addQueryChangeListener(guiItemChangeListener);
return modelNameText;
}
private PNode createColumnHeader() {
PNode itemHeader = new PNode();
final JCheckBox allCheckBox = new JCheckBox();
allCheckBox.setOpaque(false);
allCheckBox.addActionListener(new AbstractAction(){
public void actionPerformed(ActionEvent e) {
try {
String editMessage;
if (allCheckBox.isSelected()) {
editMessage = "Setting all columns in the table " + getModel().getName()
+ " to be selected";
} else {
editMessage = "Setting all columns in the table " + getModel().getName()
+ " to be un-selected";
}
queryPen.getModel().startCompoundEdit(editMessage);
for (UnmodifiableItemPNode itemNode : containedItems) {
if(itemNode.isInSelect() != ((JCheckBox)e.getSource()).isSelected()) {
itemNode.setInSelected(((JCheckBox)e.getSource()).isSelected());
}
}
} finally {
queryPen.getModel().endCompoundEdit();
}
}
});
allCheckBox.setSelected(true);
swingCheckBox = new PSwing(allCheckBox);
itemHeader.addChild(swingCheckBox);
columnNameHeader = new EditablePStyledText("select all/none", queryPen, canvas);
double textYTranslation = (swingCheckBox.getFullBounds().height - columnNameHeader.getFullBounds().height)/2;
columnNameHeader.translate(swingCheckBox.getFullBounds().width, textYTranslation);
itemHeader.addChild(columnNameHeader);
whereHeader = new EditablePStyledText("where:", queryPen, canvas);
whereHeader.translate(0, textYTranslation);
itemHeader.addChild(whereHeader);
return itemHeader;
}
public Container getModel() {
return model;
}
public String getModelTextName() {
return model.getName();
}
/**
* Returns the ItemPNode that represents the Item that contains the object
* passed into this method. If there is no ItemPNode in this container that
* represents the given item null is returned.
*/
public UnmodifiableItemPNode getItemPNode(Object item) {
Item itemInModel = model.getItem(item);
if (itemInModel == null) {
logger.debug("Item " + item + " not in model.");
return null;
}
for (UnmodifiableItemPNode itemNode : containedItems) {
if (itemInModel.getItem() == itemNode.getItem().getItem()) {
return itemNode;
}
}
return null;
}
@Override
/*
* Taken from PComposite. This keeps the title and container lines together in
* a unit but is modified to allow picking of internal components.
*/
public boolean fullPick(PPickPath pickPath) {
final int animationLength = 200;
if (super.fullPick(pickPath)) {
try {
PNode picked = pickPath.getPickedNode();
// this code won't work with internal cameras, because it doesn't pop
// the cameras view transform.
for (PNode node : containedItems) {
if (node.getAllNodes().contains(picked)) {
return true;
}
}
if (picked == swingCheckBox || picked == modelNameText
|| picked == closeButton) {
return true;
}
while (picked != this) {
pickPath.popTransform(picked.getTransformReference(false));
pickPath.popNode(picked);
picked = pickPath.getPickedNode();
}
return true;
} finally {
if (closeButton.getTransparency() == 0) {
closeButton.animateToTransparency(1, animationLength);
}
}
}
if (closeButton.getTransparency() == 1) {
closeButton.animateToTransparency(0, animationLength);
}
return false;
}
/**
* This method should be called when the focus of this container changes. It
* can be called through reflection from the {@link PNotificationCenter}.
*
* @param notification
* The notification event from the {@link PNotificationCenter}.
*/
public void setFocusAppearance(PNotification notification) {
boolean hasFocus = queryPen.getMultipleSelectEventHandler().getSelection().contains(this);
if (hasFocus) {
outerRect.setStrokePaint(QueryPen.SELECTED_CONTAINER_COLOUR);
for (PPath line : separatorLines) {
line.setStrokePaint(QueryPen.SELECTED_CONTAINER_COLOUR);
}
headerBackground.setPaint(QueryPen.SELECTED_CONTAINER_COLOUR);
moveToFront();
} else {
outerRect.setStrokePaint(QueryPen.UNSELECTED_CONTAINER_COLOUR);
for (PPath line : separatorLines) {
line.setStrokePaint(QueryPen.UNSELECTED_CONTAINER_COLOUR);
}
headerBackground.setPaint(QueryPen.UNSELECTED_CONTAINER_GRADIENT_COLOUR);
}
}
public void addQueryChangeListener(PropertyChangeListener l) {
queryChangeListeners.add(l);
}
public void removeQueryChangeListener(PropertyChangeListener l) {
queryChangeListeners.remove(l);
}
/**
* Repositions the where clauses of all of the items in this container as well
* as the where header. This returns the new x location of the where clauses.
*/
private double repositionWhereClauses() {
double maxXPos = swingCheckBox.getFullBounds().width + SEPARATOR_SIZE + columnNameHeader.getWidth() + SEPARATOR_SIZE;
for (UnmodifiableItemPNode itemNode : containedItems) {
maxXPos = Math.max(maxXPos, itemNode.getDistanceForWhere());
}
whereHeader.translate(maxXPos - whereHeader.getXOffset(), 0);
for (UnmodifiableItemPNode itemNode : containedItems) {
itemNode.positionWhere(maxXPos);
}
return maxXPos;
}
public List<UnmodifiableItemPNode> getContainedItems() {
return Collections.unmodifiableList(containedItems);
}
private void addItem(Item item) {
UnmodifiableItemPNode itemNode = createTextLine(item);
itemNode.translate(0, (modelNameText.getHeight() + BORDER_SIZE) * (2 + containedItems.size()) + BORDER_SIZE);
addChild(itemNode);
containedItems.add(itemNode);
repositionWhereAndResize();
}
private void removeItem(Item item) {
UnmodifiableItemPNode itemNode = getItemPNode(item.getItem());
if (itemNode != null) {
int containedItemsLocation = containedItems.indexOf(itemNode);
removeChild(itemNode);
containedItems.remove(itemNode);
itemNode.getItemText().removePropertyChangeListener(PNode.PROPERTY_BOUNDS, resizeOnEditChangeListener);
itemNode.getWherePStyledText().removePropertyChangeListener(PNode.PROPERTY_BOUNDS, resizeOnEditChangeListener);
itemNode.removeQueryChangeListener(guiItemChangeListener);
for (int i = containedItemsLocation; i < containedItems.size(); i++) {
containedItems.get(i).translate(0, - modelNameText.getHeight() - BORDER_SIZE);
}
repositionWhereAndResize();
}
}
private void repositionWhereAndResize() {
double maxWhereXPos = repositionWhereClauses();
if (outerRect != null) {
double maxWidth = Math.max(header.getFullBounds().getWidth(), modelNameText.getFullBounds().getWidth());
logger.debug("Header width is " + header.getFullBounds().getWidth() + " and the container name has width " + modelNameText.getFullBounds().getWidth());
for (UnmodifiableItemPNode node : containedItems) {
maxWidth = Math.max(maxWidth, node.getFullBounds().getWidth());
}
logger.debug("Max width of the container pane is " + maxWidth);
maxWidth += 2 * BORDER_SIZE;
outerRect.setWidth(maxWidth);
for (PPath line : separatorLines) {
line.setWidth(maxWidth);
}
int numStaticRows = 2;
outerRect.setHeight((modelNameText.getHeight() + BORDER_SIZE) * (numStaticRows + containedItems.size()) + BORDER_SIZE * 3);
headerBackground.setWidth(maxWidth);
headerBackClip.setWidth(maxWidth);
whereBackground.translate(maxWhereXPos - whereBackground.getXOffset(), 0);
whereBackground.setWidth(outerRect.getWidth() - whereBackground.getFullBounds().getX() - STROKE_SIZE - BORDER_SIZE - 1);
whereBackground.setHeight(outerRect.getHeight() - whereBackground.getFullBounds().getY() - STROKE_SIZE - BORDER_SIZE - 1);
setBounds(outerRect.getBounds());
}
}
/**
* This sets the PStyledText to have either the model name or the model name and alias
* depending on the model's alias.
*/
private void setVisibleAliasText() {
JEditorPane nameEditor = modelNameText.getEditorPane();
if (model.getAlias() == null || model.getAlias().trim().length() <= 0) {
nameEditor.setText(model.getName());
} else {
nameEditor.setText(model.getAlias() + " (" + model.getName() + ")");
}
modelNameText.syncWithDocument();
}
public void setContainerAlias(String newAlias) {
modelNameText.getEditorPane().setText(newAlias);
createAliasName();
}
public void cleanup() {
model.removePropertyChangeListener(containerChangeListener);
model.removeChildListener(containerChildListener);
for (Object o : getAllNodes()) {
if (o instanceof CleanupPNode && o != this) {
((CleanupPNode)o).cleanup();
}
}
PNotificationCenter.defaultCenter().removeListener(this);
}
}