/* * 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.List; import javax.swing.AbstractAction; import javax.swing.JCheckBox; import org.apache.log4j.Logger; 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 ca.sqlpower.query.StringItem; import edu.umd.cs.piccolo.PCanvas; import edu.umd.cs.piccolo.PNode; 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.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 PNode will contain all of the constants defined in the query. The constants * will be able to be aliased, joined on, and filtered same as other columns. Any * joins specified to this PNode will have the boolean operation placed in the where * clause as there is no actual table to join on. */ public class ConstantsPane extends PNode implements CleanupPNode { private static final Logger logger = Logger.getLogger(ConstantsPane.class); private static final String TITLE_STRING = "CONSTANTS"; private static final int BORDER_SIZE = 5; private static final int STROKE_SIZE = 2; /** * The string in the addingNewItemPNode so users can tell where to click * to add a new item string. */ private static final String ADDING_ITEM_STRING = "Add..."; private final PCanvas canvas; private final QueryPen queryPen; private final Container model; private final List<PropertyChangeListener> changeListeners; private PPath outerRect; /** * The text that defines the alias column. */ private PStyledText aliasHeader; private PStyledText whereHeader; private PStyledText columnHeader; /** * This styled text will be placed at the bottom of this PNode to allow * users to specify new constants. */ private EditablePStyledText addingNewItemPNode; /** * This contains all the {@link ConstantPNode} objects that are contained * and displayed by this ConstantsPane. */ private final List<ConstantPNode> constantPNodeList; /** * This moves the PNode as the model's position moves. * <p> * This refires the property change event. This may no longer be necessary. */ private final PropertyChangeListener itemChangedListener = new PropertyChangeListener() { public void propertyChange(PropertyChangeEvent evt) { if (evt.getPropertyName().equals("position")) { translate(model.getPosition().getX() - getGlobalBounds().getX(), model.getPosition().getY() - getGlobalBounds().getY()); } for (PropertyChangeListener l : changeListeners) { l.propertyChange(evt); } } }; /** * This listener adds items to this container when items are added to * the model. It also removes items from this container when they're * removed from the model. */ private final ContainerChildListener childListener = new ContainerChildListener() { public void containerChildRemoved(ContainerChildEvent evt) { int constantPosition = -1; for (ConstantPNode constantNode : constantPNodeList) { if (constantNode.getItem() == evt.getChild()) { constantPosition = constantPNodeList.indexOf(constantNode); constantPNodeList.remove(constantNode); constantNode.removeChangeListener(resizeListener); ConstantsPane.this.removeChild(constantNode); break; } } if (constantPosition != -1) { for (int i = constantPosition; i < constantPNodeList.size(); i++) { constantPNodeList.get(i).translate(0, -title.getHeight() - BORDER_SIZE); } repositionAndResize(); } } public void containerChildAdded(ContainerChildEvent evt) { addItem((Item)evt.getChild()); } }; /** * The styled text that displays the title of this PNode. */ private PStyledText title; /** * Listens for changes to the contained constants and resizes the window appropriately. */ private PropertyChangeListener resizeListener = new PropertyChangeListener() { public void propertyChange(PropertyChangeEvent evt) { for (PropertyChangeListener listener : changeListeners) { listener.propertyChange(evt); } repositionAndResize(); } }; /** * This node contains all of the column headers for this pane. */ private PNode header; /** * This checkbox will allow the user to check and uncheck all of the constanst in one click. */ private PSwing allSelectCheckbox; /** * This header background will be placed behind the container header and the * column headers. This will allow specifying a gradient across the header. * This background needs to be taller than its clipping region so the * rounded bottom gets clipped away. */ private PPath headerBackground; /** * This clipping region will clip the header background to give it a flat line at the bottom. */ private PClip headerBackClip; /** * This is the background for the area where the user can add where text. */ private PNode whereBackground; private final SPVariableHelper variables; public ConstantsPane(QueryPen mouseState, PCanvas canvas, Container containerModel) { this(mouseState, canvas, containerModel, null); } public ConstantsPane(QueryPen mouseState, PCanvas canvas, Container containerModel, SPVariableHelper variables) { this.queryPen = mouseState; this.canvas = canvas; this.model = containerModel; this.variables = variables; changeListeners = new ArrayList<PropertyChangeListener>(); constantPNodeList = new ArrayList<ConstantPNode>(); model.addPropertyChangeListener(itemChangedListener); model.addChildListener(childListener); title = new EditablePStyledText(TITLE_STRING, mouseState, canvas); addChild(title); header = new PNode(); header.translate(0, title.getHeight() + BORDER_SIZE); final JCheckBox checkbox = new JCheckBox(); checkbox.setOpaque(false); checkbox.addActionListener(new AbstractAction() { public void actionPerformed(ActionEvent e) { for (ConstantPNode node : constantPNodeList) { if(node.isInSelect() != ((JCheckBox)e.getSource()).isSelected()) { node.setSelected(checkbox.isSelected()); } } } }); allSelectCheckbox = new PSwing(checkbox); header.addChild(allSelectCheckbox); columnHeader = new EditablePStyledText("select all/none", mouseState, canvas); double headerYPos = (allSelectCheckbox.getFullBounds().getHeight() - columnHeader.getHeight())/2; double checkboxWidth = allSelectCheckbox.getFullBounds().getWidth(); columnHeader.translate(checkboxWidth , headerYPos); header.addChild(columnHeader); aliasHeader = new EditablePStyledText("alias", mouseState, canvas); aliasHeader.translate(checkboxWidth + columnHeader.getWidth() + 2 * BORDER_SIZE, headerYPos); header.addChild(aliasHeader); whereHeader = new EditablePStyledText("where", mouseState, canvas); double whereHeaderX = checkboxWidth + columnHeader.getWidth() + aliasHeader.getWidth() + 3 * BORDER_SIZE; whereHeader.translate(whereHeaderX, headerYPos); header.addChild(whereHeader); addChild(header); addingNewItemPNode = new EditablePStyledText(ADDING_ITEM_STRING, mouseState, canvas); addingNewItemPNode.addEditStyledTextListener(new EditStyledTextListener() { public void editingStopping() { String text = addingNewItemPNode.getEditorPane().getText(); if (!text.equals(ADDING_ITEM_STRING) && text.trim().length() > 0) { model.addItem(new StringItem(text)); } addingNewItemPNode.getEditorPane().setText(ADDING_ITEM_STRING); addingNewItemPNode.syncWithDocument(); } public void editingStarting() { addingNewItemPNode.getEditorPane().setText(""); } }); addingNewItemPNode.translate(0, (title.getHeight() + BORDER_SIZE) * 2 + BORDER_SIZE); addChild(addingNewItemPNode); outerRect = PPath.createRoundRectangle((float)-BORDER_SIZE, (float)-BORDER_SIZE, (float)(getFullBounds().getWidth() + 2 * BORDER_SIZE), (float)(getFullBounds().getHeight() + 3 * BORDER_SIZE), 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)(title.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)(title.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() + whereHeaderX + BORDER_SIZE, outerRect.getY() + headerClipHeight); whereBackground.setWidth(outerRect.getWidth() - whereHeaderX - STROKE_SIZE - BORDER_SIZE - 1); whereBackground.setHeight(outerRect.getHeight() - headerClipHeight - STROKE_SIZE - 1); whereBackground.setPaint(QueryPen.WHERE_BACKGROUND_COLOUR); addChild(whereBackground); addChild(headerBackClip); addChild(outerRect); whereBackground.moveToBack(); headerBackClip.moveToBack(); outerRect.moveToBack(); setBounds(outerRect.getBounds()); translate(-getGlobalBounds().getX(), -getGlobalBounds().getY()); translate(containerModel.getPosition().getX(), containerModel.getPosition().getY()); logger.debug("Loaded constants pane in position " + getGlobalBounds().getX() + ", " + getGlobalBounds().getY()); for (Item item : model.getItems()) { addItem(item); } 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()); } }); PNotificationCenter.defaultCenter().addListener(this, "setFocusAppearance", PSelectionEventHandler.SELECTION_CHANGED_NOTIFICATION, null); setFocusAppearance(new PNotification(null, null, null)); } /** * This method will shift the columns so they fit the maximum value as well * as move the adding field and resize the outer rectangle and overall bounds. */ private void repositionAndResize() { addingNewItemPNode.translate(0, (title.getHeight() + BORDER_SIZE) * (2 + constantPNodeList.size()) + BORDER_SIZE - addingNewItemPNode.getFullBounds().getY()); double translateAliasX = columnHeader.getFullBounds().getX() + columnHeader.getWidth() + BORDER_SIZE; for (ConstantPNode node : constantPNodeList) { translateAliasX = Math.max(translateAliasX, node.getAliasOffset()); } aliasHeader.translate(translateAliasX - aliasHeader.getFullBounds().getX(), 0); whereHeader.translate(translateAliasX - aliasHeader.getFullBounds().getX(), 0); for (ConstantPNode node : constantPNodeList) { node.setAliasXPosition(translateAliasX); } double translateWhereX = aliasHeader.getFullBounds().getX() + aliasHeader.getWidth() + BORDER_SIZE; logger.debug("Translating where: max x is " + translateWhereX); for (ConstantPNode node : constantPNodeList) { translateWhereX = Math.max(translateWhereX, node.getWhereOffset()); logger.debug("Translating where: max x is " + translateWhereX); } logger.debug("Translating where header " + translateWhereX + " from " + whereHeader.getFullBounds().getX()); whereHeader.translate(translateWhereX - whereHeader.getFullBounds().getX(), 0); for (ConstantPNode node : constantPNodeList) { node.setWhereXPosition(translateWhereX); } double maxWidth = header.getFullBounds().getWidth(); for (ConstantPNode node : constantPNodeList) { maxWidth = Math.max(maxWidth, node.getFullBounds().getWidth()); } outerRect.setWidth(maxWidth + 2 * BORDER_SIZE); headerBackground.setWidth(maxWidth + 2 * BORDER_SIZE); headerBackClip.setWidth(maxWidth + 2 * BORDER_SIZE); outerRect.setHeight((title.getHeight() + BORDER_SIZE) * (3 + constantPNodeList.size()) + (2 * BORDER_SIZE)); whereBackground.translate(translateWhereX - whereBackground.getFullBounds().getX(), 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()); } @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) { if (super.fullPick(pickPath)) { PNode picked = pickPath.getPickedNode(); // this code won't work with internal cameras, because it doesn't pop // the cameras view transform. //---Clickable elements for (PNode node : constantPNodeList) { if (node.getAllNodes().contains(picked)) { return true; } } if (picked == addingNewItemPNode || picked == allSelectCheckbox) { return true; } //---End clickable elements while (picked != this) { pickPath.popTransform(picked.getTransformReference(false)); pickPath.popNode(picked); picked = pickPath.getPickedNode(); } return true; } 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); headerBackground.setPaint(QueryPen.SELECTED_CONTAINER_COLOUR); moveToFront(); } else { outerRect.setStrokePaint(QueryPen.UNSELECTED_CONTAINER_COLOUR); headerBackground.setPaint(QueryPen.UNSELECTED_CONTAINER_GRADIENT_COLOUR); } } /** * Adds the item to this ConstanstPane in a new ConstantPNode. */ public void addItem(Item item) { ConstantPNode newConstantNode = new ConstantPNode(item, queryPen, canvas, this.variables); newConstantNode.addChangeListener(resizeListener); newConstantNode.translate(0, (title.getHeight() + BORDER_SIZE) * (2 + constantPNodeList.size()) + BORDER_SIZE); constantPNodeList.add(newConstantNode); ConstantsPane.this.addChild(newConstantNode); repositionAndResize(); } public void addChangeListener(PropertyChangeListener l) { changeListeners.add(l); } public void removeChangeListener(PropertyChangeListener l) { changeListeners.remove(l); } public void cleanup() { model.removePropertyChangeListener(itemChangedListener); model.removeChildListener(childListener); for (Object o : getAllNodes()) { if (o instanceof CleanupPNode && o != this) { ((CleanupPNode)o).cleanup(); } } PNotificationCenter.defaultCenter().removeListener(this); } public Container getModel() { return model; } }