Package edu.cmu.relativelayout

Source Code of edu.cmu.relativelayout.RelativeLayout

package edu.cmu.relativelayout;

import java.awt.Component;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.DisplayMode;
import java.awt.GraphicsConfiguration;
import java.awt.GraphicsDevice;
import java.awt.Insets;
import java.awt.LayoutManager2;
import java.awt.Rectangle;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import edu.cmu.relativelayout.equation.Equation;
import edu.cmu.relativelayout.equation.Variable;
import edu.cmu.relativelayout.matrix.RelativeMatrix;

/**
* The main RelativeLayout class. A LayoutManager implementation that uses Bindings and RelativeConstraints to lay out
* components.
*
* @author Brian Ellis (phoenix1701@gmail.com)
*/
public class RelativeLayout implements LayoutManager2 {

  /**
   * Returns whether RelativeLayout is in debugging mode. See {@link RelativeLayout#setDebugMode(boolean)} for more
   * information.
   *
   * @return <code>true</code> if the debugging flag is set, <code>false</code> otherwise.
   */
  public static boolean isDebugMode() {
    return RelativeMatrix.isDebugMode();
  }

  /**
   * Sets whether RelativeLayout is in debugging mode. If <code>debug</code> is set to <code>true</code>,
   * RelativeLayout will attempt to validate your entire layout every time a constraint is added or changed. This takes
   * a long time, reducing the performance of RelativeLayout by over an order of magnitude on complex layouts, but it
   * will cause any exceptions thrown while laying out the window to be traceable back to the exact line of code that
   * caused the problem. Due to the considerable performance penalty, debugging is off by default.
   *
   * @param debug <code>true</code> if debugging should be turned on, <code>false</code> otherwise
   */
  public static void setDebugMode(boolean debug) {
    RelativeMatrix.setDebugMode(debug);
  }

  /*
   * (non-Javadoc)
   *
   * @see java.awt.LayoutManager2#addLayoutComponent(java.awt.Component, java.lang.Object)
   */
  public void addLayoutComponent(Component theComp, Object theConstraints) {
    if (theConstraints instanceof RelativeConstraints) {
      RelativeConstraints relativeConstraints = (RelativeConstraints) theConstraints;

      // Following line may throw InconsistentConstraintException:
      relativeConstraints.setConstrainedObject(theComp);

      this.constraints.put(theComp, relativeConstraints);
      for (Variable v : relativeConstraints.getVariables()) {
        this.componentAssociations.put(v, theComp);
      }
    } else {
      throw new IllegalArgumentException("RelativeLayouts must use RelativeConstraints objects.");
    }
  }

  /*
   * (non-Javadoc)
   *
   * @see java.awt.LayoutManager#addLayoutComponent(java.lang.String, java.awt.Component)
   */
  public void addLayoutComponent(String theName, Component theComp) {
    throw new IllegalArgumentException("RelativeLayouts must use RelativeConstraints objects.");
  }

  /*
   * (non-Javadoc)
   *
   * @see java.awt.LayoutManager2#getLayoutAlignmentX(java.awt.Container)
   */
  public float getLayoutAlignmentX(Container theTarget) {
    return 0;
  }

  /*
   * (non-Javadoc)
   *
   * @see java.awt.LayoutManager2#getLayoutAlignmentY(java.awt.Container)
   */
  public float getLayoutAlignmentY(Container theTarget) {
    return 0;
  }

  /*
   * (non-Javadoc)
   *
   * @see java.awt.LayoutManager2#invalidateLayout(java.awt.Container)
   */
  public void invalidateLayout(Container theTarget) {
    this.backend = null;
  }

  /*
   * (non-Javadoc)
   *
   * @see java.awt.LayoutManager#layoutContainer(java.awt.Container)
   */
  public void layoutContainer(Container theParent) {
    RelativeMatrix myBackend = this.getBackend();

    initializeMatrixForContainer(theParent, myBackend);

    // Generate solutions:
    Map<Variable, Double> solutions = myBackend.solve();
    // System.out.println(solutions);

    setComponentBoundsFromVariables(theParent, solutions);
  }

  /*
   * (non-Javadoc)
   *
   * @see java.awt.LayoutManager2#maximumLayoutSize(java.awt.Container)
   */
  public Dimension maximumLayoutSize(Container theTarget) {
    return new Dimension(Integer.MAX_VALUE, Integer.MAX_VALUE);
  }

