/* * Licensed Materials - Property of Perforce Software, Inc. * © Copyright Perforce Software, Inc. 2014, 2021 * © Copyright IBM Corp. 2009, 2014 * © Copyright ILOG 1996, 2009 * All Rights Reserved. * * Note to U.S. Government Users Restricted Rights: * The Software and Documentation were developed at private expense and * are "Commercial Items" as that term is defined at 48 CFR 2.101, * consisting of "Commercial Computer Software" and * "Commercial Computer Software Documentation", as such terms are * used in 48 CFR 12.212 or 48 CFR 227.7202-1 through 227.7202-4, * as applicable. */ package interactor; import java.awt.AWTEvent; import java.awt.Cursor; import java.awt.Graphics; import java.awt.Rectangle; import java.awt.Shape; import java.awt.event.*; import java.io.IOException; import java.io.ObjectInputStream; import java.io.Serializable; import java.util.*; import java.util.logging.Logger; import java.util.logging.Level; import ilog.views.chart.IlvAxis; import ilog.views.chart.IlvChart; import ilog.views.chart.IlvChartDecoration; import ilog.views.chart.IlvChartInteractor; import ilog.views.chart.IlvCoordinateSystem; import ilog.views.chart.IlvDataInterval; import ilog.views.chart.IlvDataWindow; import ilog.views.chart.IlvDoublePoint; import ilog.views.chart.IlvStyle; import ilog.views.chart.graphic.IlvDataIndicator; import ilog.views.util.collections.IlvCollections; import ilog.views.util.IlvResourceUtil; /** * A class to interact on an <code>IlvDataIndicator</code>. * The <code>IlvChartDataIndicatorInteractor</code> class provides the * following facilities to graphically modify the definition of a data * indicator: * <ul><li>Drag a border of the indicator to modify the value of the * corresponding bound.</li> * <li>Drag the indicator itself to modify the corresponding range * definition.</li> * </ul>. * <p>The <code>IlvChartDataIndicatorInteractor</code> defines two modes of * interaction whether its <code>opaqueEdit</code> property is enabled or not. * If the property is enabled, the indicator modification is "real time": each * <code>mouseMotion</code> event immediately modifies the indicator values * and hence cannot be canceled. If the mode is disabled, the indicator * modification will only be performed when the interaction ends (on a * <code>MOUSE_RELEASED</code> event). * <p>The registered name of this interactor is "DataIndicator". */ public class IlvChartDataIndicatorInteractor extends IlvChartInteractor { // ============================ Metainformation ============================ static { IlvChartInteractor.register("DataIndicator", IlvChartDataIndicatorInteractor.class); } /** * Returns a localized name for this interactor class. */ public static String getLocalizedName(Locale locale) { return IlvResourceUtil.getBundle("messages", IlvChartDataIndicatorInteractor.class, locale) .getString("IlvChartDataIndicatorInteractor"); } // ============================ Logging Support ============================ static Logger logger; static Logger getLogger() { if (logger == null) logger = Logger.getLogger(IlvChartDataIndicatorInteractor.class.getName()); return logger; } // ===================== Customization, Bean Properties ===================== private transient IlvDataIndicator _indicator; /** * Returns the indicator that is being dragged. */ protected final IlvDataIndicator getIndicator() { return _indicator; } //-------------------------------------------------------------------------- private boolean _opaqueEdit; /** * Returns whether the <code>opaqueEdit</code> mode is enabled. * @see #setOpaqueEdit */ public final boolean isOpaqueEdit() { return _opaqueEdit; } /** * Sets whether the <code>opaqueEdit</code> mode is enabled. This property * defines how the modifications are propagated to the indicator. If * <code>true</code>, each mouse event corresponds to an immediate indicator * modification. If <code>false</code>, the modification occurs when the * interaction ends. The default value is <code>false</code>. */ public void setOpaqueEdit(boolean opaque) { _opaqueEdit = opaque; } // ============================ State Variables ============================ private transient Object _currentValue; //-------------------------------------------------------------------------- /** * This interface implements the detail behavior, as a reaction to * mouse events. */ private interface InteractionHandler extends Serializable { public Object computeValue(double dx, double dy, boolean apply); public Cursor getCursor(); } /** * This class implements the detail behavior when an indicator is * translated. */ private final class Translater implements InteractionHandler { Override public Object computeValue(double dx, double dy, boolean apply) { Object value; int type = getIndicator().getType(); switch (type) { case IlvDataIndicator.X_VALUE: case IlvDataIndicator.Y_VALUE: double v = (_currentValue != null ? ((Double)_currentValue).doubleValue() : getIndicator().getValue()); getLogger().log(Level.FINE, "v:" + v +" dx:" + dx + " dy:" + dy); v += type == IlvDataIndicator.X_VALUE ? dx : dy; if (apply) getIndicator().setValue(v); value = Double.valueOf(v); getLogger().log(Level.FINE, "value:" + value); break; default: IlvDataWindow window = (_currentValue != null ? (IlvDataWindow)_currentValue : getIndicator().getDataWindow()); if (type != IlvDataIndicator.Y_RANGE) { window.xRange.translate(dx); } if (type != IlvDataIndicator.X_RANGE) { window.yRange.translate(dy); } if (apply) { if (type == IlvDataIndicator.X_RANGE) getIndicator().setRange(window.xRange); else if (type == IlvDataIndicator.Y_RANGE) getIndicator().setRange(window.yRange); else getIndicator().setDataWindow(window); } value = window; } return value; } Override public Cursor getCursor() { return Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR); } } private InteractionHandler _translater = new Translater(); /** * This class implements the detail behavior when an indicator is * resized. */ private final class Reshaper implements InteractionHandler { static final int NONE = 0; static final int XMIN = 1; static final int XMAX = 2; static final int YMIN = 3; static final int YMAX = 4; private int bound; public Reshaper(int bound) { this.bound = bound; } Override public Cursor getCursor() { return Cursor.getPredefinedCursor(Cursor.HAND_CURSOR); } Override public Object computeValue(double dx, double dy, boolean apply) { IlvDataWindow window = (_currentValue != null ? (IlvDataWindow)_currentValue : getIndicator().getDataWindow()); if(window == null) return null; IlvDataInterval range = null; int type = getIndicator().getType(); switch (bound) { case XMIN: window.xRange.min += dx; range = window.xRange; break; case XMAX: window.xRange.max += dx; range = window.xRange; break; case YMIN: window.yRange.min += dy; range = window.yRange; break; case YMAX: window.yRange.max += dy; range = window.yRange; break; } if (apply) { if (type != IlvDataIndicator.WINDOW) getIndicator().setRange(range); else getIndicator().setDataWindow(window); } return window; } } // The detail interaction delegate. private InteractionHandler _handler; //-------------------------------------------------------------------------- private transient IlvDoublePoint _dragPoint; // ============================ Ghost handling ============================ private IlvStyle getGhostStyle() { IlvStyle style = _indicator.getStyle(); if (style == null) style = new IlvStyle(1, getGhostColor()); return style; } /** * Draws the ghost. */ Override protected void drawGhost(Graphics g) { IlvChart chart = getChart(); int type = _indicator.getType(); IlvStyle style = getGhostStyle(); IlvCoordinateSystem sys = getChart().getCoordinateSystem(getYAxisIndex()); Shape shp; switch (type) { case IlvDataIndicator.X_VALUE: case IlvDataIndicator.Y_VALUE: double value = ((Double)_currentValue).doubleValue(); int axisType = (_indicator.getAxisIndex() == -1 ? IlvAxis.X_AXIS : IlvAxis.Y_AXIS); shp = chart.getProjector().getShape(value, axisType, chart.getChartArea().getPlotRect(), sys); break; default: IlvDataWindow w = new IlvDataWindow((IlvDataWindow)_currentValue); w.intersection(sys.getVisibleWindow()); if (w.isEmpty()) return; shp = chart.getProjector().getShape(w, chart.getChartArea().getPlotRect(), sys); break; } style.renderShape(g, shp); } /** * Returns the ghost bounds. */ Override protected Rectangle getGhostBounds() { IlvChart chart = getChart(); int type = _indicator.getType(); IlvStyle style = getGhostStyle(); IlvCoordinateSystem sys = getChart().getCoordinateSystem(getYAxisIndex()); Shape shp; switch (type) { case IlvDataIndicator.X_VALUE: case IlvDataIndicator.Y_VALUE: double value = ((Double)_currentValue).doubleValue(); int axisType = (_indicator.getAxisIndex() == -1 ? IlvAxis.X_AXIS : IlvAxis.Y_AXIS); shp = chart.getProjector().getShape(value, axisType, chart.getChartArea().getPlotRect(), sys); break; default: IlvDataWindow w = new IlvDataWindow((IlvDataWindow)_currentValue); w.intersection(sys.getVisibleWindow()); if (w.isEmpty()) return new Rectangle(); shp = chart.getProjector().getShape(w, chart.getChartArea().getPlotRect(), sys); break; } return style.getShapeBounds(shp).getBounds(); } // ========================= Mouse event handling ========================= /** * Determines which indicator the user meant by clicking at (x,y). * Sets _indicator and returns true if one was found; returns false otherwise. */ private boolean findIndicator(int x, int y) { List<IlvChartDecoration> decos = getChart().getDecorations(); Iterator<IlvChartDecoration> ite = IlvCollections.reversedIterator(decos); while (ite.hasNext()) { IlvChartDecoration deco = ite.next(); if (deco instanceof IlvDataIndicator) { if (((IlvDataIndicator)deco).contains(x, y)) { _indicator = (IlvDataIndicator)deco; return true; } } } return false; } /** * After findIndicator returned true, determine which kind of interaction * to use with _indicator. */ private InteractionHandler getInteractionHandler(MouseEvent event) { IlvDoublePoint start = getData(event); InteractionHandler handler; IlvDataWindow window = _indicator.getDataWindow(); int type = _indicator.getType(); switch (type) { case IlvDataIndicator.X_VALUE: case IlvDataIndicator.Y_VALUE: // All the user can do with such an indicator is to move it. // No need to test the event's coordinates - findIndicator already did // it. handler = _translater; break; case IlvDataIndicator.X_RANGE: case IlvDataIndicator.Y_RANGE: // When clicking near one of the two borders, the user wants to move // this border; otherwise he wants to move the entire indicator. { boolean onX = type == IlvDataIndicator.X_RANGE; IlvDataInterval range; double value; if (onX) { range = window.xRange; value = start.x; } else { range = window.yRange; value = start.y; } double l = range.getLength(); double delta = l*0.10; if (value < range.min + delta) handler = new Reshaper(onX ? Reshaper.XMIN : Reshaper.YMIN); else if (value > range.max - delta) handler = new Reshaper(onX ? Reshaper.XMAX : Reshaper.YMAX); else // (value >= range.min + delta && value <= range.max - delta) handler = _translater; } break; case IlvDataIndicator.WINDOW: // When clicking near one of the four borders, the user wants to move // this border; otherwise he wants to move the entire indicator. { int nearXBorder; double distToXBorder; { double l = window.xRange.getLength(); double delta = l*0.10; if (start.x < window.xRange.min + delta) { nearXBorder = Reshaper.XMIN; IlvDoublePoint pt = new IlvDoublePoint(window.xRange.min, start.y > window.yRange.max ? window.yRange.max : start.y < window.yRange.min ? window.yRange.min : start.y); pt = toDisplay(pt); distToXBorder = Math.hypot(event.getX()-pt.x, event.getY()-pt.y); } else if (start.x > window.xRange.max - delta) { nearXBorder = Reshaper.XMAX; IlvDoublePoint pt = new IlvDoublePoint(window.xRange.max, start.y > window.yRange.max ? window.yRange.max : start.y < window.yRange.min ? window.yRange.min : start.y); pt = toDisplay(pt); distToXBorder = Math.hypot(event.getX()-pt.x, event.getY()-pt.y); } else { nearXBorder = Reshaper.NONE; distToXBorder = 0; } } int nearYBorder; double distToYBorder; { double l = window.yRange.getLength(); double delta = l*0.10; if (start.y < window.yRange.min + delta) { nearYBorder = Reshaper.YMIN; IlvDoublePoint pt = new IlvDoublePoint(start.x > window.xRange.max ? window.xRange.max : start.x < window.xRange.min ? window.xRange.min : start.x, window.yRange.min); pt = toDisplay(pt); distToYBorder = Math.hypot(event.getX()-pt.x, event.getY()-pt.y); } else if (start.y > window.yRange.max - delta) { nearYBorder = Reshaper.YMAX; IlvDoublePoint pt = new IlvDoublePoint(start.x > window.xRange.max ? window.xRange.max : start.x < window.xRange.min ? window.xRange.min : start.x, window.yRange.max); pt = toDisplay(pt); distToYBorder = Math.hypot(event.getX()-pt.x, event.getY()-pt.y); } else { nearYBorder = Reshaper.NONE; distToYBorder = 0; } } if (nearXBorder != Reshaper.NONE && nearYBorder != Reshaper.NONE) { if (distToXBorder < distToYBorder) { nearYBorder = Reshaper.NONE; } else { nearXBorder = Reshaper.NONE; } } if (nearXBorder != Reshaper.NONE) handler = new Reshaper(nearXBorder); else if (nearYBorder != Reshaper.NONE) handler = new Reshaper(nearYBorder); else handler = _translater; } break; default: throw new AssertionError(); } return handler; } /** * Called to validate the new position. You can override this * method to perform additional checks. The default implementation clip the * point to the coordinate system data window. * @param pt The new position to validate (in data coordinate system). */ protected void validate(IlvDoublePoint pt) { IlvDataInterval range = getChart().getXAxis().getDataRange(); pt.x = Math.max(range.min, Math.min(range.max, pt.x)); range = getChart().getYAxis(_indicator.getAxisIndex()).getDataRange(); pt.y = Math.max(range.min, Math.min(range.max, pt.y)); } /** * Handles the mouse events. */ Override public void processMouseEvent(MouseEvent event) { switch (event.getID()) { case MouseEvent.MOUSE_PRESSED: if (((event.getModifiersEx() & getEventMaskEx()) == getEventMaskEx()) && ((event.getModifiersEx() & ~getEventMaskEx()) == 0)) { if (findIndicator(event.getX(), event.getY())) { _dragPoint = getData(event); _handler = getInteractionHandler(event); startOperation(event); _currentValue = _handler.computeValue(0, 0, _opaqueEdit); if (!_opaqueEdit) drawGhost(); if (isConsumeEvents()) event.consume(); } } break; case MouseEvent.MOUSE_RELEASED: if ((event.getModifiersEx() & getEventMaskEx()) != getEventMaskEx()) { if (isInOperation()) { IlvDoublePoint pt = getData(event); validate(pt); if (!_opaqueEdit) drawGhost(); double dx = pt.x - _dragPoint.x; double dy = pt.y - _dragPoint.y; _handler.computeValue(dx, dy, true); endOperation(event); if (isConsumeEvents()) event.consume(); } } break; } } /** * Handles the mouse motion events. */ Override public void processMouseMotionEvent(MouseEvent event) { switch (event.getID()) { case MouseEvent.MOUSE_DRAGGED: if (isInOperation()) { IlvDoublePoint pt = getData(event); validate(pt); double dx = pt.x - _dragPoint.x; double dy = pt.y - _dragPoint.y; if (_opaqueEdit) { _currentValue = _handler.computeValue(dx, dy, true); } else { drawGhost(); _currentValue = _handler.computeValue(dx, dy, false); drawGhost(); } _dragPoint = pt; if (isConsumeEvents()) event.consume(); } break; } } /** * Handles the key event. */ Override public void processKeyEvent(KeyEvent event) { if (event.getID() == KeyEvent.KEY_PRESSED && event.getKeyCode() == KeyEvent.VK_ESCAPE) { if (!_opaqueEdit && _dragPoint != null) drawGhost(); abort(); if (isConsumeEvents()) event.consume(); } } // =================== Overrides from IlvChartInteractor =================== /** * Indicates whether the interactor can be used with a 3D chart. * @return <code>false</code>. */ Override public boolean has3DSupport() { return false; } /** * Called when the interaction starts. * You may override this method to add your own initialization * code. * @param event The event that starts the interaction. */ Override protected void startOperation(MouseEvent event) { super.startOperation(event); setAllowDrawGhost(!_opaqueEdit); if (_handler != null) setCursor(_handler.getCursor()); } /** * Called when the interaction ends. * You may override this method to add your own initialization * code. * @param event The event that ends the interaction. */ Override protected void endOperation(MouseEvent event) { super.endOperation(event); setAllowDrawGhost(false); setCursor(null); _indicator = null; _dragPoint = null; _currentValue = null; } /** * Called when the interaction has been aborted. * You may override this method to add your own initialization * code. */ Override protected void abort() { super.abort(); setAllowDrawGhost(false); setCursor(null); _indicator = null; _dragPoint = null; _currentValue = null; } // ============================= Constructors ============================= private void initTransientFields() { _dragPoint = null; _indicator = null; _currentValue = null; } /** * Creates a new <code>IlvChartDataIndicatorInteractor</code> object * associated to the BUTTON1_DOWN_MASK mouse button with the * <code>opaqueEdit</code> mode disabled. */ public IlvChartDataIndicatorInteractor() { this(0, MouseEvent.BUTTON1_DOWN_MASK, false); } /** * Creates a new <code>IlvChartEditPointInteractor</code> object associated * with the given mouse button linked to the given Y-axis. */ public IlvChartDataIndicatorInteractor(int yAxisIdx, int buttonOrKeyEventMask, boolean opaqueEdit) { super(yAxisIdx, buttonOrKeyEventMask); enableEvents(AWTEvent.MOUSE_MOTION_EVENT_MASK | AWTEvent.MOUSE_EVENT_MASK | AWTEvent.KEY_EVENT_MASK); _opaqueEdit = opaqueEdit; initTransientFields(); } // ============================= Serialization ============================= private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { in.defaultReadObject(); initTransientFields(); } }