001/* An attribute representing the size, location, and other window properties.
002
003 Copyright (c) 1998-2014 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.actor.gui;
029
030import java.awt.Frame;
031import java.awt.GraphicsDevice;
032import java.awt.GraphicsEnvironment;
033import java.awt.Rectangle;
034import java.awt.event.ComponentEvent;
035import java.awt.event.ComponentListener;
036import java.lang.ref.WeakReference;
037
038import ptolemy.data.ArrayToken;
039import ptolemy.data.BooleanToken;
040import ptolemy.data.IntToken;
041import ptolemy.data.RecordToken;
042import ptolemy.data.expr.Parameter;
043import ptolemy.gui.Top;
044import ptolemy.kernel.util.IllegalActionException;
045import ptolemy.kernel.util.InternalErrorException;
046import ptolemy.kernel.util.NameDuplicationException;
047import ptolemy.kernel.util.NamedObj;
048import ptolemy.kernel.util.Settable;
049import ptolemy.kernel.util.Workspace;
050
051///////////////////////////////////////////////////////////////////
052//// WindowPropertiesAttribute
053
054/**
055 This attribute stores properties of a window, including the width,
056 height, and location. The token in this attribute is a RecordToken
057 containing a field "bounds" with a 4-element integer array.
058 There is also a field that indicates whether the window is maximized.
059 By default, this attribute has visibility NONE, so the user will not
060 see it in parameter editing dialogs.
061
062 @author Edward A. Lee, Contributors: Jason E. Smith, Christopher Brooks
063 @version $Id$
064 @since Ptolemy II 2.1
065 @Pt.ProposedRating Red (eal)
066 @Pt.AcceptedRating Red (johnr)
067 */
068public class WindowPropertiesAttribute extends Parameter
069        implements ComponentListener {
070    /** Construct an attribute with the given name contained by the specified
071     *  entity. The container argument must not be null, or a
072     *  NullPointerException will be thrown.  This attribute will use the
073     *  workspace of the container for synchronization and version counts.
074     *  If the name argument is null, then the name is set to the empty string.
075     *  Increment the version of the workspace.
076     *  @param container The container.
077     *  @param name The name of this attribute.
078     *  @exception IllegalActionException If the attribute is not of an
079     *   acceptable class for the container, or if the name contains a period.
080     *  @exception NameDuplicationException If the name coincides with
081     *   an attribute already in the container.
082     */
083    public WindowPropertiesAttribute(NamedObj container, String name)
084            throws IllegalActionException, NameDuplicationException {
085        super(container, name);
086        setVisibility(Settable.NONE);
087
088        // The following line, if uncommented, results in
089        // icons that are defined in an external file always being
090        // exported along with the model that defines them.  This bloats
091        // the MoML files with information that is not needed. I suspect
092        // the line was put there because it wasn't clear that you need
093        // to invoke File->Save in a submodel if you want the location
094        // and position of the submodel window to be saved. It is not
095        // sufficient to invoke save just at the top level.
096        // setPersistent(true);
097    }
098
099    ///////////////////////////////////////////////////////////////////
100    ////                         public methods                    ////
101
102    /** Clone the attribute into the specified workspace. This calls the
103     *  base class and then sets the attribute public members to refer
104     *  to the attributes of the new attribute
105     *  @param workspace The workspace for the new attribute
106     *  @return A new director.
107     *  @exception CloneNotSupportedException If a derived class contains
108     *  an attribute that cannot be cloned.
109     */
110    @Override
111    public Object clone(Workspace workspace) throws CloneNotSupportedException {
112        WindowPropertiesAttribute newObject = (WindowPropertiesAttribute) super.clone(
113                workspace);
114        newObject._listeningTo = new WeakReference<Frame>(null);
115        return newObject;
116    }
117
118    /** Do nothing. This method is
119     *  invoked when the component has been made invisible.
120     *  @param event The component event.
121     */
122    @Override
123    public void componentHidden(ComponentEvent event) {
124    }
125
126    /** Record the new position. This method is
127     *  invoked when the component's position changes.
128     *  @param event The component event.
129     */
130    @Override
131    public void componentMoved(ComponentEvent event) {
132        recordProperties(_listeningTo.get());
133    }
134
135    /** Record the new size. This method is
136     *  invoked when the component's size changes.
137     *  @param event The component event.
138     */
139    @Override
140    public void componentResized(ComponentEvent event) {
141        recordProperties(_listeningTo.get());
142    }
143
144    /** Do nothing. This method is
145     *  invoked when the component has been made visible.
146     *  @param event The component event.
147     */
148    @Override
149    public void componentShown(ComponentEvent event) {
150    }
151
152    /** Set the value of the attribute to match those of the specified
153     *  frame.
154     *  @param frame The frame whose properties are to be recorded.
155     */
156    public void recordProperties(Frame frame) {
157        try {
158            Rectangle bounds = frame.getBounds();
159
160            // Determine whether the window is maximized.
161            boolean maximized = (frame.getExtendedState()
162                    & Frame.MAXIMIZED_BOTH) == Frame.MAXIMIZED_BOTH;
163
164            // Get the current values.
165            RecordToken value = null;
166            try {
167                value = (RecordToken) getToken();
168            } catch (IllegalActionException ex) {
169                throw new IllegalActionException(this, ex,
170                        "Attempting to get the value of the WindowPropertiesAttribute "
171                                + "failed.  This can happen when a model with graphical actors "
172                                + "is run using MoMLSimpleApplication "
173                                + "because MoMLSimpleApplication does not run the model in "
174                                + "the Swing event thread.  One possibility is that the model "
175                                + "needs to be run in Vergil and saved.");
176            }
177            boolean updateValue = false;
178
179            if (value == null) {
180                updateValue = true;
181            } else {
182                ArrayToken boundsToken = (ArrayToken) value.get("bounds");
183                BooleanToken maximizedToken = (BooleanToken) value
184                        .get("maximized");
185                int x = ((IntToken) boundsToken.getElement(0)).intValue();
186                int y = ((IntToken) boundsToken.getElement(1)).intValue();
187                int width = ((IntToken) boundsToken.getElement(2)).intValue();
188                int height = ((IntToken) boundsToken.getElement(3)).intValue();
189
190                if (maximizedToken == null) {
191                    maximizedToken = BooleanToken.FALSE;
192                }
193
194                // If the new values are different, then do a MoMLChangeRequest.
195                if (maximizedToken.booleanValue() != maximized || x != bounds.x
196                        || y != bounds.y || width != bounds.width
197                        || height != bounds.height) {
198                    updateValue = true;
199                }
200            }
201
202            // Check that the toplevel is non-empty so that if a user
203            // opens Kepler and closes, they are not prompted to save
204            // a blank model.
205            if (updateValue) {
206
207                // Construct values for the record token.
208                String values = "{bounds={" + bounds.x + ", " + bounds.y + ", "
209                        + bounds.width + ", " + bounds.height + "}, maximized="
210                        + maximized + "}";
211
212                // Don't call setToken(), instead use a MoMLChangeRequest so that
213                // the model is marked modified so that any changes are preserved.
214                // See "closing workflow does not save the location change of popup display windows."
215                // http://bugzilla.ecoinformatics.org/show_bug.cgi?id=5188
216                //
217                // The problem is that if the user opens a model and
218                // moves the Display actor and then closes it, they
219                // are prompted for saving.
220                //
221                // If the user wants to save the location of the
222                // Display actors, then they should explicitly save the model.
223                setToken(values);
224
225                //                 String moml = "<property name=\"" + getName()
226                //                     + "\" value=\"" + values + "\"/>";
227
228                //                 MoMLChangeRequest request = new MoMLChangeRequest(this,
229                //                         getContainer(), moml, false);
230                //                 getContainer().requestChange(request);
231                // Not clear why the following is needed, but if it isn't there,
232                // then window properties may not be recorded.
233                propagateValue();
234            }
235        } catch (IllegalActionException ex) {
236            throw new InternalErrorException(this, ex,
237                    "Can't set propertes value!");
238        }
239    }
240
241    /** Set the properties of the specified frame to match the
242     *  current value of the attribute.  If the value of the attribute
243     *  has not been set, then do nothing and return true. If the
244     *  value of this attribute is malformed in any way, then just
245     *  return false.
246     *
247     *  <p>If the x or y position is less than 0 pixels or greater
248     *  than (width - 10 pixels) or (height - 10) pixels of the
249     *  screen, then offset the position by 30 pixels so the user can
250     *  drag the window.
251     *
252     *  @param frame The frame whose properties are to be set.
253     *  @return True if successful.
254     */
255    public boolean setProperties(Frame frame) {
256        Frame listeningTo = _listeningTo.get();
257        if (listeningTo != frame) {
258            if (listeningTo != null) {
259                listeningTo.removeComponentListener(this);
260            }
261
262            frame.addComponentListener(this);
263            _listeningTo.clear();
264            _listeningTo = new WeakReference<Frame>(frame);
265        }
266
267        try {
268            RecordToken value = (RecordToken) getToken();
269
270            if (value == null) {
271                return true;
272            }
273
274            ArrayToken boundsToken = (ArrayToken) value.get("bounds");
275            BooleanToken maximizedToken = (BooleanToken) value.get("maximized");
276            int x = ((IntToken) boundsToken.getElement(0)).intValue();
277            int y = ((IntToken) boundsToken.getElement(1)).intValue();
278            int width = ((IntToken) boundsToken.getElement(2)).intValue();
279            int height = ((IntToken) boundsToken.getElement(3)).intValue();
280
281            //System.out.println("x: " + x + "  y: " + y);
282            //System.out.println("width: " + width + "  height: " + height);
283
284            // FIXME: If we change the size, should we mark the model
285            // as dirty so it gets saved?
286
287            // See ptolemy/actor/gui/test/MyFourCorners.xml for a test file
288            // that produces four plots in the four corners of a multi-screen window.
289            GraphicsEnvironment graphicsEnvironment = GraphicsEnvironment
290                    .getLocalGraphicsEnvironment();
291            GraphicsDevice[] graphicsDevices = graphicsEnvironment
292                    .getScreenDevices();
293
294            int widths[] = new int[graphicsDevices.length];
295            int heights[] = new int[graphicsDevices.length];
296            boolean widthsEqual = true;
297            boolean heightsEqual = true;
298            int maxWidth = 0;
299            int maxHeight = 0;
300
301            for (int j = 0; j < graphicsDevices.length; j++) {
302                GraphicsDevice graphicsDevice = graphicsDevices[j];
303                widths[j] = graphicsDevice.getDisplayMode().getWidth();
304                heights[j] = graphicsDevice.getDisplayMode().getHeight();
305            }
306            for (int j = 0; j < graphicsDevices.length - 1; j++) {
307                if (widths[j] != widths[j + 1]) {
308                    widthsEqual = false;
309                }
310                if (heights[j] != heights[j + 1]) {
311                    heightsEqual = false;
312                }
313            }
314            if (widthsEqual && heightsEqual) {
315                //Nominal setup
316                if (heights[0] > widths[0]) {//Width is cumulative.
317                    for (int j = 0; j < graphicsDevices.length; j++) {
318                        maxWidth += widths[j];
319                    }
320                    maxHeight = heights[0];
321                } else {//Height is cumulative.
322                    for (int j = 0; j < graphicsDevices.length; j++) {
323                        maxHeight += heights[j];
324                    }
325                    maxWidth = widths[0];
326                }
327            } else {
328                //Strange setup.
329                maxWidth = widths[0];
330                maxHeight = heights[0];
331                for (int j = 0; j < graphicsDevices.length; j++) {
332                    maxWidth = maxWidth > widths[j] ? widths[j] : maxWidth;
333                    maxHeight = maxHeight > heights[j] ? heights[j] : maxHeight;
334                }
335            }
336
337            x = x < 0 ? 0 : x;
338            y = y < 0 ? 0 : y + 0;
339
340            width = width > maxWidth ? maxWidth : width;
341            height = height > maxHeight ? maxHeight : height;
342
343            y = y + height > maxHeight ? maxHeight - height : y;
344            x = x + width > maxWidth ? maxWidth - width : x;
345
346            frame.setBounds(x, y, width, height);
347
348            if (maximizedToken != null) {
349                boolean maximized = maximizedToken.booleanValue();
350
351                if (maximized) {
352                    // FIXME: Regrettably, this doesn't make the window
353                    // actually maximized under Windows, at least.
354                    frame.setExtendedState(
355                            frame.getExtendedState() | Frame.MAXIMIZED_BOTH);
356                }
357            }
358
359            if (frame instanceof Top) {
360                // Disable centering.
361                ((Top) frame).setCentering(false);
362            }
363
364            return true;
365        } catch (Throwable throwable) {
366            return false;
367        }
368    }
369
370    ///////////////////////////////////////////////////////////////////
371    ////                         private variables                 ////
372
373    /** The frame we are listening to. */
374    private WeakReference<Frame> _listeningTo = new WeakReference<Frame>(null);
375}