  /*
   * (non-Javadoc)
   *
   * @see java.awt.LayoutManager#minimumLayoutSize(java.awt.Container)
   */
  public Dimension minimumLayoutSize(Container theParent) {
    return new Dimension(0, 0);
  }

  /**
   * Returns the preferred size of the given container given the components that are to be laid out inside it. This
   * method is called when a container using a RelativeLayout is packed using the {@link java.awt.Window#pack()} method.<br>
   * <br>
   * Since RelativeLayout is different from other layout managers in that the size of the window determines the size of
   * the controls instead of vice versa, the behavior of this method (and therefore of the
   * {@link java.awt.Window#pack()} method of a window that uses a RelativeLayout) is a little complicated. If the
   * window has dynamically sized components in it (that is, anything whose size depends on the size of the window), the
   * window will be "maximized" -- that is, it will be made as big as possible on the screen. If, however, the window
   * does <em>not</em> have dynamically sized components (that is, no controls resize if you resize the window), then
   * the window will be sized to fit its contents precisely. This will work even if there are controls anchored to the
   * right or bottom of the window, but if there are, the window may be larger than desired in some cases.<br>
   * <br>
   * Note that due to the complexity of adding center-aligned components into the mix, the layout will <em>not</em>
   * take into account components that are center-aligned in the window. If such components exist, you may need to
   * manually set the size of the window instead of, or in addition to, calling {@link java.awt.Window#pack()}.
   *
   * @see java.awt.LayoutManager#preferredLayoutSize(java.awt.Container)
   */
  public Dimension preferredLayoutSize(Container theParent) {
    synchronized (theParent.getTreeLock()) {
      RelativeMatrix myBackend = this.getBackend();

      initializeMatrixForContainer(theParent, myBackend);

      // Check for dynamic sizes
      boolean hasDynamicWidth = false;
      boolean hasDynamicHeight = false;
      for (Component thisComponent : this.constraints.keySet()) {
        RelativeConstraints theseConstraints = this.constraints.get(thisComponent);
        for (Binding thisBinding : theseConstraints.bindings) {
          // If any of our bindings uses a dimensional variable, we have a dynamic size.
          if (thisBinding.usesDimensionalVariable()) {
            if (thisBinding.isHorizontal()) {
              hasDynamicWidth = true;
            } else {
              hasDynamicHeight = true;
            }
            break;
          }
        }
      }

      // Calculate the screen size if we're going to need it.
      int screenWidth = 0;
      int screenHeight = 0;
      if (hasDynamicWidth || hasDynamicHeight) {
        GraphicsConfiguration graphicsConfig = theParent.getGraphicsConfiguration();
        if (graphicsConfig == null) {
          screenWidth = 640;
          screenHeight = 480;
        } else {
          GraphicsDevice gs = graphicsConfig.getDevice();
          DisplayMode dm = gs.getDisplayMode();
          screenWidth = dm.getWidth();
          screenHeight = dm.getHeight();
        }

        // If we found dynamic sizes, return the maximum size possible, and we're done.
        if (hasDynamicWidth && hasDynamicHeight) {
          return new Dimension(screenWidth, screenHeight);
        }
      }

      // Otherwise, we have to generate solutions:
      Map<Variable, Double> solutions = myBackend.solve();

      // Now we find the max and min extents from all four corners:
      double maxLeftExtent = 0.0, maxTopExtent = 0.0;
      double minRightExtent = theParent.getSize().getWidth(), minBottomExtent = theParent.getSize().getHeight();

      for (Variable v : solutions.keySet()) {
        RelativeVariable rv = (RelativeVariable) v;

        boolean isRightExtent = false, isBottomExtent = false;

        if (rv.getComponent() == theParent) {
          // We can ignore the container itself.
          continue;
        }

        try {
          // Now, if this component has a Binding to the right or bottom edge of the window, we're going to take note of
          // that fact for later:
          if (this.constraints.get(rv.getComponent()) == null) {
            throw new UnknownComponentException(rv, theParent);
          }
          List<Binding> bindings = this.constraints.get(rv.getComponent()).bindings;
          for (Binding b : bindings) {
            if (b.getFixedComponent() == theParent && (b.getRelativePosition().getFixedEdge() == Edge.RIGHT)) {
              isRightExtent = true;
            } else if (b.getFixedComponent() == theParent && (b.getRelativePosition().getFixedEdge() == Edge.BOTTOM)) {
              isBottomExtent = true;
            }
          }

        } catch (NullPointerException e) {
          e.printStackTrace();
        }

        if (rv.getVariableType() == VariableType.X) {
          // This variable is the x-coordinate of a component.
          if (isRightExtent) {
            // And the component had a binding to the right edge of the window.
            double myX = solutions.get(rv);
            // See how far from the right this component extends, and take the min:
            if (myX < minRightExtent) {
              minRightExtent = myX;
            }
          } else {
            // The component did not have a binding to the right edge of the window.
            double myX = solutions.get(rv) + rv.getComponent().getPreferredSize().width;
            // See how far from the left this component extends, and take the max:
            if (myX > maxLeftExtent) {
              maxLeftExtent = myX;
            }
          }
        } else if (rv.getVariableType() == VariableType.Y) {
          // This variable is the y-coordinate of a component.
          if (isBottomExtent) {
            // And the component had a binding to the bottom edge of the window.
            double myY = solutions.get(rv);
            // See how far from the bottom this component extends, and take the min:
            if (myY < minBottomExtent) {
              minBottomExtent = myY;
            }
          } else {
            // The component did not have a binding to the bottom edge of the window.
            double myY = solutions.get(rv) + rv.getComponent().getPreferredSize().height;

            // See how far from the top this component extends, and take the max:
            if (myY > maxTopExtent) {
              maxTopExtent = myY;
            }
          }
        }
      }
      // Note: what are we forgetting here? That's right, center-aligned components! Something to do for later, I
      // guess...

      // Okay. So now we have all the info that we need, but there's just one problem. The minRightExtent and
      // minBottomExtent all depend on the width and height of the window -- if the width is -1, and the minLeftExtent
      // is -25, that means that the extent of the furthest control from the right isn't -25, but rather 24. So we need
      // to subtract:
      minRightExtent = theParent.getSize().getWidth() - minRightExtent;
      minBottomExtent = theParent.getSize().getHeight() - minBottomExtent;

      // Now, to find out how wide and tall the window needs to be to fit all this, worst-case, we just add the two
      // extents together. This may make the window larger than strictly necessary, but it's better to be too big than
      // too small.

      Insets insets = theParent.getInsets();
      Dimension finalSize =
          new Dimension((int) maxLeftExtent + (int) minRightExtent + insets.left + insets.right, (int) maxTopExtent
              + (int) minBottomExtent + insets.top + insets.bottom);

      if (hasDynamicHeight) {
        finalSize.height = screenHeight;
      }
      if (hasDynamicWidth) {
        finalSize.width = screenWidth;
      }

      // theParent.setSize(oldSize);
      return finalSize;
    }
  }

