/*
* Copyright (c) 2007 BUSINESS OBJECTS SOFTWARE LIMITED
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* * Neither the name of Business Objects nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
/*
* ValueEditor.java
* Created: Jan 13, 2001
* By: Luke Evans
*/
package org.openquark.gems.client.valueentry;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Insets;
import java.awt.KeyboardFocusManager;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Vector;
import javax.swing.JComponent;
import javax.swing.JPanel;
import javax.swing.event.MouseInputAdapter;
import org.openquark.cal.compiler.TypeExpr;
import org.openquark.cal.valuenode.ValueNode;
import org.openquark.gems.client.utilities.SmoothHighlightBorder;
/**
* The abstract base class for all ValueEditors.
* Creation date: (1/13/2001 12:20:34 AM)
* @author Luke Evans
*/
public abstract class ValueEditor extends JPanel {
/** The parent ValueEditor associated with this ValueEditor. */
private ValueEditor parentValueEditor = null;
/** Every ValueEditor has a corresponding ValueNode for its data. */
private ValueNode valueNode = null;
/** the ValueNode that this ValueEditor is modifying */
private ValueNode ownerValueNode = null;
/** The manager for this value editor. */
protected final ValueEditorManager valueEditorManager;
/** The manager of the hierarchy that this valueEditor resides in */
protected final ValueEditorHierarchyManager valueEditorHierarchyManager;
/** The context for this ValueEditor. */
private ValueEditorContext context = null;
/** The List containing the ValueEditorListeners. */
private final List<ValueEditorListener> listenerList;
/** Indicates whether this ValueEditor and its children are editable. */
private boolean editable = true;
/** Indicates whether this editor is resizable. */
private boolean resizable = false;
/** Indicates whether this editor is moveable. */
private boolean moveable = false;
/** The maximum size to which this editor may be resized. */
private Dimension maxResizeDimension = null;
/** The minimum size to which this editor may be resized. */
private Dimension minResizeDimension = null;
/**
* This is a bit of a workaround to the problem that when a component is "removed" from a container
* and it has focus, an attempt is made to move focus to the next focusable component in the
* container's focus cycle. The fix involves setting a focus policy for the TableTop that will
* pretend the is no "next focusable component" when it sees that the component being removed
* belongs to a Value Editor that is closing.
*
* A flag to indicate if this editor is in the process of closing.
*/
private boolean editorIsClosing = false;
/**
* KeyListener used to listen for user's commit (Enter) and cancel (Esc) input.
* Add it to components of ValueEditors when you want the above functionality.
*/
public class ValueEditorKeyListener extends KeyAdapter {
@Override
public void keyPressed(KeyEvent evt) {
if (evt.getKeyCode() == KeyEvent.VK_ENTER) {
handleCommitGesture();
evt.consume(); // Don't want the control with the focus to perform its action.
} else if (evt.getKeyCode() == KeyEvent.VK_ESCAPE) {
handleCancelGesture();
evt.consume();
}
}
}
/**
* Contains info regarding the ValueEditors associated with a particular ValueNode.
* Currently, it has the size data of the ValueEditors using a particular ValueNode.
*/
public static class Info {
private final Dimension editorSize;
private final List<Integer> componentWidths;
/**
* Constructor for a ValueEditor.Info with size only.
* @param size the size of the ValueEditor.
*/
public Info(Dimension size) {
this(size, null);
}
/**
* Constructor for a ValueEditor.Info with size and component widths.
* @param editorSize the size of the ValueEditor.
* @param componentWidths the widths of components of the value editor, or null if none.
*/
public Info(Dimension editorSize, List<Integer> componentWidths) {
this.editorSize = new Dimension(editorSize);
this.componentWidths = componentWidths == null ? null : new ArrayList<Integer>(componentWidths);
}
/**
* @return the size of the ValueEditor.
*/
public Dimension getEditorSize() {
return new Dimension(editorSize);
}
/**
* @return the widths of components of the value editor, or null if none.
*/
public List<Integer> getComponentWidths() {
return componentWidths == null ? null : Collections.unmodifiableList(componentWidths);
}
}
/**
* Creates a new ValueEditor.
* @param valueEditorHierarchyManager
*/
protected ValueEditor(ValueEditorHierarchyManager valueEditorHierarchyManager) {
if (valueEditorHierarchyManager == null) {
throw new NullPointerException("valueEditorHierarchyManager must not be null.");
}
this.valueEditorHierarchyManager = valueEditorHierarchyManager;
this.valueEditorManager = valueEditorHierarchyManager.getValueEditorManager();
// Use a synchronized list.
listenerList = new Vector<ValueEditorListener>();
if (valueEditorManager.useTypeColour()) {
setOpaque(false);
setBackground(getBackground());
}
setBorder(valueEditorManager.getValueEditorBorder(this));
MouseInputAdapter resizeListener = new ValueEditorResizeMouseListener(this);
addMouseListener(resizeListener);
addMouseMotionListener(resizeListener);
}
/**
* Get the component which by default has focus.
* This will be called, for instance, when the editor is activated
* @return Component the default component to receive focus, or null if none.
*/
public abstract Component getDefaultFocusComponent();
/**
* Notify the value editor that it has been activated.
* Some value editors may want to perform some setup, such as enabling/disabling buttons.
* This function is called if the editor is activated - either as a result of the hierarchy collapsing
* to it or because the user explicitly clicks on it.
*/
public void editorActivated() {
}
/**
* Commit the value node currently under edit in this editor.
*/
protected void commitValue() {
// default: do nothing (valuenode is already set).
notifyValueCommitted();
}
/**
* Cancel the edit of the value node currently under edit in this editor.
*/
protected void cancelValue() {
// default: do nothing.
notifyValueCanceled();
}
/**
* Notify the hierarchy manager that this editor has received a "commit" request.
* Examples include pressing the <ENTER> key or pressing an "OK" button.
* This method will handle closing the editor and committing the value as necessary.
*/
protected void handleCommitGesture() {
valueEditorHierarchyManager.handleCommitGesture(this);
}
/**
* Notify the hierarchy manager that this editor received a "cancel" request.
* Examples include pressing the <ESC> key or pressing an "Cancel" button.
* This method will handle closing the editor and canceling the value as necessary.
*/
protected void handleCancelGesture() {
valueEditorHierarchyManager.handleCancelGesture(this);
}
/**
* Makes a copy of the ValueNode in this ValueEditor to the clip board.
*/
public void copyToClipboard() {
valueEditorManager.copyToClipboard(getValueNode());
}
/**
* Makes a copy of the ValueNode in this ValueEditor to the clipboard.
* Also erases the value in this ValueEditor (values are defaulted).
*/
public void cutToClipboard() {
copyToClipboard();
ValueNode newVN = valueEditorManager.getValueNodeBuilderHelper().getValueNodeForTypeExpr(getValueNode().getTypeExpr());
setOwnerValueNode(newVN);
setInitialValue();
revalidate();
valueEditorHierarchyManager.activateCurrentEditor();
}
/**
* If there is a value in the clipboard and that value matches the data type
* in this ValueEditor, then sets the value in this ValueEditor to the value
* in the clipboard.
*/
public void pasteFromClipboard() {
ValueNode clipboardValueNode = valueEditorManager.getClipboardValue();
// First, need to place check for 'compatible' types.
// Also, if there's no clipboard value, then no point going on.
if ((clipboardValueNode == null) || !clipboardValueNode.getTypeExpr().sameType(getValueNode().getTypeExpr())) {
// Type incompatible. Do nothing.
return;
}
setOwnerValueNode(clipboardValueNode);
setInitialValue();
revalidate();
valueEditorHierarchyManager.activateCurrentEditor();
}
/**
* Returns the parent editor (the editor which spawned this editor) if any.
* Creation date: (03/14/02 12:25:53 PM)
* @return ValueEditor the parent value editor, or null if none.
*/
public final ValueEditor getParentValueEditor() {
return parentValueEditor;
}
/**
* Returns the ValueNode for this ValueEditor.
* @return ValueNode
*/
public final ValueNode getValueNode() {
return valueNode;
}
/**
* Checks whether or not this ValueEditor (or its parts [Eg: textfield, button]) has focus.
* @return boolean True if this ValueEditor(or its parts) has focus. False otherwise.
*/
public boolean hasOverallFocus() {
// Traverse up the component hierarchy from the current focus owner.
// Note that we can't simply stop at the first ValueEditor, since a value editor
// may be composed of other value editors.
for (Component focusOwner = KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusOwner();
focusOwner != null;
focusOwner = focusOwner.getParent()) {
if (focusOwner == ValueEditor.this) {
return true;
}
}
return false;
}
/**
* Returns whether this editor or any of its childrens holds the focus.
* @return boolean
*/
public boolean childrenHasFocus() {
ValueEditor child = valueEditorHierarchyManager.getChildEditor(this);
if (hasOverallFocus()) {
return true;
}
if (child != null) {
return child.childrenHasFocus();
}
return false;
}
/**
* Returns True if this editor is in the process of closing and False otherwise.
* @return boolean
*/
public boolean isEditorClosing() {
return editorIsClosing;
}
/**
* Notify this editor that it is about to be closed. This function simply sets the
* editorIsClosing member variable to the specified boolean value.
* TODO: reduce scope.
* todoFW: remove this -- there is a better way (and I've found it) !
* @param isClosing boolean should be True if the editor is closing and False otherwise.
*/
public void setEditorIsClosing(boolean isClosing) {
editorIsClosing = isClosing;
}
/**
* Refreshes the ValueEditor to display the latest data.
* Note: The default implementation is to do nothing.
* All subclasses that need this will have to override this method.
* This method is called if the editor that is in the hierarchy below this editor is closed
* and this editor in turn becomes the current editor. It is also called when an editor that was
* previously not showing on screen is now shown on screen.
*/
public void refreshDisplay() {
}
/**
* Replaces the current valueNode with newValueNode.
* Notice you are replacing the current working copy of the valueNode, not the ownerValueNode
* @param newValueNode
* @param preserveInfo
*/
public void replaceValueNode(ValueNode newValueNode, boolean preserveInfo) {
if (preserveInfo) {
// Put in new entry (null info preserves nothing..).
Info info = valueEditorManager.getInfo(this.valueNode);
valueEditorManager.associateInfo(newValueNode, info);
}
this.valueNode = newValueNode;
}
/**
* Change the value represented by this value editor.
* This value editor must be of the correct type to edit the new value.
* @param newValue the new owner value node to be represented.
*/
public final void changeOwnerValue(ValueNode newValue) {
// Now set the data as appropriate
setOwnerValueNode(newValue);
setInitialValue();
revalidate();
}
/**
* Sets the parentValueEditor for this ValueEditor.
* Creation date: (03/14/2002 12:52:45 PM)
* @param newParentValueEditor ValueEditor The new parent of this ValueEditor.
*/
final public void setParentValueEditor(ValueEditor newParentValueEditor) {
parentValueEditor = newParentValueEditor;
}
/**
* Call this method to make the ValueEditor initialize its values.
* Note: Usually call this method after adding the ValueEditor to the display, setting its ValueNode,
* and correctly updating the currentEditor and the currentEditorStack, since some
* implementations of this method will require that pre-condition.
*/
abstract public void setInitialValue();
/**
* Sets the ownerValueNode for this ValueEditor.
* Also initializes the background/border colour of this ValueEditor.
* @param newValueNode
*/
public void setOwnerValueNode(ValueNode newValueNode) {
this.ownerValueNode = newValueNode;
this.valueNode = newValueNode.copyValueNode();
if (getBorder() instanceof SmoothHighlightBorder) {
Color typeColor = valueEditorManager.getTypeColour(valueNode.getTypeExpr());
setBorder(new SmoothHighlightBorder(typeColor, moveable));
}
}
/**
* Get the context for this value editor.
* @return ValueEditorContext
*/
public ValueEditorContext getContext() {
// If there's no context yet, just create an immutable context.
if (context == null) {
context = new ValueEditorContext() {
public TypeExpr getLeastConstrainedTypeExpr() {
return getValueNode().getTypeExpr();
}
};
}
return context;
}
/**
* Set the context for this ValueEditor.
* @param context
*/
public void setContext(ValueEditorContext context) {
this.context = context;
}
/**
* Set the editable state for the ValueEditor.
* Normally a value editor is always editable. However, in some
* cases we may want a non-editable value editor. For example:
* A ValueEntryPanel containing a large a textual result would be too
* small to show all the text and there is no provision for scrolling.
* The user can call up the StringValueEditor, which does scroll, in
* order to more easily read all the text. But we don't want them
* trying to edit the result.
*/
public void setEditable(boolean b) {
editable = b;
}
/**
* Return whether this ValueEditor is editable, based on the editable flags for this Value Editor and
* its ancestors (if any).
* @return boolean whether or not this ValueEditor is editable.
*/
public boolean isEditable() {
// Not switchable if this or any ancestors are ValueEditors which aren't editable
for (ValueEditor editor = this; editor != null; editor = editor.getParentValueEditor()) {
if (!editor.editable) {
return false;
}
}
return true;
}
/**
* Overide the setSize() method so that in addition to setting the editor size it will also
* make sure the editor doesn't resize beyond the bounds of its parent.
* @param width the new width of the editor
* @param height the new height of the editor
*/
@Override
public void setSize(int width, int height) {
Component parent = getParent();
if (parent != null && parent.getWidth() > 0 && parent.getHeight() > 0) {
Point location = getLocation();
Rectangle parentRect = new Rectangle(parent.getWidth(), parent.getHeight());
int x = getX();
int y = getY();
if (parent instanceof JComponent) {
parentRect = ((JComponent) parent).getVisibleRect();
}
if (location.x + width > parentRect.x + parentRect.width) {
// Check if we can move editor to the left.
int diff = location.x + width - parentRect.x - parentRect.width;
x = getX() - diff;
if (x < parentRect.x) {
width -= diff - parentRect.x + x;
x = parentRect.x;
}
}
if (location.y + height > parentRect.y + parentRect.height) {
// Check if we can move editor up.
int diff = location.y + height - parentRect.y - parentRect.height;
y = getY() - diff;
if (y < parentRect.y) {
height -= diff - parentRect.y + y;
y = parentRect.y;
}
}
setLocation(x, y);
}
super.setSize(width, height);
}
/**
* @see #setSize(int, int)
*/
@Override
public void setSize(Dimension d) {
setSize(d.width, d.height);
}
/**
* Overrides setLocation to ensure that the editor is completely within the bounds of its parent.
* If the new location places it outside parent bounds, then the location will be adjusted.
* @param x the new x location
* @param y the new y location
*/
@Override
public void setLocation(int x, int y) {
Component parent = getParent();
if (parent != null && parent.getWidth() > 0 && parent.getHeight() > 0) {
Rectangle parentRect = new Rectangle(parent.getWidth(), parent.getHeight());
x = ValueEditorManager.clamp(parentRect.x, x, parentRect.x + parentRect.width - getWidth());
y = ValueEditorManager.clamp(parentRect.y, y, parentRect.y + parentRect.height - getHeight());
}
super.setLocation(x, y);
}
/**
* @see #setLocation(int, int)
*/
@Override
public void setLocation(Point p) {
setLocation(p.x, p.y);
}
/**
* Get the hierarchy manager managing this value editor.
* @return ValueEditorHierarchyManager
*/
public ValueEditorHierarchyManager getValueEditorHierarchyManager() {
return valueEditorHierarchyManager;
}
/**
* Returns the ownerValueNode
* @return ValueNode
*/
public ValueNode getOwnerValueNode() {
return ownerValueNode;
}
/**
* Adds a listener to listen for data value changed events.
* Note: Currently, the def'n of a "Data value change" is if the old String value in the ValueNode differs
* with the new String value in the ValueNode.
* Note: Will do nothing if listener is null.
* @param listener
*/
public void addValueEditorListener(ValueEditorListener listener) {
if (listener != null) {
listenerList.add(listener);
}
}
/**
* Removes the listener from the list of listeners to be notified upon a ValueEditorEvent.
* @param listener
*/
public void removeValueEditorListener(ValueEditorListener listener) {
listenerList.remove(listener);
}
/**
* Notify listeners that the value of the edit in progress has been committed.
*/
protected void notifyValueCommitted() {
ValueNode oldValue = getOwnerValueNode();
for (int i = 0, listenerCount = listenerList.size(); i < listenerCount; i++) {
ValueEditorListener listener = listenerList.get(i);
listener.valueCommitted(new ValueEditorEvent(this, oldValue));
}
}
/**
* Notify listeners that the edit in progress has been canceled.
*/
protected void notifyValueCanceled() {
for (int i = 0, listenerCount = listenerList.size(); i < listenerCount; i++) {
ValueEditorListener listener = listenerList.get(i);
listener.valueCanceled(new ValueEditorEvent(this, null));
}
}
/**
* Notify listeners that the value of the edit in progress has changed.
*/
protected void notifyValueChanged(ValueNode oldValueNode) {
for (int i = 0, listenerCount = listenerList.size(); i < listenerCount; i++) {
ValueEditorListener listener = listenerList.get(i);
listener.valueChanged(new ValueEditorEvent(this, oldValueNode));
}
}
/**
* We override this to now draw in a background beneath the border, so that it
* can be partly transparent.
* @param g the graphics object to draw with
*/
@Override
public void paintComponent(Graphics g) {
Insets insets = getInsets();
g.setColor(getBackground());
g.fillRect(insets.left, insets.top, getWidth() - insets.left - insets.right, getHeight() - insets.top - insets.bottom);
}
/**
* Sets whether the editor is moveable. If it is moveable, then the border will be
* drawn with a little title bar (if the editor is using the default editor border).
* @param moveable whether or not this editor should be moveable
*/
public void setMoveable(boolean moveable) {
this.moveable = moveable;
if (getBorder() instanceof SmoothHighlightBorder) {
Color borderColor = getBackground();
if (valueNode != null) {
borderColor = valueEditorManager.getTypeColour(valueNode.getTypeExpr());
}
setBorder(new SmoothHighlightBorder(borderColor, moveable));
}
}
/**
* @return whether or not this editor is moveable
*/
public boolean isMoveable() {
return moveable;
}
/**
* @param resizable whether or not this editor should be resizable
*/
public void setResizable(boolean resizable) {
this.resizable = resizable;
}
/**
* @return whether or not this editor is resizable
*/
public boolean isResizable() {
return resizable;
}
/**
* This method is called if the user manually resizes the value editor.
* Subclasses can implement this to perform special actions they may require.
*/
protected void userHasResized() {
}
/**
* Sets the maximum size to which an editor may be resized. We use this instead
* of Swing's setMaximumSize so that we don't interfere with the managed layout
* of ValueEditors in the DataInspector.
* @param maxResizeDimension the maximum size to which the editor can be resized
*/
public void setMaxResizeDimension(Dimension maxResizeDimension) {
this.maxResizeDimension = maxResizeDimension;
}
/**
* Sets the minimum size to which an editor may be resized. We use this instead
* of Swing's setMinimumSize so that we don't interfere with the managed layout
* of ValueEditors in the DataInspector.
* @param minResizeDimension the minimum size to which the editor can be resized
*/
public void setMinResizeDimension(Dimension minResizeDimension) {
this.minResizeDimension = minResizeDimension;
}
/**
* @return the maximum size to which the editor can be resized
*/
protected Dimension getMaxResizeDimension() {
return maxResizeDimension;
}
/**
* @return the minimum size to which the editor can be resized
*/
protected Dimension getMinResizeDimension() {
return minResizeDimension;
}
}