/*
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.Stroke;
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import javax.swing.AbstractAction;
import javax.swing.BorderFactory;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;
import javax.swing.JTextField;
import javax.swing.KeyStroke;
import javax.swing.SwingUtilities;
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;
/**
* Any diagram component that is box-shaped is derived from this class. It
* handles all mouse-clicks and painting problems for them, and keeps track of
* their sub-components in a map, so that code can reference them by database
* object rather than exact component.
* <p>
* This class also handles click-and-rename capabilities by allowing subclasses
* to specify a name label. It then calls those classes back when the name is
* changed by the user.
*
* @author Richard Holland <holland@ebi.ac.uk>
* @version $Revision: 1.35 $, $Date: 2007-10-31 10:32:56 $, modified by
* $Author: rh4 $
* @since 0.5
*/
public abstract class BoxShapedComponent extends JPanel implements
DiagramComponent, TransactionListener {
/**
* Subclasses use this if the component needs recalculating.
*/
protected boolean needsRecalc = false;
/**
* Subclasses use this if the component needs repainting.
*/
protected boolean needsRepaint = false;
private boolean changed = false;
private static final float BOX_DASHSIZE = 6.0f; // 72 = 1 inch
private static final float BOX_DOTSIZE = 2.0f; // 72 = 1 inch
private static final float BOX_LINEWIDTH = 1.0f; // 72 = 1 inch
private static final float BOX_MITRE_TRIM = 10.0f; // 72 = 1 inch
private static final Stroke DOTTED_DASHED_OUTLINE = new BasicStroke(
BoxShapedComponent.BOX_LINEWIDTH, BasicStroke.CAP_ROUND,
BasicStroke.JOIN_ROUND, BoxShapedComponent.BOX_MITRE_TRIM,
new float[] { BoxShapedComponent.BOX_DASHSIZE,
BoxShapedComponent.BOX_DOTSIZE,
BoxShapedComponent.BOX_DOTSIZE,
BoxShapedComponent.BOX_DOTSIZE }, 0);
private static final Stroke DASHED_OUTLINE = new BasicStroke(
BoxShapedComponent.BOX_LINEWIDTH, BasicStroke.CAP_ROUND,
BasicStroke.JOIN_ROUND, BoxShapedComponent.BOX_MITRE_TRIM,
new float[] { BoxShapedComponent.BOX_DASHSIZE,
BoxShapedComponent.BOX_DASHSIZE }, 0);
private static final Stroke DOTTED_OUTLINE = new BasicStroke(
BoxShapedComponent.BOX_LINEWIDTH, BasicStroke.CAP_ROUND,
BasicStroke.JOIN_ROUND, BoxShapedComponent.BOX_MITRE_TRIM,
new float[] { BoxShapedComponent.BOX_DOTSIZE,
BoxShapedComponent.BOX_DOTSIZE }, 0);
private static final Stroke INDEXED_OUTLINE = new BasicStroke(
BoxShapedComponent.BOX_LINEWIDTH, BasicStroke.CAP_ROUND,
BasicStroke.JOIN_ROUND, BoxShapedComponent.BOX_MITRE_TRIM,
new float[] { BoxShapedComponent.BOX_DASHSIZE,
BoxShapedComponent.BOX_DOTSIZE,
BoxShapedComponent.BOX_DOTSIZE,
BoxShapedComponent.BOX_DOTSIZE,
BoxShapedComponent.BOX_DOTSIZE,
BoxShapedComponent.BOX_DOTSIZE,
BoxShapedComponent.BOX_DOTSIZE,
BoxShapedComponent.BOX_DOTSIZE }, 0);
private static final Stroke OUTLINE = new BasicStroke(
BoxShapedComponent.BOX_LINEWIDTH, BasicStroke.CAP_ROUND,
BasicStroke.JOIN_ROUND, BoxShapedComponent.BOX_MITRE_TRIM);
private static final Color SELECTED_COLOUR = Color.WHITE;
private static final Color RENAMING_BORDER_COLOUR = Color.BLACK;
private Diagram diagram;
private boolean indexed = false;
private boolean restricted = false;
private boolean compounded = false;
private boolean draggable = false;
private boolean selectable = false;
private boolean renameable = false;
private boolean selected = false;
private boolean beingRenamed = false;
private JTextField name;
private TransactionListener object;
private Object state;
private Stroke stroke;
private RenderingHints renderHints;
// OK to use map, as the components are recreated, not changed.
private final BeanMap subComponents = new BeanMap(new HashMap());
/**
* Constructs a box-shaped component around the given database object to be
* represented in the given diagram.
*
* @param object
* the database object to represent.
* @param diagram
* the diagram to display ourselves in.
*/
public BoxShapedComponent(final TransactionListener object,
final Diagram diagram) {
super();
// Remember settings.
this.object = object;
this.diagram = diagram;
// Turn on the mouse.
this.enableEvents(AWTEvent.MOUSE_EVENT_MASK);
this.setDoubleBuffered(true); // Stop flicker.
// Make sure we're not transparent.
this.setOpaque(true);
// 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);
Transaction.addTransactionListener(this);
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 transactionResetVisibleModified() {
// Ignore, for now.
}
public void transactionResetDirectModified() {
// 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.getDiagram().isNeedsRecalc())
this.recalculateDiagramComponent();
else if (this.needsRepaint && !this.getDiagram().isNeedsRepaint())
this.repaintDiagramComponent();
this.needsRecalc = false;
this.needsRepaint = false;
}
/**
* Adds a sub-component to the map, but not to the diagram. This means that
* the component will take care of rendering these sub-components within its
* own bounds, but it is possibly to directly query the diagram to find out
* exactly how that sub-component has been rendered.
*
* @param object
* the model object the component represents.
* @param component
* the component representing the model object.
*/
protected void addSubComponent(final Object object,
final DiagramComponent component) {
this.subComponents.put(object, component);
}
protected void paintBorder(final Graphics g) {
final Graphics2D g2d = (Graphics2D) g;
// Override the stroke so that we get dotted outlines when appropriate.
if (this.stroke != null)
g2d.setStroke(this.stroke);
super.paintBorder(g2d);
if (this.changed) {
this.getBorder().paintBorder(this, g,
DiagramComponent.GLOW_WIDTH / 2,
DiagramComponent.GLOW_WIDTH / 2,
this.getWidth() - DiagramComponent.GLOW_WIDTH,
this.getHeight() - DiagramComponent.GLOW_WIDTH);
this.getBorder().paintBorder(this, g, DiagramComponent.GLOW_WIDTH,
DiagramComponent.GLOW_WIDTH,
this.getWidth() - DiagramComponent.GLOW_WIDTH*2,
this.getHeight() - DiagramComponent.GLOW_WIDTH*2);
}
}
protected void paintComponent(final Graphics g) {
final Graphics2D g2d = (Graphics2D) g;
g2d.setRenderingHints(this.renderHints);
super.paintComponent(g);
}
protected void processMouseEvent(final MouseEvent evt) {
boolean eventProcessed = false;
// Is it a right-click?
if (evt.isPopupTrigger()) {
// Build the basic menu.
// If right-click on selected item, then
// get multi menu and return that instead.
final JPopupMenu contextMenu;
if (this.getDiagram().isSelected(this)) {
contextMenu = this.getMultiContextMenu();
// Customise the context menu for this box's database object.
if (this.getDiagram().getDiagramContext() != null) {
final Set selectedItems = new HashSet();
for (final Iterator i = this.getDiagram()
.getSelectedItems().iterator(); i.hasNext();)
selectedItems.add(((BoxShapedComponent) i.next())
.getObject());
this.getDiagram().getDiagramContext()
.populateMultiContextMenu(contextMenu,
selectedItems, this.getObject().getClass());
}
} else {
this.getDiagram().deselectAll();
contextMenu = this.getContextMenu();
// Customise the context menu for this box's database object.
if (this.getDiagram().getDiagramContext() != null)
this.getDiagram().getDiagramContext().populateContextMenu(
contextMenu, this.getObject());
}
// Display.
if (contextMenu.getComponentCount() > 0) {
eventProcessed = true;
contextMenu.show(this, evt.getX(), evt.getY());
}
} else if (evt.getButton() == MouseEvent.BUTTON1)
if (this.isSelectable() && evt.getClickCount() == 1) {
if (!this.beingRenamed) {
eventProcessed = true;
if (evt.isControlDown() || evt.isMetaDown())
this.getDiagram().toggleGroupItem(this);
else if (evt.isShiftDown()) {
final List currentlySelected = new ArrayList(this
.getDiagram().getSelectedItems());
if (currentlySelected.size() > 0) {
final BoxShapedComponent previousSelection = (BoxShapedComponent) currentlySelected
.get(currentlySelected.size() - 1);
final Class clazz = previousSelection.getObject()
.getClass();
final Collection selection = this.getDiagram()
.getComponentsInRegion(previousSelection,
this, clazz);
selection.remove(previousSelection);
this.getDiagram().toggleGroupItems(selection);
} else
this.getDiagram().toggleGroupItem(this);
} else
this.getDiagram().toggleItem(this);
}
} else if (this.isRenameable() && !this.beingRenamed
&& evt.getClickCount() > 1) {
eventProcessed = true;
this.getDiagram().selectOnlyItem(this);
this.startRename();
}
// Pass it on up if we're not interested.
if (!eventProcessed)
super.processMouseEvent(evt);
else
evt.consume();
}
public JPopupMenu getContextMenu() {
final JPopupMenu contextMenu = new JPopupMenu();
// No additional entries for us yet.
return contextMenu;
}
public JPopupMenu getMultiContextMenu() {
final JPopupMenu contextMenu = new JPopupMenu();
// No additional entries for us yet.
return contextMenu;
}
public Diagram getDiagram() {
return this.diagram;
}
public TransactionListener getObject() {
return this.object;
}
public Object getState() {
return this.state;
}
public BeanMap getSubComponents() {
return this.subComponents;
}
public void recalculateDiagramComponent() {
// Remove everything.
this.removeAll();
final Object state = this.getState();
this.doRecalculateDiagramComponent();
this.diagram.needsSubComps = !this.getSubComponents().isEmpty();
if (state != null)
this.setState(state);
// Update and paint.
this.revalidate();
this.repaintDiagramComponent();
}
/**
* This method actually does the work.
*/
protected abstract void doRecalculateDiagramComponent();
public void repaintDiagramComponent() {
this.updateAppearance();
}
/**
* 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
* thick outline. Otherwise, it appears with a normal outline. This
* overrides compounded and restricted settings.
*
* @param indexed
* <tt>true</tt> if the component is to appear with a thick
* outline. The default is <tt>false</tt>.
*/
public void setIndexed(final boolean indexed) {
this.indexed = indexed;
}
public void setState(final Object state) {
this.state = state;
}
/**
* Can the user rename this by double-clicking on it?
*
* @return <tt>true</tt> if they can.
*/
public boolean isRenameable() {
return this.renameable && this.name != null;
}
/**
* Can the user rename this by double-clicking on it?
*
* @param renameable
* <tt>true</tt> if they can.
*/
public void setRenameable(final boolean renameable) {
this.renameable = renameable;
}
/**
* Can the user select this by single-clicking on it?
*
* @return <tt>true</tt> if they can.
*/
public boolean isSelectable() {
return this.selectable && this.name != null;
}
/**
* Can the user select this by single-clicking on it?
*
* @param selectable
* <tt>true</tt> if they can.
*/
public void setSelectable(final boolean selectable) {
this.selectable = selectable;
}
/**
* Can the user drag this?
*
* @return <tt>true</tt> if they can.
*/
public boolean isDraggable() {
return this.draggable;
}
/**
* Can the user drag this?
*
* @param draggable
* <tt>true</tt> if they can.
*/
public void setDraggable(final boolean draggable) {
this.draggable = draggable;
}
/**
* Call this method when the component has become selected.
*/
public void select() {
this.selected = true;
this.cancelRename();
}
/**
* Call this method when the component has become deselected.
*/
public void deselect() {
this.selected = false;
this.cancelRename();
}
/**
* Sets the text field that will represent the selectable/renameable portion
* of this component. It will gain a mouse listener which will allow the
* user to single-click (select/highlight) and double-click (rename) the
* field. It also gains a keyboard listener to listen for Enter+Escape hits
* whilst renaming.
*
* @param name
* the text field to use.
*/
public void setRenameTextField(final JTextField name) {
this.name = name;
this.name.setDisabledTextColor(this.name.getForeground());
this.name.setBackground(BoxShapedComponent.SELECTED_COLOUR);
this.name.getInputMap().put(
KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), "enterPressed");
this.name.getActionMap().put("enterPressed", new AbstractAction() {
private static final long serialVersionUID = 1L;
public void actionPerformed(final ActionEvent e) {
if (BoxShapedComponent.this.isBeingRenamed())
BoxShapedComponent.this.doRename();
}
});
this.name.getInputMap().put(
KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), "escapePressed");
this.name.getActionMap().put("escapePressed", new AbstractAction() {
private static final long serialVersionUID = 1L;
public void actionPerformed(final ActionEvent e) {
if (BoxShapedComponent.this.isBeingRenamed())
BoxShapedComponent.this.cancelRename();
}
});
this.name.addMouseListener(new MouseListener() {
public void mouseClicked(final MouseEvent e) {
BoxShapedComponent.this.processEvent(e);
}
public void mouseEntered(final MouseEvent e) {
BoxShapedComponent.this.processEvent(e);
}
public void mouseExited(final MouseEvent e) {
BoxShapedComponent.this.processEvent(e);
}
public void mousePressed(final MouseEvent e) {
BoxShapedComponent.this.processEvent(e);
}
public void mouseReleased(final MouseEvent e) {
BoxShapedComponent.this.processEvent(e);
}
});
this.deselect();
}
/**
* Call this method to make the component go into rename behaviour state.
*/
public void startRename() {
this.beingRenamed = true;
this.name.setText(this.getEditableName());
this.name.setBorder(BorderFactory
.createLineBorder(BoxShapedComponent.RENAMING_BORDER_COLOUR));
this.name.setEditable(true);
this.name.setEnabled(true);
this.name.setOpaque(true);
this.name.requestFocus();
}
/**
* Call this method to take the component out of rename behaviour state and
* revert to the state it was before, without accepting any changes the user
* may already have typed.
*/
public void cancelRename() {
this.beingRenamed = false;
this.name.setBorder(BorderFactory.createEmptyBorder());
this.name.setEditable(false);
this.name.setEnabled(false);
this.name.setOpaque(this.isSelected());
this.name.setText(this.getDisplayName());
}
/**
* Obtain the display name for this box-shaped object, to display above the
* box (as opposed to the editable name).
*
* @return the display name.
*/
public abstract String getDisplayName();
/**
* Returns the name the user can edit. This can be different from the name
* that is displayed at other times when the component is not being renamed.
*
* @return the editable name.
*/
public String getEditableName() {
return this.getDisplayName();
}
private void doRename() {
this.performRename(this.name.getText());
this.cancelRename();
}
/**
* Override this method to be informed when the user has completed a rename
* and wishes to enforce the new name.
*
* @param newName
* the new name entered by the user.
*/
public void performRename(final String newName) {
}
/**
* Is this component currently in rename behaviour state?
*
* @return <tt>true</tt> if it is.
*/
public boolean isBeingRenamed() {
return this.beingRenamed;
}
/**
* Is this component currently in select behaviour state?
*
* @return <tt>true</tt> if it is.
*/
public boolean isSelected() {
return this.selected;
}
public void updateAppearance() {
final DiagramContext mod = this.getDiagram().getDiagramContext();
if (mod != null) {
if (mod.isMasked(this.getObject()))
if (this instanceof ColumnComponent) {
final TableComponent parent = (TableComponent) SwingUtilities
.getAncestorOfClass(TableComponent.class, this);
if (parent != null && parent.isHidingMaskedCols()) {
this.setVisible(false);
return;
}
} else if (this.getDiagram().isHideMasked()) {
this.setVisible(false);
return;
}
this.setVisible(true);
mod.customiseAppearance(this, this.getObject());
}
if (this.indexed)
this.stroke = BoxShapedComponent.INDEXED_OUTLINE;
else if (this.restricted)
this.stroke = this.compounded ? BoxShapedComponent.DOTTED_DASHED_OUTLINE
: BoxShapedComponent.DASHED_OUTLINE;
else
this.stroke = this.compounded ? BoxShapedComponent.DOTTED_OUTLINE
: BoxShapedComponent.OUTLINE;
this.setBorder(BorderFactory.createLineBorder(
this.changed ? DiagramComponent.GLOW_COLOUR : this
.getForeground(), 1));
}
}