/*
Copyright (c) 2003-2009 ITerative Consulting Pty Ltd. All Rights Reserved.
Redistribution and use in source and binary forms, with or without modification, are permitted
provided that the following conditions are met:
o Redistributions of source code must retain the above copyright notice, this list of conditions and
the following disclaimer.
o 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.
o This jcTOOL Helper Class software, whether in binary or source form may not be used within,
or to derive, any other product without the specific prior written permission of the copyright holder
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS "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.
*/
package DisplayProject.events;
import java.awt.Component;
import java.awt.Container;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.WeakHashMap;
import javax.swing.JComboBox;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JMenu;
import javax.swing.JPopupMenu;
import javax.swing.JScrollBar;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.SwingUtilities;
import javax.swing.table.JTableHeader;
import javax.swing.table.TableCellRenderer;
import javax.swing.table.TableColumn;
import javax.swing.table.TableColumnModel;
import DisplayProject.PaletteList;
import DisplayProject.UIutils;
import DisplayProject.controls.ArrayField;
import DisplayProject.controls.OutlineField;
import DisplayProject.table.ArrayFieldCellHelper;
import DisplayProject.table.ArrayFieldCellRenderer;
import DisplayProject.table.ArrayTableComponentCorrector;
import Framework.ErrorMgr;
import Framework.EventHandle;
import Framework.ParameterHolder;
/**
* This class is used by ChildXXX events (eg ChildMouseEnter) to post the events to the
* interested parents<p>
* <p>
* Child events tend to be rarely used in Forte, so this class just implements the obvious
* solution of posting the event to the parent, then the parent's parent and so on up the
* heirarchy. This is not the most efficient solution, which would be posting the events
* directly on the class that wants them, but then we would need to monitor which parents
* expect which events and also compensate for any changes in the heirarchy. <p>
* <p>
* This class also ensures that no child is registered for the same event multiple times,
* otherwise if 2 different parents ask for say ChildClick events then both parents would
* receive it twice<p>
*
* @author tfaulkes
*
*/
public abstract class ChildEventHelper {
private static Map<JComponent, Set<String>> knownChildEvents = new WeakHashMap<JComponent, Set<String>>();
/**
* This utility method fires an event from the parent of the passed component all the way up the hierarchy.
* It understands children within JTables and ensures the event is posted only to each parent widget,
* irrespective of whether the child is a renderer which is attached to the JTable or not.<p>
* <p>
* The child component will be added into the parameter table with the name of "child"
*
* @param pChild - The child component
* @param pEventName - The name of the child event
* @param pParams - Any parameters other than "child" that need to get posted. May be null
*/
public static void postEventToAllParents(Component pChild, String pEventName, Hashtable<String, Object> pParams) {
ArrayList<EventHandle> list = new ArrayList<EventHandle>();
postEventToAllParents(pChild, pEventName, pParams, list);
// Now actually post the events
for (EventHandle h : list) {
// TF:24/04/2008: Changed this to use the client event manager to get the right order for events
ClientEventManager.postEvent(h);
}
}
/**
* This utility method fires an event from the parent of the passed component all the way up the hierarchy.
* It understands children within JTables and ensures the event is posted only to each parent widget,
* irrespective of whether the child is a renderer which is attached to the JTable or not.<p>
* <p>
* The child component will be added into the parameter table with the name of "child"
*
* @param pChild - The child component
* @param pEventName - The name of the child event
* @param pParams - Any parameters other than "child" that need to get posted. May be null
* @param pList - A list in which to store the returned events. Cannot be null
*/
public static void postEventToAllParents(Component pChild, String pEventName, Hashtable<String, Object> pParams, List<EventHandle> pList) {
if (pList == null) {
NullPointerException errorVar = new NullPointerException("pList cannot be null");
ErrorMgr.addError(errorVar);
throw errorVar;
}
Container mum = pChild.getParent();
// TF:26/9/07:If we're posting based on a Table header, we need to post first on the array field then up the chain
if (pChild instanceof JTableHeader) {
mum = ((JTableHeader)pChild).getTable();
}
// TF:23/9/07: JTables also need to post the event on themselves, as they really do own the event
else if (pChild instanceof JTable) {
mum = (JTable)pChild;
}
else if (mum instanceof JPopupMenu) {
//CONV_REM:jm added this piece of logic because it blew when the mum parent was the scheduler window
// which is not an instance of Jcomponent
// TF:21/1/08:Removed this code because it prevented the popup menu ever getting the child
// popup event, which is important in some cases.
// Container mumInvoker = (Container)((JPopupMenu) mum).getInvoker();
// if(mumInvoker instanceof JComponent) {
// mum = (JComponent) mumInvoker;
// } else {
// mum = (Container) mumInvoker;
// }
}
Hashtable<String, Object> params = pParams;
if (params == null) {
params = new Hashtable<String, Object>();
}
params.put("child", new ParameterHolder(pChild));
// TF:27/9/07:Revamped this logic as our renderers are now in panels.
ArrayField owningArray = ArrayFieldCellHelper.getArrayField(pChild);
int row = 0;
int column = 0;
// Extract the X and Y co-ordinate from the event which may be in array coordinates
ParameterHolder pX = (ParameterHolder)params.get("x");
ParameterHolder pY = (ParameterHolder)params.get("y");
/*
* There are 2 cases when considering children events of array fields:
* 1) The component cell renderer/editor is attached to the array field as a child. This is typically when the
* editor is active. In this case the click we get is in the coordinate space of the child, but the row
* and column are not set properly. We can map the coordinate space of the child to the coordinate space
* of the array and then determine the row and column from there.
*
* 2) The component cell renderer/editor is not attached to the array field. In this case, the component will
* have a client property set which refers to the array and the location is in the coordinate space of the
* array. We can use this to determine the row and column and then map the coordinate down to the
* coordinate space of the child.
*/
while (mum != null){
if (mum == owningArray) {
if (pX != null && pY != null) {
// We are a direct child of the table, remap the coordinate space to the array to get the row and column
Point p = new Point(UIutils.milsToPixels(pX.getInt()), UIutils.milsToPixels(pY.getInt()));
Point arrayPoint = SwingUtilities.convertPoint(pChild, p, mum);
row = owningArray.rowAtPoint(arrayPoint);
column = owningArray.columnAtPoint(arrayPoint);
// Store the real row and column in the array. This isn't actually done in Forte, but makes life
// much easier if we want to determine which cell generated the click.
params.put( "row", new ParameterHolder(row+1) );
params.put( "column", new ParameterHolder(column+1) );
}
owningArray = null;
}
pList.add(new EventHandle(mum, pEventName, params));
// TF:18/06/2008:Fixed this so menus work properly
if (mum instanceof JPopupMenu)
mum = (Container)((JPopupMenu)mum).getInvoker();
else
mum = mum.getParent();
}
if (owningArray != null) {
// Now the X and Y coordinates are in the array space, we need to translate them to the child coordinate space and get
// the correct row and column
if (pX != null && pY != null) {
Point p = new Point(UIutils.milsToPixels(pX.getInt()), UIutils.milsToPixels(pY.getInt()));
row = owningArray.rowAtPoint(p);
column = owningArray.columnAtPoint(p);
// Store the real row and column in the array. This isn't actually done in Forte, but makes life
// much easier if we want to determine which cell generated the click.
params.put( "row", new ParameterHolder(row+1) );
params.put( "column", new ParameterHolder(column+1) );
Rectangle r = owningArray.getCellRect(row, column, false);
p.x -= r.x;
p.y -= r.y;
// Now X and Y are in owningArray coordinate space, need to translate them into pChild's
// Point childLoc = SwingUtilities.convertPoint(owningArray, p, pChild);
Point childLoc = p;
pX.setInt(UIutils.pixelsToMils(childLoc.x));
pY.setInt(UIutils.pixelsToMils(childLoc.y));
}
// Go through and post this to the parent of the array also.
mum = owningArray;
while (mum != null){
pList.add(new EventHandle(mum, pEventName, params));
mum = mum.getParent();
}
}
}
/**
* This utility method fires an event from the parent of the passed component all the way up the hierarchy.
* It understands children within JTables and ensures the event is posted only to each parent widget,
* irrespective of whether the child is a renderer which is attached to the JTable or not.<p>
* <p>
* The child component will be added into the parameter table with the name of "child"
*
*/
public static void postEventToAllParents(Component pChild, String pEventName) {
postEventToAllParents(pChild, pEventName, null);
}
private static void _installOnAllChildren(JFrame pRoot, String pEvent, ChildInstallableListener pInstaller) {
_installOnAllChildren((JComponent)pRoot.getContentPane(), (JComponent)pRoot.getContentPane(), pEvent, pInstaller);
}
private static void _installOnAllChildren(JComponent pRoot, JComponent pParent, String pEvent, ChildInstallableListener pInstaller) {
if (pParent == null) {
return;
}
/* PM:17/4/08
* OutlineFields don't post AfterValueChange
*/
if (pParent instanceof OutlineField){
return;
}
if (pParent instanceof ArrayField) {
ArrayField table = (ArrayField)pParent;
// TF:23/9/07:We sometimes need to post the event on the table as well. For example, consider a mouse double click
// over a non-editable cell in a table. Forte could process the double click and send it through to the event loop,
// but in java the renderer will not get the double click (it's not really there, after all) so the array will get
// it. Hence we need to listen for it on the array as well
// _installOnChild(pRoot, pParent, table, pEvent, pInstaller);
TableColumnModel cm = table.getColumnModel();
for (int c = 0; c < cm.getColumnCount(); c++){
TableColumn tc = cm.getColumn(c);
TableCellRenderer tcr = tc.getCellRenderer();
// Handle ArrayFieldCellRenderer. CraigM 30/08/2007
if (tcr != null && tcr instanceof ArrayFieldCellRenderer) {
tcr = ((ArrayFieldCellRenderer)tcr).getRenderer();
}
// TF:28/9/07:Removed this dependancy on the FormattedCellRenderer, not sure why it was there. Also changed to the correct column
if (tcr != null /*&& !(tcr instanceof FormattedCellRenderer)*/){
JComponent cr = (JComponent)tcr.getTableCellRendererComponent(table, null, false, false, 0, c);
// TF:28/9/07:Some of our "atomic" children are comprised of other children. For example, a radiolist is a JList
// with scrollbars, a drop list has a drop list
//if (cr.getComponentCount() == 0) {
_installOnChild(pRoot, table, cr, pEvent, pInstaller);
//}
//else {
_installOnAllChildren(pRoot, cr, pEvent, pInstaller);
//}
}
// CraigM:02/07/2008 - Switched code to use array cell helper
JComponent ce = (JComponent)ArrayFieldCellHelper.getArrayEditorComponent(table, 0, c);
if (ce != null) {
_installOnChild(pRoot, table, ce, pEvent, pInstaller);
_installOnAllChildren(pRoot, ce, pEvent, pInstaller);
if (ce instanceof ArrayTableComponentCorrector) {
JComponent field = ((ArrayTableComponentCorrector)ce).correctPostingComponent();
if (field != null ) {
_installOnChild(pRoot, table, (JComponent)field, pEvent, pInstaller);
}
}
}
}
return;
}
//PM:10/11/07 support for menus
else if (pParent instanceof JMenu){
JMenu menu = (JMenu)pParent;
for (int i = 0; i < menu.getMenuComponentCount(); i++) {
if (!(menu.getMenuComponent(i) instanceof JComponent))
continue;
JComponent aChild = (JComponent)menu.getMenuComponent(i);
ChildEventHelper._installOnChild(pRoot, pParent, aChild, pEvent, pInstaller);
}
}
else {
for (int i = 0; i < pParent.getComponentCount(); i++) {
if (!(pParent.getComponent(i) instanceof JComponent))
continue;
JComponent aChild = (JComponent)pParent.getComponent(i);
if (aChild instanceof JScrollBar) {
if (pParent instanceof JScrollPane) {
// Don't post event on a scrollPane's scroll bars, or we get an awful lot of spurious afterValueChange events
return;
}
}
ChildEventHelper._installOnChild(pRoot, pParent, aChild, pEvent, pInstaller);
}
}
}
private static void _installOnChild(JComponent pRoot, JComponent pParent, JComponent pChild, String pEvent, ChildInstallableListener pInstaller) {
// Ensure we're not already registered for this event
Set<String> knownEventsForWidget;
synchronized (knownChildEvents) {
knownEventsForWidget = knownChildEvents.get(pChild);
if (knownEventsForWidget == null) {
knownEventsForWidget = new HashSet<String>();
knownChildEvents.put(pChild, knownEventsForWidget);
}
}
if (!knownEventsForWidget.contains(pEvent)) {
pInstaller.installChildForParent(pChild, pParent, pEvent);
knownEventsForWidget.add(pEvent);
_installOnAllChildren(pRoot, pChild, pEvent, pInstaller);
}
else if (pInstaller instanceof ReinstallableListener) {
if (((ReinstallableListener)pInstaller).reinstall(pChild, pParent, pEvent)) {
_installOnAllChildren(pRoot, pChild, pEvent, pInstaller);
}
}
}
/**
* For every child that belongs to the passed parent, install the given event notification on it
* using the passed installer.
* @param pParent
* @param pEvent
* @param pInstaller
*/
public static void installOnAllChildren(JComponent pParent, String pEvent, ChildInstallableListener pInstaller) {
_installOnAllChildren(pParent, pParent, pEvent, pInstaller);
}
/**
* For every child that belongs to the Window, install the given event notification on it
* using the passed installer.
* @param pParent
* @param pEvent
* @param pInstaller
*/
public static void installOnAllChildren(JFrame pParent, String pEvent, ChildInstallableListener pInstaller) {
_installOnAllChildren(pParent, pEvent, pInstaller);
}
/**
* TF:28/9/07: JComboboxes are a pain because they're comprised of sub widgets and the mouse events etc
* do not propegate down to the children. See
* http://bugs.sun.com/bugdatabase/view_bug.do;jsessionid=4c4e8224ced82ffffffffaf3337c3360f51e?bug_id=4144505
* for more details. This method propegates the events down to the children of the control, primarily for
* use with comboboxes
*/
public static void installMouseEvents(JComponent comp, MouseListener listener) {
for (int i = comp.getComponentCount()-1; i >= 0; i--) {
comp.getComponent(i).addMouseListener(listener);
}
}
public static MouseEvent fixMouseEvent(MouseEvent e) {
Component c = e.getComponent();
// CraigM:22/04/2008:Added PaletteList
if (c.getParent() instanceof JComboBox || c.getParent() instanceof PaletteList) {
MouseEvent newEvent = SwingUtilities.convertMouseEvent(c, e, c.getParent());
return newEvent;
}
else {
return e;
}
}
}