1 // Copyright 2002 Adam Megacz, see the COPYING file for licensing [GPL]
4 // **** This file must be preprocessed before compilation ****
9 // FIXME: reflow before allowing js to read from width/height
10 // FIXME: due to font inheritance, we must dirty and mark all null-font descendents of a node if its font changes
11 // FEATURE: fastpath for rows=1/cols=1
12 // FEATURE: reflow starting with a certain child
13 // FEATURE: separate mark_for_reflow and mark_for_resize
19 import org.xwt.util.*;
23 * Encapsulates the data for a single XWT box as well as all layout
28 * This is the real meat of XWT. Part of its monolithic design is for
29 * performance reasons: deep inheritance heirarchies are slow, and
30 * neither javago nor GCJ can inline across class boundaries.
33 * <p>The rendering process consists of three phases; each requires
34 * one DFS pass over the tree</p>
36 * <ol><li> <b>repacking</b>: children of a box are packed into columns
37 * and rows according to their colspan/rowspan attributes and
38 * ordering. Minimum and maximum sizes of columns are computed.
40 * <li> <b>resizing</b>: width/height and x/y positions of children
41 * are assigned. If a PosChange or SizeChange is triggered,
42 * <tt>abort</tt> will be set and the resizing process will
45 * <li> <b>repainting</b>: children draw their content onto the
49 * The first two passes together are called the <i>reflow</i> phase.
51 * Reflowing is done in a seperate pass since PosChanges and
52 * SizeChanges trigger an abort; if rendering were done in the same
53 * pass, rendering work done prior to the abort would be wasted.
55 * Repacking is seperate from resizing since a box's size depends on
56 * both the box's parent's size (so the traversal must be preorder)
57 * contentwidths of siblings both before and after the box, which in
58 * turn depend on all descendents of the siblings. FIXME
60 * A note on coordinates: the Box class represents regions
61 * internally as x,y,w,h tuples, even though the DoubleBuffer class
62 * uses x1,y1,x2,y2 tuples.
64 public final class Box extends JS.Scope {
66 public Box() { super(null); }
69 // Misc instance data ////////////////////////////////////////////////////////////////
71 public static boolean abort = false;
72 private static int sizePosChangesSinceLastRender = 0;
75 // Misc instance data ////////////////////////////////////////////////////////////////
77 boolean needs_reflow = true;
78 //#define MARK_FOR_REFLOW_this for(Box b2 = this; b2 != null && !b2.needs_reflow; b2 = b2.parent) b2.needs_reflow = true;
79 //#define MARK_FOR_REFLOW_b for(Box b2 = b; b2 != null && !b2.needs_reflow; b2 = b2.parent) b2.needs_reflow = true;
80 //#define MARK_FOR_REFLOW_b_parent for(Box b2 = b.parent; b2 != null && !b2.needs_reflow; b2 = b2.parent) b2.needs_reflow = true;
82 private boolean mouseinside = false;
84 Surface surface = null; // null on all non-root boxen
88 // Geometry ////////////////////////////////////////////////////////////////////////////
90 // xwt can be compiled with 16-bit lengths to save memory on small devices
92 //#define MAX_LENGTH Integer.MAX_VALUE
93 //#define MIN_LENGTH Integer.MIN_VALUE
95 // always correct (set directly by user)
98 LENGTH maxwidth = MAX_LENGTH;
99 LENGTH maxheight = MAX_LENGTH;
100 private LENGTH hpad = 0;
101 private LENGTH vpad = 0;
102 private String text = null;
103 private String font = null;
104 private LENGTH textwidth = 0;
105 private LENGTH textheight = 0;
108 private int rows = 1;
109 private int cols = 0;
110 private int rowspan = 1;
111 private int colspan = 1;
113 // computed during reflow
118 private int row = 0; // FIXME short
119 private int col = 0; // FIXME short
120 private LENGTH contentwidth = 0; // == max(minwidth, textwidth, sum(child.contentwidth) + pad)
121 private LENGTH contentheight = 0;
124 // Rendering Properties ///////////////////////////////////////////////////////////
126 //private SVG.VP path = null;
127 //private SVG.Paint fill = null;
128 //private SVG.Paint stroke = null;
130 private Picture image; // will disappear
131 private int fillcolor = 0x00000000; // will become SVG.Paint
132 private int strokecolor = 0xFF000000; // will become SVG.Paint
134 private String cursor = null; // the cursor for this box
137 public boolean invisible = false; // true iff the Box is invisible
138 private boolean absolute = false; // If true, the box will be positioned absolutely
139 private boolean vshrink = false; // If true, the box will shrink to the smallest vertical size possible
140 private boolean hshrink = false; // If true, the box will shrink to the smallest horizontal size possible
141 private boolean tile = false; // FIXME: drop this?
143 // Instance Methods /////////////////////////////////////////////////////////////////////
146 /** Adds the intersection of (x,y,w,h) and the node's current actual geometry to the Surface's dirty list */
147 public final void dirty() { dirty(x, y, width, height); }
148 public final void dirty(int x, int y, int w, int h) {
149 for(Box cur = this; cur != null; cur = cur.parent) {
150 w = min(x + w, this.x + this.width) - max(x, this.x);
151 h = min(y + h, this.y + this.height) - max(y, this.y);
154 if (w <= 0 || h <= 0) return;
155 if (cur.parent == null && cur.surface != null) cur.surface.dirty(x, y, w, h);
160 * Given an old and new mouse position, this will update <tt>mouseinside</tt> and check
161 * to see if this node requires any Enter, Leave, or Move notifications.
163 * @param forceleave set to true by the box's parent if the mouse is inside an older
164 * sibling, which is covering up this box.
166 void Move(int oldmousex, int oldmousey, int mousex, int mousey) { Move(oldmousex, oldmousey, mousex, mousey, false); }
167 void Move(int oldmousex, int oldmousey, int mousex, int mousey, boolean forceleave) {
169 boolean wasinside = mouseinside;
170 boolean isinside = !invisible && inside(mousex, mousey) && !forceleave;
171 mouseinside = isinside;
173 if (!wasinside && !isinside) return;
175 if (traps == null) { }
176 else if (!wasinside && isinside && traps.get("Enter") != null) put("Enter", this);
177 else if (wasinside && !isinside && traps.get("Leave") != null) put("Leave", this);
178 else if (wasinside && isinside && (mousex != oldmousex || mousey != oldmousey) && traps.get("Move") != null) put("Move", this);
180 if (isinside && cursor != null) getRoot().cursor = cursor;
182 // if the mouse has moved into our padding region, it is considered 'outside' all our children
183 if (!(mousex >= x + hpad && mousey >= y + vpad &&
184 mousex < x + width - hpad && mousey < y + height + vpad)) forceleave = true;
186 for(Box b = getChild(numChildren() - 1); b != null; b = b.prevSibling()) {
187 b.Move(oldmousex, oldmousey, mousex, mousey, forceleave);
188 if (b.inside(mousex, mousey)) forceleave = true;
193 // Reflow ////////////////////////////////////////////////////////////////////////////////////////
198 resize(x, y, width, height);
201 /** Checks if the Box's size has changed, dirties it if necessary, and makes sure childrens' sizes are up to date */
203 if (!needs_reflow || numChildren() == 0) return;
205 // --- Phase 0 ----------------------------------------------------------------------
207 for(Box child = getChild(0); child != null; child = child.nextSibling()) {
209 if (abort) { MARK_FOR_REFLOW_this; return; }
212 // --- Phase 1 ----------------------------------------------------------------------
213 // assign children to their row/column positions (assuming constrained columns)
214 //#repeat x/y y/x width/height col/row row/col cols/rows colspan/rowspan colWidth/rowHeight numRowsInCol/numColsInRow INNER/INNER2 maxwidth/maxheight minwidth/minheight contentwidth/contentheight colMaxWidth/rowMaxHeight OUTER/OUTER2 INNER/INNER2
216 int[] numRowsInCol = new int[cols]; // the number of cells occupied in each column
217 Box child = getChild(0);
218 for(child = child.nextSibling(); child != null && (child.absolute || child.invisible); child = child.nextSibling());
219 OUTER: for(int row=0; child != null; row++) {
220 for(int col=0; child != null && col < cols;) {
221 INNER: while(true) { // scan across the row, looking for an unoccupied gap at least as wide as the child
222 while(col < cols && numRowsInCol[col] > row) col++;
223 for(int i=col; i < cols && i < col + child.colspan; i++)
224 if (numRowsInCol[col] > row) { col = i + 1; continue INNER; }
227 if (col + child.colspan >= cols) break;
228 for(int i=col; i < col + child.colspan; i++) numRowsInCol[i] += child.rowspan;
230 col += child.colspan;
231 for(child = child.nextSibling(); child != null && (child.absolute || child.invisible); child = child.nextSibling());
237 // --- Phase 2 ----------------------------------------------------------------------
238 // compute the min/max sizes of the columns and rows and set our contentwidth
239 //#repeat x/y y/x width/height col/row cols/rows colspan/rowspan colWidth/rowHeight maxwidth/maxheight minwidth/minheight contentwidth/contentheight colMaxWidth/rowMaxHeight
241 LENGTH[] colWidth = new LENGTH[cols];
242 for(Box child = getChild(0); child != null; child = child.nextSibling())
243 colWidth[child.col] = max(colWidth[child.col], child.contentwidth / child.colspan);
244 for(int col=0; col<cols; col++) contentwidth += colWidth[col];
245 contentwidth = max(textwidth, contentwidth);
246 contentwidth = bound(minwidth, contentwidth, maxwidth);
251 void resize(LENGTH x, LENGTH y, LENGTH width, LENGTH height) {
253 // --- Phase 1 ----------------------------------------------------------------------
254 // run PosChange/SizeChange, dirty as needed
255 if (x != this.x || y != this.y || width != this.width || height != this.height) {
256 parent.dirty(this.x, this.y, this.width, this.height);
257 dirty(x, y, width, height);
258 boolean sizechange = false, poschange = false;
259 if (traps != null && (this.width != width || this.height != height) && traps.get("SizeChange") != null) sizechange = true;
260 if (traps != null && (this.x != x || this.y != y) && traps.get("PosChange") != null) poschange = true;
261 this.width = width; this.height = height; this.x = x; this.y = y;
262 if (sizechange || poschange)
263 if (sizePosChangesSinceLastRender == 500) {
264 if (Log.on) Log.logJS(this, "Warning, more than 500 SizeChange/PosChange traps triggered since last complete render");
266 sizePosChangesSinceLastRender++;
267 if (sizechange) put("SizeChange", Boolean.TRUE);
268 if (poschange) put("PosChange", Boolean.TRUE);
275 // --- short circuit ----------------------------------------------------------------
276 if (!needs_reflow) return;
277 needs_reflow = false;
280 // --- Phase 2 ----------------------------------------------------------------------
281 // compute the min/max sizes of the columns and rows and set initial width/height to minimums
283 //#repeat x/y y/x width/height col/row cols/rows colspan/rowspan colWidth/rowHeight maxwidth/maxheight minwidth/minheight contentwidth/contentheight colMaxWidth/rowMaxHeight
284 LENGTH[] colWidth = new LENGTH[cols];
285 LENGTH[] colMaxWidth = new LENGTH[cols];
288 for(Box child = getChild(0); child != null; child = child.nextSibling()) {
289 //#repeat x/y y/x width/height col/row cols/rows colspan/rowspan colWidth/rowHeight maxwidth/maxheight minwidth/minheight contentwidth/contentheight colMaxWidth/rowMaxHeight hshrink/vshrink
290 colWidth[child.col] = max(colWidth[child.col], child.contentwidth / child.colspan);
291 colMaxWidth[child.col] = min(colMaxWidth[child.col], (child.hshrink ? child.minwidth : child.maxwidth) / child.colspan);
296 // --- Phase 3 ----------------------------------------------------------------------
297 // hand out the slack
299 //#repeat x/y y/x width/height col/row cols/rows colspan/rowspan colWidth/rowHeight maxwidth/maxheight minwidth/minheight contentwidth/contentheight colMaxWidth/rowMaxHeight
301 for(int i=0; i<cols; i++) slack -= colWidth[i];
303 // FEATURE: inefficient
304 int startslack = slack;
305 int increment = max(1, slack / cols);
306 for(int col=0; col < cols && slack > 0; col++) {
307 slack += colWidth[col];
308 colWidth[col] = min(colMaxWidth[col], colWidth[col] + increment);
309 slack -= colWidth[col];
311 if (slack == startslack) break;
316 // --- Phase 4 ----------------------------------------------------------------------
317 // assign children's new sizes and positions and recurse
318 for(Box child = getChild(0); child != null; child = child.nextSibling()) {
321 //#repeat x/y y/x width/height col/row cols/rows colspan/rowspan colWidth/rowHeight maxwidth/maxheight minwidth/minheight contentwidth/contentheight colMaxWidth/rowMaxHeight hshrink/vshrink
322 child.width = 0; for(int i=child.col; i<child.colspan; i++) child.width += colWidth[i];
323 diff = bound(child.contentwidth, child.width, child.hshrink ? child.contentwidth : child.maxwidth) - child.width;
324 child.x = 0; for(int i=0; i<child.col; i++) child.x += colWidth[i];
325 if (diff < 0) child.x += -1 * (diff / 2);
328 child.resize(child.x, child.y, child.width, child.height);
335 // Rendering Pipeline /////////////////////////////////////////////////////////////////////
337 /** Renders self and children within the specified region. All rendering operations are clipped to xIn,yIn,wIn,hIn */
338 void render(int xIn, int yIn, int wIn, int hIn, DoubleBuffer buf) {
339 if (abort || invisible) return;
341 // intersect the x,y,w,h rendering window with ourselves; quit if it's empty
342 int x = max(xIn, parent == null ? 0 : this.x);
343 int y = max(yIn, parent == null ? 0 : this.y);
344 int w = min(xIn + wIn, (parent == null ? 0 : this.x) + width) - x;
345 int h = min(yIn + hIn, (parent == null ? 0 : this.y) + height) - y;
346 if (w <= 0 || h <= 0) return;
348 if ((fillcolor & 0xFF000000) != 0x00000000 || parent == null)
349 buf.fillRect(x, y, x + w, y + h, (fillcolor & 0xFF000000) != 0 ? fillcolor : 0xFF777777);
352 if (tile) renderTiledImage(x, y, w, h, buf);
353 else renderStretchedImage(x, y, w, h, buf);
355 if (text != null && !text.equals("")) renderText(x, y, w, h, buf);
357 // now subtract the pad region from the clip region before proceeding
358 int x2 = max(x, x + hpad);
359 int y2 = max(y, y + vpad);
360 int w2 = min(x + w, x + width - hpad) - x2;
361 int h2 = min(y + h, y + height - vpad) - y2;
363 for(Box b = getChild(0); b != null; b = b.nextSibling()) b.render(x2, y2, w2, h2, buf);
366 void renderStretchedImage(int x, int y, int w, int h, DoubleBuffer buf) {
367 buf.setClip(x, y, w + x, h + y);
369 int width = x + this.width - x;
370 int height = y + this.height - y;
374 int hstretch = width / image.getWidth();
375 if (hstretch == 0) hstretch = -1 * image.getWidth() / width;
376 int vstretch = height / image.getHeight();
377 if (vstretch == 0) vstretch = -1 * image.getHeight() / height;
378 if (hstretch < vstretch) height = image.getHeight() * width / image.getWidth();
379 else width = image.getWidth() * height / image.getHeight();
383 buf.drawPicture(image, x, y, x + width, y + height, 0, 0, image.getWidth(), image.getHeight());
384 buf.setClip(0, 0, buf.getWidth(), buf.getHeight());
387 void renderTiledImage(int x, int y, int w, int h, DoubleBuffer buf) {
388 int iw = image.getWidth();
389 int ih = image.getHeight();
391 for(int i=(x - x)/iw; i <= (x + w - x)/iw; i++) {
392 for(int j=(y - y)/ih; j<= (y + h - y)/ih; j++) {
394 int dx1 = max(i * iw + x, x);
395 int dy1 = max(j * ih + y, y);
396 int dx2 = min((i+1) * iw + x, x + w);
397 int dy2 = min((j+1) * ih + y, y + h);
399 int sx1 = dx1 - (i*iw) - x;
400 int sy1 = dy1 - (j*ih) - y;
401 int sx2 = dx2 - (i*iw) - x;
402 int sy2 = dy2 - (j*ih) - y;
404 if (dx2 - dx1 > 0 && dy2 - dy1 > 0 && sx2 - sx1 > 0 && sy2 - sy1 > 0)
405 buf.drawPicture(image, dx1, dy1, dx2, dy2, sx1, sy1, sx2, sy2);
410 void renderText(int x, int y, int w, int h, DoubleBuffer buf) {
412 if ((textcolor & 0xFF000000) == 0x00000000) return;
413 buf.setClip(x, y, w + x, h + y);
415 buf.drawString(font(), text, x + hpad, y + vpad + Platform.getMaxAscent(font()) - 1, textcolor);
416 buf.setClip(0, 0, buf.getWidth(), buf.getHeight());
417 int i=0; while(i<font().length() && !Character.isDigit(font().charAt(i))) i++;
419 if (font().lastIndexOf('d') > i) {
420 for(int j = x + hpad; j < x + hpad + textdim(0); j += 2)
421 buf.fillRect(j, y + vpad + (xwf == null ? Platform.getMaxAscent(font()) : xwf.getMaxAscent()) + 2,
422 j + 1, y + vpad + (xwf == null ? Platform.getMaxAscent(font()) : xwf.getMaxAscent()) + 2 + 1,
425 } else if (font().lastIndexOf('u') > i) {
426 buf.fillRect(x + hpad,
427 y + vpad + (xwf == null ? Platform.getMaxAscent(font()) : xwf.getMaxAscent()) + 2,
428 x + hpad + textdim(0),
429 y + vpad + (xwf == null ? Platform.getMaxAscent(font()) : xwf.getMaxAscent()) + 2 + 1,
436 // Methods to implement org.xwt.js.JS //////////////////////////////////////
438 public Object callMethod(Object method, JS.Array args, boolean checkOnly) throws JS.Exn {
439 if ("indexof".equals(method)) {
440 if (checkOnly) return Boolean.TRUE;
441 if (args.length() != 1 || args.elementAt(0) == null || !(args.elementAt(0) instanceof Box)) return new Integer(-1);
442 Box b = (Box)args.elementAt(0);
443 if (b.parent != Box.this) {
444 if (redirect == null || redirect == Box.this) return new Integer(-1);
445 return redirect.callMethod(method, args, checkOnly);
447 return new Integer(b.getIndexInParent());
449 } else if ("apply".equals(method)) {
450 if (checkOnly) return Boolean.TRUE;
451 if (args.elementAt(0) instanceof String) {
452 String templatename = (String)args.elementAt(0);
453 Template t = Template.getTemplate(templatename, null);
455 if (Log.on) Log.logJS(this, "template " + templatename + " not found");
457 if (ThreadMessage.suspendThread()) try {
458 JS.Callable callback = args.length() < 2 ? null : (Callable)args.elementAt(1);
459 t.apply(this, null, null, callback, 0, t.numUnits());
461 ThreadMessage.resumeThread();
464 } else if (args.elementAt(0) instanceof JS && !(args.elementAt(0) instanceof Box)) {
465 JS s = (JS)args.elementAt(0);
466 Object[] keys = s.keys();
467 for(int j=0; j<keys.length; j++) put(keys[j].toString(), s.get(keys[j]));
474 /** Returns the i_th child */
475 public Object get(int i) {
476 if (redirect == null) return null;
477 if (redirect != this) return redirect.get(i);
478 return i >= numChildren() || i < 0 ? null : getChild(i);
482 * Inserts value as child i; calls remove() if necessary.
483 * This method handles "reinserting" one of your children properly.
484 * INVARIANT: after completion, getChild(min(i, numChildren())) == newnode
485 * WARNING: O(n) runtime, unless i == numChildren()
487 public void put(int i, Object value) {
490 if (value != null && !(value instanceof Box)) {
491 if (Log.on) Log.logJS(this, "attempt to set a numerical property on a box to anything other than a box");
492 } else if (redirect == null) {
493 if (Log.on) Log.logJS(this, "attempt to add/remove children to/from a node with a null redirect");
494 } else if (redirect != this) {
495 Box b = value == null ? (Box)redirect.get(i) : (Box)value;
496 redirect.put(i, value);
498 } else if (value == null) {
499 if (i >= 0 && i < numChildren()) {
504 } else if (value instanceof RootProxy) {
505 if (Log.on) Log.logJS(this, "attempt to reparent a box via its proxy object");
507 Box newnode = (Box)value;
509 // check if box being moved is currently target of a redirect
510 for(Box cur = newnode.parent; cur != null; cur = cur.parent)
511 if (cur.redirect == newnode) {
512 if (Log.on) Log.logJS(this, "attempt to move a box that is the target of a redirect");
516 // check for recursive ancestor violation
517 for(Box cur = this; cur != null; cur = cur.parent)
518 if (cur == newnode) {
519 if (Log.on) Log.logJS(this, "attempt to make a node a parent of its own ancestor");
520 if (Log.on) Log.log(this, "box == " + this + " ancestor == " + newnode);
524 if (numKids > 15 && children == null) convert_to_array();
526 newnode.parent = this;
528 if (children == null) {
529 if (firstKid == null) {
531 newnode.prevSibling = newnode;
532 newnode.nextSibling = newnode;
533 } else if (i >= numKids) {
534 newnode.prevSibling = firstKid.prevSibling;
535 newnode.nextSibling = firstKid;
536 firstKid.prevSibling.nextSibling = newnode;
537 firstKid.prevSibling = newnode;
540 for(int j=0; j<i; j++) cur = cur.nextSibling;
541 newnode.prevSibling = cur.prevSibling;
542 newnode.nextSibling = cur;
543 cur.prevSibling.nextSibling = newnode;
544 cur.prevSibling = newnode;
545 if (i == 0) firstKid = newnode;
550 if (i >= children.size()) {
551 newnode.indexInParent = children.size();
552 children.addElement(newnode);
554 children.insertElementAt(newnode, i);
555 for(int j=i; j<children.size(); j++)
556 getChild(j).indexInParent = j;
560 // need both of these in case child was already uncalc'ed
563 MARK_FOR_REFLOW_this;
567 // note that JavaScript box[0] will invoke put(int i), not put(String s)
572 public Object get(Object name) { return get(name, false); }
573 public Object get(Object name_, boolean ignoretraps) {
574 if (name_ instanceof Number) return get(((Number)name_).intValue());
576 if (!(name_ instanceof String)) return null;
577 String name = (String)name_;
578 if (name.equals("")) return null;
580 // See if we're reading back the function value of a trap
581 if (name.charAt(0) == '_') {
582 if (name.charAt(1) == '_') name = name.substring(2);
583 else name = name.substring(1);
584 Trap t = Trap.getTrap(this, name);
585 return t == null ? null : t.f;
588 // See if we're triggering a trap
589 Trap t = traps == null || ignoretraps ? null : (Trap)traps.get(name);
590 if (t != null && t.isreadtrap) return t.perform(Trap.emptyargs);
592 // Check for a special handler
593 SpecialBoxProperty gph = (SpecialBoxProperty)SpecialBoxProperty.specialBoxProperties.get(name);
594 if (gph != null) return gph.get(this);
596 Object ret = super.get(name);
597 if (name.startsWith("$") && ret == null)
598 if (Log.on) Log.logJS(this, "WARNING: attempt to access " + name + ", but no child with id=\"" + name.substring(1) + "\" found");
602 public Object[] keys() {
603 Object[] ret = new Object[numChildren()];
604 for(int i=0; i<ret.length; i++) ret[i] = new Integer(i);
610 * @param ignoretraps if set, no traps will be triggered (set when 'cascade' reaches the bottom of the trap stack)
611 * @param rp if this put is being performed via a root proxy, rp is the root proxy.
613 public void put(Object name, Object value) { put(name, value, false, null); }
614 public void put(Object name, Object value, boolean ignoretraps) { put(name, value, ignoretraps, null); }
615 public void put(Object name_, Object value, boolean ignoretraps, RootProxy rp) {
616 if (name_ instanceof Number) { put(((Number)name_).intValue(), value); return; }
617 if (!(name_ instanceof String)) { super.put(name_,value); return; }
618 String name = name_.toString();
619 if (!ignoretraps && traps != null) {
620 Trap t = (Trap)traps.get(name);
622 JS.Array arg = new JS.Array();
623 arg.addElement(value);
625 arg.setElementAt(null, 0);
630 // don't want to really cascade down to the box on this one
631 if (name.equals("0")) return;
633 SpecialBoxProperty gph = (SpecialBoxProperty)SpecialBoxProperty.specialBoxProperties.get(name);
634 if (gph != null) { gph.put(name, this, value); return; }
636 if (name.charAt(0) == '_') {
637 if (value != null && !(value instanceof JS.Callable)) {
638 if (Log.on) Log.logJS(this, "attempt to put a non function value (" + value + ") to " + name);
639 } else if (value != null && !(value instanceof JS.CompiledFunction)) {
640 if (Log.on) Log.logJS(this, "attempt to put a non-compiled function value (" + value + ") to " + name);
641 } else if (name.charAt(1) == '_') {
642 name = name.substring(2).intern();
643 Trap t = Trap.getTrap(this, name);
644 if (t != null) t.delete();
645 if (value != null) Trap.addTrap(this, name, ((JS.CompiledFunction)value), true, rp);
647 name = name.substring(1).intern();
648 Trap t = Trap.getTrap(this, name);
649 if (t != null) t.delete();
650 if (value != null) Trap.addTrap(this, name, ((JS.CompiledFunction)value), false, rp);
655 super.put(name, value);
659 // Tree Manipulation /////////////////////////////////////////////////////////////////////
661 /** The parent of this node */
662 private Box parent = null;
664 // Variables used in Vector mode */
665 /** INVARIANT: if (parent != null) parent.children.elementAt(indexInParent) == this */
666 private int indexInParent;
667 private Vec children = null;
669 // Variables used in linked-list mode
670 private int numKids = 0;
671 private Box nextSibling = null;
672 private Box prevSibling = null;
673 private Box firstKid = null;
675 // when we get more than 15 children, we switch to array-mode
676 private void convert_to_array() {
677 children = new Vec(numKids);
680 children.addElement(cur);
681 cur.indexInParent = children.size() - 1;
682 cur = cur.nextSibling;
683 } while (cur != firstKid);
686 /** remove this node from its parent; INVARIANT: whenever the parent of a node is changed, remove() gets called. */
687 public void remove() {
688 if (parent == null) {
689 if (surface != null) surface.dispose(true);
692 Box oldparent = parent;
693 if (oldparent == null) return;
694 MARK_FOR_REFLOW_this;
698 if (parent.children != null) {
699 parent.children.removeElementAt(indexInParent);
700 for(int j=indexInParent; j<parent.children.size(); j++)
701 (parent.getChild(j)).indexInParent = j;
704 if (parent.firstKid == this) {
705 if (nextSibling == this) parent.firstKid = null;
706 else parent.firstKid = nextSibling;
709 prevSibling.nextSibling = nextSibling;
710 nextSibling.prevSibling = prevSibling;
716 if (oldparent != null) { Box b = oldparent; MARK_FOR_REFLOW_b; }
718 // note that JavaScript box[0] will invoke put(int i), not put(String s)
719 if (oldparent != null) oldparent.put("0", this);
722 /** returns our next sibling (parent[ourindex + 1]) */
723 public final Box nextSibling() {
724 if (parent == null) return null;
725 if (parent.children == null) {
726 if (nextSibling == parent.firstKid) return null;
729 if (indexInParent >= parent.children.size() - 1) return null;
730 return (Box)parent.children.elementAt(indexInParent + 1);
734 /** returns our next sibling (parent[ourindex + 1]) */
735 public final Box prevSibling() {
736 if (parent == null) return null;
737 if (parent.children == null) {
738 if (this == parent.firstKid) return null;
741 if (indexInParent == 0) return null;
742 return (Box)parent.children.elementAt(indexInParent - 1);
746 /** Returns the parent of this node */
747 public Box getParent() { return parent; }
749 /** Returns ith child */
750 public Box getChild(int i) {
751 if (children == null) {
752 if (firstKid == null) return null;
753 if (i >= numKids) return null;
754 if (i == numKids - 1) return firstKid.prevSibling;
756 for(int j=0; j<i; j++) cur = cur.nextSibling;
759 if (i >= children.size() || i < 0) return null;
760 return (Box)children.elementAt(i);
764 /** Returns the number of children */
765 public int numChildren() {
766 if (children == null) {
767 if (firstKid == null) return 0;
769 for(Box cur = firstKid.nextSibling; cur != firstKid; i++) cur = cur.nextSibling;
772 return children.size();
776 /** Returns our index in our parent */
777 public int getIndexInParent() {
778 if (parent == null) return 0;
779 if (parent.children == null) {
781 for(Box cur = this; cur != parent.firstKid; i++) cur = cur.prevSibling;
784 return indexInParent;
788 /** returns the root of the surface that this box belongs to */
789 public final Box getRoot() {
790 if (parent == null && surface != null) return this;
791 if (parent == null) return null;
792 return parent.getRoot();
796 // Root Proxy ///////////////////////////////////////////////////////////////////////////////
798 // FEATURE: use xwt.graft() here
799 RootProxy myproxy = null;
800 public JS getRootProxy() {
801 if (myproxy == null) myproxy = new RootProxy(this);
805 private static class RootProxy extends JS {
807 RootProxy(Box b) { this.box = b; }
808 public Object get(Object name) { return box.get(name); }
809 public void put(Object name, Object value) { box.put(name, value, false, this); }
810 public Object[] keys() { return box.keys(); }
811 public Object callMethod(Object method, JS.Array args, boolean justChecking) { return box.callMethod(method,args,justChecking); }
815 // Trivial Helper Methods (should be inlined) /////////////////////////////////////////
817 static final int min(int a, int b) { if (a<b) return a; else return b; }
818 static final double min(double a, double b) { if (a<b) return a; else return b; }
819 static final int max(int a, int b) { if (a>b) return a; else return b; }
820 static final int min(int a, int b, int c) { if (a<=b && a<=c) return a; else if (b<=c && b<=a) return b; else return c; }
821 static final int max(int a, int b, int c) { if (a>=b && a>=c) return a; else if (b>=c && b>=a) return b; else return c; }
822 static final int bound(int a, int b, int c) { if (c < b) return c; if (a > b) return a; return b; }
823 final boolean inside(int x, int y) { return (!invisible && x >= this.x && y >= this.y && x < this.x + width && y < this.y + height); }
825 /** figures out what box in this subtree of the Box owns the pixel at x,y relitave to the Surface */
826 public static Box whoIs(Box cur, int x, int y) {
828 // WARNING: this method is called from the event-queueing
829 // thread -- it may run concurrently with ANY part of XWT, and
830 // is UNSYNCHRONIZED for performance reasons. BE CAREFUL
833 if (cur.invisible) return null;
834 if (!cur.inside(x,y)) return cur.parent == null ? cur : null;
836 for(int i=cur.numChildren() - 1; i>=0; i--) {
837 Box child = cur.getChild(i);
838 if (child == null) continue; // since this method is unsynchronized, we have to double-check
839 if (!child.invisible && child.inside(x,y)) { cur = child; continue OUTER; }
847 * A helper class for properties of Box which require special
850 * To avoid excessive use of String.equals(), the Box.get() and
851 * Box.put() methods employ a Hash keyed on property names that
852 * require special handling. The value stored in the Hash is an
853 * instance of an anonymous subclass of SpecialBoxProperty, which knows
854 * how to handle get()s and put()s for that property name. There
855 * should be one anonymous subclass of SpecialBoxProperty for each
856 * specially-handled property on Box.
858 static class SpecialBoxProperty {
860 SpecialBoxProperty() { }
862 /** stores instances of SpecialBoxProperty; keyed on property name */
863 static Hash specialBoxProperties = new Hash(200, 3);
865 /** this method defines the behavior when the property is get()ed from b */
866 Object get(Box b) { return null; }
868 /** this method defines the behavior when the property is put() to b */
869 void put(Box b, Object value) { }
871 /** this method defines the behavior when the property is put() to b, allows a single SpecialBoxProperty to serve multiple properties */
872 void put(String name, Box b, Object value) { put(b, value); }
875 //#repeat fillcolor/strokecolor
876 specialBoxProperties.put("fillcolor", new SpecialBoxProperty() {
877 public Object get(Box b) {
878 if ((b.fillcolor & 0xFF000000) == 0) return null;
879 String red = Integer.toHexString((b.fillcolor & 0x00FF0000) >> 16);
880 String green = Integer.toHexString((b.fillcolor & 0x0000FF00) >> 8);
881 String blue = Integer.toHexString(b.fillcolor & 0x000000FF);
882 if (red.length() < 2) red = "0" + red;
883 if (blue.length() < 2) blue = "0" + blue;
884 if (green.length() < 2) green = "0" + green;
885 return "#" + red + green + blue;
887 public void put(Box b, Object value) {
888 int newcolor = b.fillcolor;
889 String s = value == null ? null : value.toString();
890 if (value == null) newcolor = 0x00000000;
891 else if (s.length() > 0 && s.charAt(0) == '#')
893 newcolor = 0xFF000000 |
894 (Integer.parseInt(s.substring(1, 3), 16) << 16) |
895 (Integer.parseInt(s.substring(3, 5), 16) << 8) |
896 Integer.parseInt(s.substring(5, 7), 16);
897 } catch (NumberFormatException e) {
898 Log.log(this, "invalid color " + s);
901 // FIXME put named colors back in
906 specialBoxProperties.put("text", new SpecialBoxProperty() {
907 public Object get(Box b) { return b.text; }
908 public void put(Box b, Object value) {
909 String t = value == null ? "null" : value.toString();
910 if (t.equals(b.text)) return;
911 // FIXME text is broken
913 specialBoxProperties.put("font", new SpecialBoxProperty() {
914 public Object get(Box b) { return b.font; }
915 public void put(Box b, Object value) {
916 b.font = value == null ? null : value.toString();
921 specialBoxProperties.put("thisbox", new SpecialBoxProperty() {
922 public Object get(Box b) { return b; }
923 public void put(Box b, Object value) {
924 if (value == null) b.remove();
925 else if (value.equals("window") || value.equals("frame")) Platform.createSurface(b, value.equals("frame"), true);
926 else if (Log.on) Log.log(this, "put invalid value to 'thisbox' property: " + value);
930 specialBoxProperties.put("orient", new SpecialBoxProperty() {
931 public Object get(Box b) {
932 if (b.redirect == null) return "horizontal";
933 else if (b.redirect != b) return get(b.redirect);
934 else if (b.cols == 1) return "vertical";
935 else if (b.rows == 1) return "horizontal";
938 public void put(Box b, Object value) {
939 if (value == null) return;
940 if (b.redirect == null) return;
941 if (b.redirect != b) { put(b.redirect, value); return; }
942 if (value.equals("vertical")) {
943 if (b.rows == 0) return;
944 b.rows = 0; b.cols = 1;
945 } else if (value.equals("horizontal")) {
946 if (b.cols == 0) return;
947 b.cols = 0; b.rows = 1;
949 Log.log(this, "invalid value put to orient property: " + value);
953 specialBoxProperties.put("static", new SpecialBoxProperty() {
954 public Object get(Box b) {
956 JS.Thread.fromJavaThread(java.lang.Thread.currentThread()).getCurrentCompiledFunction().getSourceName();
957 for(int i=0; i<cfsn.length() - 1; i++)
958 if (cfsn.charAt(i) == '.' && (cfsn.charAt(i+1) == '_' || Character.isDigit(cfsn.charAt(i+1)))) {
959 cfsn = cfsn.substring(0, i);
962 return Static.getStatic(cfsn);
966 specialBoxProperties.put("shrink", new SpecialBoxProperty() {
967 public Object get(Box b) { return (b.vshrink && b.hshrink) ? Boolean.TRUE : Boolean.FALSE; }
968 public void put(Box b, Object value) { b.put("hshrink", value); b.put("vshrink", value); }
971 //#repeat hshrink/vshrink
972 specialBoxProperties.put("hshrink", new SpecialBoxProperty() {
973 public Object get(Box b) { return new Boolean(b.hshrink); }
974 public void put(Box b, Object value) {
975 boolean newshrink = stob(value);
976 if (b.hshrink == newshrink) return;
977 b.hshrink = newshrink;
984 specialBoxProperties.put("x", new SpecialBoxProperty() {
985 public Object get(Box b) {
986 if (b.surface == null) return new Integer(0);
987 if (b.invisible) return new Integer(0);
988 return new Integer(b.x);
990 public void put(Box b, Object value) {
992 if (b.parent == null && b.surface != null) {
993 // FIXME this gets hosed by the #repeat
994 //b.surface.setLocation(b.x, b.y);
995 b.surface.centerSurfaceOnRender = false;
1001 //#repeat width/height minwidth/minheight maxwidth/maxheight
1002 specialBoxProperties.put("width", new SpecialBoxProperty() {
1003 public Object get(Box b) { return new Integer(b.width); }
1004 public void put(Box b, Object value) {
1005 b.width = stoi(value);
1006 if (b.parent == null && b.surface != null) {
1007 // FIXME this gets hosed...
1008 //b.surface.setSize(max(Surface.scarPicture.getWidth(), b.width),
1009 //max(Surface.scarPicture.getHeight(), b.height));
1012 b.minwidth = b.minheight = b.width;
1018 //#repeat cols/rows rows/cols
1019 specialBoxProperties.put("cols", new SpecialBoxProperty() {
1020 public Object get(Box b) { return new Double(b.cols); }
1021 public void put(Box b, Object value) {
1022 b.cols = stoi(value);
1023 if (b.cols == 0 && b.rows == 0) b.rows = 1;
1024 if (b.cols != 0 && b.rows != 0) b.rows = 0;
1029 //#repeat colspan/rowspan
1030 specialBoxProperties.put("colspan", new SpecialBoxProperty() {
1031 public Object get(Box b) { return new Double(b.colspan); }
1032 public void put(Box b, Object value) { b.colspan = stoi(value); MARK_FOR_REFLOW_b; }
1036 specialBoxProperties.put("tile", new SpecialBoxProperty() {
1037 public Object get(Box b) { return b.tile ? Boolean.TRUE : Boolean.FALSE; }
1038 public void put(Box b, Object value) {
1039 boolean newtile = stob(value);
1040 if (newtile == b.tile) return;
1045 specialBoxProperties.put("invisible", new SpecialBoxProperty() {
1046 public Object get(Box b) {
1047 for (Box cur = b; cur != null; cur = cur.parent) { if (cur.invisible) return Boolean.TRUE; }
1048 return Boolean.FALSE;
1050 public void put(Box b, Object value) {
1051 boolean newinvisible = stob(value);
1052 if (newinvisible == b.invisible) return;
1053 b.invisible = newinvisible;
1054 if (b.parent == null) {
1055 if (b.surface != null) b.surface.setInvisible(newinvisible);
1058 MARK_FOR_REFLOW_b_parent;
1059 b.parent.dirty(b.x, b.y, b.width, b.height);
1063 specialBoxProperties.put("absolute", new SpecialBoxProperty() {
1064 public Object get(Box b) { return b.absolute ? Boolean.TRUE : Boolean.FALSE; }
1065 public void put(Box b, Object value) {
1066 boolean newabsolute = stob(value);
1067 if (newabsolute == b.absolute) return;
1068 b.absolute = newabsolute;
1069 if (b.parent != null) MARK_FOR_REFLOW_b_parent;
1072 specialBoxProperties.put("image", new SpecialBoxProperty() {
1073 public Object get(Box b) { return b.image == null ? null : ImageDecoder.imageToNameMap.get(b.image); }
1074 public void put(Box b, Object value) {
1075 if ((value == null && b.image == null) ||
1076 (value != null && b.image != null && value.equals(ImageDecoder.imageToNameMap.get(b.image)))) return;
1077 String s = value == null ? null : value.toString();
1078 if (s == null || s.equals("")) b.image = null;
1080 if ((b.image = ImageDecoder.getPicture(s)) == null) {
1081 if (Log.on) Log.logJS(Box.class, "unable to load image " + s);
1083 b.minwidth = b.maxwidth = b.image.getWidth();
1084 b.minheight = b.maxheight = b.image.getHeight();
1091 //#repeat globalx/globaly x/y
1092 specialBoxProperties.put("globalx", new SpecialBoxProperty() {
1093 public Object get(Box b) { return new Integer(b.parent == null || b.surface == null ? 0 : b.x); }
1094 public void put(Box b, Object value) {
1095 if (b.surface == null || b.parent == null) return;
1096 b.put("x", new Integer(stoi(value) - stoi(get(b.parent))));
1101 specialBoxProperties.put("cursor", new SpecialBoxProperty() {
1102 public Object get(Box b) { return b.cursor; }
1103 public void put(Box b, Object value) {
1104 b.cursor = (String)value;
1105 if (b.surface == null) return;
1107 // see if we need to update the surface cursor
1108 Surface surface = b.getRoot().surface;
1109 String tempcursor = surface.cursor;
1110 b.Move(surface.mousex, surface.mousey, surface.mousex, surface.mousey);
1111 if (surface.cursor != tempcursor) surface.syncCursor();
1115 //#repeat mousex/mousey x/y
1116 specialBoxProperties.put("mousex", new SpecialBoxProperty() {
1117 public Object get(Box b) { return new Integer(b.getRoot().surface == null ? 0 : b.getRoot().surface.mousex - b.x); }
1121 specialBoxProperties.put("xwt", new SpecialBoxProperty() {
1122 public Object get(Box b) { return XWT.singleton; }
1125 specialBoxProperties.put("mouseinside", new SpecialBoxProperty() {
1126 public Object get(Box b) { return b.mouseinside ? Boolean.TRUE : Boolean.FALSE; }
1129 specialBoxProperties.put("numchildren", new SpecialBoxProperty() {
1130 public Object get(Box b) {
1131 if (b.redirect == null) return new Integer(0);
1132 if (b.redirect != b) return get(b.redirect);
1133 return new Integer(b.numChildren());
1136 SpecialBoxProperty mouseEventHandler = new SpecialBoxProperty() {
1137 public void put(String name, Box b, Object value) {
1138 Surface surface = b.getRoot().surface;
1139 if (surface == null) return;
1140 for(Box c = b.prevSibling(); c != null; c = c.prevSibling()) {
1141 Box siblingChild = whoIs(c, surface.mousex, surface.mousey);
1142 if (siblingChild != null) {
1143 siblingChild.put(name, value);
1147 if (b.parent != null) b.parent.put(name, value);
1150 specialBoxProperties.put("Press1", mouseEventHandler);
1151 specialBoxProperties.put("Press2", mouseEventHandler);
1152 specialBoxProperties.put("Press3", mouseEventHandler);
1153 specialBoxProperties.put("Release1", mouseEventHandler);
1154 specialBoxProperties.put("Release2", mouseEventHandler);
1155 specialBoxProperties.put("Release3", mouseEventHandler);
1156 specialBoxProperties.put("Click1", mouseEventHandler);
1157 specialBoxProperties.put("Click2", mouseEventHandler);
1158 specialBoxProperties.put("Click3", mouseEventHandler);
1159 specialBoxProperties.put("DoubleClick1", mouseEventHandler);
1160 specialBoxProperties.put("DoubleClick2", mouseEventHandler);
1161 specialBoxProperties.put("DoubleClick3", mouseEventHandler);
1163 specialBoxProperties.put("root", new SpecialBoxProperty() {
1164 public Object get(Box b) {
1165 if (b.getRoot() == null) return null;
1166 else if (b.parent == null) return b;
1167 else return b.getRoot().getRootProxy();
1170 specialBoxProperties.put("Minimized", new SpecialBoxProperty() {
1171 public Object get(Box b) {
1172 if (b.parent == null && b.surface != null) return b.surface.minimized ? Boolean.TRUE : Boolean.FALSE;
1175 public void put(Box b, Object value) {
1176 if (b.surface == null) return;
1177 boolean val = stob(value);
1178 if (b.parent == null && b.surface.minimized != val) b.surface.setMinimized(val);
1182 specialBoxProperties.put("Maximized", new SpecialBoxProperty() {
1183 public Object get(Box b) {
1184 if (b.parent == null && b.surface != null) return b.surface.maximized ? Boolean.TRUE : Boolean.FALSE;
1187 public void put(Box b, Object value) {
1188 if (b.surface == null) return;
1189 boolean val = stob(value);
1190 if (b.parent == null && b.surface.maximized != val) b.surface.setMaximized(val);
1194 specialBoxProperties.put("toback", new SpecialBoxProperty() {
1195 public void put(Box b, Object value) {
1196 if (b.parent == null && stob(value) && b.surface != null) b.surface.toBack();
1200 specialBoxProperties.put("tofront", new SpecialBoxProperty() {
1201 public void put(Box b, Object value) {
1202 if (b.parent == null && stob(value) && b.surface != null) b.surface.toFront();
1206 //#repeat hscar/vscar
1207 specialBoxProperties.put("hscar", new SpecialBoxProperty() {
1208 public void put(Box b, Object value) {
1209 if (b.parent == null && b.surface != null) {
1210 b.surface.hscar = stoi(value);
1211 b.surface.dirty(0, 0, b.surface.width, b.surface.height);
1212 b.surface.Refresh();
1218 specialBoxProperties.put("Close", new SpecialBoxProperty() {
1219 public void put(Box b, Object value) {
1220 if (b.parent == null && b.surface != null) b.surface.dispose(true);
1224 // these are all do-nothings; just to prevent space from getting taken up in the params Hash.
1225 specialBoxProperties.put("KeyPressed", new SpecialBoxProperty()); // FIXME should cascade
1226 specialBoxProperties.put("KeyReleased", new SpecialBoxProperty()); // FIXME should cascade
1227 specialBoxProperties.put("PosChange", new SpecialBoxProperty());
1228 specialBoxProperties.put("SizeChange", new SpecialBoxProperty());
1231 specialBoxProperties.put("hpad", new SpecialBoxProperty() {
1232 public Object get(Box b) {
1233 if (b.redirect == null) return new Integer(0);
1234 if (b.redirect != b) return get(b.redirect);
1235 return new Integer(b.hpad);
1237 public void put(Box b, Object value) {
1238 if (b.redirect == null) return;
1239 if (b.redirect != b) { put(b.redirect, value); return; }
1240 int newval = stoi(value);
1241 if (newval == b.hpad) return;
1248 //#repeat minwidth/minheight maxwidth/maxheight
1249 specialBoxProperties.put("minwidth", new SpecialBoxProperty() {
1250 public Object get(Box b) { return new Integer(b.minwidth); }
1251 public void put(Box b, Object value) {
1252 if (stoi(value) == b.minwidth) return;
1253 b.minwidth = stoi(value);
1257 specialBoxProperties.put("maxwidth", new SpecialBoxProperty() {
1258 public Object get(Box b) { return new Integer(b.maxwidth); }
1259 public void put(Box b, Object value) {
1260 if (stoi(value) == b.maxwidth) return;
1261 b.maxwidth = stoi(value);
1267 specialBoxProperties.put("redirect", new SpecialBoxProperty() {
1268 public void put(Box b, Object value) { }
1269 public Object get(Box b) {
1270 if (b.redirect == null) return null;
1271 if (b.redirect == b) return Boolean.TRUE;
1272 return get(b.redirect);
1277 // FIXME: need to be able to read this back
1278 specialBoxProperties.put("titlebar", new SpecialBoxProperty() {
1279 public void put(Box b, Object value) { surface.setTitleBarText(value.toString()); }
1280 public Object get(Box b) { return b.ti; }
1283 // FIXME: need to be able to read this back
1284 specialBoxProperties.put("icon", new SpecialBoxProperty() {
1285 public void put(Box b, Object value) {
1286 Picture pic = Box.getPicture(value.toString());
1287 if (pic != null) surface.setIcon(pic);
1288 else if (Log.on) Log.log(this, "unable to load icon " + value);
1290 public Object get(Box b) { return b.id; }
1296 /** helper that converts a String to a boolean according to JavaScript coercion rules */
1297 public static boolean stob(Object o) {
1298 if (o == null) return false;
1299 return Boolean.TRUE.equals(o) || "true".equals(o);
1304 /** helper that converts a String to an int according to JavaScript coercion rules */
1305 public static int stoi(Object o) {
1306 if (o == null) return 0;
1307 if (o instanceof Integer) return ((Integer)o).intValue();
1310 if (!(o instanceof String)) s = o.toString();
1313 try { return Integer.parseInt(s.indexOf('.') == -1 ? s : s.substring(0, s.indexOf('.'))); }
1314 catch (NumberFormatException e) { return 0; }