/** * 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."); } } }