  /*
   * (non-Javadoc)
   *
   * @see java.awt.LayoutManager#removeLayoutComponent(java.awt.Component)
   */
  public void removeLayoutComponent(Component theComp) {
    this.constraints.remove(theComp);
    for (Iterator<Variable> iter = this.componentAssociations.keySet().iterator(); iter.hasNext();) {
      Variable var = iter.next();
      if (this.componentAssociations.get(var) == theComp) {
        iter.remove();
        getBackend().removeEquation(var);
      }
    }
  }

  /**
   * A helper method that adds default identities for the given component. This is important so that if no
   * RelativePosition is defined that explicitly involves some variable of that component, it will default to something
   * reasonable (0 for X and Y, and the component's preferred size for Width and Height).
   *
   * @param myBackend The matrix we want to add identities to.
   * @param c The component for which we want to add identities.
   */
  private void addIdentitiesForComponent(RelativeMatrix myBackend, Component c, boolean isParent) {

    Dimension size;

    if (!isParent) {
      size = c.getPreferredSize();
    } else {
      size = c.getSize();
    }

    ConcreteEquation identity = new ConcreteEquation();
    Variable variable = RelativeVariable.get(c, VariableType.WIDTH);
    this.componentAssociations.put(variable, c);
    identity.setCoefficient(1, variable);
    identity.setRightHandSide(size.width);
    myBackend.addEquation(variable, identity);

    identity = new ConcreteEquation();
    variable = RelativeVariable.get(c, VariableType.HEIGHT);
    this.componentAssociations.put(variable, c);
    identity.setCoefficient(1, variable);
    identity.setRightHandSide(size.height);
    myBackend.addEquation(variable, identity);

    identity = new ConcreteEquation();
    variable = RelativeVariable.get(c, VariableType.X);
    this.componentAssociations.put(variable, c);
    identity.setCoefficient(1, variable);
    identity.setRightHandSide(0);
    myBackend.addEquation(variable, identity);

    identity = new ConcreteEquation();
    variable = RelativeVariable.get(c, VariableType.Y);
    this.componentAssociations.put(variable, c);
    identity.setCoefficient(1, variable);
    identity.setRightHandSide(0);
    myBackend.addEquation(variable, identity);
  }

