001/* An action that is associated with a figure.
002
003 Copyright (c) 2000-2018 The Regents of the University of California.
004 All rights reserved.
005 Permission is hereby granted, without written agreement and without
006 license or royalty fees, to use, copy, modify, and distribute this
007 software and its documentation for any purpose, provided that the above
008 copyright notice and the following two paragraphs appear in all copies
009 of this software.
010
011 IN NO EVENT SHALL THE UNIVERSITY OF CALIFORNIA BE LIABLE TO ANY PARTY
012 FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES
013 ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF
014 THE UNIVERSITY OF CALIFORNIA HAS BEEN ADVISED OF THE POSSIBILITY OF
015 SUCH DAMAGE.
016
017 THE UNIVERSITY OF CALIFORNIA SPECIFICALLY DISCLAIMS ANY WARRANTIES,
018 INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
019 MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE
020 PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, AND THE UNIVERSITY OF
021 CALIFORNIA HAS NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES,
022 ENHANCEMENTS, OR MODIFICATIONS.
023
024 PT_COPYRIGHT_VERSION_2
025 COPYRIGHTENDKEY
026
027 */
028package ptolemy.vergil.toolbox;
029
030import java.awt.Component;
031import java.awt.Frame;
032import java.awt.event.ActionEvent;
033import java.awt.geom.Rectangle2D;
034import java.lang.ref.WeakReference;
035import java.util.Iterator;
036
037import javax.swing.AbstractAction;
038import javax.swing.JButton;
039import javax.swing.JMenu;
040import javax.swing.JMenuItem;
041import javax.swing.JPopupMenu;
042
043import diva.canvas.CanvasComponent;
044import diva.canvas.CanvasLayer;
045import diva.canvas.CanvasPane;
046import diva.canvas.CanvasUtilities;
047import diva.canvas.Figure;
048import diva.canvas.FigureLayer;
049import diva.canvas.event.LayerEvent;
050import diva.canvas.interactor.BasicGrabHandle;
051import diva.graph.GraphController;
052import diva.graph.GraphModel;
053import diva.graph.GraphPane;
054import diva.graph.JGraph;
055import diva.gui.toolbox.JContextMenu;
056import diva.util.UserObjectContainer;
057import ptolemy.kernel.util.InternalErrorException;
058import ptolemy.kernel.util.Location;
059import ptolemy.kernel.util.NamedObj;
060
061///////////////////////////////////////////////////////////////////
062//// FigureAction
063
064/**
065 An action that is attached to a figure on a named object.
066 Such an action is fired in one of several ways.
067 The first way is through an ActionInteractor that is attached
068 to the figure.  The second way is through a context menu that is created
069 on the figure.  A third way is through a hotkey.
070 Unfortunately, the source of the event is different in
071 these cases.  This class makes it easier to write an action that is
072 triggered by any mechanism. Such an action would be derived from this
073 class, and would invoke super.actionPerformed() first in its own
074 actionPerformed() method.
075
076 @author Steve Neuendorffer and Edward A. Lee
077 @version $Id$
078 @since Ptolemy II 1.0
079 @Pt.ProposedRating Red (eal)
080 @Pt.AcceptedRating Red (johnr)
081 */
082@SuppressWarnings("serial")
083public class FigureAction extends AbstractAction {
084    /** Construct an action that is attached to a figure on a named object.
085     *  @param name The name of the object.
086     */
087    public FigureAction(String name) {
088        super(name);
089    }
090
091    ///////////////////////////////////////////////////////////////////
092    ////                         public methods                    ////
093
094    /** Determine the target Ptolemy II object, the originating frame,
095     *  and the X, Y position of the action, if possible.  After this
096     *  is invoked, the other public methods can be used to access
097     *  this data.
098     *  @param e The event.
099     */
100    @Override
101    public void actionPerformed(ActionEvent e) {
102        Object source = e.getSource();
103        Component parent = null;
104
105        if (source instanceof LayerEvent) {
106            _sourceType = CANVAS_TYPE;
107
108            // Action activated using an ActionInteractor.
109            LayerEvent event = (LayerEvent) source;
110            CanvasLayer layer = event.getLayerSource();
111            GraphPane pane = (GraphPane) layer.getCanvasPane();
112            GraphController controller = pane.getGraphController();
113            GraphModel model = controller.getGraphModel();
114
115            _figure = event.getFigureSource();
116
117            // Set the target.
118            if (_figure == null) {
119                _target = (NamedObj) model.getRoot();
120            } else {
121                Object object = _figure.getUserObject();
122                _target = (NamedObj) model.getSemanticObject(object);
123            }
124
125            // Set the position.
126            _x = event.getX();
127            _y = event.getY();
128
129            // Set the parent.
130            CanvasPane canvasPane = layer.getCanvasPane();
131            parent = canvasPane.getCanvas();
132        } else if (source instanceof JMenuItem) {
133            // Action activated using a context menu or submenu.
134            JMenuItem item = (JMenuItem) source;
135            // Find the original context menu.
136            Component contextMenu = item.getParent();
137            if (!(contextMenu instanceof JContextMenu)) {
138                // Probably a submenu.
139                // FIXME: This only supports one level of submenus.
140                if (contextMenu instanceof JPopupMenu) {
141                    contextMenu = ((JPopupMenu) contextMenu).getInvoker();
142                }
143                if (contextMenu instanceof JMenu) {
144                    contextMenu = contextMenu.getParent();
145                }
146            }
147            if (contextMenu instanceof JContextMenu) {
148                _sourceType = CONTEXTMENU_TYPE;
149                JContextMenu menu = (JContextMenu) contextMenu;
150                parent = menu.getInvoker();
151                _target = (NamedObj) menu.getTarget();
152                _x = item.getX();
153                _y = item.getY();
154            } else {
155                // Not implicit location.. should there be?
156                _sourceType = MENUBAR_TYPE;
157            }
158            /*
159             } else if (source instanceof JMenuItem) {
160             // Action activated using a context menu.
161             JMenuItem item = (JMenuItem) source;
162
163             if (item.getParent() instanceof JContextMenu) {
164             _sourceType = CONTEXTMENU_TYPE;
165
166             JContextMenu menu = (JContextMenu) item.getParent();
167             parent = menu.getInvoker();
168             _target = (NamedObj) menu.getTarget();
169             _x = item.getX();
170             _y = item.getY();
171             } else {
172             // Not implicit location.. should there be?
173             _sourceType = MENUBAR_TYPE;
174             }
175             */
176        } else if (source instanceof JButton) {
177            // presumably we are in a toolbar...
178            _sourceType = TOOLBAR_TYPE;
179            _target = null;
180            parent = ((Component) source).getParent();
181        } else if (source instanceof JGraph) {
182            // This is an absurdly convoluted way to get the info we need.
183            // But there seems to be no other way.
184            // This is an architectural flaw in vergil.
185            GraphPane pane = ((JGraph) source).getGraphPane();
186            FigureLayer layer = pane.getForegroundLayer();
187            CanvasComponent currentFigure = layer.getCurrentFigure();
188            GraphController controller = pane.getGraphController();
189            GraphModel model = controller.getGraphModel();
190
191            if (currentFigure != null) {
192                _target = null;
193
194                while (_target == null && currentFigure != null) {
195                    Object object = currentFigure;
196
197                    if (object instanceof Figure) {
198                        object = ((Figure) currentFigure).getUserObject();
199                    }
200
201                    _target = (NamedObj) model.getSemanticObject(object);
202                    currentFigure = currentFigure.getParent();
203                }
204
205                // NOTE: _target may end up null here!
206                if (_target == null) {
207                    // On 5/29/09, Edward wrote:
208                    // "If you select a transition in an FSM, put the
209                    // mouse on the blue box that is the grab handle,
210                    // and hit Command-L (to look inside), you get an
211                    // ugly exception on the command line.
212                    // If you put the mouse on the transition but not
213                    // on the blue box, you correctly get a message
214                    // that there is no refinement."
215                    //
216                    // So, if the current figure is a BasicGrabHandle, we
217                    // do not throw the exception
218                    if (!(layer
219                            .getCurrentFigure() instanceof BasicGrabHandle)) {
220                        throw new InternalErrorException(
221                                "Internal error: FigureLayer \""
222                                        + layer.getCurrentFigure()
223                                        + "\" has no associated Ptolemy II object!");
224                    }
225                }
226            } else {
227                _target = (NamedObj) model.getRoot();
228            }
229
230            _sourceType = HOTKEY_TYPE;
231
232            // FIXME: set _x and _y.  How to do this?
233            _x = 0;
234            _y = 0;
235
236            // Set the parent.
237            CanvasPane canvasPane = layer.getCanvasPane();
238            parent = canvasPane.getCanvas();
239        } else {
240            _sourceType = null;
241            _target = null;
242            parent = null;
243            _x = 0;
244            _y = 0;
245        }
246
247        if (parent != null) {
248            while (parent.getParent() != null) {
249                parent = parent.getParent();
250            }
251        }
252
253        if (parent instanceof Frame) {
254            _frame = new WeakReference(parent);
255        } else {
256            _frame = null;
257        }
258    }
259
260    /** Return the figure of this action.
261     *  @return The figure of this action.
262     */
263    public Figure getFigure() {
264        return _figure;
265    }
266
267    // FIXME: The following methods should all be protected.
268
269    /** Return the frame responsible for triggering this action,
270     *  or null if none could be found.  This can be used to set the
271     *  owner of any dialogs triggered by this event.  This must
272     *  be called after actionPerformed(), and is typically called
273     *  inside the actionPerformed() method of a subclass.
274     *  @return The frame that triggered this action.
275     */
276    public Frame getFrame() {
277        return (Frame) _frame.get();
278    }
279
280    /** Return the source type of this action, which is one of
281     *  CANVAS_TYPE, CONTEXTMENU_TYPE, TOOLBAR_TYPE, MENUBAR_TYPE,
282     *  HOTKEY_TYPE, or null if none was recognized.
283     *  @return The source type of this action.
284     */
285    public SourceType getSourceType() {
286        return _sourceType;
287    }
288
289    /** Return the target Ptolemy II object for this action,
290     *  or null if none could be found.  This is typically the object
291     *  whose icon is the figure on which this action was invoked.
292     *  This must be called after actionPerformed(), and is typically called
293     *  inside the actionPerformed() method of a subclass.
294     *  @return The object on which this action was invoked.
295     */
296    public NamedObj getTarget() {
297        return _target;
298    }
299
300    /** Return the horizontal position of the action, or 0 if this
301     *  is not relevant (e.g., the action was triggered by a toolbar button).
302     *  This must be called after actionPerformed(), and is typically called
303     *  inside the actionPerformed() method of a subclass.
304     *  @return The x position of the action.
305     */
306    public int getX() {
307        return _x;
308    }
309
310    /** Return the vertical position of the action, or 0 if this
311     *  is not relevant (e.g., the action was triggered by a toolbar button).
312     *  This must be called after actionPerformed(), and is typically called
313     *  inside the actionPerformed() method of a subclass.
314     *  @return The y position of the action.
315     */
316    public int getY() {
317        return _y;
318    }
319
320    /** Determine a new location for a figure if another figure is
321     *  already at that location.
322     *  @param x The x value of the proposed location.
323     *  @param y The y value of the proposed location.
324     *  @param xOffset The x offset to be used if a figure is found.
325     *  @param yOffset The y offset to be used if a figure is found.
326     *  @param figureClass The Class of the figure to avoid.
327     *  @param foregroundLayer The layer containing the figures.
328     *  @param visibleRectangle The rectangle that describe the bounds
329     *  of the visible pane.
330     *  @return An array of two doubles (x and y) that represents either
331     *  the original location or an offset location that does not obscure
332     *  an object of class <i>figure</i>.
333     */
334    static public double[] offsetFigure(double x, double y, double xOffset,
335            double yOffset, Class<?> figureClass, FigureLayer foregroundLayer,
336            Rectangle2D visibleRectangle) {
337        // Solve the problem of items from the toolbar overlapping.
338        // See http://bugzilla.ecoinformatics.org/show_bug.cgi?id=3002
339
340        // This method is in this class so that we can handle
341        // ports and relations.
342
343        double[] point = new double[2];
344        point[0] = x;
345        point[1] = y;
346
347        double originalX = x;
348        double originalY = y;
349
350        // See EditorDropTarget for similar code.
351
352        double halo = foregroundLayer.getPickHalo();
353        double width = halo * 2;
354
355        // Used to handle cases where we get to the edge.
356        int xMax = 0;
357        int yMax = 0;
358
359        // Set to true if we need to check for a Figure at x and y
360        boolean checkFigure = false;
361        do {
362            // If we are looping again, we set checkFigure to false
363            // until we later possibly find a Figure.
364            checkFigure = false;
365
366            // The rectangle in which we search for a Figure.
367            Rectangle2D region = new Rectangle2D.Double(point[0] - halo,
368                    point[1] - halo, width, width);
369
370            // Iterate through figures within the region.
371            Iterator<?> foregroundFigures = foregroundLayer.getFigures()
372                    .getIntersectedFigures(region).figuresFromFront();
373            Iterator<?> pickFigures = CanvasUtilities
374                    .pickIter(foregroundFigures, region);
375
376            while (pickFigures.hasNext() && !checkFigure) {
377                CanvasComponent possibleFigure = (CanvasComponent) pickFigures
378                        .next();
379                if (possibleFigure == null) {
380                    // Nothing to see here, move along - there is no Figure.
381                    break;
382                } else if (possibleFigure instanceof UserObjectContainer) {
383                    // Work our way up the CanvasComponent parent tree
384                    // See EditorDropTarget for similar code.
385                    Object userObject = null;
386
387                    while (possibleFigure instanceof UserObjectContainer
388                            && userObject == null && !checkFigure) {
389                        userObject = ((UserObjectContainer) possibleFigure)
390                                .getUserObject();
391                        if (userObject instanceof Location && (figureClass
392                                .isInstance(userObject)
393                                || figureClass.isInstance(possibleFigure))) {
394                            // We found a figure here, so we will
395                            // loop again.
396                            checkFigure = true;
397                            point[0] += xOffset;
398                            point[1] += yOffset;
399
400                            // Check to make sure we are not outside the view
401                            if (point[0] > visibleRectangle.getWidth()) {
402                                point[0] = originalX;
403                                point[1] = originalY
404                                        - PASTE_OFFSET * 2 * ++xMax;
405                                if (point[1] < 0) {
406                                    point[1] = originalY
407                                            + PASTE_OFFSET * 2 * ++xMax;
408                                }
409                            }
410
411                            if (point[1] > visibleRectangle.getHeight()) {
412                                point[0] = originalX
413                                        - PASTE_OFFSET * 2 * ++yMax;
414                                if (point[0] < 0) {
415                                    point[0] = originalX
416                                            + PASTE_OFFSET * 2 * ++xMax;
417                                }
418                                point[1] = originalY;
419
420                            }
421
422                            // Fail safe. Don't try forever, just give up.
423                            if (point[0] < 0 || point[1] < 0
424                                    || point[0] > visibleRectangle.getWidth()
425                                    || point[1] > visibleRectangle
426                                            .getHeight()) {
427                                // Can't do anything here, so return.
428                                point[0] = originalX + 0.5 * xOffset;
429                                point[1] = originalY + 0.5 * yOffset;
430                                return point;
431                            }
432                        }
433                        possibleFigure = possibleFigure.getParent();
434                    }
435                }
436            }
437        } while (checkFigure);
438        return point;
439    }
440
441    ///////////////////////////////////////////////////////////////////
442    ////                         public variables                  ////
443
444    /** When the action was fired from a canvas interactor.
445     */
446    public static final SourceType CANVAS_TYPE = new SourceType("canvas");
447
448    /** When the action was fired from a context menu.
449     */
450    public static final SourceType CONTEXTMENU_TYPE = new SourceType(
451            "contextmenu");
452
453    /** When the action was fired from a hotkey.
454     */
455    public static final SourceType HOTKEY_TYPE = new SourceType("hotkey");
456
457    /** When the action was fired from a menubar.
458     */
459    public static final SourceType MENUBAR_TYPE = new SourceType("menubar");
460
461    /** Offset used when pasting objects. See also OffsetMoMLChangeRequest. */
462    public static final int PASTE_OFFSET = 20;
463
464    /** When the action was fired from a toolbar icon.
465     */
466    public static final SourceType TOOLBAR_TYPE = new SourceType("toolbar");
467
468    ///////////////////////////////////////////////////////////////////
469    ////                         protected methods                 ////
470
471    ///////////////////////////////////////////////////////////////////
472    ////                         protected variables               ////
473
474    ///////////////////////////////////////////////////////////////////
475    ////                         inner classes                     ////
476
477    /** The source of the action. */
478    public static class SourceType {
479        /** Construct a SourceType.
480         *  @param name The name of the SourceType.
481         */
482        private SourceType(String name) {
483            _name = name;
484        }
485
486        /** Get the name of the SourceType.
487         *  @return the name.
488         */
489        public String getName() {
490            return _name;
491        }
492
493        private String _name;
494    }
495
496    ///////////////////////////////////////////////////////////////////
497    ////                         private variables                 ////
498    private Figure _figure = null;
499
500    private WeakReference _frame = null;
501
502    private SourceType _sourceType = null;
503
504    private NamedObj _target = null;
505
506    private int _x = 0;
507
508    private int _y = 0;
509}