/**
* ElementBoxView.java
* (c) Peter Bielik and Radek Burget, 2011-2012
*
* SwingBox 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 3 of the License, or
* (at your option) any later version.
*
* SwingBox 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 SwingBox. If not, see <http://www.gnu.org/licenses/>.
*
*/
package org.fit.cssbox.swingbox.view;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.Shape;
import java.util.Map;
import java.util.Vector;
import javax.swing.SizeRequirements;
import javax.swing.event.DocumentEvent;
import javax.swing.text.AttributeSet;
import javax.swing.text.CompositeView;
import javax.swing.text.Element;
import javax.swing.text.SimpleAttributeSet;
import javax.swing.text.View;
import javax.swing.text.ViewFactory;
import org.fit.cssbox.layout.BlockBox;
import org.fit.cssbox.layout.Box;
import org.fit.cssbox.layout.ElementBox;
import org.fit.cssbox.swingbox.util.Anchor;
import org.fit.cssbox.swingbox.util.Constants;
/**
* @author Peter Bielik
* @author Radek Burget
*/
public class ElementBoxView extends CompositeView implements CSSBoxView
{
protected ElementBox box;
protected Anchor anchor;
protected int order;
/** the cache of attributes */
private AttributeSet attributes;
/** decides whether to construct a cache from current working properties */
private boolean refreshAttributes;
private boolean refreshProperties;
private Dimension oldDimension;
private int majorAxis;
private boolean majorAllocValid;
private boolean minorAllocValid;
private boolean majorReqValid;
private boolean minorReqValid;
private SizeRequirements majorRequest;
private SizeRequirements minorRequest;
/**
* @param elem
*/
public ElementBoxView(Element elem)
{
// Y axis as default
super(elem);
majorAxis = Y_AXIS;
AttributeSet tmpAttr = elem.getAttributes();
Object obj = tmpAttr.getAttribute(Constants.ATTRIBUTE_BOX_REFERENCE);
Integer i = (Integer) tmpAttr.getAttribute(Constants.ATTRIBUTE_DRAWING_ORDER);
order = (i == null) ? -1 : i;
if (obj != null && obj instanceof ElementBox)
{
box = (ElementBox) obj;
if (box instanceof BlockBox)
{
if (((BlockBox) box).isFloating())
{
majorAxis = X_AXIS;
}
}
}
else
{
throw new IllegalArgumentException("Box reference is null or not an instance of ElementBox");
}
obj = tmpAttr.getAttribute(Constants.ATTRIBUTE_ANCHOR_REFERENCE);
if (obj != null && obj instanceof Anchor)
{
anchor = (Anchor) obj;
}
else
{
throw new IllegalArgumentException("Anchor reference is null or not an instance of Anchor");
}
oldDimension = new Dimension();
loadElementAttributes();
}
private void loadElementAttributes()
{
org.w3c.dom.Element elem = Anchor.findAnchorElement(box.getElement());
Map<String, String> elementAttributes = anchor.getProperties();
if (elem != null)
{
anchor.setActive(true);
elementAttributes.put(Constants.ELEMENT_A_ATTRIBUTE_HREF, elem.getAttribute("href"));
elementAttributes.put(Constants.ELEMENT_A_ATTRIBUTE_NAME, elem.getAttribute("name"));
elementAttributes.put(Constants.ELEMENT_A_ATTRIBUTE_TITLE, elem.getAttribute("title"));
String target = elem.getAttribute("target");
if ("".equals(target))
{
target = "_self";
}
elementAttributes.put(Constants.ELEMENT_A_ATTRIBUTE_TARGET, target);
// System.err.println("## Anchor at : " + this + " attr: "+
// elementAttributes);
}
else
{
anchor.setActive(false);
elementAttributes.clear();
}
}
@Override
public String toString()
{
String s = getClass().getSimpleName();
s += " " + order;
if (box != null)
s = s + ": " + box;
return s;
}
@Override
public int getDrawingOrder()
{
return order;
}
/**
* Fetches the tile axis property. This is the axis along which the child
* views are tiled.
*
* @return the major axis of the box, either <code>View.X_AXIS</code> or
* <code>View.Y_AXIS</code>
*
*/
public int getAxis()
{
return majorAxis;
}
/**
* Sets the tile axis property. This is the axis along which the child views
* are tiled.
*
* @param axis
* either <code>View.X_AXIS</code> or <code>View.Y_AXIS</code>
*
*/
public void setAxis(int axis)
{
boolean axisChanged = (axis != majorAxis);
majorAxis = axis;
if (axisChanged)
{
preferenceChanged(null, true, true);
}
}
/**
* Invalidates the layout along an axis. This happens automatically if the
* preferences have changed for any of the child views. In some cases the
* layout may need to be recalculated when the preferences have not changed.
* The layout can be marked as invalid by calling this method. The layout
* will be updated the next time the <code>setSize</code> method is called
* on this view (typically in paint).
*
* @param axis
* either <code>View.X_AXIS</code> or <code>View.Y_AXIS</code>
*
*/
public void layoutChanged(int axis)
{
if (axis == majorAxis)
{
majorAllocValid = false;
}
else
{
minorAllocValid = false;
}
}
/**
* Determines if the layout is valid along the given axis.
*
* @param axis
* either <code>View.X_AXIS</code> or <code>View.Y_AXIS</code>
*
*/
protected boolean isLayoutValid(int axis)
{
if (axis == majorAxis)
{
return majorAllocValid;
}
else
{
return minorAllocValid;
}
}
/**
* Establishes the parent view for this view. This is guaranteed to be
* called before any other methods if the parent view is functioning
* properly.
* <p>
* This is implemented to forward to the superclass as well as call the
* setPropertiesFromAttributes() method to set the paragraph properties from
* the css attributes. The call is made at this time to ensure the ability
* to resolve upward through the parents view attributes.
*
* Establishes the parent view for this view. This is guaranteed to be
* called before any other methods if the parent view is functioning
* properly. This is also the last method called, since it is called to
* indicate the view has been removed from the hierarchy as well. When this
* method is called to set the parent to null, this method does the same for
* each of its children, propogating the notification that they have been
* disconnected from the view tree. If this is reimplemented,
* <code>super.setParent()</code> should be called.
*
* @param parent
* the new parent, or <code>null</code> if the view is being
* removed from a parent
*/
@Override
public void setParent(View parent)
{
super.setParent(parent);
if (parent != null)
{
setPropertiesFromAttributes(getElement().getAttributes());
refreshAttributes = true;
refreshProperties = false;
}
else
{
// we are removed from a hierarchy
attributes = null;
box = null;
refreshAttributes = true;
refreshProperties = false;
}
}
/**
* Sets the properties from attributes (working variables).
*
* @param attributes
* the new properties
*/
protected void setPropertiesFromAttributes(AttributeSet attributes)
{
}
protected Anchor getAnchor()
{
return this.anchor;
}
@Override
public void replace(int offset, int length, View[] views)
{
super.replace(offset, length, views);
// System.err.println("Replace : " + views.length + " view count " +
// getViewCount());
majorReqValid = false;
majorAllocValid = false;
minorReqValid = false;
minorAllocValid = false;
}
@Override
protected void forwardUpdate(DocumentEvent.ElementChange ec,
DocumentEvent e, Shape a, ViewFactory f)
{
boolean wasValid = isLayoutValid(majorAxis);
super.forwardUpdate(ec, e, a, f);
// determine if a repaint is needed
if (wasValid && (!isLayoutValid(majorAxis)))
{
// Repaint is needed because one of the tiled children
// have changed their span along the major axis. If there
// is a hosting component and an allocated shape we repaint.
Component c = getContainer();
if ((a != null) && (c != null))
{
Rectangle alloc = getInsideAllocation(a);
c.repaint(alloc.x, alloc.y, alloc.width, alloc.height);
}
}
}
@Override
public void preferenceChanged(View child, boolean width, boolean height)
{
boolean majorChanged = (majorAxis == X_AXIS) ? width : height;
boolean minorChanged = (majorAxis == X_AXIS) ? height : width;
if (majorChanged)
{
majorReqValid = false;
majorAllocValid = false;
}
if (minorChanged)
{
minorReqValid = false;
minorAllocValid = false;
}
super.preferenceChanged(child, width, height);
}
@Override
public int getResizeWeight(int axis)
{
// checkRequests(axis);
if (axis == majorAxis)
{
if ((majorRequest.preferred != majorRequest.minimum)
|| (majorRequest.preferred != majorRequest.maximum)) { return 1; }
}
else
{
if ((minorRequest.preferred != minorRequest.minimum)
|| (minorRequest.preferred != minorRequest.maximum)) { return 1; }
}
return 0;
}
@Override
public AttributeSet getAttributes()
{
if (refreshAttributes)
{
attributes = createAttributes();
refreshAttributes = false;
refreshProperties = false;
}
// always returns the same instance.
// We need to know, if somebody modifies us outside..
return attributes;
}
protected SimpleAttributeSet createAttributes()
{
SimpleAttributeSet res = new SimpleAttributeSet();
res.addAttribute(Constants.ATTRIBUTE_ANCHOR_REFERENCE, anchor);
res.addAttribute(Constants.ATTRIBUTE_BOX_REFERENCE, box);
return res;
}
private SizeRequirements getRequirements(int axis, SizeRequirements r)
{
if (r == null)
{
r = new SizeRequirements();
}
r.alignment = 0f; // 0.5f;
if (axis == X_AXIS)
{
r.maximum = r.minimum = r.preferred = box.getWidth();// box.getContentWidth();
}
else
{
r.maximum = r.minimum = r.preferred = box.getHeight();// box.getContentHeight();
}
return r;
}
public void updateProperties()
{
invalidateProperties(); // we are lazy :)
}
protected void invalidateCache()
{
refreshAttributes = true;
}
protected void invalidateProperties()
{
refreshProperties = true;
}
@Override
public float getAlignment(int axis)
{
checkRequests(axis);
if (axis == majorAxis)
{
return majorRequest.alignment;
}
else
{
return minorRequest.alignment;
}
}
@Override
public void paint(Graphics graphics, Shape allocation)
{
//System.out.println("Paint: " + box + " in " + allocation);
Graphics2D g;
if (graphics instanceof Graphics2D)
g = (Graphics2D) graphics;
else
throw new RuntimeException("Unknown graphics environment, java.awt.Graphics2D required !");
Rectangle clip = toRect(g.getClip());
//box.getVisualContext().updateGraphics(g);
//box.drawBackground(g);
Rectangle alloc = toRect(allocation);
int count = getViewCount();
for (int i = 0; i < count; i++)
{
Rectangle bounds = new Rectangle(alloc);
childAllocation(i, bounds);
if (clip.intersects(bounds))
getView(i).paint(g, allocation);
}
}
// /**
// * renders given child, possible to override and customize.
// *
// * @param g
// * graphics context
// * @param v
// * the View
// * @param rect
// * an allocation
// * @param index
// * the index of view
// */
/*protected void paintChild(Graphics g, View v, Shape rect, int index)
{
// System.err.println("Painting " + v);
v.paint(g, rect);
}*/
@Override
public Shape getChildAllocation(int index, Shape a)
{
// zvyraznovanie !
if (a != null /* && isAllocationValid() */)
{
Box tmpBox = getBox(getView(index));
Rectangle alloc = (a instanceof Rectangle) ? (Rectangle) a : a.getBounds();
//alloc.setBounds(tmpBox.getAbsoluteBounds());
alloc.setBounds(getCompleteBoxAllocation(tmpBox));
return alloc;
}
return null;
}
@Override
protected void childAllocation(int index, Rectangle alloc)
{
// set allocation (== the bounds) for a view
//alloc.setBounds(getBox(getView(index)).getAbsoluteBounds());
alloc.setBounds(getCompleteBoxAllocation(getBox(getView(index))));
}
/**
* Obtains the allocation of a box together with all its child boxes.
* @param b the box
* @return the smallest rectangle containing the box and all its child boxes
*/
private Rectangle getCompleteBoxAllocation(Box b)
{
Rectangle ret = b.getAbsoluteBounds();
if (b instanceof ElementBox)
{
ElementBox eb = (ElementBox) b;
for (int i = eb.getStartChild(); i < eb.getEndChild(); i++)
{
Box child = eb.getSubBox(i);
if (child.isVisible())
{
Rectangle r = getCompleteBoxAllocation(child);
ret.add(r);
}
}
}
return ret.intersection(b.getClipBlock().getClippedContentBounds());
}
@Override
public float getPreferredSpan(int axis)
{
checkRequests(axis);
float marginSpan = (axis == X_AXIS) ? getLeftInset() + getRightInset()
: getTopInset() + getBottomInset();
if (axis == majorAxis)
{
return ((float) majorRequest.preferred) + marginSpan;
}
else
{
return ((float) minorRequest.preferred) + marginSpan;
}
}
@Override
public float getMinimumSpan(int axis)
{
checkRequests(axis);
float marginSpan = (axis == X_AXIS) ? getLeftInset() + getRightInset()
: getTopInset() + getBottomInset();
if (axis == majorAxis)
{
return ((float) majorRequest.minimum) + marginSpan;
}
else
{
return ((float) minorRequest.minimum) + marginSpan;
}
}
@Override
public float getMaximumSpan(int axis)
{
checkRequests(axis);
float marginSpan = (axis == X_AXIS) ? getLeftInset() + getRightInset()
: getTopInset() + getBottomInset();
if (axis == majorAxis)
{
return ((float) majorRequest.maximum) + marginSpan;
}
else
{
return ((float) minorRequest.maximum) + marginSpan;
}
}
// --- local methods ----------------------------------------------------
/**
* Are the allocations for the children still valid?
*
* @return true if allocations still valid
*/
protected boolean isAllocationValid()
{
return (majorAllocValid && minorAllocValid);
}
/**
* Determines if a point falls before an allocated region.
*
* @param x
* the X coordinate >= 0
* @param y
* the Y coordinate >= 0
* @param innerAlloc
* the allocated region; this is the area inside of the insets
* @return true if the point lies before the region else false
*/
@Override
protected boolean isBefore(int x, int y, Rectangle innerAlloc)
{
// System.err.println("isBefore: " + innerAlloc + " my bounds " +
// box.getAbsoluteBounds());
// System.err.println("XY: " + x + " : " + y);
innerAlloc.setBounds(box.getAbsoluteBounds());
if (majorAxis == View.X_AXIS)
{
return (x < innerAlloc.x);
}
else
{
return (y < innerAlloc.y);
}
}
/**
* Determines if a point falls after an allocated region.
*
* @param x
* the X coordinate >= 0
* @param y
* the Y coordinate >= 0
* @param innerAlloc
* the allocated region; this is the area inside of the insets
* @return true if the point lies after the region else false
*/
@Override
protected boolean isAfter(int x, int y, Rectangle innerAlloc)
{
// System.err.println("isAfter: " + innerAlloc + " my bounds " +
// box.getAbsoluteBounds());
// System.err.println("XY: " + x + " : " + y);
innerAlloc.setBounds(box.getAbsoluteBounds());
if (majorAxis == View.X_AXIS)
{
return (x > (innerAlloc.width + innerAlloc.x));
}
else
{
return (y > (innerAlloc.height + innerAlloc.y));
}
}
@Override
protected View getViewAtPoint(int x, int y, Rectangle alloc)
{
View retv = null;
int retorder = -1;
Vector<View> leaves = new Vector<View>();
findLeaves(this, leaves);
for (View leaf : leaves)
{
View v = leaf;
if (v instanceof CSSBoxView)
{
Box b = getBox(v);
if (locateBox(b, x, y) != null)
{
while (v.getParent() != null && v.getParent() != this)
v = v.getParent();
//System.out.println("Candidate: " + v + " (leaf: " + leaf + ")");
int o = ((CSSBoxView) v).getDrawingOrder();
if (retv == null || o >= retorder) //next box is drawn after the current one
{
retv = v;
retorder = order;
alloc.setBounds(getCompleteBoxAllocation(b));
}
}
}
}
//System.out.println("At " + x + ":" + y + " found " + retv);
return retv;
}
private void findLeaves(View root, Vector<View> leaves)
{
if (root instanceof ElementBoxView)
{
ElementBoxView ev = (ElementBoxView) root;
if (ev.getViewCount() == 0)
leaves.add(ev);
else
{
for (int i = 0; i < ev.getViewCount(); i++)
findLeaves(ev.getView(i), leaves);
}
}
else
leaves.add(root);
}
/**
* Locates a box from its position
*/
private Box locateBox(Box root, int x, int y)
{
if (root.isVisible())
{
Box found = null;
Rectangle bounds = root.getAbsoluteContentBounds().intersection(root.getClipBlock().getClippedContentBounds());
if (bounds.contains(x, y))
found = root;
//find if there is something smallest that fits among the child boxes
if (root instanceof ElementBox)
{
ElementBox eb = (ElementBox) root;
for (int i = eb.getStartChild(); i < eb.getEndChild(); i++)
{
Box inside = locateBox(((ElementBox) root).getSubBox(i), x, y);
if (inside != null)
{
if (found == null)
found = inside;
else
{
if (inside.getAbsoluteBounds().width * inside.getAbsoluteBounds().height < found.getAbsoluteBounds().width * found.getAbsoluteBounds().height)
found = inside;
}
}
}
}
return found;
}
else
return null;
}
@Override
public void setSize(float width, float height)
{
if (oldDimension.width != width)
{
oldDimension.setSize((int) width, oldDimension.height);
layoutChanged(X_AXIS);
}
if (oldDimension.height != height)
{
oldDimension.setSize(oldDimension.width, (int) height);
layoutChanged(Y_AXIS);
}
/*
* in current implementation we do not support propagation do childs,
* because, if there is a change, world is rebuilt..
*/
}
/**
* Validates layout.
*
* @param dim
* the new dimension of valid area. Validation run against this
* @return true, if layout during validation process has been changed.
*/
protected boolean validateLayout(Dimension dim)
{
if (majorAxis == X_AXIS)
{
majorRequest = getRequirements(X_AXIS, majorRequest);
minorRequest = getRequirements(Y_AXIS, minorRequest);
oldDimension.setSize(majorRequest.preferred, minorRequest.preferred);
}
else
{
majorRequest = getRequirements(Y_AXIS, majorRequest);
minorRequest = getRequirements(X_AXIS, minorRequest);
oldDimension.setSize(minorRequest.preferred, majorRequest.preferred);
}
majorReqValid = true;
minorReqValid = true;
majorAllocValid = true;
minorAllocValid = true;
return false;
}
private void checkRequests(int axis)
{
if ((axis != X_AXIS) && (axis != Y_AXIS)) { throw new IllegalArgumentException(
"Invalid axis: " + axis); }
if (axis == majorAxis)
{
if (!majorReqValid)
{
majorRequest = getRequirements(axis, majorRequest);
majorReqValid = true;
}
}
else if (!minorReqValid)
{
minorRequest = getRequirements(axis, minorRequest);
minorReqValid = true;
}
}
/**
* Converts an Shape to instance of rectangle
*
* @param a
* the shape
* @return the rectangle
*/
public static final Rectangle toRect(Shape a)
{
return a instanceof Rectangle ? (Rectangle) a : a.getBounds();
}
/**
* Calculates intersection of two rectangles
*
* @param src1
* the src1
* @param src2
* the src2
* @param dest
* the dest
* @return true, if there is non empty intersection
*/
public static final boolean intersection(Rectangle src1, Rectangle src2, Rectangle dest)
{
int x1 = Math.max(src1.x, src2.x);
int y1 = Math.max(src1.y, src2.y);
int x2 = Math.min(src1.x + src1.width, src2.x + src2.width);
int y2 = Math.min(src1.y + src1.height, src2.y + src2.height);
dest.setBounds(x1, y1, x2 - x1, y2 - y1);
if (dest.width <= 0 || dest.height <= 0)
return false;
return true; // non-empty intersection
}
/**
* Gets the box reference from properties
*
* @param v
* the view, instance of CSSBoxView.
* @return the box set in properties.
*/
public static final Box getBox(CSSBoxView v)
{
try
{
AttributeSet attr = v.getAttributes();
return (Box) attr.getAttribute(Constants.ATTRIBUTE_BOX_REFERENCE);
} catch (Exception e)
{
throw new IllegalStateException(e);
}
}
/**
* Gets the box reference from properties.
*
* @param v
* just a view.
* @return the box set in properties, if there is one.
*/
public static final Box getBox(View v)
{
if (v instanceof CSSBoxView) return getBox((CSSBoxView) v);
AttributeSet attr = v.getAttributes();
if (attr == null)
{
throw new NullPointerException("AttributeSet of " + v.getClass().getName() + "@"
+ Integer.toHexString(v.hashCode()) + " is set to NULL.");
}
Object obj = attr.getAttribute(Constants.ATTRIBUTE_BOX_REFERENCE);
if (obj != null && obj instanceof Box)
{
return (Box) obj;
}
else
{
throw new IllegalArgumentException("Box reference in attributes is not an instance of a Box.");
}
}
}