001/* Base class for graph controllers for objects that can have icons.
002
003 Copyright (c) 2003-2016 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.basic;
029
030import java.awt.event.ActionEvent;
031import java.awt.geom.AffineTransform;
032import java.awt.geom.NoninvertibleTransformException;
033import java.awt.geom.Point2D;
034import java.awt.geom.Rectangle2D;
035
036import javax.swing.Action;
037
038import diva.canvas.Figure;
039import diva.canvas.FigureLayer;
040import diva.graph.GraphException;
041import diva.graph.GraphPane;
042import diva.graph.NodeRenderer;
043import diva.gui.GUIUtilities;
044import diva.gui.toolbox.FigureIcon;
045import ptolemy.actor.IOPort;
046import ptolemy.actor.gui.Configuration;
047import ptolemy.kernel.Entity;
048import ptolemy.kernel.util.InternalErrorException;
049import ptolemy.kernel.util.Location;
050import ptolemy.kernel.util.NamedObj;
051import ptolemy.moml.MoMLChangeRequest;
052import ptolemy.vergil.actor.ExternalIOPortController;
053import ptolemy.vergil.kernel.AttributeController;
054import ptolemy.vergil.toolbox.EditIconAction;
055import ptolemy.vergil.toolbox.FigureAction;
056import ptolemy.vergil.toolbox.MenuActionFactory;
057import ptolemy.vergil.toolbox.RemoveIconAction;
058import ptolemy.vergil.toolbox.SnapConstraint;
059
060///////////////////////////////////////////////////////////////////
061//// WithIconGraphController
062
063/**
064 A base class for Ptolemy II graph controllers for objects that can have
065 icons. This adds to the base class the context menu items "Edit Custom Icon"
066 and "Remove Custom Icon".  This also adds a port controller.
067
068 @author Edward A. Lee
069 @version $Id$
070 @since Ptolemy II 4.0
071 @Pt.ProposedRating Red (eal)
072 @Pt.AcceptedRating Red (johnr)
073 */
074public abstract class WithIconGraphController extends BasicGraphController {
075    /** Create a new controller.
076     */
077    public WithIconGraphController() {
078        super();
079    }
080
081    ///////////////////////////////////////////////////////////////////
082    ////                         public methods                    ////
083
084    /** Get a location for a port that hasn't got a location yet.
085     * @param pane The GraphPane.
086     * @param frame The BasicGraphFrame.
087     * @param _prototype The port.
088     * @return The location.
089     */
090    static public double[] getNewPortLocation(GraphPane pane,
091            BasicGraphFrame frame, IOPort _prototype) {
092        Point2D center = frame.getCenter();
093
094        // If we are zoomed in, then place the ports in the canvas
095        // view, not way off yonder.
096        //Rectangle2D visiblePart = frame.getVisibleRectangle();
097        BasicGraphFrame basicGraphFrame = frame;
098        Rectangle2D visiblePart = basicGraphFrame.getVisibleCanvasRectangle();
099
100        double[] p;
101        if (_prototype.isInput() && _prototype.isOutput()) {
102            p = _offsetFigure(center.getX(),
103                    visiblePart.getY() + visiblePart.getHeight() - _PORT_OFFSET,
104                    FigureAction.PASTE_OFFSET * 2, 0, pane, frame);
105        } else if (_prototype.isInput()) {
106            p = _offsetFigure(visiblePart.getX() + _PORT_OFFSET, center.getY(),
107                    0, FigureAction.PASTE_OFFSET * 2, pane, frame);
108        } else if (_prototype.isOutput()) {
109            p = _offsetFigure(
110                    visiblePart.getX() + visiblePart.getWidth() - _PORT_OFFSET,
111                    center.getY(), 0, FigureAction.PASTE_OFFSET * 2, pane,
112                    frame);
113        } else {
114            p = _offsetFigure(center.getX(), center.getY(),
115                    FigureAction.PASTE_OFFSET * 2,
116                    FigureAction.PASTE_OFFSET * 2, pane, frame);
117        }
118        return p;
119    }
120
121    /** Set the configuration.  This is used by some of the controllers
122     *  when opening files or URLs.
123     *  @param configuration The configuration.
124     */
125    @Override
126    public void setConfiguration(Configuration configuration) {
127        super.setConfiguration(configuration);
128        _portController.setConfiguration(configuration);
129        _editIconAction.setConfiguration(configuration);
130        _removeIconAction.setConfiguration(configuration);
131    }
132
133    ///////////////////////////////////////////////////////////////////
134    ////                         protected methods                 ////
135
136    /** Create the controllers for nodes in this graph.
137     *  In this base class, a port controller with PARTIAL access is created.
138     *  This is called by the constructor, so derived classes that
139     *  override this must be careful not to reference local variables
140     *  defined in the derived classes, because the derived classes
141     *  will not have been fully constructed by the time this is called.
142     */
143    @Override
144    protected void _createControllers() {
145        super._createControllers();
146        _portController = new ExternalIOPortController(this,
147                AttributeController.PARTIAL);
148    }
149
150    // NOTE: The following method name does not have a leading underscore
151    // because it is a diva method.
152
153    /** Initialize all interaction on the graph pane. This method
154     *  is called by the setGraphPane() method of the superclass.
155     *  This initialization cannot be done in the constructor because
156     *  the controller does not yet have a reference to its pane
157     *  at that time.  Regrettably, the canvas is not yet associated
158     *  with the GraphPane, so you can't do any initialization that
159     *  involves the canvas.
160     */
161    @Override
162    protected void initializeInteraction() {
163        super.initializeInteraction();
164
165        //GraphPane pane = getGraphPane();
166        _menuFactory.addMenuItemFactory(new MenuActionFactory(_editIconAction));
167        _menuFactory
168                .addMenuItemFactory(new MenuActionFactory(_removeIconAction));
169    }
170
171    ///////////////////////////////////////////////////////////////////
172    ////                         private methods                   ////
173
174    /** Offset a figure if another figure is already at that location.
175     *  @param x The x value of the proposed location.
176     *  @param y The y value of the proposed location.
177     *  @param xOffset The x offset to be used if a figure is found.
178     *  @param yOffset The x offset to be used if a figure is found.
179     *  @param pane The GraphPane.
180     *  @param frame The BasicGraphFrame.
181     *  @return An array of two doubles (x and y) that represents either
182     *  the original location or an offset location that does not obscure
183     *  an object of class <i>figure</i>.
184     */
185    static private double[] _offsetFigure(double x, double y, double xOffset,
186            double yOffset, GraphPane pane, BasicGraphFrame frame) {
187        FigureLayer foregroundLayer = pane.getForegroundLayer();
188
189        Rectangle2D visibleRectangle;
190        if (frame != null) {
191            visibleRectangle = frame.getVisibleRectangle();
192        } else {
193            visibleRectangle = pane.getCanvas().getVisibleSize();
194        }
195        double[] point = FigureAction.offsetFigure(x, y, xOffset, yOffset,
196                diva.canvas.connector.TerminalFigure.class, foregroundLayer,
197                visibleRectangle);
198        return point;
199    }
200
201    ///////////////////////////////////////////////////////////////////
202    ////                         protected variables               ////
203
204    /** The edit custom icon action. */
205    protected static final EditIconAction _editIconAction = new EditIconAction();
206
207    /** The port controller. */
208    protected NamedObjController _portController;
209
210    /** The remove custom icon action. */
211    protected static final RemoveIconAction _removeIconAction = new RemoveIconAction();
212
213    ///////////////////////////////////////////////////////////////////
214    ////                         private variables                 ////
215
216    /** Offset of ports from the visible border. */
217    private static double _PORT_OFFSET = 20.0;
218
219    ///////////////////////////////////////////////////////////////////
220    ////                         inner classes                     ////
221
222    ///////////////////////////////////////////////////////////////////
223    //// NewPortAction
224
225    /** An action to create a new port. */
226    @SuppressWarnings("serial")
227    public class NewPortAction extends FigureAction {
228        /** Create a new port that has the same input, output, and
229         *  multiport properties as the specified port.  If the specified
230         *  port is null, then a new port that is neither an input, an
231         *  output, nor a multiport will be created.
232         *  @param prototype Prototype port.
233         *  @param description The description used for menu entries and
234         *   tooltips.
235         *  @param mnemonicKey The KeyEvent field for the mnemonic key to
236         *   use in the menu.
237         */
238        public NewPortAction(IOPort prototype, String description,
239                int mnemonicKey) {
240            // null as the fourth arg means get the figure from the
241            // _portController
242            this(prototype, description, mnemonicKey, null);
243        }
244
245        /** Create a new port that has the same input, output, and
246         *  multiport properties as the specified port and has icons
247         *  associated with being unselected, rollover, rollover
248         *  selected, and selected.  If the specified port is null,
249         *  then a new port that is neither an input, an output, nor a
250         *  multiport will be created.
251         *
252         *  @param prototype Prototype port.
253         *  @param description The description used for menu entries and
254         *   tooltips.
255         *  @param mnemonicKey The KeyEvent field for the mnemonic key to
256         *   use in the menu.
257         *  @param iconRoles A matrix of Strings, where each element
258         *  consists of two Strings, the absolute URL of the icon
259         *  and the key that represents the role of the icon.  The keys
260         *  are usually static fields from this class, such as
261         *  {@link diva.gui.GUIUtilities#LARGE_ICON},
262         *  {@link diva.gui.GUIUtilities#ROLLOVER_ICON},
263         *  {@link diva.gui.GUIUtilities#ROLLOVER_SELECTED_ICON} or
264         *  {@link diva.gui.GUIUtilities#SELECTED_ICON}.
265         *  If this parameter is null, then the icon comes from
266         *  the calling getNodeRenderer() on the {@link #_portController}.
267         *  @see diva.gui.GUIUtilities#addIcons(Action, String[][])
268         */
269        public NewPortAction(IOPort prototype, String description,
270                int mnemonicKey, String[][] iconRoles) {
271            super(description);
272            _prototype = prototype;
273
274            if (iconRoles != null) {
275                GUIUtilities.addIcons(this, iconRoles);
276            } else {
277                // Creating the renderers this way is rather nasty..
278                // Standard toolbar icons are 25x25 pixels.
279                NodeRenderer renderer = _portController.getNodeRenderer();
280
281                Object location = null;
282
283                if (_prototype != null) {
284                    location = _prototype.getAttribute("_location");
285                }
286
287                Figure figure = renderer.render(location);
288
289                FigureIcon icon = new FigureIcon(figure, 25, 25, 1, true);
290                putValue(GUIUtilities.LARGE_ICON, icon);
291            }
292            putValue("tooltip", description);
293            putValue(GUIUtilities.MNEMONIC_KEY, Integer.valueOf(mnemonicKey));
294        }
295
296        /** Create a new port. */
297        @Override
298        public void actionPerformed(ActionEvent e) {
299            super.actionPerformed(e);
300
301            double x;
302            double y;
303
304            if (getSourceType() == TOOLBAR_TYPE
305                    || getSourceType() == MENUBAR_TYPE) {
306                // No location in the action, so put it in the middle.
307                BasicGraphFrame frame = WithIconGraphController.this.getFrame();
308                GraphPane pane = getGraphPane();
309
310                if (frame != null) {
311                    if (_prototype != null) {
312                        // Put in the middle of the visible part.
313                        double[] p = getNewPortLocation(pane, frame,
314                                _prototype);
315                        x = p[0];
316                        y = p[1];
317
318                    } else {
319                        // Put in the middle of the visible part.
320                        Point2D center = frame.getCenter();
321
322                        x = center.getX();
323                        y = center.getY();
324                    }
325                } else {
326                    // Put in the middle of the pane.
327                    Point2D center = pane.getSize();
328                    x = center.getX() / 2;
329                    y = center.getY() / 2;
330                }
331            } else {
332                // Transform
333                AffineTransform current = getGraphPane().getTransformContext()
334                        .getTransform();
335                AffineTransform inverse;
336
337                try {
338                    inverse = current.createInverse();
339                } catch (NoninvertibleTransformException ex) {
340                    throw new RuntimeException(ex.toString());
341                }
342
343                Point2D point = new Point2D.Double(getX(), getY());
344
345                inverse.transform(point, point);
346                x = point.getX();
347                y = point.getY();
348            }
349
350            AbstractBasicGraphModel graphModel = (AbstractBasicGraphModel) getGraphModel();
351            final double[] point = SnapConstraint.constrainPoint(x, y);
352            final NamedObj toplevel = graphModel.getPtolemyModel();
353
354            if (!(toplevel instanceof Entity)) {
355                throw new InternalErrorException(
356                        "Cannot invoke NewPortAction on an object "
357                                + "that is not an instance of Entity.");
358            }
359
360            String name = "port";
361            if (_prototype != null) {
362                if (_prototype.isInput() && !_prototype.isOutput()) {
363                    name = "in";
364                }
365                if (!_prototype.isInput() && _prototype.isOutput()) {
366                    name = "out";
367                }
368            }
369
370            final String portName = toplevel.uniqueName(name);
371            final String locationName = "_location";
372
373            // Create the port.
374            StringBuffer moml = new StringBuffer();
375            moml.append("<port name=\"" + portName + "\">\n");
376            moml.append("<property name=\"" + locationName
377                    + "\" class=\"ptolemy.kernel.util.Location\"/>\n");
378
379            if (_prototype != null) {
380                if (_prototype.isInput()) {
381                    moml.append("<property name=\"input\"/>");
382                }
383
384                if (_prototype.isOutput()) {
385                    moml.append("<property name=\"output\"/>");
386                }
387
388                if (_prototype.isMultiport()) {
389                    // Set the width of the multiport to -1 so that the width is inferred.
390                    // See ptolemy/actor/lib/test/auto/VectorDisassemblerComposite.xml
391                    moml.append(
392                            "<property name=\"width\" class=\"ptolemy.data.expr.Parameter\" value=\"-1\"/>");
393                    moml.append("<property name=\"multiport\"/>");
394                }
395            }
396
397            moml.append("</port>");
398
399            MoMLChangeRequest request = new MoMLChangeRequest(this, toplevel,
400                    moml.toString()) {
401                @Override
402                protected void _execute() throws Exception {
403                    super._execute();
404
405                    // Set the location of the icon.
406                    // Note that this really needs to be done after
407                    // the change request has succeeded, which is why
408                    // it is done here.  When the graph controller
409                    // gets around to handling this, it will draw
410                    // the icon at this location.
411                    // NOTE: The cast is safe because it is checked
412                    // above, and presumably a reasonable GUI would
413                    // provide no mechanism for creating a port on
414                    // something that is not an entity.
415                    NamedObj newObject = ((Entity) toplevel).getPort(portName);
416                    Location location = (Location) newObject
417                            .getAttribute(locationName);
418                    location.setLocation(point);
419                }
420            };
421
422            request.setUndoable(true);
423            toplevel.requestChange(request);
424
425            try {
426                request.waitForCompletion();
427            } catch (Exception ex) {
428                ex.printStackTrace();
429                throw new GraphException(ex);
430            }
431        }
432
433        private IOPort _prototype;
434
435        /** Offset a figure if another figure is already at that location.
436         *  @param x The x value of the proposed location.
437         *  @param y The y value of the proposed location.
438         *  @param xOffset The x offset to be used if a figure is found.
439         *  @param yOffset The x offset to be used if a figure is found.
440         *  @return An array of two doubles (x and y) that represents either
441         *  the original location or an offset location that does not obscure
442         *  an object of class <i>figure</i>.
443         */
444        protected double[] _offsetFigure(double x, double y, double xOffset,
445                double yOffset) {
446
447            double[] point = WithIconGraphController._offsetFigure(x, y,
448                    xOffset, yOffset, getGraphPane(),
449                    WithIconGraphController.this.getFrame());
450            return point;
451        }
452    }
453}