/**
* InteractiveWidget.java
* Copyright (C) 2010 New Zealand Digital Library, http://expeditee.org
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
package org.expeditee.items.widgets;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.lang.reflect.Constructor;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.CommandLineParser;
import org.apache.commons.cli.GnuParser;
import org.apache.commons.cli.HelpFormatter;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException;
import org.expeditee.Util;
import org.expeditee.actions.Actions;
import org.expeditee.core.Anchoring;
import org.expeditee.core.Clip;
import org.expeditee.core.Colour;
import org.expeditee.core.Dimension;
import org.expeditee.core.EcosystemSpecific;
import org.expeditee.core.Fill;
import org.expeditee.core.Point;
import org.expeditee.core.bounds.AxisAlignedBoxBounds;
import org.expeditee.gio.EcosystemManager;
import org.expeditee.gio.EcosystemManager.Ecosystem;
import org.expeditee.gio.GraphicsManager;
import org.expeditee.gui.DisplayController;
import org.expeditee.gui.Frame;
import org.expeditee.gui.FrameIO;
import org.expeditee.gui.FreeItems;
import org.expeditee.items.Item;
import org.expeditee.items.ItemParentStateChangedEvent;
import org.expeditee.items.ItemUtils;
import org.expeditee.items.Text;
/**
* The bridge between swing space and Expeditee space
*
* @author Brook
*
*/
public abstract class Widget implements EcosystemSpecific
{
/** The colour to fill the widget area with when not rendering the widget itself (i.e. when picked up). */
protected final static Colour FREESPACE_BACKCOLOR = Colour.FromRGB255(100, 100, 100);
/** The minimum border thickness for widgets. */
public final static float DEFAULT_MINIMUM_BORDER_THICKNESS = 1.0f;
/* GUIDE:
* d1--l1---d2
* | |
* l4 | X | l2
* | |
* d4---l3--d3
*/
/** The top-left corner of the widget. */
private WidgetCorner _d1;
/** The top-right corner of the widget. */
private WidgetCorner _d2;
/** The bottom-right corner of the widget. */
private WidgetCorner _d3;
/** The bottom-left corner of the widget. */
private WidgetCorner _d4;
/** The top edge of the widget. */
private WidgetEdge _l1;
/** The right edge of the widget. */
private WidgetEdge _l2;
/** The bottom edge of the widget. */
private WidgetEdge _l3;
/** The left edge of the widget. */
private WidgetEdge _l4;
/** Used for quickly returning item list. */
private List- _expediteeItems;
// Widget size restrictions
/** Minimum width of the widget. */
private int _minWidth = 50;
/** Minimum height of the widget. */
private int _minHeight = 50;
/** Maximum width of the widget. */
private int _maxWidth = 300;
/** Maximum height of the widget. */
private int _maxHeight = 300;
/** The Expeditee item that is used for saving widget state. */
protected Text _textRepresentation;
/** The anchoring of the widget. */
protected Anchoring _anchoring = new Anchoring();
/**
* Creates a InteractiveWidget from a text item.
*
* @param source
* Must not be null, first line of text used - which the format
* must be as follows: "@iw: <> [<>] [<>] [: [<>] [<>]
* [...]]".
*
* e.g: "@iw: org.expeditee.items.SampleWidget1 100 20 : 2" creates a
* SampleWidget1 with width = 100 and height = 20 with 1 argument = "2"
*
* @return An InteractiveWidget instance. Never null.
*
* @throws NullPointerException
* if source is null
*
* @throws IllegalArgumentException
* if source's text is in the incorrect format or if source's
* parent is null
*
* @throws InteractiveWidgetNotAvailableException
* If the given widget class name in the source text doesn't
* exist or not an InteractiveWidget or the widget does not
* supply a valid constructor for creation.
*
* @throws InteractiveWidgetInitialisationFailedException
* The sub-constructor failed - depends on the type of widget
* being instantainiated.
*
* class names beginning with $, the $ will be replaced with
* "org.expeditee.items."
*/
public static Widget createWidget(final Text source) throws InteractiveWidgetNotAvailableException,
InteractiveWidgetInitialisationFailedException
{
if (source == null) {
throw new NullPointerException("source");
}
if (source.getParent() == null) {
throw new IllegalArgumentException("source's parent is null, InteractiveWidget's must be created from Text items with non-null parents");
}
final String TAG = ItemUtils.GetTag(ItemUtils.TAG_IWIDGET);
String text = source.getText();
if (text == null) {
throw new IllegalArgumentException("source does not have any text");
}
text = text.trim();
// Check starts with the widget tag and separator
if (!text.startsWith(TAG + ":")) {
throw new IllegalArgumentException("Source text must begin with \"" + TAG + ":\"");
}
// skip over the '@iw:' preamble
text = text.substring(TAG.length() + 1).trim();
int index = 0;
if (text.length() > 0) {
// Having removed @iw:, then next ':' is used for signifying start of arguments
index = text.indexOf(':');
}
if (index == 0) {
throw new IllegalArgumentException("Source text must begin with \"" + TAG + "\"");
}
//
// Step 1:
// For an X-rayable text item in the form:
// @iw: [options] width height :
//
// the 'core' part of the X-rayable text item
// i.e, the text between the first ':' and the second ':'
//
//
final String tokens_str = (index == -1) ? text : text.substring(0, index);
String[] tokens = Text.parseArgsApache(tokens_str);
// make anything starting with a '-' lowercase
for (int i=0; i as left anchor for the vertical lefthand edge of the interactive widget" )
.hasArg()
.withArgName("")
.create("al");
@SuppressWarnings("static-access")
Option anchor_right_opt = OptionBuilder.withLongOpt( "anchorright" )
.withDescription( "use as right anchor for the vertical righthand edge of the interactive widget" )
.hasArg()
.withArgName("")
.create("ar");
@SuppressWarnings("static-access")
Option anchor_top_opt = OptionBuilder.withLongOpt( "anchortop" )
.withDescription( "use as top anchor for the horizontal top end of the interactive widget" )
.hasArg()
.withArgName("")
.create("at");
@SuppressWarnings("static-access")
Option anchor_bottom_opt = OptionBuilder.withLongOpt( "anchorbottom" )
.withDescription( "use as bottom anchor for the horizontal bottom edge of the interactive widget" )
.hasArg()
.withArgName("")
.create("ab");
options.addOption(anchor_left_opt);
options.addOption(anchor_right_opt);
options.addOption(anchor_top_opt);
options.addOption(anchor_bottom_opt);
*/
CommandLine core_line;
try {
// parse the command line arguments
core_line = parser.parse( options, tokens );
// Update tokens to be version with the options removed
tokens = core_line.getArgs();
}
catch( final ParseException exp ) {
System.err.println( "Unexpected exception:" + exp.getMessage() );
core_line = null;
}
final HelpFormatter formatter = new HelpFormatter();
final StringWriter usage_str_writer = new StringWriter();
final PrintWriter printWriter = new PrintWriter(usage_str_writer);
String widget_name = tokens[0];
final String widget_prefix = "org.expeditee.items.widgets";
if (widget_name.startsWith(widget_prefix)) {
widget_name = widget_name.substring(widget_prefix.length());
}
formatter.printHelp(printWriter, 80, TAG + ":" + widget_name + " [options] width height", null, options, 4, 0, null);
//System.out.println(usage_str_writer.toString());
float width = -1, height = -1;
if (tokens.length < 1) {
throw new IllegalArgumentException("Missing widget class name in source text");
}
try {
if (tokens.length >= 2) { // parse optional width
width = Integer.parseInt(tokens[1]);
width = (width <= 0) ? width = -1 : width;
}
if (tokens.length >= 3) { // parse optional height
height = Integer.parseInt(tokens[2]);
height = (height <= 0) ? height = -1 : height;
}
} catch (final NumberFormatException nfe) {
throw new IllegalArgumentException(
"Bad width or height given in source text", nfe);
}
if (tokens.length > 3) {
throw new IllegalArgumentException(
"to many arguments given before \":\" in source text");
}
String classname = tokens[0];
if (classname.charAt(0) == '$'){
classname = classname.substring(1);
}
// Attempt to locate the class using reflection
final Class> iwclass = findIWidgetClass(classname);
if (iwclass == null) {
throw new InteractiveWidgetNotAvailableException(classname
+ " does not exist or is not an InteractiveWidget");
}
//
// Step 2:
// For an X-rayable text item in the form:
// @iw: [options] width height :
//
// Parse the part
// Extract out the parameters - if any
String[] args = null;
if (index > 0) { // index of the first ":"
if (text.length()>(index+1)) {
final String args_str = text.substring(index + 1);
args = Text.parseArgsApache(args_str);
}
}
Widget inst = null;
try {
// Instantiate the widget - passing the params
final Class parameterTypes[] = new Class[] { Text.class, String[].class };
final Constructor ct = iwclass.getConstructor(parameterTypes);
final Object arglist[] = new Object[] { source, args };
inst = (Widget) ct.newInstance(arglist);
} catch (final Exception e) {
throw new InteractiveWidgetNotAvailableException(
"Failed to create instance via reflection: " + e.toString(),
e);
}
// Check that the widget is suitable for the current platform
final Ecosystem systemType = EcosystemManager.getType();
if (!inst.isSupportedOnEcosystem(systemType)) {
// Try create a JavaFXSwingWidget if creating a Swing widget in JavaFX
if (systemType == Ecosystem.JavaFX && inst.isSupportedOnEcosystem(Ecosystem.Swing)) {
inst = new JavaFXSwingWidget((SwingWidget) inst);
} else {
throw new InteractiveWidgetNotAvailableException(classname + " is not supported while using a " + systemType + " system.");
}
}
// Step 3:
// Set up the size and position of the widget
// Use default dimensions if not provided (or provided as negative
// values)
if (width <= 0) {
width = inst.getWidth();
}
if (height <= 0) {
height = inst.getHeight();
}
inst.setSize(width, height);
// Apply any anchor values supplied in the core part of the @iw item
Integer anchor_left = null;
Integer anchor_right = null;
Integer anchor_top = null;
Integer anchor_bottom = null;
if(core_line.hasOption( "anchorleft" ) ) {
final String al_str = core_line.getOptionValue( "anchorleft" );
anchor_left = (int) Float.parseFloat(al_str);
}
if(core_line.hasOption( "anchorright" ) ) {
final String ar_str = core_line.getOptionValue( "anchorright" );
anchor_right = (int) Float.parseFloat(ar_str);
}
if(core_line.hasOption( "anchortop" ) ) {
final String at_str = core_line.getOptionValue( "anchortop" );
anchor_top = (int) Float.parseFloat(at_str);
}
if(core_line.hasOption( "anchorbottom" ) ) {
final String ab_str = core_line.getOptionValue( "anchorbottom" );
anchor_bottom = (int) Float.parseFloat(ab_str);
}
inst.setAnchorCorners(anchor_left,anchor_right,anchor_top,anchor_bottom);
return inst;
}
/**
* Locates the class from the classname of an InteractiveWidget class
*
* @param classname
* The name of the class to search
* @return Null if doesn't exist or not an InteractiveWidget
*/
private static Class findIWidgetClass(String classname)
{
if(classname == null) {
return null;
}
// try just the classname
try {
final Class c = Class.forName(classname); // attempt to find the class
// If one is found, ensure that it is a descendant of an
// InteractiveWidget
for (Class superclass = c.getSuperclass(); superclass != null
&& superclass != Item.class; superclass = superclass
.getSuperclass()) {
if (superclass == Widget.class) {
return c;
}
}
} catch (final ClassNotFoundException e) {
}
// see if the class is a widget with invalid capitalisation, or missing the widget package prefix
if(classname.startsWith(Actions.WIDGET_PACKAGE)) {
classname = classname.substring(Actions.WIDGET_PACKAGE.length());
}
try {
final Class c = Class.forName(Actions.getClassName(classname)); // attempt to find the class
// If one is found, ensure that it is a descendant of an
// InteractiveWidget
for (Class superclass = c.getSuperclass(); superclass != null
&& superclass != Item.class; superclass = superclass
.getSuperclass()) {
if (superclass == Widget.class) {
return c;
}
}
} catch (final ClassNotFoundException e) {
}
// Doesn't exist or not an InteractiveWidget
return null;
}
/**
* Arguments represent the widgets current state state. They are
* used for saving, loading, creating and cloning Special formatting is done
* for you.
*
* @see #getData()
*
* @return Can be null for no params.
*/
protected abstract String[] getArgs();
/**
* Data represents the widgets current state state. For any state
* information you do not wish to be defined as arguments (e.g. metadata),
* you can set as the widgets source items data.
*
* The default implementation returns null. Override to make use of.
*
* @see #getArgs()
*
* @return Null for for data. Otherwise the data that represent this widgets
* current state
*/
protected List getData()
{
return null;
}
/**
* Constructor
*
* @param source
* Must not be null. Neither must it's parent
*
* @param component
* Must not be null.
*
* @param minWidth
* The min width restriction for the widget. If negative, then
* there is no restriction.
*
* @param maxWidth
* The max width restriction for the widget. If negative, then
* there is no restriction.
*
* @param minHeight
* The min height restriction for the widget. If negative, then
* there is no restriction.
*
* @param maxHeight
* The max height restriction for the widget. If negative, then
* there is no restriction.
*
* @throws NullPointerException
* If source, component if null.
*
* @throws IllegalArgumentException
* If source's parent is null. If maxWidth smaller than minWidth
* and maxWidth larger or equal to zero or if maxHeight smaller
* than minHeight && maxHeight is larger or equal to zero
*
*/
protected Widget(final Text source, final int minWidth, final int maxWidth, final int minHeight, final int maxHeight)
{
if (source == null) {
throw new NullPointerException("source");
}
if (source.getParent() == null) {
throw new IllegalArgumentException("source's parent is null, InteractiveWidget's must be created from Text items with non-null parents");
}
//addThisAsContainerListenerToContent();
//addKeyListenerToWidget();
_textRepresentation = source;
setSizeRestrictions(minWidth, maxWidth, minHeight, maxHeight); // throws IllegalArgumentException's
final int x = source.getX();
final int y = source.getY();
final int width = (_minWidth < 0) ? 10 : _minWidth;
final int height = (_minHeight < 0) ? 10 : _minHeight;
final Frame idAllocator = _textRepresentation.getParent();
// create WidgetCorners
_d1 = new WidgetCorner(x, y, idAllocator.getNextItemID(), this);
_d2 = new WidgetCorner(x + width, y, idAllocator.getNextItemID(), this);
_d3 = new WidgetCorner(x + width, y + height, idAllocator.getNextItemID(), this);
_d4 = new WidgetCorner(x, y + height, idAllocator.getNextItemID(), this);
// create WidgetEdges
_l1 = new WidgetEdge(_d1, _d2, idAllocator.getNextItemID(), this);
_l2 = new WidgetEdge(_d2, _d3, idAllocator.getNextItemID(), this);
_l3 = new WidgetEdge(_d3, _d4, idAllocator.getNextItemID(), this);
_l4 = new WidgetEdge(_d4, _d1, idAllocator.getNextItemID(), this);
final Collection
- enclist = new ArrayList
- (4);
enclist.add(_d1);
enclist.add(_d2);
enclist.add(_d3);
enclist.add(_d4);
_d1.setEnclosedList(enclist);
_d2.setEnclosedList(enclist);
_d3.setEnclosedList(enclist);
_d4.setEnclosedList(enclist);
_expediteeItems = new ArrayList
- (8); // Note: order important
_expediteeItems.add(_d1);
_expediteeItems.add(_d2);
_expediteeItems.add(_d3);
_expediteeItems.add(_d4);
_expediteeItems.add(_l1);
_expediteeItems.add(_l2);
_expediteeItems.add(_l3);
_expediteeItems.add(_l4);
setWidgetEdgeColor(source.getBorderColor());
setWidgetEdgeThickness(source.getThickness());
}
/**
* Sets the restrictions - checks values.
*
* @param minWidth
* The min width restriction for the widget. If negative, then
* there is no restriction.
*
* @param maxWidth
* The max width restriction for the widget. If negative, then
* there is no restriction.
*
* @param minHeight
* The min height restriction for the widget. If negative, then
* there is no restriction.
*
* @param maxHeight
* The max height restriction for the widget. If negative, then
* there is no restriction.
*
* @throws IllegalArgumentException
* If maxWidth smaller than minWidth and maxWidth larger or
* equal to zero or if maxHeight smaller than minHeight &&
* maxHeight is larger or equal to zero
*
*/
private void setSizeRestrictions(final int minWidth, final int maxWidth, final int minHeight, final int maxHeight)
{
_minWidth = minWidth;
if (maxWidth < _minWidth && maxWidth >= 0) {
throw new IllegalArgumentException("maxWidth smaller than the min Width");
}
_maxWidth = maxWidth;
_minHeight = minHeight;
if (maxHeight < _minHeight && maxHeight >= 0) {
throw new IllegalArgumentException("maxHeight smaller than the min Height");
}
_maxHeight = maxHeight;
}
public int getMinWidth()
{
return _minWidth;
}
public int getMinHeight()
{
return _minHeight;
}
public int getMaxWidth()
{
return _maxWidth;
}
public int getMaxHeight()
{
return _maxHeight;
}
/**
* This can be overridden for creating custom copies. The default
* implementation creates a new widget based on the current state of the
* widget (via getArgs).
*
* @see InteractiveWidget#getArgs().
*
* @return A copy of this widget.
*
*/
public Widget copy() throws InteractiveWidgetNotAvailableException, InteractiveWidgetInitialisationFailedException
{
final Text t = _textRepresentation.copy();
final String clonedAnnotation = getAnnotationString();
t.setText(clonedAnnotation);
t.setData(getData());
return Widget.createWidget(t);
}
/**
* Notifies widget of delete
*/
public void onDelete()
{
// Allocate new ID's
Frame parent = getParentFrame();
if (parent == null) {
parent = DisplayController.getCurrentFrame();
}
if (parent != null) {
for (final Item i : _expediteeItems) {
i.setID(parent.getNextItemID());
}
}
invalidateLink();
}
/**
* Note updates the source text with current state info
*
* @return The Text item that this widget was created from.
*/
public Text getSource()
{
// Build the annotation string such that it represents this widgets
// current state
final String newAnnotation = getAnnotationString();
// Set the new text
_textRepresentation.setText(newAnnotation);
// Set the data
_textRepresentation.setData(getData());
return _textRepresentation;
}
/**
* @return The current representation for this widget. The representation
* stores link information, data etc... It is used for saving and
* loading of the widget. Never null.
*
*/
protected Item getCurrentRepresentation()
{
return _textRepresentation;
}
/**
* @return The Expeditee annotation string.
*/
protected String getAnnotationString() {
// Create tag and append classname
final StringBuilder sb = new StringBuilder(ItemUtils.GetTag(ItemUtils.TAG_IWIDGET));
sb.append(':');
sb.append(' ');
sb.append(getClass().getName());
if (_anchoring.getLeftAnchor() != null) {
sb.append(" --anchorLeft " + Math.round(_anchoring.getLeftAnchor()));
}
if (_anchoring.getRightAnchor() != null) {
sb.append(" --anchorRight " + Math.round(_anchoring.getRightAnchor()));
}
if (_anchoring.getTopAnchor() != null) {
sb.append(" --anchorTop " + Math.round(_anchoring.getTopAnchor()));
}
if (_anchoring.getBottomAnchor() != null) {
sb.append(" --anchorBottom " + Math.round(_anchoring.getBottomAnchor()));
}
// Append size information if needed (not an attribute of text items)
if (!isFixedSize()) {
sb.append(' ');
sb.append(getWidth());
sb.append(' ');
sb.append(getHeight());
}
// Append arguments if any
final String stateArgs = Util.formatArgs(getArgs());
if (stateArgs != null) {
sb.append(':');
sb.append(stateArgs);
}
return sb.toString();
}
/**
* Sets both the new size as well as the new min/max widget/height
* restrictions.
*
* @param minWidth
* The min width restriction for the widget. If negative, then
* there is no restriction.
*
* @param maxWidth
* The max width restriction for the widget. If negative, then
* there is no restriction.
*
* @param minHeight
* The min height restriction for the widget. If negative, then
* there is no restriction.
*
* @param maxHeight
* The max height restriction for the widget. If negative, then
* there is no restriction.
*
* @param newWidth
* Clamped to new restrictions.
*
* @param newHeight
* Clamped to new restrictions.
*
* @throws IllegalArgumentException
* If maxWidth smaller than minWidth and maxWidth larger or
* equal to zero or if maxHeight smaller than minHeight &&
* maxHeight is larger or equal to zero
*
* @see #setSize(float, float)
*
*/
public void setSize(final int minWidth, final int maxWidth, final int minHeight, final int maxHeight, final float newWidth, final float newHeight)
{
setSizeRestrictions(minWidth, maxWidth, minHeight, maxHeight);
setSize(newWidth, newHeight);
}
/**
* Clamped to current min/max width/height.
*
* @param width
* Clamped to current restrictions.
* @param height
* Clamped to current restrictions.
*
* @see #setSize(int, int, int, int, float, float)
*/
public void setSize(float width, float height)
{
// If 'width' and 'height' exceed the min/max values for width/height
// => clamp to the relevant min/max value
if (width < _minWidth && _minWidth >= 0) {
width = _minWidth;
} else if (width > _maxWidth && _maxWidth >= 0) {
width = _maxWidth;
}
if (height < _minHeight && _minHeight >= 0) {
height = _minHeight;
} else if (height > _maxHeight && _maxHeight >= 0) {
height = _maxHeight;
}
// Remember current isFloating() values
final boolean vfloating[] = new boolean[] { _d1.isFloating(), _d2.isFloating(), _d3.isFloating(), _d4.isFloating() };
_d1.setFloating(true);
_d2.setFloating(true);
_d3.setFloating(true);
_d4.setFloating(true);
final float xr = _d1.getX() + width;
final float yb = _d1.getY() + height;
_d2.setX(xr);
_d3.setX(xr);
_d3.setY(yb);
_d4.setY(yb);
// Restore isFloating() values
_d1.setFloating(vfloating[0]);
_d2.setFloating(vfloating[1]);
_d3.setFloating(vfloating[2]);
_d4.setFloating(vfloating[3]);
onSizeChanged();
}
public void setAnchorCorners(final Integer left, final Integer right, final Integer top, final Integer bottom)
{
setAnchorLeft(left);
setAnchorRight(right);
setAnchorTop(top);
setAnchorBottom(bottom);
}
public void setPosition(final int x, final int y)
{
if (x == getX() && y == getY()) {
return;
}
// Remember current isFloating() values
final boolean vfloating[] = new boolean[] { _d1.isFloating(), _d2.isFloating(), _d3.isFloating(), _d4.isFloating() };
final int width = getWidth();
final int height = getHeight();
invalidateLink();
_d1.setFloating(true);
_d2.setFloating(true);
_d3.setFloating(true);
_d4.setFloating(true);
_d1.setPosition(x, y);
_d2.setPosition(x + width, y);
_d3.setPosition(x + width, y + height);
_d4.setPosition(x, y + height);
// Restore isFloating() values
_d1.setFloating(vfloating[0]);
_d2.setFloating(vfloating[1]);
_d3.setFloating(vfloating[2]);
_d4.setFloating(vfloating[3]);
invalidateLink();
onMoved();
}
private boolean _settingPositionFlag = false; // used for recursion
/**
* Updates position of given WidgetCorner to the given (x,y),
* and updates related values (connected corners, width and height)
*
* @param src
* @param x
* @param y
* @return False if need to call super.setPosition
*/
public boolean setPositions(final WidgetCorner src, final float x, final float y)
{
if (_settingPositionFlag) {
return false;
}
_settingPositionFlag = true;
invalidateLink();
// Check to see if the widget is fully being picked up
final boolean isAllPickedUp = (_d1.isFloating() && _d2.isFloating() && _d3.isFloating() && _d4.isFloating());
// If so, then this will be called one by one ..
if (isAllPickedUp) {
src.setPosition(x, y);
} else {
float newX = x;
// Reference:
// D1 D2
// D3 D4
//
// GUIDE:
// d1--l1---d2
// | |
// l4 | X | l2
// | |
// d4---l3--d3
//
// X Positions
if (src == _d1 || src == _d4) {
// Check min X constraint
if (_minWidth >= 0) {
if ((_d2.getX() - x) < _minWidth) {
newX = _d2.getX() - _minWidth;
}
}
// Check max X constraint
if (_maxWidth >= 0) {
if ((_d2.getX() - x) > _maxWidth) {
newX = _d2.getX() - _maxWidth;
}
}
if (!(src == _d4 && _d1.isFloating() && _d4.isFloating())) {
_d1.setX(newX);
}
if (!(src == _d1 && _d4.isFloating() && _d1.isFloating())) {
_d4.setX(newX);
}
} else if (src == _d2 || src == _d3) {
// Check min X constraint
if (_minWidth >= 0) {
if ((x - _d1.getX()) < _minWidth) {
newX = _d1.getX() + _minWidth;
}
}
// Check max X constraint
if (_maxWidth >= 0) {
if ((x - _d1.getX()) > _maxWidth) {
newX = _d1.getX() + _maxWidth;
}
}
if (!(src == _d3 && _d2.isFloating() && _d3.isFloating())) {
_d2.setX(newX);
}
if (!(src == _d2 && _d3.isFloating() && _d2.isFloating())) {
_d3.setX(newX);
}
}
float newY = y;
// Y Positions
if (src == _d1 || src == _d2) {
// Check min Y constraint
if (_minHeight >= 0) {
if ((_d4.getY() - y) < _minHeight) {
newY = _d4.getY() - _minHeight;
}
}
// Check max Y constraint
if (_maxHeight >= 0) {
if ((_d4.getY() - y) > _maxHeight) {
newY = _d4.getY() - _maxHeight;
}
}
if (!(src == _d2 && _d1.isFloating() && _d2.isFloating())) {
_d1.setY(newY);
}
if (!(src == _d1 && _d2.isFloating() && _d1.isFloating())) {
_d2.setY(newY);
}
} else if (src == _d3 || src == _d4) {
// Check min Y constraint
if (_minHeight >= 0) {
if ((y - _d1.getY()) < _minHeight) {
newY = _d1.getY() + _minHeight;
}
}
// Check max Y constraint
if (_maxHeight >= 0) {
if ((y - _d1.getY()) > _maxHeight) {
newY = _d1.getY() + _maxHeight;
}
}
if (!(src == _d4 && _d3.isFloating() && _d4.isFloating())) {
_d3.setY(newY);
}
if (!(src == _d3 && _d4.isFloating() && _d3.isFloating())) {
_d4.setY(newY);
}
}
}
// Update source text position so position is remembered from loading
final float newTextX = getX();
final float newTextY = getY();
if (_textRepresentation.getX() != newTextX || _textRepresentation.getY() != newTextY) {
_textRepresentation.setPosition(newTextX, newTextY);
}
_settingPositionFlag = false;
invalidateLink();
onMoved();
onSizeChanged();
return true;
}
public int getX()
{
return Math.min(_d1.getX(), _d2.getX());
}
public int getY()
{
return Math.min(_d1.getY(), _d4.getY());
}
public int getWidth()
{
return Math.abs(_d2.getX() - _d1.getX());
}
public int getHeight()
{
return Math.abs(_d4.getY() - _d1.getY());
}
public Point getPosition()
{
return new Point(getX(), getY());
}
public Dimension getSize()
{
return new Dimension(getWidth(), getHeight());
}
/**
* The order of the items in the list is as specified: _d1 _d2 _d3 _d4 _l1
* _l2 _l3 _l4
*
* @return All of the Expeditee items that form the boundaries of this
* widget in an unmodifiable list
*/
public List
- getItems()
{
return Collections.unmodifiableList(_expediteeItems);
}
public final void onParentStateChanged(final ItemParentStateChangedEvent e)
{
switch (e.getEventType()) {
case ItemParentStateChangedEvent.EVENT_TYPE_REMOVED:
case ItemParentStateChangedEvent.EVENT_TYPE_REMOVED_VIA_OVERLAY:
case ItemParentStateChangedEvent.EVENT_TYPE_HIDDEN:
EcosystemManager.removeInteractiveWidget(this);
break;
case ItemParentStateChangedEvent.EVENT_TYPE_ADDED:
case ItemParentStateChangedEvent.EVENT_TYPE_ADDED_VIA_OVERLAY:
case ItemParentStateChangedEvent.EVENT_TYPE_SHOWN:
case ItemParentStateChangedEvent.EVENT_TYPE_SHOWN_VIA_OVERLAY:
EcosystemManager.addInteractiveWidget(this);
addWidgetContent(e);
break;
}
DisplayController.invalidateItem(_d1, getContentBounds());
// Forward filtered event to upper classes...
onParentStateChanged(e.getEventType());
}
/**
* Subclassing Widgets overwrite to add their specific content to the Frame.
* For example, a widget being Java Swing must add their Swing component to Expeditee's content pane.
* @param e Can be used to identify if it is appropriate to draw the widget.
*/
abstract protected void addWidgetContent(final ItemParentStateChangedEvent e);
/**
* Subclassing Widgets overwrite to specify their own functionality as to how key events
* are to be forwarded to Expeditee.
*/
abstract protected void addKeyListenerToWidget();
/**
* Subclassing widgets overwrite to specify how content will respond to being added and removed.
*/
abstract protected void addThisAsContainerListenerToContent();
/**
* Subclassing widgets overwrite to provide the size and position of their content.
* For example, a widget using Java Swing might return _swingComponent.getBounds().
* @return The bounds of the content that is being drawn.
*/
abstract public AxisAlignedBoxBounds getContentBounds();
/**
* Due to absolute positioning...
* @param parent
*/
abstract protected void layout();
/**
* Subclassing widgets overwrite to respond to changes in widget bounds.
*/
abstract protected void onBoundsChanged();
/**
* Override to make use of. Internally this is reported once by all corners,
* but is filtered out so that this method is invoked once per event.
*
* @param eventType
* The {@link ItemParentStateChangedEvent#getEventType()} that
* occured.
*
*/
protected void onParentStateChanged(final int eventType)
{
}
/**
* @return True if at least one corner is floating
*/
public boolean isFloating()
{
return _d1.isFloating() || _d2.isFloating() || _d3.isFloating() || _d4.isFloating();
}
public boolean areCornersFullyAnchored()
{
return _d1.getParent() != null && _d2.getParent() != null && _d3.getParent() != null && _d4.getParent() != null;
}
/**
*
* @return The current bounds for this widget. Never null.
*/
public AxisAlignedBoxBounds getBounds()
{
return getContentBounds();
//return new AxisAlignedBoxBounds(getX(), getY(), getWidth(), getHeight());
}
protected void paintLink()
{
// If this widget is linked .. then draw the link icon
if (_textRepresentation.getLink() != null || _textRepresentation.hasAction()) {
_textRepresentation.paintLinkGraphic(getLinkX(), getLinkY());
}
}
private int getLinkX()
{
return getX() - Item.LEFT_MARGIN;
}
private int getLinkY()
{
return getY() + (getHeight() / 2);
}
/** Invoked whenever the widget is to be repainted in free space. */
protected void paintInFreeSpace()
{
final GraphicsManager g = EcosystemManager.getGraphicsManager();
g.drawRectangle(new Point(getX(), getY()), new Dimension(getWidth(), getHeight()), 0.0, new Fill(FREESPACE_BACKCOLOR), null, null, null);
}
public final void paint()
{
paintWidget();
paintLink();
}
/** Should be overridden by native widgets to draw themselves. */
protected abstract void paintWidget();
/**
* Called from the widgets corners: Whenever one of the corners are invoked
* for a refill of the enclosed area.
*
* If the widget is floating (e.g. currently picked up / rubberbanding) then
* a shaded area is drawn instead of the actual widget so the manipulation
* of the widget is as smooth as possible.
*
* @param g
* @param notifier
*/
void paintFill()
{
//if (_swingComponent.getParent() == null) {
// Note that frames with @f may want to paint the widgets so do not
// paint over the widget interface in these cases: must only
// paint if an object is floating
if (isFloating()) {
paintInFreeSpace();
paintLink();
}
//}
}
/**
* @return True if this widget cannot be resized in either directions
*/
public boolean isFixedSize()
{
return this._minHeight == this._maxHeight &&
this._minWidth == this._maxWidth &&
this._minHeight >= 0 &&
this._minWidth >= 0;
}
/**
* Removes this widget from the parent frame or free space.
*
* @return True if removed from a parent frame. Thus a parent changed event
* will be invoked.
*
* False if removed purely from free space.
*/
protected boolean removeSelf()
{
final Frame parent = getParentFrame();
if (parent != null) {
parent.removeAllItems(_expediteeItems);
}
FreeItems.getInstance().removeAll(_expediteeItems);
return (parent != null);
}
/**
* @return The parent frame. Null if has none. Note: Based on corners
* parents.
*/
public Frame getParentFrame()
{
Frame parent = null;
if (_d1.getParent() != null) {
parent = _d1.getParent();
} else if (_d2.getParent() != null) {
parent = _d2.getParent();
} else if (_d3.getParent() != null) {
parent = _d3.getParent();
} else if (_d4.getParent() != null) {
parent = _d4.getParent();
}
return parent;
}
protected void invalidateSelf()
{
final AxisAlignedBoxBounds dirty = new AxisAlignedBoxBounds(getX(), getY(), getWidth(), getHeight());
DisplayController.invalidateArea(dirty);
invalidateLink();
}
/**
* Invalidates the link for this widget - if it has one.
*/
protected void invalidateLink()
{
if (_textRepresentation.getLink() != null || _textRepresentation.hasAction()) {
final AxisAlignedBoxBounds linkArea = _textRepresentation.getLinkDrawArea(getLinkX(), getLinkY());
DisplayController.invalidateArea(linkArea);
}
}
/**
* @see ItemUtils#isVisible(Item)
*
* @return True if this widget is visible from the current frame. Considers
* overlays and vectors.
*
*/
public boolean isVisible()
{
return ItemUtils.isVisible(_d1);
}
/**
* Invoked whenever the widget have moved. Can override.
*
*/
protected abstract void onMoved();
/**
* Invoked whenever the widget have moved. Can override.
*
*/
protected abstract void onSizeChanged();
/**
* Override to have a custom min border thickness for your widget.
*
* @see #DEFAULT_MINIMUM_BORDER_THICKNESS
*
* @return The minimum border thickness. Should be larger or equal to zero.
*
*/
public float getMinimumBorderThickness()
{
return DEFAULT_MINIMUM_BORDER_THICKNESS;
}
/**
* Looks fors a dataline in the current representation of the widget.
*
* @see #getCurrentRepresentation
* @see #getStrippedDataInt(String, int)
* @see #getStrippedDataLong(String, long)
*
* @param tag
* The prefix of a dataline that will be matched. Must be larger
* than zero and not null.
*
* @return The first dataline that matched the prefix - without the
* prefix. Null if their was no data that matched the given prefix.
*
* @throws IllegalArgumentException
* If tag is null.
*
* @throws NullPointerException
* If tag is empty.
*/
protected String getStrippedDataString(final String tag)
{
if (tag == null) {
throw new NullPointerException("tag");
}
if (tag.length() == 0) {
throw new IllegalArgumentException("tag is empty");
}
if (getCurrentRepresentation().getData() != null) {
for (final String str : getCurrentRepresentation().getData()) {
if (str != null && str.startsWith(tag) && str.length() > tag.length()) {
return str.substring(tag.length());
}
}
}
return null;
}
/**
* Looks fors a dataline in the current representation of the widget.
*
* @see #getCurrentRepresentation
* @see #getStrippedDataString(String)
* @see #getStrippedDataLong(String, long)
*
* @param tag
* The prefix of a dataline that will be matched. Must be larger
* the zero and not null.
*
* @param defaultValue
* The default value if the tag does not exist or contains
* invalid data.
*
* @return The first dataline that matched the prefix - parsed as an
* int (after the prefix). defaultValue if their was no data that
* matched the given prefix or the data was invalid.
*
* @throws IllegalArgumentException
* If tag is null.
*
* @throws NullPointerException
* If tag is empty.
*
*/
protected Integer getStrippedDataInt(final String tag, final Integer defaultValue)
{
String strippedStr = getStrippedDataString(tag);
if (strippedStr != null) {
strippedStr = strippedStr.trim();
if (strippedStr.length() > 0) {
try {
return Integer.parseInt(strippedStr);
} catch (final NumberFormatException e) { /* Consume */
}
}
}
return defaultValue;
}
/**
* Looks for a dataline in the current representation of the widget.
*
* @see #getCurrentRepresentation
* @see #getStrippedDataString(String)
* @see #getStrippedDataInt(String, int)
*
* @param tag
* The prefix of a dataline that will be matched. Must be larger
* the zero and not null.
*
* @param defaultValue
* The default value if the tag does not exist or contains
* invalid data.
*
* @return The first dataline that matched the prefix - parsed as a
* long (after the prefix). defaultValue if their was no data that
* matched the given prefix or the data was invalid.
*
* @throws IllegalArgumentException
* If tag is null.
*
* @throws NullPointerException
* If tag is empty.
*
*/
protected Long getStrippedDataLong(final String tag, final Long defaultValue)
{
String strippedStr = getStrippedDataString(tag);
if (strippedStr != null) {
strippedStr = strippedStr.trim();
if (strippedStr.length() > 0) {
try {
return Long.parseLong(strippedStr);
} catch (final NumberFormatException e) { /* Consume */
}
}
}
return defaultValue;
}
/**
* Looks for a data-line in the current representation of the widget.
*
* @see #getCurrentRepresentation
* @see #getStrippedDataString(String)
* @see #getStrippedDataLong(String, long)
*
* @param tag
* The prefix of a data-line that will be matched. Must be larger
* the zero and not null.
*
* @param defaultValue
* The default value if the tag does not exist or contains
* invalid data.
*
* @return The first data-line that matched the prefix - if
* case insensitive match is 'true' then return true, otherwise
* false. defaultValue if their was no data that
* matched the given prefix or the data was invalid.
*
* @throws IllegalArgumentException
* If tag is null.
*
* @throws NullPointerException
* If tag is empty.
*
*/
protected Boolean getStrippedDataBool(final String tag, final Boolean defaultValue) {
String strippedStr = getStrippedDataString(tag);
if (strippedStr != null) {
strippedStr = strippedStr.trim();
if (strippedStr.length() > 0) {
return strippedStr.equalsIgnoreCase("true") ? true : false;
}
}
return defaultValue;
}
/**
* All data is removed that is prefixed with the given tag.
*
* @param tag
* The prefix of the data lines to remove. Must be larger the
* zero and not null.
*
* @throws IllegalArgumentException
* If tag is null.
*
* @throws NullPointerException
* If tag is empty.
*
*/
protected void removeData(final String tag)
{
updateData(tag, null);
}
// TODO: Ambiguous name. Refactor. cts16
protected void addDataIfCaseInsensitiveNotExists(String tag)
{
if (tag == null) {
throw new NullPointerException("tag");
}
List data = getCurrentRepresentation().getData();
if (data == null) {
data = new LinkedList();
}
for (final String s : data) {
if (s != null && s.equalsIgnoreCase(tag)) {
return;
}
}
data.add(tag);
getCurrentRepresentation().setData(data);
}
/**
* Updates the data with a given tag. All data is removed that is prefixed
* with the given tag. Then a new line is added (if not null).
*
* Note that passing newData with null is the equivelant of removing tag
* lines.
*
* @param tag
* The prefix of the data lines to remove. Must be larger the
* zero and not null.
*
* @param newData
* The new line to add. Can be null - for not adding anything.
*
* @throws IllegalArgumentException
* If tag is null.
*
* @throws NullPointerException
* If tag is empty.
*
* @see #removeData(String)
*
*/
protected void updateData(final String tag, final String newData)
{
if (tag == null) {
throw new NullPointerException("tag");
} else if (tag.length() == 0) {
throw new IllegalArgumentException("tag is empty");
}
// Get current data
List data = getCurrentRepresentation().getData();
if (data != null) {
for (int i = 0; i < data.size(); i++) {
final String str = data.get(i);
if (str != null && str.startsWith(tag)) {
data.remove(i);
}
}
}
if (newData != null) {
if (data != null) {
data.add(newData);
} else {
data = new LinkedList();
data.add(newData);
getCurrentRepresentation().setData(data);
}
}
}
public boolean containsData(final String str) {
assert(str != null);
if (getCurrentRepresentation().getData() != null) {
return getCurrentRepresentation().getData().contains(str);
}
return false;
}
public boolean containsDataTrimmedIgnoreCase(final String str) {
assert(str != null);
if (getCurrentRepresentation().getData() != null) {
for (final String data : getCurrentRepresentation().getData()) {
if (data != null && data.trim().equalsIgnoreCase(str)) {
return true;
}
}
}
return false;
}
/**
* Sets the link for this widget.
*
* @param link
* The new frame link. Can be null (for no link)
*
* @param linker
* The text item creating the link. Null if not created from
* a text item.
*/
public void setLink(final String link, final Text linker)
{
// Make sure the link is redrawn when a link is added
if (link == null) {
invalidateLink();
}
getSource().setLink(link);
if (link != null) {
invalidateLink();
}
}
public void setBackgroundColor(final Colour c)
{
getSource().setBackgroundColor(c);
}
/**
* @return The link for this widget. Null if none.
*/
public String getLink()
{
return _textRepresentation.getLink();
}
/**
* Note: That if the widget has no parent (e.g. the widget is a
* free-item) then the absolute link returned will be for the frameset of
* the current frame.
*
* @return The absolute link for this item. Null if there is no link, or if
* there is no parent for this widget and the current frame is
* unavailable.
*
*/
public String getAbsoluteLink()
{
// Note: cannot return the source absolute link since it does not have
// a parent ... thus must manually format link
final String link = getLink();
if (link == null || link.length() == 0) {
return null;
}
if (FrameIO.isPositiveInteger(link)) { // relative - convert to
// absolute
// Get the frameset of this item
Frame parent = getParentFrame();
if (parent == null) {
parent = DisplayController.getCurrentFrame();
}
if (parent == null) {
return null;
}
final String framesetName = parent.getFramesetName();
if (framesetName == null) {
return null;
}
return framesetName + link;
} else if (FrameIO.isValidFrameName(link)) { // already absolute
return link;
}
return null;
}
/**
* Sets the border color for the widget.
* That is, for the source (so it is remembered) and also for all the
* corners/edges.
*
* @param c
* The color to set.
*/
public void setWidgetEdgeColor(final Colour c)
{
// Indirectly invokes setSourceBorderColor accordingly
for (final Item i : _expediteeItems) {
i.setColor(c);
}
}
/**
* Sets the thickness of the widget edge.
*
* @see Item#setThickness(float)
*
* @param thickness
* The new thickness to set.
*/
public void setWidgetEdgeThickness(final float thickness) {
_l1.setThickness(thickness, true);
//for (Item i : _expediteeItems) i.setThickness(thickness);
// Above indirectly invokes setSourceThickness accordingly
}
/**
* Override to dis-allow widget thickness manipulation from the user.
* @return
*/
public boolean isWidgetEdgeThicknessAdjustable() {
return true;
}
// TODO: Maybe rename setSource* .. to update* ... These should actually be friendly!
public void setSourceColor(final Colour c) {
_textRepresentation.setColor(c);
}
public void setSourceBorderColor(final Colour c) {
_textRepresentation.setBorderColor(c);
}
public void setSourceFillColor(final Colour c) {
_textRepresentation.setFillColor(c);
}
public void setSourceThickness(final float newThickness, final boolean setConnected) {
_textRepresentation.setThickness(newThickness, setConnected);
}
public void setSourceData(final List data) {
_textRepresentation.setData(data);
}
protected Point getOrigin() {
return _d1.getPosition(); // NOTE FROM BROOK: This flips around ... the origin can be any point
}
protected Item getFirstCorner() {
return _d1;
}
public void setAnchorLeft(final Integer anchor)
{
_anchoring.setLeftAnchor(anchor);
// Anchor left-edge corners (dots) as well
_d1.setAnchorCornerX(anchor,null);
_d4.setAnchorCornerX(anchor,null);
if (anchor != null) {
setPositions(_d1, anchor, _d1.getY());
invalidateSelf();
}
// Move X-rayable item as well
getCurrentRepresentation().setAnchorLeft(anchor);
}
public void setAnchorRight(final Integer anchor) {
_anchoring.setRightAnchor(anchor);
// Anchor right-edge corners (dots) as well
_d2.setAnchorCornerX(null,anchor); // right
_d3.setAnchorCornerX(null,anchor); // right
if (anchor != null) {
setPositions(_d2, DisplayController.getFramePaintAreaWidth() - anchor, _d2.getY());
invalidateSelf();
}
if (_anchoring.getLeftAnchor() == null) {
// Prefer having the X-rayable item at anchorLeft position (if defined) over moving to anchorRight
getCurrentRepresentation().setAnchorRight(anchor);
}
}
public void setAnchorTop(final Integer anchor) {
_anchoring.setTopAnchor(anchor);
// Anchor top-edge corners (dots) as well
_d1.setAnchorCornerY(anchor,null);
_d2.setAnchorCornerY(anchor,null);
if (anchor != null) {
setPositions(_d2, _d2.getX(), anchor);
invalidateSelf();
}
// Move X-rayable item as well
getCurrentRepresentation().setAnchorTop(anchor);
}
public void setAnchorBottom(final Integer anchor) {
_anchoring.setBottomAnchor(anchor);
// Anchor bottom-edge corners (dots) as well
_d3.setAnchorCornerY(null,anchor);
_d4.setAnchorCornerY(null,anchor);
if (anchor != null) {
setPositions(_d3, _d3.getX(), DisplayController.getFramePaintAreaHeight() - anchor);
invalidateSelf();
}
if (_anchoring.getTopAnchor() == null) {
// Prefer having the X-rayable item at anchorTop position (if defined) over moving to anchorBottom
getCurrentRepresentation().setAnchorBottom(anchor);
}
}
public boolean isAnchored() {
return (isAnchoredX()) || (isAnchoredY());
}
public boolean isAnchoredX() {
return _anchoring.isXAnchored();
}
public boolean isAnchoredY() {
return _anchoring.isYAnchored();
}
/**
* Call from expeditee for representing the name of the item.
* Override to return custom name.
*
* Note: Used for the new frame title when creating links for widgets.
*
* @return
* The name representing this widget
*/
public String getName() {
return this.toString();
}
/**
* Event called when the widget is left clicked while there are items attached to the FreeItems buffer.
* Used to enable expeditee like text-widget interaction for left mouse clicks.
* @return true if event was handled (no pass through), otherwise false.
*/
public boolean ItemsLeftClickDropped() {
return false;
}
/**
* Event called when the widget is middle clicked while there are items attached to the FreeItems buffer.
* Used to enable expeditee like text-widget interaction for middle mouse clicks.
* @return true if event was handled (no pass through), otherwise false.
*/
public boolean ItemsMiddleClickDropped() {
return false;
}
/**
* Event called when the widget is left clicked while there are items attached to the FreeItems buffer.
* Used to enable expeditee like text-widget interaction for right mouse clicks
* @return true if event was handled (no pass through), otherwise false.
*/
public boolean ItemsRightClickDropped() {
return false;
}
/** Gets the clip area for this widget. */
public Clip getClip()
{
final Clip clip = new Clip(getContentBounds());
return clip;
}
public void onResized() {
invalidateSelf();
onBoundsChanged();
layout();
}
}