/*******************************************************************************
* CogTool Copyright Notice and Distribution Terms
* CogTool 1.3, Copyright (c) 2005-2013 Carnegie Mellon University
* This software is distributed under the terms of the FSF Lesser
* Gnu Public License (see LGPL.txt).
*
* CogTool is free software; you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation; either version 2.1 of the License, or
* (at your option) any later version.
*
* CogTool is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with CogTool; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*
* CogTool makes use of several third-party components, with the
* following notices:
*
* Eclipse SWT version 3.448
* Eclipse GEF Draw2D version 3.2.1
*
* Unless otherwise indicated, all Content made available by the Eclipse
* Foundation is provided to you under the terms and conditions of the Eclipse
* Public License Version 1.0 ("EPL"). A copy of the EPL is provided with this
* Content and is also available at http://www.eclipse.org/legal/epl-v10.html.
*
* CLISP version 2.38
*
* Copyright (c) Sam Steingold, Bruno Haible 2001-2006
* This software is distributed under the terms of the FSF Gnu Public License.
* See COPYRIGHT file in clisp installation folder for more information.
*
* ACT-R 6.0
*
* Copyright (c) 1998-2007 Dan Bothell, Mike Byrne, Christian Lebiere &
* John R Anderson.
* This software is distributed under the terms of the FSF Lesser
* Gnu Public License (see LGPL.txt).
*
* Apache Jakarta Commons-Lang 2.1
*
* This product contains software developed by the Apache Software Foundation
* (http://www.apache.org/)
*
* jopt-simple version 1.0
*
* Copyright (c) 2004-2013 Paul R. Holser, Jr.
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*
* Mozilla XULRunner 1.9.0.5
*
* The contents of this file are subject to the Mozilla Public License
* Version 1.1 (the "License"); you may not use this file except in
* compliance with the License. You may obtain a copy of the License at
* http://www.mozilla.org/MPL/.
* Software distributed under the License is distributed on an "AS IS"
* basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the
* License for the specific language governing rights and limitations
* under the License.
*
* The J2SE(TM) Java Runtime Environment version 5.0
*
* Copyright 2009 Sun Microsystems, Inc., 4150
* Network Circle, Santa Clara, California 95054, U.S.A. All
* rights reserved. U.S.
* See the LICENSE file in the jre folder for more information.
******************************************************************************/
package edu.cmu.cs.hcii.cogtool.model;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.EventObject;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import edu.cmu.cs.hcii.cogtool.util.DuplicateNameException;
import edu.cmu.cs.hcii.cogtool.util.GlobalAttributed;
import edu.cmu.cs.hcii.cogtool.util.GraphicsUtil;
import edu.cmu.cs.hcii.cogtool.util.NameChangeAlert;
import edu.cmu.cs.hcii.cogtool.util.NamedObject;
import edu.cmu.cs.hcii.cogtool.util.NullSafe;
import edu.cmu.cs.hcii.cogtool.util.ObjectLoader;
import edu.cmu.cs.hcii.cogtool.util.ObjectSaver;
/**
* The standard implementation of the CogTool <code>Frame</code> interface.
*
* @author mlh
*/
public class Frame extends GlobalAttributed implements NamedObject, Cloneable
{
/**
* Version number
* 0 is initial version
* 1 is the version which supports backgroundBounds.
* New loader required for migration.
* Does an image load/dispose to get image size.
*/
public static final int edu_cmu_cs_hcii_cogtool_model_Frame_version = 1;
public static final String nameVAR = "name";
public static final String widgetColorVAR = "widgetColor";
public static final String widgetsVAR = "widgets";
public static final String eltGroupsVAR = "eltGroups";
public static final String devicesVAR = "devices";
public static final String originVAR = "origin";
public static final String backgroundVAR = "background";
public static final String backgroundBoundsVAR = "backgroundBounds";
public static final String incidentTransitionsVAR = "incidentTransitions";
public static final String listenTimeVAR = "listenTimeInSecs";
public static final String speakerTextVAR = "speakerText";
/**
* The containing design
*/
protected Design design;
/**
* The name used, must be unique
*/
protected String name;
/**
* The set of widgets on a frame.
*/
protected Set<IWidget> widgets = new LinkedHashSet<IWidget>();
/**
* The set of element groups in a frame.
*/
protected Set<FrameElementGroup> eltGroups =
new LinkedHashSet<FrameElementGroup>();
/**
* The map of input devices that is used. Keyboard, touchscreen.. etc
* Maps DeviceType to InputDevice.
*/
protected Map<DeviceType, InputDevice> devices =
new LinkedHashMap<DeviceType, InputDevice>();
/**
* The default origin point, (0,0)
*/
protected DoublePoint origin = new DoublePoint(0.0, 0.0);
/**
* The background image for this frame.
* Stored as a byte array for storage purposes.
*/
protected byte[] background = null;
/**
* The background image size. This is needed to implement lazy loading and
* for accurately determining the frame's size without loading the image.
* Also needed for future support of non-0,0 origin images.
*/
protected DoubleRectangle backgroundBounds = null;
/**
* The set of INCOMING transitions.
*/
protected Set<Transition> incidentTransitions = new HashSet<Transition>();
/**
* The color widgets should be. This is used for differentiating widgets
* from images.
*/
protected int widgetColor = GraphicsUtil.defaultWidgetColor;
/**
* The time in seconds that the modeled user is expected to listen to
* the auditory output of the frame's speaker.
*/
protected double listenTimeInSecs = Frame.NO_LISTEN_TIME;
/**
* The text that defines the auditory output from the frame's speaker.
*/
protected String speakerText = "";
// The alerter event object for speaker changes
protected Frame.SpeakerChange speakerChgEvent = new Frame.SpeakerChange(this);
/**
* Constants for supporting cut/copy clipboard modes;
* these will ultimately be used for the purpose in the ObjectSaver.
* NOTE: By convention, null is used to signify normal file persistence.
*/
public static final String COPY_FRAME_ONLY = "FrameOnly";
/**
* Indication that the listen time is not specified by the user.
*/
public static final double NO_LISTEN_TIME = 0.0;
/**
* The saver model for version 0.
* Saves
* name
* WidgetColor
* devices
* widgets
* associations
* origin
* background
* incident transitions.
*/
private static ObjectSaver.IDataSaver<Frame> SAVER =
new ObjectSaver.ADataSaver<Frame>() {
@Override
public int getVersion()
{
return edu_cmu_cs_hcii_cogtool_model_Frame_version;
}
@Override
public void saveData(Frame v, ObjectSaver saver)
throws IOException
{
saver.saveString(v.name, nameVAR);
saver.saveInt(v.widgetColor, widgetColorVAR);
saver.saveObject(v.widgets, widgetsVAR);
saver.saveObject(v.eltGroups, eltGroupsVAR);
saver.saveObject(v.devices, devicesVAR);
saver.saveObject(v.origin, originVAR);
saver.saveObject(v.background, backgroundVAR);
saver.saveObject(v.backgroundBounds, backgroundBoundsVAR);
saver.saveDouble(v.listenTimeInSecs, listenTimeVAR);
saver.saveString(v.speakerText, speakerTextVAR);
Object purpose = saver.getPurpose();
// If copying only selected frames to the clipboard,
// do not save incident transitions
if (purpose == Frame.COPY_FRAME_ONLY) {
saver.saveObject(new HashSet<Transition>(),
incidentTransitionsVAR);
}
else {
saver.saveObject(v.incidentTransitions,
incidentTransitionsVAR);
}
}
};
/**
* Function to register a saver
*
*/
public static void registerSaver()
{
ObjectSaver.registerSaver(Frame.class.getName(), SAVER);
}
/**
* Functions to load a saver for Version 1
*
* Loads name, origin, background, backgroundBounds, widgetColor, widgets,
* associations, devices, and incident transitions
*/
public static class FrameLoader extends ObjectLoader.AObjectLoader<Frame>
{
@Override
public Frame createObject()
{
return new Frame();
}
/**
* set the name, origin and background objects
*/
@Override
public void set(Frame target, String variable, Object value)
{
if (variable != null) {
if (variable.equals(nameVAR)) {
target.name = (String) value;
}
else if (variable.equals(originVAR)) {
target.origin = (DoublePoint) value;
}
else if (variable.equals(backgroundVAR)) {
target.background = (byte[]) value;
}
else if (variable.equals(backgroundBoundsVAR)) {
target.backgroundBounds = (DoubleRectangle) value;
}
else if (variable.equals(speakerTextVAR)) {
target.speakerText = (String) value;
}
}
}
/**
* Set the integer value for the widget color.
*/
@Override
public void set(Frame target, String variable, int value)
{
if (variable != null) {
if (variable.equals(widgetColorVAR)) {
target.widgetColor = value;
}
}
}
@Override
public void set(Frame target, String variable, double value)
{
if (variable != null) {
if (variable.equals(listenTimeVAR)) {
target.listenTimeInSecs = value;
}
}
}
/**
* Create the collection for incident transitions, widgets,
* and associations.
*/
@Override
public Collection<?> createCollection(Frame target,
String variable,
int size)
{
if (variable != null) {
if (variable.equals(widgetsVAR)) {
return target.widgets;
}
else if (variable.equals(eltGroupsVAR)) {
return target.eltGroups;
}
else if (variable.equals(incidentTransitionsVAR)) {
return target.incidentTransitions;
}
}
return null;
}
/**
* Set up the map for Devices.
*/
@Override
public Map<?, ?> createMap(Frame target, String variable, int size)
{
if (variable != null) {
if (variable.equals(devicesVAR)) {
return target.devices;
}
}
return null;
}
@Override
public ObjectLoader.IAggregateLoader getLoader(String variable)
{
if (variable.equals(widgetsVAR)) {
return new ObjectLoader.AAggregateLoader() {
@Override
public <T> void addToCollection(ObjectLoader l,
Collection<? super T> c,
T v)
{
super.addToCollection(l, c, v);
// Collection is 0, Frame is 1
Object parent = l.getPendingObject(1);
((IWidget) v).setFrame((Frame) parent);
}
};
}
if (variable.equals(devicesVAR)) {
return new ObjectLoader.AAggregateLoader() {
@Override
public <K, V> void putInMap(ObjectLoader l,
Map<K, V> m,
K key,
V v)
{
super.putInMap(l, m, key, v);
// Map is 0, Frame is 1
Object parent = l.getPendingObject(1);
((InputDevice) v).setFrame((Frame) parent);
}
};
}
return super.getLoader(variable);
}
}
private static ObjectLoader.IObjectLoader<Frame> LOADER = new FrameLoader();
/**
* Loader for version 0 overrides
* public void set(Object target, String variable, Object value)
* to call a utility function for getting the size of an image.
* This slightly breaks the bounds of the model to the ui
* but it is isolated into a util method and besides it's more or less
* required anyway.
*/
private static ObjectLoader.IObjectLoader<Frame> frameLoaderV0 =
new FrameLoader()
{
@Override
public void set(Frame target, String variable, Object value)
{
// When the background is loaded, get the size and store it.
// If the background is null, leave the bounds as null.
if (variable.equals(backgroundVAR) && (value != null)) {
target.backgroundBounds =
GraphicsUtil.getImageBounds((byte[]) value);
}
super.set(target, variable, value);
}
};
public static ObjectLoader.IObjectLoader<Frame> getImportLoader()
{
return frameLoaderV0;
}
/**
* Register the loader
*/
public static void registerLoader()
{
ObjectLoader.registerLoader(Frame.class.getName(),
0,
frameLoaderV0);
ObjectLoader.registerLoader(Frame.class.getName(),
edu_cmu_cs_hcii_cogtool_model_Frame_version,
LOADER);
}
/**
* Checks if 2 objects are identical
*/
public static boolean isIdentical(Frame l, Frame r)
{
return
l.name.equals(r.name) &&
l.widgets.equals(r.widgets) &&
l.eltGroups.equals(r.eltGroups) &&
l.devices.equals(r.devices) &&
l.origin.equals(r.origin) &&
((l.background == r.background) || ((l.background != null) &&
Arrays.equals(l.background, r.background))) &&
l.incidentTransitions.equals(r.incidentTransitions) &&
(l.widgetColor == r.widgetColor) ;
}
/* really, package visibility! */
public static void setFrameDevices(Frame f, Set<DeviceType> deviceTypes)
{
if (deviceTypes == null) {
throw new IllegalArgumentException("InputDeviceType set cannot be null!");
}
// Create InputDevice objects for the input-only devices
// used by the Design.
Iterator<DeviceType> types = deviceTypes.iterator();
while (types.hasNext()) {
DeviceType type = types.next();
f.addInputDevice(type);
}
}
/**
* Initialize the frame with the given name and an empty widget set.
*
* @param nm the name of the frame (for display), must not be null or empty
* @param deviceTypes set of DeviceType instances for defining
* transitions from devices to frames
* @throws IllegalArgumentException if nm is null or empty
* @author mlh
*/
public Frame(String nm, Set<DeviceType> deviceTypes)
{
if ((nm == null) || nm.equals("")) {
throw new IllegalArgumentException("Frame name cannot be null or empty!");
}
name = nm;
setFrameDevices(this, deviceTypes);
}
/**
* Zero-argument constructor for use by persistence loading.
*/
protected Frame() { }
/**
* Fetch the design that contains this frame.
*
* @return the design containing this frame
* @author mlh
*/
public Design getDesign()
{
return design;
}
/**
* Reset the design containing this frame.
*
* @param d the design that contains this frame
*/
public void setDesign(Design d)
{
design = d;
}
/**
* Fetch the name of the frame, used for display.
*
* @return the frame's name
* @author mlh
*/
public String getName()
{
return name;
}
/**
* Change the name of the frame, used for display.
* <p>
* When done, registered alert handlers are notified with
* a <code>NameChangeAlert</code> instance.
*
* @param newName the new frame name, must not be null or empty
* @throws IllegalArgumentException if newName is null or empty
* @author mlh
*/
public void setName(String newName)
{
if ((newName == null) || newName.equals("")) {
throw new IllegalArgumentException("Frame name cannot be null or empty!");
}
name = newName;
raiseAlert(new NameChangeAlert(this));
}
/**
* Get the current location of the Frame in the structure view.
*
* @return the current location of the Frame
* @author mlh
*/
public DoublePoint getFrameOrigin()
{
return origin;
}
/**
* Set the location of the top-left corner of the Frame.
* <p>
* When done, registered alert handlers are notified with
* a <code>OriginChange</code> instance.
*
* @param newX the x-coordinate of the new location
* @param newY the y-coordinate of the new location
* @author mlh
*/
public void setFrameOrigin(double newX, double newY)
{
DoublePoint oldOrigin = new DoublePoint(origin);
origin.x = newX;
origin.y = newY;
raiseAlert(new Frame.OriginChange(this, oldOrigin));
}
/**
* Set the location of the top-left corner of the Frame.
* <p>
* When done, registered alert handlers are notified with
* a <code>OriginChange</code> instance.
*
* @param newOrigin the new location
* @author mlh
*/
public void setFrameOrigin(DoublePoint newOrigin)
{
setFrameOrigin(newOrigin.x, newOrigin.y);
}
/**
* Determine frame's extent; union of all widget bounds and
* the background image's bounds (if any). If totally empty,
* a zero width/height rectangle at 0.0, 0.0 is returned.
* Unless there are negative coordinates, the top left will be 0.0, 0.0
*/
public DoubleRectangle getFrameBounds()
{
DoubleRectangle extent = new DoubleRectangle(0.0, 0.0, 0.0, 0.0);
if (backgroundBounds != null) {
extent.unionInto(backgroundBounds.getX(),
backgroundBounds.getY(),
backgroundBounds.getWidth(),
backgroundBounds.getHeight());
}
Iterator<IWidget> allWidgets = widgets.iterator();
while (allWidgets.hasNext()) {
IWidget widget = allWidgets.next();
DoubleRectangle widgetBds = widget.getEltBounds();
extent.unionInto(widgetBds.getX(),
widgetBds.getY(),
widgetBds.getWidth(),
widgetBds.getHeight());
}
return extent;
}
/**
* Move the location of the top-left corner of the Frame.
* <p>
* When done, registered alert handlers are notified with
* a <code>OriginChange</code> instance.
*
* @param deltaX the amount to move the x-coordinate
* @param deltaY the amount to move the y-coordinate
* @author mlh
*/
public void moveFrameOrigin(double deltaX, double deltaY)
{
DoublePoint oldOrigin = new DoublePoint(origin);
origin.x += deltaX;
origin.y += deltaY;
raiseAlert(new Frame.OriginChange(this, oldOrigin));
}
/**
* Move the location of the top-left corner of the Frame.
* <p>
* When done, registered alert handlers are notified with
* a <code>OriginChange</code> instance.
*
* @param deltaOrigin the vector by which to move the Frame
* @author mlh
*/
public void moveFrameOrigin(DoublePoint deltaOrigin)
{
moveFrameOrigin(deltaOrigin.x, deltaOrigin.y);
}
/**
* Return the data associated with the frame's background image.
*
* @return the data associated with the frame's background image
*/
public byte[] getBackgroundImage()
{
return background;
}
/**
* Set the frame's background image with the given image data.
*
* Require that callers also set the bounds at the same time.
* image bounds and the image are TIED together, you should
* not be able to change the image with out also changing the frame
* of course, we can just require this, with out enforcing it this way.
*
* TODO: This needs more description of what the image data represents.
*
* note: Bounds only examined if i is DIFFERENT then currently stored.
*
* @param i the image data
*/
public void setBackgroundImage(byte[] img, DoubleRectangle bounds)
{
if (img != background) {
background = img;
backgroundBounds = bounds;
raiseAlert(new Frame.BackgroundImageChange(this,
Frame.BackgroundImageChange.IMAGE_CONTENT_CHANGE));
}
}
/**
* Get the background's bounds
* Can be null if the background image is null.
* @return
*/
public DoubleRectangle getBackgroundBounds()
{
return backgroundBounds;
}
/**
* Set the background image's bounds.
* The bounds are normally centered at 0,0.
*
* This method should only be called to MOVE the image to a new location
* Currently this is not supported in the UI Model.
*
* Please specify the bounds when you set the background image.
* @param bounds
*/
public void setBackgroundBounds(DoubleRectangle bounds)
{
if ((background == null) && (bounds != null)) {
throw new GraphicsUtil.ImageException("Trying to set a non-null bounds "
+ "for a null background on Frame: "
+ toString());
}
else if ((background != null) && (bounds == null)) {
throw new GraphicsUtil.ImageException("Trying to set a null bounds for "
+ "a non-null background on Frame: "
+ toString());
}
else if ((background == null) && (bounds == null)) {
bounds = null; // No need to raise a new alert.
}
else if (! bounds.equals(backgroundBounds)) {
backgroundBounds = bounds;
raiseAlert(new Frame.BackgroundImageChange(this,
Frame.BackgroundImageChange.IMAGE_BOUNDS_CHANGE));
}
}
/**
* Fetch the entire set of widgets.
*
* @return the frame's widgets
* @author mlh
*/
public Set<IWidget> getWidgets()
{
return widgets;
}
/**
* Fetch the entire set of element groups.
*
* @return the frame's groups
* @author mlh
*/
public Set<FrameElementGroup> getEltGroups()
{
return eltGroups;
}
/**
* Fetch the association with the given name.
*/
public FrameElementGroup getEltGroup(String name)
{
for (FrameElementGroup testGrp : eltGroups) {
String testName = testGrp.getName();
if ((testName != null) && testName.equals(name)) {
return testGrp;
}
}
return null;
}
/**
* Fetch the widget of the frame of the specified name.
* <p>
* A frame's widgets must have mutually distinct names.
* <p>
* If the widget is not found, <code>null</code> is returned.
*
* @param widgetName the name of the widget to find
* @return the widget of the given name held by the frame,
* or <code>null</code> if not found
* @author mlh
*/
public IWidget getWidget(String widgetName)
{
for (IWidget testWidget : widgets) {
if (testWidget.getName().equals(widgetName)) {
return testWidget;
}
}
return null;
}
/**
* Does this frame contain the specified widget
*/
public boolean containsWidget(IWidget widget)
{
return widgets.contains(widget);
}
/**
* Add the given widget to the end of the frame's list of widgets.
* <p>
* Each implementation must check for widget name uniqueness.
* <p>
* When done, registered alert handlers are notified with
* a <code>WidgetChange</code> instance.
* <p>
* Throws <code>DuplicateNameException</code> if given widget
* has the same name as one already held by the frame.
*
* @param newWidget the widget to add
* @exception DuplicateNameException
* @author mlh
*/
public void addWidget(IWidget newWidget) throws DuplicateNameException
{
// Check for name collisions before adding the widget
if (isWidgetNameTaken(newWidget.getName())) {
throw new DuplicateNameException();
}
int newLevel = widgets.size();
IWidget groupMember = getGroupMember(newWidget);
if (groupMember != null) {
newLevel = groupMember.getLevel();
}
// Create the new widget at the top level
newWidget.setLevel(newLevel);
widgets.add(newWidget);
// Set the frame parent for this widget.
newWidget.setFrame(this);
raiseAlert(new Frame.WidgetChange(this, newWidget, Frame.WidgetChange.ELEMENT_ADD));
}
/**
* Find the widget of the given name and, if found, remove from
* the frame's list of widgets.
* <p>
* When done, registered alert handlers are notified with
* a <code>WidgetChange</code> instance.
*
* @param widgetName the name of the widget to remove
* @return true iff the widget was successfully removed
* @author mlh
*/
public boolean removeWidget(String widgetName)
{
IWidget widgetToRemove = getWidget(widgetName);
if (widgetToRemove != null) {
return removeWidget(widgetToRemove);
}
return false;
}
/**
* Remove the given widget from the frame's list of widgets,
* if it contains that widget.
* <p>
* When done, registered alert handlers are notified with
* a <code>WidgetChange</code> instance.
*
* @param widgetToRemove the widget to remove
* @return true iff the widget was successfully removed
* @author mlh
*/
public boolean removeWidget(IWidget widgetToRemove)
{
// Bring the widget to the top layer so our layering system does not
// get confused.
setWidgetLevel(Integer.MAX_VALUE, widgetToRemove);
if (widgets.remove(widgetToRemove)) {
raiseAlert(new Frame.WidgetChange(this,
widgetToRemove,
Frame.WidgetChange.ELEMENT_DELETE));
return true;
}
return false;
}
/**
* Add the given association to the end of the frame's list of associations.
* <p>
* Each implementation must check for association name uniqueness.
* <p>
* When done, registered alert handlers are notified with
* a <code>ElementChange</code> instance.
*
* @param newEltGroup the widget to add
* @author mlh
*/
public void addEltGroup(FrameElementGroup newEltGroup)
{
eltGroups.add(newEltGroup);
raiseAlert(new Frame.FrameEltGrpChange(this,
newEltGroup,
Frame.FrameEltGrpChange.ELEMENT_ADD));
}
/**
* Remove the given association from the frame's list of associations,
* if it contains that association.
* <p>
* When done, registered alert handlers are notified with
* a <code>ElementtChange</code> instance.
*
* @param groupToRemove the association to remove
* @return true iff the widget was successfully removed
* @author mlh
*/
public boolean removeEltGroup(FrameElementGroup groupToRemove)
{
if (eltGroups.remove(groupToRemove)) {
raiseAlert(new Frame.FrameEltGrpChange(this,
groupToRemove,
Frame.FrameEltGrpChange.ELEMENT_DELETE));
return true;
}
return false;
}
/**
* Returns another member of the widget's group, if it has a group and
* another member exists; null otherwise
*/
protected IWidget getGroupMember(IWidget newWidget)
{
SimpleWidgetGroup group = newWidget.getParentGroup();
if (group != null) {
int index = 0;
if (group.size() > 1) {
IWidget w = group.get(index);
if (w != newWidget) {
return w;
}
return group.get(index + 1);
}
if (newWidget instanceof ChildWidget) {
return ((ChildWidget) newWidget).getParent();
}
}
return null;
}
/**
* Set the level on a widget.
* The level will be adjusted to lie in the range from 0-widgetListSize
*
* Other widgets are adjusted up or down depending on what the new value is
*
* An alert is generated at the end of all changes.
*/
public void setWidgetLevel(int newLevel, IWidget widget)
{
// Store the old level
int oldLevel = widget.getLevel();
// Validate the new level; we want new widgets to be on top!
int maxLevel = widgets.size() - 1;
if (newLevel > maxLevel) {
newLevel = maxLevel;
}
else if (newLevel < 0) {
newLevel = 0;
}
// If there is a change, move through list realigning other widgets
if (newLevel != oldLevel) {
SimpleWidgetGroup parentGroup = widget.getParentGroup();
Iterator<IWidget> widgetIterator = widgets.iterator();
while (widgetIterator.hasNext()) {
IWidget otherWidget = widgetIterator.next();
int level = otherWidget.getLevel();
// Every widget in the same SimpleWidgetGroup has the same level
if ((parentGroup != null) &&
(otherWidget.getParentGroup() == parentGroup))
{
otherWidget.setLevel(newLevel);
}
// Send backward if was above and is now below
else if ((oldLevel < level) && (level <= newLevel)) {
otherWidget.setLevel(level - 1);
}
// bring forward if was below and is now above
else if ((newLevel <= level) && (level < oldLevel)) {
otherWidget.setLevel(level + 1);
}
// otherwise, the other widget's level is either below
// both the old/new levels or above both the old/new levels.
}
// Invoke raiseAlert here to maintain efficiency instead of
// invoking it once for every widget whose level is changed above
// in the while loop.
widget.setLevel(newLevel);
raiseAlert(new Frame.WidgetChange(this,
widget,
Frame.WidgetChange.WIDGET_LEVEL_CHANGED));
}
}
/**
* Fetch the input device associated with the Frame of the given type.
*
* @param type
* @return the input device associated with the Frame of the given type
* @author mlh
*/
public InputDevice getInputDevice(DeviceType type)
{
return devices.get(type);
}
/**
* Add a new input device to the frame of the given device type
*/
public void addInputDevice(DeviceType type)
{
if ((devices.get(type) == null) &&
(type.getNature() != DeviceType.OUTPUT_ONLY) &&
! type.isModeledByDisplay())
{
InputDevice newDevice = new InputDevice(type.toString(), type);
devices.put(type, newDevice);
newDevice.setFrame(this);
raiseAlert(new Frame.InputDeviceChange(this, newDevice));
}
}
/**
* Return all input devices associated with the Frame.
* The collection contains InputDevice instances.
*
* @return all input devices associated with the Frame
*/
public Collection<InputDevice> getInputDevices()
{
return devices.values();
}
/**
* Record that the given Transition is incident upon this Frame.
*
* @param transition the object expressing the Transition to this Frame
* @throws IllegalArgumentException if transition == null
* @throws IllegalArgumentException if transition is not incident on this frame
* @author mlh
*/
public void addIncidentTransition(Transition transition)
{
if (transition == null) {
throw new IllegalArgumentException("Incident transition to add must not be null!");
}
if (this != transition.getDestination()) {
throw new IllegalArgumentException("Transition to add must be incident on this Frame!");
}
incidentTransitions.add(transition);
}
/**
* Record that the given Transitions are incident upon this Frame.
*
* @throws IllegalArgumentException if transition.getDestination() != this
* @param transitions an array of Transitions to this Frame
* @author centgraf
*/
public void addIncidentTransitions(Transition[] transitions)
{
// Maps Transition to the IWidget that is its source.
for (Transition transition : transitions) {
addIncidentTransition(transition);
}
}
/**
* Remove the specified Transition that is incident upon this Frame.
*
* @param transition the object expressing the Transition to this Frame
* @throws IllegalArgumentException if transition == null
* @return true if the removal was successful, false if there was no such transition here
* @author mlh
*/
public boolean removeIncidentTransition(Transition transition)
{
if (transition == null) {
throw new IllegalArgumentException("Incident transition to remove must not be null!");
}
return incidentTransitions.remove(transition);
}
/**
* Remove all incident Transitions
*
* @return the array of all removed Transitions
* @author mlh
*/
public Transition[] removeIncidentTransitions()
{
int i = -1;
Transition[] transitions =
new Transition[incidentTransitions.size()];
// Maps Transition to the IWidget that is its source.
Iterator<Transition> iTransitions =
incidentTransitions.iterator();
while (iTransitions.hasNext()) {
Transition transition = iTransitions.next();
transitions[++i] = transition;
// Try to avoid concurrent modify exception
iTransitions.remove();
transition.getSource().removeTransition(transition);
}
return transitions;
}
/**
* Returns the mapping of incident Transition instances to the
* corresponding source instances.
*
* @return the object reflecting the mapping of Transition instances
* to their corresponding source instances
* @author mlh
*/
public Set<Transition> getIncidentTransitions()
{
return incidentTransitions;
}
/**
* Check to see if the widget's name is take by another widget.
*/
public boolean isWidgetNameTaken(String widgetName)
{
return getWidget(widgetName) != null;
}
/**
* Set the name of a widget, throws a duplicate name exception if
* the name is already used.
*/
public void setWidgetName(String widgetName, IWidget widget)
throws DuplicateNameException
{
if (! widget.getName().equals(widgetName)) {
// Check whether the requested name is already in use
if (isWidgetNameTaken(widgetName)) {
throw new DuplicateNameException();
}
// set the name
widget.setName(widgetName);
}
}
/**
* Set the name of an element group, throws a duplicate name exception if
* the name is already used.
*/
public void setEltGroupName(String eltGroupName, FrameElementGroup eltGrp)
throws DuplicateNameException
{
String oldName = eltGrp.getName();
if ((oldName == null) || ! oldName.equals(eltGroupName)) {
// Check whether the requested name is already in use
if (getEltGroup(eltGroupName) != null) {
throw new DuplicateNameException();
}
// set the name
eltGrp.setName(eltGroupName);
}
}
/**
*
* The platform-independent color value is encoded in the following format:
* bits 0-7: blue; bits 7-15: green; bits 16-23: red; bits 24-31: alpha
*/
public int getWidgetColor()
{
return widgetColor;
}
/**
* Set the color for this widget.
* Takes an int which uses is encoded
* The platform-independent color value is encoded in the following format:
* bits 0-7: blue; bits 7-15: green; bits 16-23: red; bits 24-31: alpha
*/
public void setWidgetColor(int color)
{
if (widgetColor != color) {
widgetColor = color;
raiseAlert(new Frame.WidgetChange(this, null,
Frame.WidgetChange.WIDGET_COLORS_CHANGED));
}
}
/**
* Get the time in seconds that the user is expected to
* listen to the auditory output from the Frame's speaker.
*/
public double getListenTimeInSecs()
{
return listenTimeInSecs;
}
/**
* Set the time in seconds that the user is expected to
* listen to the auditory output from the Frame's speaker.
*/
public void setListenTimeInSecs(double listenTime)
{
listenTimeInSecs = listenTime;
raiseAlert(speakerChgEvent);
}
/**
* Get the text that defines the auditory output from the Frame's speaker.
*/
public String getSpeakerText()
{
return speakerText;
}
/**
* Set the text that defines the auditory output from the Frame's speaker.
*/
public void setSpeakerText(String speakerTxt)
{
if (speakerTxt == null) {
speakerTxt = "";
}
speakerText = speakerTxt;
raiseAlert(speakerChgEvent);
}
protected static class DuplicateWidgetSituator
extends SimpleWidgetGroup.AWidgetDuplicator
{
protected Frame frameCopy = null;
protected Map<IWidget, IWidget> widgetMap =
new HashMap<IWidget, IWidget>();
protected int recursionDepth = 0;
public void placeInContext(IWidget origWidget, IWidget widgetCopy)
{
frameCopy.addWidget(widgetCopy);
widgetMap.put(origWidget, widgetCopy);
}
public IWidget getDuplicate(IWidget origWidget)
{
return widgetMap.get(origWidget);
}
public Frame getCurrentFrameContext()
{
return frameCopy;
}
public void resetRecursion(Frame frame)
{
frameCopy = frame;
// Prevent leak
recursionDepth--;
if (recursionDepth == 0) {
widgetMap.clear();
reset();
}
}
public void reset(Frame frame)
{
recursionDepth++;
frameCopy = frame;
}
}
public static class ElementChange<T extends FrameElement> extends EventObject
{
/**
* Added a new element.
* The new element is passed in the event
*/
public static final int ELEMENT_ADD = 0;
/**
* The element is deleted.
* The deleted element is passed in the message
*/
public static final int ELEMENT_DELETE = 1;
/**
* Holder for the action that occurred
*/
public int action;
/**
* If not null, the element effected by the action
*/
protected T element;
/**
* Initialize the semantic change representing an add or a remove.
*
* @param frame the frame that was modified
* @param elementChg the element added or removed
* @param act the type of action
* @author alex
*/
public ElementChange(Frame frame, T elementChg, int act)
{
super(frame);
this.element = elementChg;
this.action = act;
}
public T getChangeElement()
{
return this.element;
}
}
/**
* Semantic change for <code>addWidget</code> and
* <code>removeWidget</code>.
* <p>
* Widgets are added in no particular order. Thus, only one kind
* of add is allowed.
*
* @author mlh
* @see ListChange
*/
public static class WidgetChange extends ElementChange<IWidget>
{
/**
* Changing level of a widget.
* Best to redraw all on this event.
*/
public static final int WIDGET_LEVEL_CHANGED = 2;
/**
* Change color of all widgets. Redraw all.
*/
public static final int WIDGET_COLORS_CHANGED = 3;
/**
* Moved widget from one group to another
*/
public static final int WIDGET_REORDER = 4;
/**
* Initialize the semantic change representing an add or a remove.
*
* @param frame the frame that was modified
* @param widgetChg the widget added or removed
* @param add a flag indicating whether the change is an add
* or a remove
* @author alex
*/
public WidgetChange(Frame frame, IWidget widgetChg, int act)
{
super(frame, widgetChg, act);
}
}
public static class FrameEltGrpChange extends ElementChange<FrameElementGroup>
{
public FrameEltGrpChange(Frame frame,
FrameElementGroup elementChg,
int act)
{
super(frame, elementChg, act);
}
}
/**
* Event for changing the background image of a frame.
*
*/
public static class BackgroundImageChange extends EventObject
{
public static int IMAGE_CONTENT_CHANGE = 0;
public static int IMAGE_BOUNDS_CHANGE = 1;
int changeType;
/**
* @param frame
* @param type: one of IMAGE_CONENT_CHANGE, IMAGE_BOUNDS_CHANGE
*/
public BackgroundImageChange(Frame frame, int type)
{
super(frame);
changeType = type;
}
}
/**
* Event to indicate that the origin of a frame has changed.
* The old origin is included in the message,
* the new origin must be obtained from the source of the event.
*/
public static class OriginChange extends EventObject
{
public DoublePoint oldOrigin;
/**
* @param frame the frame whose origin was changed
* @param oldOrigin the previous origin of the frame
*/
public OriginChange(Frame frame, DoublePoint oldValue)
{
super(frame);
oldOrigin = oldValue;
}
}
/**
* Event to indicate that the characteristics of the auditory output
* from the Frame's speaker have changed. The new user listening time
* and speaker text must be obtained from the source of the event.
*/
public static class SpeakerChange extends EventObject
{
/**
* @param frame the frame whose speaker properties were changed
*/
public SpeakerChange(Frame frame)
{
super(frame);
}
}
/**
* Event to indicate that a new input device has been added to the frame
*/
public static class InputDeviceChange extends EventObject
{
public InputDevice newDevice;
/**
* @param frame the frame whose speaker properties were changed
*/
public InputDeviceChange(Frame frame, InputDevice d)
{
super(frame);
newDevice = d;
}
}
/**
* Support for duplicating Frames in a context. The difficulty
* is that deep copies of a Frame requires copying Transitions
* emanating from the Frame's Widgets and InputDevices and
* the Transitions track destination Frames, which may or may not
* be in the set of Frames being duplicated.
* <p>
* An implementation of this interface may choose to add the
* duplicated Frame during getOrDuplicateFrame or not;
* recordDuplicateFrame will be called whenever
* getOrDuplicateFrame decides it needs to duplicate (vs. fetch).
*
* @author mlh
*/
public static interface IFrameDuplicator
{
/**
* Fetch the given Frame's duplicate if it has already
* been duplicated or duplicate it otherwise.
*
* @param frameToCopy the frame that should be duplicated
* @return the frame copy
*/
public Frame getOrDuplicate(Frame frameToCopy);
/**
* Record the given duplicate Frame so that, if referenced
* in a graph-like fashion, it may be fetched instead of
* duplicated again.
*
* @param originalFrame the original frame that was duplicated
* @param frameDuplicate the frame that is the duplicate
*/
public void recordDuplicateFrame(Frame originalFrame,
Frame frameDuplicate);
}
public static IWidget duplicateWidget(IWidget widget,
Frame.IFrameDuplicator duplicator,
SimpleWidgetGroup.IWidgetDuplicator widgetSituator)
{
// If already copied, simply return the copy.
IWidget widgetCopy = widgetSituator.getDuplicate(widget);
if (widgetCopy != null) {
// TODO: (dfm) does the following, or something like it,
// need to be here, perhaps? It seems to have been omitted
// from duplicate(String, IFrameDuplicator) as part of a
// complex refactoring in revision 3050? Or is this done
// somewhere else?
// widgetSituator.placeInContext(widget, widgetCopy);
return widgetCopy;
}
if (widget instanceof MenuHeader) {
SimpleWidgetGroup newHdrGroup =
widgetSituator.getGroup(widget.getParentGroup());
widgetCopy = ((MenuHeader) widget).duplicate(newHdrGroup,
duplicator,
widgetSituator);
}
else if (widget instanceof PullDownHeader) {
widgetCopy = ((PullDownHeader) widget).duplicate(duplicator,
widgetSituator);
IWidget origSelected =
(IWidget) widget.getAttribute(WidgetAttributes.SELECTION_ATTR);
if (! NullSafe.equals(origSelected, WidgetAttributes.NONE_SELECTED))
{
IWidget selectedCopy =
widgetSituator.getDuplicate(origSelected);
widgetCopy.setAttribute(WidgetAttributes.SELECTION_ATTR,
selectedCopy);
}
}
else if (widget instanceof ListItem) {
SimpleWidgetGroup newListGroup =
widgetSituator.getGroup(widget.getParentGroup());
widgetCopy =
((ListItem) widget).duplicate(newListGroup, duplicator);
}
else if (widget instanceof GridButton) {
SimpleWidgetGroup newGridGroup =
widgetSituator.getGroup(widget.getParentGroup());
widgetCopy =
((GridButton) widget).duplicate(newGridGroup, duplicator);
}
else if (widget instanceof ContextMenu) {
widgetCopy = ((ContextMenu) widget).duplicate(duplicator,
widgetSituator);
}
else if (! (widget instanceof ChildWidget)) {
widgetCopy = widget.duplicate(duplicator);
}
if (widgetCopy != null) {
widgetSituator.placeInContext(widget, widgetCopy);
}
return widgetCopy;
} // duplicateWidget
public static IWidget duplicateWidget(IWidget widget,
Frame.IFrameDuplicator duplicator,
SimpleWidgetGroup.IWidgetDuplicator widgetSituator,
double moveByX,
double moveByY)
{
IWidget widgetCopy =
duplicateWidget(widget, duplicator, widgetSituator);
if (widgetCopy == null) {
throw new IllegalStateException("Incorrect type for duplicating a structured widget");
}
widgetCopy.moveElement(moveByX, moveByY);
return widgetCopy;
}
public static void duplicateRemoteLabel(FrameElement elt,
FrameElement eltCopy,
Frame.IFrameDuplicator duplicator,
SimpleWidgetGroup.IWidgetDuplicator widgetSituator,
double moveByX,
double moveByY)
{
// Must check if this element has a remote label widget; since labels
// cannot have labels, the recursive call to duplicateWidget
// will terminate!
FrameElement remoteLabelOwner = elt.getRemoteLabelOwner();
// If the given element cannot have a remote label, nothing to do
if (remoteLabelOwner == null) {
return;
}
IWidget remoteLabel =
(IWidget)
remoteLabelOwner.getAttribute(WidgetAttributes.REMOTE_LABEL_ATTR);
// If the element does not have a remote label, nothing to do
// Otherwise, duplicate the remote label and reset both attributes
// (duplicated label and duplicated owner) to refer to each other.
// If the label has already been duplicated, the duplicateWidget will
// simply use widgetSituator to return the copy.
if (! NullSafe.equals(remoteLabel, WidgetAttributes.NONE_SELECTED)) {
IWidget remoteLabelCopy = duplicateWidget(remoteLabel,
duplicator,
widgetSituator,
moveByX,
moveByY);
FrameElement ownerCopy = eltCopy.getRemoteLabelOwner();
ownerCopy.setAttribute(WidgetAttributes.REMOTE_LABEL_ATTR,
remoteLabelCopy);
// Update the label copy to refer to the element copy as its owner
remoteLabelCopy.setAttribute(WidgetAttributes.REMOTE_LABEL_OWNER_ATTR,
ownerCopy);
}
} // duplicateRemoteLabel
protected static DuplicateWidgetSituator widgetSituator =
new DuplicateWidgetSituator();
protected FrameElementGroup duplicateGroup(FrameElementGroup eltGroup,
Map<FrameElementGroup,
FrameElementGroup> copies,
Frame frameCopy,
Frame.IFrameDuplicator duplicator)
{
FrameElementGroup eltGroupCopy = copies.get(eltGroup);
if (eltGroupCopy == null) {
eltGroupCopy = eltGroup.twin();
Iterator<FrameElement> grpMembers = eltGroup.iterator();
while (grpMembers.hasNext()) {
FrameElement mbrElt = grpMembers.next();
FrameElement eltCopy = null;
if (mbrElt instanceof IWidget) {
eltCopy = widgetSituator.getDuplicate((IWidget) mbrElt);
}
else if (mbrElt instanceof SimpleWidgetGroup) {
eltCopy = widgetSituator.getGroup((SimpleWidgetGroup) mbrElt);
}
else if (mbrElt instanceof FrameElementGroup) {
eltCopy = duplicateGroup((FrameElementGroup) mbrElt,
copies,
frameCopy,
duplicator);
}
if (eltCopy != null) {
eltGroupCopy.add(eltCopy);
}
}
duplicateRemoteLabel(eltGroup, eltGroupCopy,
duplicator, widgetSituator,
0.0, 0.0);
frameCopy.addEltGroup(eltGroupCopy);
copies.put(eltGroup, eltGroupCopy);
}
return eltGroupCopy;
} // duplicateGroup
/**
* Create a "deep" copy of this frame.
* <p>
* Because a "deep" copy involves the Transitions that emanate from
* this frame's Widgets and/or InputDevices, a FrameDuplicator
* is required to manage the generated duplicates.
* <p>
* When called from Design.duplicate, the FrameDuplicator will
* "place" the copy in the duplicate Design. Otherwise, it may
* be the responsibility of the caller to "place" the copy
* (usually by adding it to an Design).
*
* @param newName the name of the resulting copy
* @param duplicator the manager of duplicate Frame instances
* @return the frame copy
* @author mlh
*/
public Frame duplicate(String newName, Frame.IFrameDuplicator duplicator)
{
Frame frameCopy = new Frame(newName, devices.keySet());
// Invoke in stack-like manner since nested duplication may
// call this duplicate method recursively.
Frame oldFrameContext = widgetSituator.getCurrentFrameContext();
widgetSituator.reset(frameCopy);
duplicator.recordDuplicateFrame(this, frameCopy);
// Deep copy transitions for each InputDevice
Iterator<InputDevice> deviceIt = devices.values().iterator();
while (deviceIt.hasNext()) {
InputDevice device = deviceIt.next();
InputDevice deviceCopy =
frameCopy.getInputDevice(device.getDeviceType());
deviceCopy.duplicateTransitions(device, duplicator);
}
// Adjust properties
frameCopy.setFrameOrigin(origin);
frameCopy.setBackgroundImage(background, backgroundBounds);
frameCopy.setWidgetColor(widgetColor);
frameCopy.setSpeakerText(speakerText);
frameCopy.setListenTimeInSecs(listenTimeInSecs);
// Copy list of widgets so we may sort according to level
// (BECAUSE! addWidget will reset the level!)
List<IWidget> widgetsCopy = new ArrayList<IWidget>(widgets);
Collections.sort(widgetsCopy, Widget.WidgetLevelComparator.ONLY);
// Add deep copies of the contained widgets.
Iterator<IWidget> widgetIt = widgetsCopy.iterator();
while (widgetIt.hasNext()) {
IWidget widget = widgetIt.next();
IWidget widgetCopy =
duplicateWidget(widget, duplicator, widgetSituator);
duplicateRemoteLabel(widget, widgetCopy,
duplicator, widgetSituator,
0.0, 0.0);
}
widgetSituator.completeWork();
// To hold copied frame element groups
Map<FrameElementGroup, FrameElementGroup> eltGroupCopies =
new HashMap<FrameElementGroup, FrameElementGroup>();
// All of the widgets and IWidgetGroups have been copied as well
// as their remote labels; now handle frame element groups.
Iterator<FrameElementGroup> eltGroupIt = eltGroups.iterator();
while (eltGroupIt.hasNext()) {
FrameElementGroup eltGroup = eltGroupIt.next();
FrameElementGroup eltGroupCopy = duplicateGroup(eltGroup,
eltGroupCopies,
frameCopy,
duplicator);
duplicateRemoteLabel(eltGroup, eltGroupCopy,
duplicator, widgetSituator,
0.0, 0.0);
}
// Reset frame context in case called recursively
// Now can reset recursion -- note: used by duplicateGroup and
// duplicateRemoteLabel above
widgetSituator.resetRecursion(oldFrameContext);
frameCopy.copyAttributes(this);
return frameCopy;
} // duplicate
}