/* * XmFormLayout.java $Revision: 1.1 $ (a Java layout manager * for AWT 1.1 which was inspired by the Motif XmForm widget) * Copyright (C) 1996 Softbear Inc. (info@softbear.com) * Latest version at http://www.softbear.com/java/xmformlm * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library 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 * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public * License along with this library; if not, write to the Free * Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. */ /* * Changes to Revision 1.0 on 11/13/1997 by Ben Hill (ben@opnsys.com) * PreferredSize changed to getPreferredSize * insets changed to getInsets * size changed to getSize * reshape changed to setBounds *** * */ /* * * XmFormLayout is a Java layout manager which arranges display * components according to constraints (it was inspired by * Motif's XmForm widget). Here is an example of how it is used: * * public class TestXFL extends java.applet.Applet { * * public void init() { * String constraints[] = { * "Humpty.bottom=form.bottom-23", * "Humpty.left=form.left+21", * "Humpty.right=Tweedle.right", * "Tweedle.left=Humpty.left", * "Tweedle.top=form.top+24", * "Tweedle.bottom=Humpty.top-10", * "Dumpty.bottom=Humpty.bottom", * "Dumpty.left=Humpty.right+30", * "Dumpty.right=form.right-22", * "Dumpty.top=Humpty.top", * "Dee.bottom=Tweedle.bottom", * "Dee.top=Tweedle.top", * "Dee.left=Dumpty.left", * "Dee.right=Dumpty.right", * }; * * this.setLayout(new XmFormLayout(constraints)); * this.add("Humpty", new java.awt.Button("Humpty")); * java.awt.TextArea tweedle = new java.awt.TextArea(15,15); * tweedle.appendText("Tweedle"); * this.add("Tweedle", tweedle); * this.add("Dumpty", new java.awt.Button("Dumpty")); * java.awt.TextArea dee = new java.awt.TextArea(); * dee.appendText("Dee"); * this.add("Dee", dee); * } * } * */ public class XmFormLayout implements java.awt.LayoutManager { /* * Component tuple constants, these must range from 0...6. * Attribute numbers must range from BOTTOM...TOP. */ final static int COMPONENT = 0; final static int BOTTOM = 1; final static int LEFT = 2; final static int RIGHT = 3; final static int TOP = 4; final static int BITSET = 5; final static int NAME = 6; /* * Other constants. */ final static int FORM = -32765; final static int INVALID = -32766; final static int UNBOUND = -32767; final static boolean debug = false; final static boolean bigDebug = false; /* * The constraints and the results of the * last reshap() computations. */ String myName; java.awt.Dimension formDimensions; // (as of last reconstrain) java.awt.Point formLocation; // (as of last reconstrain) java.util.Vector tuples; // (4 attrs, component, bitset) java.util.Hashtable nameToIndex; String constraints[]; boolean changedFlag; /* * Add a component. */ public void addLayoutComponent(String name, java.awt.Component component) { Object tuple[] = new Object[7]; tuple[XmFormLayout.COMPONENT] = component; tuple[XmFormLayout.BITSET] = new java.util.BitSet(); tuple[XmFormLayout.NAME] = name; int componentNr = this.tuples.size(); this.tuples.addElement(tuple); this.nameToIndex.put(name, new Integer(componentNr)); this.changedFlag = true; } /* * Parses an expression 'a.b+c' (or 'a.b-c' or 'a.b' or * '+c' or '-c') into an rvalue 'a.b' and a signed * number 'c'. If the rvalue is unbound then return * XmFormLayout.UNBOUND, otherwise return the value of * the expression. */ private final int evaluate(String expression) throws Exception { int i, s = 1, v = 0; if (this.debug) { System.out.println(this.myName + "'evaluate() expr: " + expression + "\n"); } i = expression.indexOf('+'); if (i == -1) { i = expression.indexOf('-'); s = -1; } if (i == -1) { v = this.getAttr(expression); } else if (i > 1) { String rvalue = expression.substring(0, i); v = this.getAttr(rvalue); } if (i != -1 && v != XmFormLayout.UNBOUND) { String unicodeNumber = expression.substring(i+1); int number = Integer.parseInt(unicodeNumber, 10); v += (s*number); } return v; } /* * Enforce explicit constraints by binding lvalues and * also marking those lvalues which can't be bound. * Parses an explicit constraint 'a.b=c.d+e' into * an lvalue 'a.b' and an expression 'c.d+e'. * Returns true if all possible explicit lvalues are bound. */ private final boolean explicit(java.util.BitSet constraintBitset) throws Exception { if (this.debug) { System.out.println(this.myName + "'explicit()...\n"); } boolean deferFlag = false; if (this.constraints == null || this.constraints.length == 0) { throw new Exception(this.myName + "'explicit() no constraints"); } for (int i = 0; i < this.constraints.length; ++i) { if (constraintBitset.get(i)) { continue; } String constraint = this.constraints[i]; if (this.debug) { System.out.println(this.myName + "'explicit() constraint: " + constraint + "\n"); } int j = constraint.indexOf('='); if (j < 3) { throw new Exception(this.myName + "'explicit() invalid constraint: " + constraint); } String lvalue = constraint.substring(0, j); String expression = constraint.substring(j+1); if (this.setAttr(lvalue, this.evaluate(expression))) { constraintBitset.set(i); } else { deferFlag = true; } } return !deferFlag; } /* * If the attribute is bound then return its' * value, otherwise return XmFormLayout.UNBOUND. */ private final int getAttr(int componentNr, int attrNr) { if (this.bigDebug) { System.out.println(this.myName + "'getAttr(): c=" + componentNr + ", a=" + attrNr + "\n"); } int v = XmFormLayout.UNBOUND; if (componentNr == XmFormLayout.FORM) { if (attrNr == XmFormLayout.BOTTOM) { v = this.formLocation.y + this.formDimensions.height; } else if (attrNr == XmFormLayout.LEFT) { v = this.formLocation.x; } else if (attrNr == XmFormLayout.RIGHT) { v = this.formLocation.x + this.formDimensions.width; } else if (attrNr == XmFormLayout.TOP) { v = this.formLocation.y; } } else if (isBound(componentNr, attrNr)) { Object tuple[] = (Object[]) this.tuples.elementAt(componentNr); v = ((Integer) tuple[attrNr]).intValue(); } return v; } /* * If the attribute is bound then return its' * value, otherwise return XmFormLayout.UNBOUND. */ private final int getAttr(String fqAttrName) throws Exception { int tuple[] = this.parseAttr(fqAttrName); int value = this.getAttr(tuple[0], tuple[1]); if (this.debug) { System.out.println(this.myName + "'getAttr(): fqAttr=" + fqAttrName + ", value=" + value + "\n"); } return value; } private final int getAttrNr(String attrName) { if (this.bigDebug) { System.out.println(this.myName + "'getAttrNr() attr: " + attrName + "\n"); } int attrNr = XmFormLayout.INVALID; // invalid attr # (never happens) if (attrName.equals("bottom")) { attrNr = XmFormLayout.BOTTOM; } else if (attrName.equals("left") || attrName.equals("x")) { attrNr = XmFormLayout.LEFT; } else if (attrName.equals("right")) { attrNr = XmFormLayout.RIGHT; } else if (attrName.equals("top") || attrName.equals("y")) { attrNr = XmFormLayout.TOP; } return attrNr; } private final java.util.BitSet getBitset(int componentNr) { Object tuple[] = (Object[]) this.tuples.elementAt(componentNr); return (java.util.BitSet) tuple[XmFormLayout.BITSET]; } private final java.awt.Component getComponent(int componentNr) { Object tuple[] = (Object[]) this.tuples.elementAt(componentNr); return (java.awt.Component) tuple[XmFormLayout.COMPONENT]; } private final String getComponentName(int componentNr) { Object tuple[] = (Object[]) this.tuples.elementAt(componentNr); return (String) tuple[XmFormLayout.NAME]; } private final int getComponentNr(String componentName) throws Exception { if (this.bigDebug) { System.out.println(this.myName + "'getComponentNr() attr: " + componentName + "\n"); } Object n = this.nameToIndex.get(componentName); if (n == null) { throw new Exception(this.myName + "'getComponentNr() not found: " + componentName); } return ((Integer)n).intValue(); } /* * Returns the preferred offset (width or height) * between the specified attribute and its' opposite. */ private final int getOffset(int componentNr, int attrNr) { int offset = XmFormLayout.INVALID; // invalid offset (never happens) java.awt.Component component = this.getComponent(componentNr); if (attrNr == XmFormLayout.BOTTOM) { offset = -component.getPreferredSize().height; } else if (attrNr == XmFormLayout.LEFT) { offset = component.getPreferredSize().width; } else if (attrNr == XmFormLayout.RIGHT) { offset = -component.getPreferredSize().width; } else if (attrNr == XmFormLayout.TOP) { offset = component.getPreferredSize().height; } return offset; } /* * Enforces implicit constraints by binding component * attributes which haven't an lvalue in ANY explicit * constraint and whose opposite attribute is already * bound, e.g. if foo.top isn't an lvalue in any explicit * constraint, and foo.bottom is bound, and foo has a * preferred height of 123, then enforce this implicit * constraint: * "foo.top=foo.bottom-123" * Returns true if all possible implicit constraints are bound. */ private final boolean implicit() throws Exception { if (this.debug) { System.out.println(this.myName + "'implicit()...\n"); } boolean deferFlag = false; for (int c = 0; c < this.tuples.size(); ++c) { for (int a = XmFormLayout.BOTTOM; a <= XmFormLayout.TOP; ++a) { if (this.isMarked(c, a) == false) { if (this.debug) { System.out.println(this.myName + "'implicit(): c=" + c + ", a=" + a + "\n"); } int o = this.opposite(a); if (this.isBound(c, o) == true) { // this also marks the attr this.setAttr(c, a, this.getAttr(c, o) + this.getOffset(c, o)); } else { deferFlag = true; } } } } return !deferFlag; } /* * Returns true if this component's attribute was * bound; that is, if this.setAttr() was called. */ private final boolean isBound(int componentNr, int attributeNr) { java.util.BitSet bitset = this.getBitset(componentNr); return bitset.get(attributeNr); } /* * Returns true if this component's attribute is * an lvalue in any explicit constraint; that is, * if this.setMarked() was called. */ private final boolean isMarked(int componentNr, int attributeNr) { java.util.BitSet bitset = this.getBitset(componentNr); return bitset.get(attributeNr + (XmFormLayout.TOP+1)); } /* * Reshape all components in the container. Typically, * this causes a call to this.reconstrain() only the first time. */ public void layoutContainer(java.awt.Container parent) { java.awt.Dimension dummy = this.preferredLayoutSize(parent); // reconstrain if necessary if (this.debug) { System.out.println(this.myName + "'layoutContainer(): w=" + dummy.width + ", h=" + dummy.height + ", changedFlag=" + changedFlag + " (should be false)\n"); } if (!this.changedFlag) { for (int c = 0; c < this.tuples.size(); ++c) { Object tuple[] = (Object[]) this.tuples.elementAt(c); int x = ((Integer)tuple[XmFormLayout.LEFT]).intValue(); int y = ((Integer)tuple[XmFormLayout.TOP]).intValue(); int w = ((Integer)tuple[XmFormLayout.RIGHT]).intValue() - x; int h = ((Integer)tuple[XmFormLayout.BOTTOM]).intValue() - y; if (this.debug) { System.out.println(this.myName + "'layoutContainer(): c=" + (String)tuple[XmFormLayout.NAME] + ", x=" + x + ", y=" + y + ", w=" + w + ", h=" + h + "\n"); } java.awt.Component component = (java.awt.Component) tuple[XmFormLayout.COMPONENT]; component.setBounds(x, y, w, h); } } } /* * XmFormLayout knowns only one size (the preferred size). */ public java.awt.Dimension minimumLayoutSize(java.awt.Container parent) { return preferredLayoutSize(parent); } private final int opposite(int attrNr) { int oppositeAttrNr = XmFormLayout.INVALID; // invalid attr # (never happens) if (attrNr == XmFormLayout.BOTTOM) { oppositeAttrNr = XmFormLayout.TOP; } else if (attrNr == XmFormLayout.LEFT) { oppositeAttrNr = XmFormLayout.RIGHT; } else if (attrNr == XmFormLayout.RIGHT) { oppositeAttrNr = XmFormLayout.LEFT; } else if (attrNr == XmFormLayout.TOP) { oppositeAttrNr = XmFormLayout.BOTTOM; } return oppositeAttrNr; } /* * Parse a fully-qualified attribute name 'a.b' into * a component 'a' and an attribute 'b', and return * the component number and attribute number. */ private final int[] parseAttr(String fqAttrName) throws Exception { int i = fqAttrName.indexOf('.'); if (i < 1) { throw new Exception(this.myName + "'parseAttr() invalid fqAttr: " + fqAttrName); } String componentName = fqAttrName.substring(0, i); String attrName = fqAttrName.substring(i+1); int tuple[] = new int[2]; if (componentName.equals("form")) { tuple[0] = XmFormLayout.FORM; } else { tuple[0] = this.getComponentNr(componentName); } tuple[1] = this.getAttrNr(attrName); return tuple; } public java.awt.Dimension preferredLayoutSize(java.awt.Container parent) { java.awt.Insets insets = parent.getInsets(); int dx = insets.left + insets.right; int dy = insets.top + insets.bottom; if (this.debug) { System.out.println(this.myName + "'preferredLayoutSize(): dx=" + dx + ", dy=" + dy + "\n"); } int x = insets.left; int y = insets.top; int w = parent.getSize().width - dx; int h = parent.getSize().height - dy; this.reshape(x, y, w, h); return new java.awt.Dimension(this.formDimensions.width + dx, this.formDimensions.height + dy); } /* * Removes a component. This isn't particularly fast, * but it is rarely called, if ever. */ public void removeLayoutComponent(java.awt.Component oldComponent) { int nrComponents = this.tuples.size(); for (int c = 0; c < nrComponents; ++c) { if (this.getComponent(c) == oldComponent) { String componentName = this.getComponentName(c); this.nameToIndex.remove(componentName); this.changedFlag = true; } } } /* * Layout the container according to constraints. * This may be time-consuming, so it should only be * done if changedFlag is true. */ private final void reconstrain() throws Exception { java.util.BitSet constraintBitset = new java.util.BitSet(); for (int c = 0; c < this.tuples.size(); ++c) { java.util.BitSet componentBitset = this.getBitset(c); /* * The componentBitset has no clearAllBits() * method so AND with any blank bitset to clear it. */ componentBitset.and(constraintBitset); } boolean deferFlag; int nrTries = 0; do { if (this.debug) { System.out.println(this.myName + "'reconstrain(): iteration=" + (nrTries+1) + "*****\n"); } deferFlag = false; if (! this.explicit(constraintBitset)) { deferFlag = true; } if (! this.implicit()) { deferFlag = true; } } while (deferFlag && ++nrTries < 30); if (deferFlag) { throw new Exception(this.myName + "'reconstrain() too many iterations: " + nrTries); } this.changedFlag = false; } /* * If the container's location or shape has * changed, or if any of the component's have * changed, then re-compute the constraints; * otherwise use what was already computed. */ private final void reshape(int x, int y, int w, int h) { if (this.formDimensions.width != w || this.formDimensions.height != h) { if (this.debug) { System.out.println(this.myName + "'reshape(): ***SIZE CHANGED*** w=" + w + ", h=" + h + " *******\n"); } this.formDimensions.width = w; this.formDimensions.height = h; this.changedFlag = true; } if (this.formLocation.x != x || this.formLocation.y != y) { this.formLocation.x = x; this.formLocation.y = y; this.changedFlag = true; } if (changedFlag) { try { this.reconstrain(); } catch (Exception exception) { if (this.debug) { System.out.println(this.myName + "'EXCEPTION: " + exception.getMessage() + "\n"); } } } } /* * Try to set a attribute; returns true if the was set * or false if it was deferred (the latter will happen * if the value is XmFormLayout.UNBOUND). */ private final boolean setAttr(int componentNr, int attrNr, int value) throws Exception { if (this.debug) { System.out.println(this.myName + "'setAttr(): c=" + componentNr + ", a=" + attrNr + ", v=" + value + "\n"); } if (componentNr == XmFormLayout.FORM) { throw new Exception(this.myName + "'setAttr() can't set form\n"); } this.setMarked(componentNr, attrNr); boolean boundFlag = false; if (value != XmFormLayout.UNBOUND) { Object tuple[] = (Object[]) this.tuples.elementAt(componentNr); tuple[attrNr] = new Integer(value); this.setBound(componentNr, attrNr); boundFlag = true; } return boundFlag; } /* * Try to set a attribute 'c.a'; returns true if the * was set or false if it was deferred (the latter * will happen if the value is XmFormLayout.UNBOUND). */ private final boolean setAttr(String fqAttrName, int value) throws Exception { if (this.debug) { System.out.println(this.myName + "'setAttr() fqAttr=" + fqAttrName + ", value=" + value + "\n"); } int tuple[] = this.parseAttr(fqAttrName); return this.setAttr(tuple[0], tuple[1], value); } private final void setBound(int componentNr, int attrNr) { if (this.bigDebug) { System.out.println(this.myName + "'setBound(): c=" + componentNr + ", a=" + attrNr + "\n"); } java.util.BitSet bitset = this.getBitset(componentNr); bitset.set(attrNr); } private final void setMarked(int componentNr, int attrNr) { java.util.BitSet bitset = this.getBitset(componentNr); bitset.set(attrNr + (XmFormLayout.TOP+1)); } /* * The constructor. */ public XmFormLayout(String newConstraints[]) { this.myName = (((Object) this).getClass()).getName(); if (newConstraints == null && this.debug) { System.out.println(this.myName + "'XmFormLayout() newConstraints is null!\n"); } this.constraints = newConstraints; this.formDimensions = new java.awt.Dimension(0,0); this.formLocation = new java.awt.Point(0,0); this.nameToIndex = new java.util.Hashtable(); this.tuples = new java.util.Vector(); this.changedFlag = true; } }