001 /* 002 * Copyright 2002 - 2007 JEuclid, http://jeuclid.sf.net 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017 /* $Id: JEuclidView.java,v 371548310efa 2010/08/09 21:15:33 max $ */ 018 019 package net.sourceforge.jeuclid.layout; 020 021 import java.awt.Color; 022 import java.awt.Graphics2D; 023 import java.awt.Image; 024 import java.awt.Rectangle; 025 import java.awt.RenderingHints; 026 import java.awt.geom.Line2D; 027 import java.awt.geom.Rectangle2D; 028 import java.awt.image.BufferedImage; 029 import java.util.ArrayList; 030 import java.util.HashMap; 031 import java.util.LinkedList; 032 import java.util.List; 033 import java.util.Map; 034 035 import net.sourceforge.jeuclid.DOMBuilder; 036 import net.sourceforge.jeuclid.LayoutContext; 037 import net.sourceforge.jeuclid.context.Parameter; 038 import net.sourceforge.jeuclid.elements.generic.DocumentElement; 039 import net.sourceforge.jeuclid.elements.presentation.token.Mo; 040 041 import org.apache.commons.logging.Log; 042 import org.apache.commons.logging.LogFactory; 043 import org.w3c.dom.Node; 044 import org.w3c.dom.NodeList; 045 import org.w3c.dom.events.Event; 046 import org.w3c.dom.events.EventListener; 047 import org.w3c.dom.events.EventTarget; 048 import org.w3c.dom.views.AbstractView; 049 import org.w3c.dom.views.DocumentView; 050 051 /** 052 * @version $Revision: 371548310efa $ 053 */ 054 public class JEuclidView implements AbstractView, LayoutView, EventListener { 055 056 private static final Log LOGGER = LogFactory.getLog(JEuclidView.class); 057 058 private final LayoutableDocument document; 059 060 private final Map<Node, LayoutInfo> layoutMap; 061 062 private final LayoutContext context; 063 064 private final Graphics2D graphics; 065 066 /** 067 * Default Constructor. 068 * 069 * @param node 070 * document to layout. 071 * @param layoutGraphics 072 * Graphics context to use for layout calculations. This should 073 * be compatible to the context used for painting, but does not 074 * have to be the same. If it is null, a default Graphics context 075 * is created. 076 * @param layoutContext 077 * layoutContext to use. 078 */ 079 public JEuclidView(final Node node, final LayoutContext layoutContext, 080 final Graphics2D layoutGraphics) { 081 assert node != null : "Node must not be null"; 082 assert layoutContext != null : "LayoutContext must not be null"; 083 if (node instanceof LayoutableDocument) { 084 this.document = (LayoutableDocument) node; 085 } else { 086 this.document = DOMBuilder.getInstance().createJeuclidDom(node, 087 true, true); 088 } 089 if (layoutGraphics == null) { 090 final Image tempimage = new BufferedImage(1, 1, 091 BufferedImage.TYPE_INT_ARGB); 092 this.graphics = (Graphics2D) tempimage.getGraphics(); 093 } else { 094 this.graphics = layoutGraphics; 095 } 096 this.context = layoutContext; 097 this.layoutMap = new HashMap<Node, LayoutInfo>(); 098 } 099 100 /** 101 * replace old node with new node in JEuclid document. 102 * 103 * @param jDocOld 104 * old JEuclid document 105 * @param oldNode 106 * Node to remove 107 * @param newNode 108 * Node to insert 109 * 110 * @return new JEuclid Document 111 */ 112 public static DocumentElement replaceNodes(final DocumentElement jDocOld, 113 final Node oldNode, final Node newNode) { 114 DocumentElement jDocNew; 115 Node imported; 116 Node parent; 117 List<Integer> path; 118 int i; 119 120 // create jeuclid dom of node 121 jDocNew = DOMBuilder.getInstance().createJeuclidDom(newNode); 122 123 // check if newNode is root of new tree 124 if (newNode.getParentNode().getParentNode() == null) { 125 return jDocNew; 126 } else { 127 imported = jDocOld.importNode(jDocNew.getDocumentElement(), true); 128 129 path = new ArrayList<Integer>(); 130 parent = oldNode; 131 132 while (parent.getParentNode() != null) { 133 i = 0; 134 while (parent.getPreviousSibling() != null) { 135 parent = parent.getPreviousSibling(); 136 i--; 137 } 138 path.add(-i); 139 parent = parent.getParentNode(); 140 } 141 142 parent = jDocOld.getDocumentElement(); 143 for (i = path.size() - 2; i > 0; i--) { 144 parent = parent.getChildNodes().item(path.get(i)); 145 } 146 147 final Node realOldNode = parent.getChildNodes().item(path.get(0)); 148 149 JEuclidView.LOGGER.debug("replace " + realOldNode.getNodeName() 150 + " with " + imported.getNodeName() + " under " 151 + parent.getNodeName()); 152 153 parent.insertBefore(imported, realOldNode); 154 parent.removeChild(realOldNode); 155 156 return jDocOld; 157 } 158 } 159 160 /** {@inheritDoc} */ 161 public DocumentView getDocument() { 162 return this.document; 163 } 164 165 /** 166 * Draw this view onto a Graphics context. 167 * 168 * @param x 169 * x-offset for left edge 170 * @param y 171 * y-offset for baseline 172 * @param g 173 * Graphics context for painting. Should be compatible to the 174 * context used during construction, but does not have to be 175 * the same. 176 */ 177 public void draw(final Graphics2D g, final float x, final float y) { 178 this.layout(); 179 final RenderingHints hints = g.getRenderingHints(); 180 if ((Boolean) this.context.getParameter(Parameter.ANTIALIAS)) { 181 hints.add(new RenderingHints(RenderingHints.KEY_ANTIALIASING, 182 RenderingHints.VALUE_ANTIALIAS_ON)); 183 } 184 hints.add(new RenderingHints(RenderingHints.KEY_STROKE_CONTROL, 185 RenderingHints.VALUE_STROKE_NORMALIZE)); 186 hints.add(new RenderingHints(RenderingHints.KEY_RENDERING, 187 RenderingHints.VALUE_RENDER_QUALITY)); 188 g.setRenderingHints(hints); 189 190 final boolean debug = (Boolean) this.context 191 .getParameter(Parameter.DEBUG); 192 this.drawNode(this.document, g, x, y, debug); 193 194 } 195 196 private void drawNode(final LayoutableNode node, final Graphics2D g, 197 final float x, final float y, final boolean debug) { 198 199 final LayoutInfo myInfo = this.getInfo(node); 200 if (debug) { 201 final float x1 = x; 202 final float x2 = x + myInfo.getWidth(LayoutStage.STAGE2); 203 final float y1 = y - myInfo.getAscentHeight(LayoutStage.STAGE2); 204 final float y2 = y + myInfo.getDescentHeight(LayoutStage.STAGE2); 205 g.setColor(Color.BLUE); 206 g.draw(new Line2D.Float(x1, y1, x2, y1)); 207 g.draw(new Line2D.Float(x1, y1, x1, y2)); 208 g.draw(new Line2D.Float(x2, y1, x2, y2)); 209 g.draw(new Line2D.Float(x1, y2, x2, y2)); 210 g.setColor(Color.RED); 211 g.draw(new Line2D.Float(x1, y, x2, y)); 212 } 213 for (final GraphicsObject go : myInfo.getGraphicObjects()) { 214 go.paint(x, y, g); 215 } 216 217 for (final LayoutableNode child : node.getChildrenToDraw()) { 218 final LayoutInfo childInfo = this.getInfo(child); 219 this.drawNode(child, g, 220 x + childInfo.getPosX(LayoutStage.STAGE2), y 221 + childInfo.getPosY(LayoutStage.STAGE2), debug); 222 } 223 } 224 225 private LayoutInfo layout() { 226 return this.layout(this.document, LayoutStage.STAGE2, this.context); 227 } 228 229 private LayoutInfo layout(final LayoutableNode node, 230 final LayoutStage toStage, final LayoutContext parentContext) { 231 final LayoutInfo info = this.getInfo(node); 232 233 if (node instanceof EventTarget) { 234 final EventTarget evtNode = (EventTarget) node; 235 evtNode.addEventListener("DOMSubtreeModified", this, false); 236 evtNode.addEventListener(Mo.MOEVENT, this, false); 237 } 238 239 if (LayoutStage.NONE.equals(info.getLayoutStage())) { 240 LayoutStage childMinStage = LayoutStage.STAGE2; 241 int count = 0; 242 for (final LayoutableNode l : node.getChildrenToLayout()) { 243 final LayoutInfo in = this.layout(l, LayoutStage.STAGE1, node 244 .getChildLayoutContext(count, parentContext)); 245 count++; 246 if (LayoutStage.STAGE1.equals(in.getLayoutStage())) { 247 childMinStage = LayoutStage.STAGE1; 248 } 249 } 250 node.layoutStage1(this, info, childMinStage, parentContext); 251 } 252 if (LayoutStage.STAGE1.equals(info.getLayoutStage()) 253 && LayoutStage.STAGE2.equals(toStage)) { 254 int count = 0; 255 for (final LayoutableNode l : node.getChildrenToLayout()) { 256 this.layout(l, LayoutStage.STAGE2, node 257 .getChildLayoutContext(count, parentContext)); 258 count++; 259 } 260 node.layoutStage2(this, info, parentContext); 261 } 262 return info; 263 } 264 265 /** {@inheritDoc} */ 266 public LayoutInfo getInfo(final LayoutableNode node) { 267 if (node == null) { 268 return null; 269 } 270 LayoutInfo info = this.layoutMap.get(node); 271 if (info == null) { 272 info = new LayoutInfoImpl(); 273 this.layoutMap.put(node, info); 274 } 275 return info; 276 } 277 278 /** 279 * @return width of this view. 280 */ 281 public float getWidth() { 282 final LayoutInfo info = this.layout(); 283 return info.getWidth(LayoutStage.STAGE2); 284 } 285 286 /** 287 * @return ascent height. 288 */ 289 public float getAscentHeight() { 290 final LayoutInfo info = this.layout(); 291 return info.getAscentHeight(LayoutStage.STAGE2); 292 } 293 294 /** 295 * @return descent height. 296 */ 297 public float getDescentHeight() { 298 final LayoutInfo info = this.layout(); 299 return info.getDescentHeight(LayoutStage.STAGE2); 300 } 301 302 /** {@inheritDoc} */ 303 public Graphics2D getGraphics() { 304 return this.graphics; 305 } 306 307 /** {@inheritDoc} */ 308 public void handleEvent(final Event evt) { 309 final EventTarget origin = evt.getCurrentTarget(); 310 if (origin instanceof LayoutableNode) { 311 final LayoutableNode lorigin = (LayoutableNode) origin; 312 final LayoutInfo info = this.getInfo(lorigin); 313 info.setLayoutStage(LayoutStage.NONE); 314 } 315 } 316 317 /** 318 * Data structure for storing a {@link Node} along with its rendering 319 * boundary ({@link Rectangle2D}). 320 */ 321 public static final class NodeRect { 322 private final Node node; 323 324 private final Rectangle2D rect; 325 326 private NodeRect(final Node n, final Rectangle2D r) { 327 this.node = n; 328 this.rect = r; 329 } 330 331 /** 332 * @return The Node this rectangle refers to. 333 */ 334 public Node getNode() { 335 return this.node; 336 } 337 338 /** 339 * @return The rendering boundary. 340 */ 341 public Rectangle2D getRect() { 342 return this.rect; 343 } 344 345 /** {@inheritDoc} */ 346 @Override 347 public String toString() { 348 final StringBuilder b = new StringBuilder(); 349 b.append(this.node).append('/').append(this.rect); 350 return b.toString(); 351 } 352 353 } 354 355 /** 356 * Get the node and rendering information from a mouse position. 357 * 358 * @param x 359 * x-coord 360 * @param y 361 * y-coord 362 * @param offsetX 363 * starting x position offset 364 * @param offsetY 365 * starting y position offset 366 * @return list of nodes with rendering information 367 */ 368 public List<JEuclidView.NodeRect> getNodesAt(final float x, 369 final float y, final float offsetX, final float offsetY) { 370 this.layout(); 371 final List<JEuclidView.NodeRect> nodes = new LinkedList<JEuclidView.NodeRect>(); 372 this.getNodesAtRec(x, y, offsetX, offsetY, this.document, nodes); 373 return nodes; 374 } 375 376 /** 377 * Check whether the given mouse position (with given offset) is in the 378 * rendering area of the given node - if so, append it to the nodes list 379 * 380 * @param x 381 * x-coord 382 * @param y 383 * y-coord 384 * @param offsetX 385 * x position offset to node 386 * @param offsetY 387 * y position offset to node 388 * @param node 389 * node to check 390 * @param nodesSoFar 391 * vector of nodes so far 392 */ 393 private void getNodesAtRec(final float x, final float y, 394 final float offsetX, final float offsetY, final Node node, 395 final List<JEuclidView.NodeRect> nodesSoFar) { 396 if (node instanceof LayoutableNode) { 397 final LayoutInfo info = this.layoutMap.get(node); 398 399 // this will be STAGE2 400 final LayoutStage stage = info.getLayoutStage(); 401 402 // find top-left corner of rendering area for this node 403 final float infoX = info.getPosX(stage) + offsetX; 404 final float infoY = info.getPosY(stage) + offsetY 405 - info.getAscentHeight(stage); 406 407 // create rectangle of rendered node area 408 final Rectangle2D.Float rect = new Rectangle.Float(infoX, infoY, 409 info.getWidth(stage), info.getAscentHeight(stage) 410 + info.getDescentHeight(stage)); 411 412 // record node and rectangle if it contains the mouse position 413 if (rect.contains(x, y)) { 414 nodesSoFar.add(new NodeRect(node, rect)); 415 416 // recurse on child nodes 417 final NodeList nodeList = node.getChildNodes(); 418 for (int i = 0; i < nodeList.getLength(); i++) { 419 this.getNodesAtRec(x, y, infoX, infoY 420 + info.getAscentHeight(stage), nodeList.item(i), 421 nodesSoFar); 422 } 423 } 424 } 425 } 426 427 /** 428 * Gets the absolute Bounds for a given node and offset. May return null 429 * if the node could not be found. 430 * 431 * @param offsetX 432 * x position offset to node 433 * @param offsetY 434 * y position offset to node 435 * 436 * @param node 437 * A layoutable node which was layouted in the current view. 438 * @return the rectangle with the absolute bounds or null if the given 439 * node was not layouted in this view. 440 * 441 */ 442 public Rectangle2D getRect(final float offsetX, final float offsetY, 443 final LayoutableNode node) { 444 this.layout(); 445 final LayoutInfo info = this.layoutMap.get(node); 446 final Rectangle2D retVal; 447 if (info == null) { 448 retVal = null; 449 } else { 450 LayoutableNode recNode = node; 451 float recInfoX = info.getPosX(LayoutStage.STAGE2) + offsetX; 452 float recInfoY = info.getPosY(LayoutStage.STAGE2) + offsetY 453 - info.getAscentHeight(LayoutStage.STAGE2); 454 while (recNode.getParentNode() instanceof LayoutableNode) { 455 recNode = (LayoutableNode) recNode.getParentNode(); 456 final LayoutInfo recInfo = this.layoutMap.get(recNode); 457 recInfoX = recInfoX + recInfo.getPosX(LayoutStage.STAGE2); 458 recInfoY = recInfoY + recInfo.getPosY(LayoutStage.STAGE2); 459 } 460 retVal = new Rectangle.Float(recInfoX, recInfoY, info 461 .getWidth(LayoutStage.STAGE2), info 462 .getAscentHeight(LayoutStage.STAGE2) 463 + info.getDescentHeight(LayoutStage.STAGE2)); 464 } 465 return retVal; 466 } 467 468 }