/* Copyright (C) 2006 EBI This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the itmplied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ package org.biomart.builder.view.gui.diagrams.components; import java.awt.AWTEvent; import java.awt.BasicStroke; import java.awt.Color; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.RenderingHints; import java.awt.Shape; import java.awt.Stroke; import java.awt.event.MouseEvent; import java.awt.geom.Rectangle2D; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.util.Collections; import javax.swing.JComponent; import javax.swing.JPopupMenu; import org.biomart.builder.model.Relation; import org.biomart.builder.view.gui.diagrams.Diagram; import org.biomart.builder.view.gui.diagrams.contexts.DiagramContext; import org.biomart.common.utils.BeanMap; import org.biomart.common.utils.Transaction; import org.biomart.common.utils.Transaction.TransactionEvent; import org.biomart.common.utils.Transaction.TransactionListener; /** * This component represents a relation between two keys, in the form of a line. * The path of the line is defined by one of the layout managers provided with * MartBuilder. * * @author Richard Holland <holland@ebi.ac.uk> * @version $Revision: 1.37 $, $Date: 2007-12-20 18:43:46 $, modified by * $Author: arek $ * @since 0.5 */ public class RelationComponent extends JComponent implements DiagramComponent, TransactionListener { /** * Subclasses use this if the component needs repainting. */ protected boolean needsRepaint = false; /** * Subclasses use this if the component needs recalculating. */ protected boolean needsRecalc = false; private boolean changed = false; private static final float RELATION_DASHSIZE = 6.0f; // 72 = 1 inch private static final float RELATION_DOTSIZE = 2.0f; // 72 = 1 inch private static final float RELATION_LINEWIDTH = 1.0f; // 72 = 1 inch private static final float RELATION_MITRE_TRIM = 10.0f; // 72 = 1 inch private static final Stroke ONE_MANY = new BasicStroke( RelationComponent.RELATION_LINEWIDTH, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND, RelationComponent.RELATION_MITRE_TRIM); private static final Stroke ONE_ONE = new BasicStroke( RelationComponent.RELATION_LINEWIDTH * 2.0f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND, RelationComponent.RELATION_MITRE_TRIM); private static final Stroke ONE_MANY_DOTTED = new BasicStroke( RelationComponent.RELATION_LINEWIDTH, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND, RelationComponent.RELATION_MITRE_TRIM, new float[] { RelationComponent.RELATION_DOTSIZE, RelationComponent.RELATION_DOTSIZE }, 0); private static final Stroke ONE_ONE_DOTTED = new BasicStroke( RelationComponent.RELATION_LINEWIDTH * 2.0f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND, RelationComponent.RELATION_MITRE_TRIM, new float[] { RelationComponent.RELATION_DOTSIZE, RelationComponent.RELATION_DOTSIZE }, 0); private static final Stroke ONE_MANY_DASHED = new BasicStroke( RelationComponent.RELATION_LINEWIDTH, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND, RelationComponent.RELATION_MITRE_TRIM, new float[] { RelationComponent.RELATION_DASHSIZE, RelationComponent.RELATION_DASHSIZE }, 0); private static final Stroke ONE_ONE_DASHED = new BasicStroke( RelationComponent.RELATION_LINEWIDTH * 2.0f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND, RelationComponent.RELATION_MITRE_TRIM, new float[] { RelationComponent.RELATION_DASHSIZE, RelationComponent.RELATION_DASHSIZE }, 0); private static final Stroke ONE_MANY_DOTTED_DASHED = new BasicStroke( RelationComponent.RELATION_LINEWIDTH, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND, RelationComponent.RELATION_MITRE_TRIM, new float[] { RelationComponent.RELATION_DASHSIZE, RelationComponent.RELATION_DOTSIZE, RelationComponent.RELATION_DOTSIZE, RelationComponent.RELATION_DOTSIZE }, 0); private static final Stroke ONE_ONE_DOTTED_DASHED = new BasicStroke( RelationComponent.RELATION_LINEWIDTH * 2.0f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND, RelationComponent.RELATION_MITRE_TRIM, new float[] { RelationComponent.RELATION_DASHSIZE, RelationComponent.RELATION_DOTSIZE, RelationComponent.RELATION_DOTSIZE, RelationComponent.RELATION_DOTSIZE }, 0); private static final Stroke OUTLINE = new BasicStroke(); private static final long serialVersionUID = 1; /** * Constant referring to modified relation colour. */ public static Color MODIFIED_COLOUR = Color.BLUE; /** * Constant referring to handmade relation colour. */ public static Color HANDMADE_COLOUR = Color.GREEN; /** * Constant referring to incorrect relation colour. */ public static Color INCORRECT_COLOUR = Color.RED; /** * Constant referring to masked relation colour. */ public static Color MASKED_COLOUR = Color.RED; /** * Constant referring to normal relation colour. */ public static Color NORMAL_COLOUR = Color.DARK_GRAY; /** * Constant referring to subclassed relation colour. */ public static Color SUBCLASS_COLOUR = Color.RED; /** * Constant referring to unrolled relation colour. */ public static Color UNROLLED_COLOUR = Color.CYAN; private boolean restricted = false; private boolean compounded = false; private boolean loopback = false; private Diagram diagram; private Shape lineShape; private Shape outline; private Relation object; private RenderingHints renderHints; private Object state; private Stroke stroke; private final PropertyChangeListener repaintListener = new PropertyChangeListener() { public void propertyChange(final PropertyChangeEvent e) { RelationComponent.this.needsRepaint = true; } }; /** * The constructor constructs a component around a given relation, and * associates the component with the given diagram. * * @param relation * the relation to show in the component. * @param diagram * the diagram to show this component in. */ public RelationComponent(final Relation relation, final Diagram diagram) { super(); // Remember settings. this.object = relation; this.diagram = diagram; // Turn on the mouse. this.enableEvents(AWTEvent.MOUSE_EVENT_MASK); this.setDoubleBuffered(true); // Stop flicker. // Make sure we're transparent. this.setOpaque(false); // Set-up rendering hints. this.renderHints = new RenderingHints(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); this.renderHints.put(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); // Draw our contents, as we don't have any child classes that // do this for us unfortunately. this.recalculateDiagramComponent(); Transaction.addTransactionListener(this); // Repaint events. relation.addPropertyChangeListener("directModified", this.repaintListener); relation.getFirstKey().addPropertyChangeListener("status", this.repaintListener); relation.getFirstKey().getTable().addPropertyChangeListener("masked", this.repaintListener); relation.getFirstKey().getTable().addPropertyChangeListener( "dimensionMasked", this.repaintListener); relation.getSecondKey().addPropertyChangeListener("status", this.repaintListener); relation.getSecondKey().getTable().addPropertyChangeListener("masked", this.repaintListener); relation.getSecondKey().getTable().addPropertyChangeListener( "dimensionMasked", this.repaintListener); this.changed = this.getObject().isVisibleModified(); } public void setDirectModified(final boolean modified) { // Ignore, for now. } public boolean isDirectModified() { return false; } public void setVisibleModified(final boolean modified) { // Ignore, for now. } public boolean isVisibleModified() { return false; } public void transactionResetDirectModified() { // Ignore, for now. } public void transactionResetVisibleModified() { // Ignore, for now. } public void transactionStarted(final TransactionEvent evt) { // Ignore, for now. } public void transactionEnded(final TransactionEvent evt) { final boolean visMod = this.getObject().isVisibleModified() && this.getDiagram().getMartTab().getPartitionViewSelection() == null; this.needsRepaint |= this.changed ^ visMod; this.changed = visMod; if (this.needsRecalc) this.recalculateDiagramComponent(); else if (this.needsRepaint) this.repaintDiagramComponent(); this.needsRecalc = false; this.needsRepaint = false; } protected void paintComponent(final Graphics g) { final Graphics2D g2d = (Graphics2D) g; g2d.setRenderingHints(this.renderHints); if (this.changed) { g2d.setColor(DiagramComponent.GLOW_COLOUR); g2d.setStroke(new BasicStroke(DiagramComponent.GLOW_WIDTH)); g2d.draw(this.outline); } g2d.setColor(this.getForeground()); g2d.setStroke(this.stroke); g2d.draw(this.loopback ? this.outline : this.lineShape); } protected void processMouseEvent(final MouseEvent evt) { boolean eventProcessed = false; if (evt.getButton() != 0) this.getDiagram().deselectAll(); // Is it a right-click? if (evt.isPopupTrigger()) { // Build the basic menu. final JPopupMenu contextMenu = this.getContextMenu(); // Customise it using the diagram context. if (this.getDiagram().getDiagramContext() != null) this.getDiagram().getDiagramContext().populateContextMenu( contextMenu, this.getObject()); // Display. if (contextMenu.getComponentCount() > 0) contextMenu.show(this, evt.getX(), evt.getY()); // We have successfully handled the event. eventProcessed = true; } // Pass the event on up if we're not interested. if (!eventProcessed) super.processMouseEvent(evt); } public boolean contains(final int x, final int y) { // Clicks are on us if they are within a certain distance // of the outline shape. return this.outline != null && this.outline.intersects(new Rectangle2D.Double(x - RelationComponent.RELATION_LINEWIDTH * 2, y - RelationComponent.RELATION_LINEWIDTH * 2, RelationComponent.RELATION_LINEWIDTH * 4, RelationComponent.RELATION_LINEWIDTH * 4)); } public JPopupMenu getContextMenu() { final JPopupMenu contextMenu = new JPopupMenu(); // No additional entries for us yet. // Return it. return contextMenu; } public JPopupMenu getMultiContextMenu() { final JPopupMenu contextMenu = new JPopupMenu(); // No additional entries for us yet. // Return it. return contextMenu; } public Diagram getDiagram() { return this.diagram; } /** * Returns the diagram component representing the first key of this * relation. * * @return the diagram component for the first key. */ public KeyComponent getFirstKeyComponent() { return (KeyComponent) this.diagram.getDiagramComponent(((Relation)this.getObject()) .getFirstKey()); } public TransactionListener getObject() { return this.object; } /** * Returns the diagram component representing the second key of this * relation. * * @return the diagram component for the second key. */ public KeyComponent getSecondKeyComponent() { return (KeyComponent) this.diagram.getDiagramComponent(((Relation)this.getObject()) .getSecondKey()); } public Object getState() { return this.state; } public BeanMap getSubComponents() { // We have no sub-components. return new BeanMap(Collections.EMPTY_MAP); } public void recalculateDiagramComponent() { // Nothing to do here. } public void repaintDiagramComponent() { this.updateAppearance(); this.repaint(); } /** * Sets the shape for us to display the outline of. This will usually be a * line, however it's up to the layout manager entirely. * * @param shape * the shape this relation should take on screen. */ public void setLineShape(final Shape shape) { // Only change if the shape has changed. if (this.lineShape != shape || this.lineShape != null && !this.lineShape.equals(shape)) { this.lineShape = shape; // Update the outline of the relation shape accordingly. if (this.lineShape != null) this.outline = RelationComponent.OUTLINE .createStrokedShape(this.lineShape); } // Update our appearance. this.updateAppearance(); } public void setState(final Object state) { this.state = state; } /** * If this is set to <tt>true</tt> then the component will appear with a * dashed outline. Otherwise, it appears with a solid outline. * * @param restricted * <tt>true</tt> if the component is to appear with a dashed * outline. The default is <tt>false</tt>. */ public void setRestricted(final boolean restricted) { this.restricted = restricted; } /** * If this is set to <tt>true</tt> then the component will appear with a * dotted outline. Otherwise, it appears with a solid outline. * * @param compounded * <tt>true</tt> if the component is to appear with a dotted * outline. The default is <tt>false</tt>. */ public void setCompounded(final boolean compounded) { this.compounded = compounded; } /** * If this is set to <tt>true</tt> then the component will appear with a * double outline. Otherwise, it appears with a single outline. * * @param loopback * <tt>true</tt> if the component is to appear with a double * outline. The default is <tt>false</tt>. */ public void setLoopback(final boolean loopback) { this.loopback = loopback; } public void updateAppearance() { // Use the context to alter us first. final DiagramContext mod = this.getDiagram().getDiagramContext(); if (mod != null) if (this.getDiagram().isHideMasked() && mod.isMasked(this.getObject())) { this.setVisible(false); return; } else mod.customiseAppearance(this, this.getObject()); // Work out what style to draw the relation line. final Stroke oldStroke = this.stroke; if (((Relation)this.getObject()).isOneToOne()) this.stroke = this.restricted ? this.compounded ? RelationComponent.ONE_ONE_DOTTED_DASHED : RelationComponent.ONE_ONE_DASHED : this.compounded ? RelationComponent.ONE_ONE_DOTTED : RelationComponent.ONE_ONE; else this.stroke = this.restricted ? this.compounded ? RelationComponent.ONE_MANY_DOTTED_DASHED : RelationComponent.ONE_MANY_DASHED : this.compounded ? RelationComponent.ONE_MANY_DOTTED : RelationComponent.ONE_MANY; this.setVisible(true); // Force repaint of area if stroke changed. if (oldStroke != this.stroke) { this.revalidate(); this.repaint(this.getBounds()); } if (this.getObject() != null) this.setToolTipText(this.getObject().toString()); } }