  /**
   * Gets an instance of the backend matrix, creating one if necessary. This method should be used rather than accessing
   * this.backend directly to avoid crashes or duplicated instances.
   */
  private RelativeMatrix getBackend() {
    if (this.backend == null) {
      this.backend = new RelativeMatrix();
    }
    return this.backend;
  }

  /**
   * Initializes the given matrix for the given container by setting up identities in the matrix for each component's x,
   * y, width, and height as well as that of the container itself. x and y are set to 0, and width and height are set to
   * either the size or the preferredSize of the component, whichever is available.
   *
   * @param theParent The container we are laying out.
   * @param myBackend The backend matrix we are using.
   */
  private void initializeMatrixForContainer(Container theParent, RelativeMatrix myBackend) {

    // Initialize the matrix with the preferred size for all components.
    for (Component c : this.constraints.keySet()) {
      addIdentitiesForComponent(myBackend, c, false);
    }

    // And don't forget the window!
    addIdentitiesForComponent(myBackend, theParent, true);

    // Add all the constraints' equations to the matrix:
    for (RelativeConstraints element : this.constraints.values()) {
      List<Equation> equations = element.getEquations();
      List<Variable> variables = element.getVariables();
      for (int i = 0; i < equations.size(); i++) {
        myBackend.addEquation(variables.get(i), equations.get(i));
      }
    }
  }

  /**
   * Takes a map of variables and their bound values, and sets the bounds of all the components in the layout based on
   * their variables' values.
   *
   * @param theContainer
   * @param solutions The map of variables and their bound values.
   */
  private void setComponentBoundsFromVariables(Container theContainer, Map<Variable, Double> solutions) {
    for (Variable v : solutions.keySet()) {
      // Associate this variable with a component and a variable type:
      Component comp = this.componentAssociations.get(v);

      if (comp == null) {
        // We might have a stale matrix, but if there's no component associated with this variable, we can just skip it
        // because we're guaranteed not to have any constraints that depend on that component anyway. This is a bit
        // voodoo, but it works.
        continue;
      }

      if (comp == theContainer) {
        // We don't want to change the bounds of the parent, ever!
        continue;
      }

      Rectangle bounds = comp.getBounds();

      // Set that attribute of the relevant component to the value in the solution:
      double value = solutions.get(v);

      RelativeVariable rv = (RelativeVariable) v;

      Insets insets = comp.getParent().getInsets();

      if (rv.getVariableType() == VariableType.X) {
        comp.setBounds(insets.left + (int) value, bounds.y, bounds.width, bounds.height);
      } else if (rv.getVariableType() == VariableType.Y) {
        comp.setBounds(bounds.x, insets.top + (int) value, bounds.width, bounds.height);
      } else if (rv.getVariableType() == VariableType.WIDTH) {
        comp.setBounds(bounds.x, bounds.y, (int) value, bounds.height);
      } else if (rv.getVariableType() == VariableType.HEIGHT) {
        comp.setBounds(bounds.x, bounds.y, bounds.width, (int) value);
      } else {
        throw new IllegalArgumentException("Encountered an illegal variable (" + v.getName()
            + ") while laying out a container.");
      }
    }
  }

  /**
   * The mapping between each component and the constraint with which it was associated when it was added to the layout.
   */
  private Map<Component, RelativeConstraints> constraints = new HashMap<Component, RelativeConstraints>();

  /**
   * The mapping between each variable in the back-end matrix and the component with which it is associated. This is
   * technically no longer needed because we're using RelativeVariables, which store their component association, so it
   * should probably be refactored out.
   */
  private Map<Variable, Component> componentAssociations = new HashMap<Variable, Component>();

  /**
   * The backend matrix we're using to lay out this container. Don't access or set this directly; use getBackend()
   * instead, which will automatically create one if needed.
   */
  private RelativeMatrix backend;

}
TOP

Related Classes of edu.cmu.relativelayout.RelativeLayout

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.